Optimize checks of test database state by moving into Python.

Previously, the generate-fixtures shell script by called into Django
multiple times in order to check whether the database was in a
reasonable state.  Since there's a lot of overhead to starting up
Django, this resulted in `test-backend` and `test-js-with-casper`
being quite slow to run a single small test (2.8s or so) even on my
very fast laptop.

We fix this is by moving the checks into a new Python library, so that
we can avoid paying the Django startup overhead 3 times unnecessarily.
The result saves about 1.2s (~40%) from the time required to run a
single backend test.

Fixes #1221.
This commit is contained in:
Tomasz Kolek
2016-09-13 22:40:13 +02:00
committed by Tim Abbott
parent 498317d533
commit dbeab6aa6f
5 changed files with 118 additions and 15 deletions

View File

@@ -11,6 +11,7 @@ try:
# outside a Zulip virtualenv.
from typing import Iterable
import requests
import django
except ImportError as e:
print("ImportError: {}".format(e))
print("You need to run the Zulip tests inside a Zulip dev environment.")
@@ -39,6 +40,13 @@ parser.add_option('--remote-debug',
default=False)
(options, args) = parser.parse_args()
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.dirname(TOOLS_DIR))
from zerver.lib.test_fixtures import is_template_database_current
os.environ['DJANGO_SETTINGS_MODULE'] = 'zproject.test_settings'
django.setup()
os.environ['PYTHONUNBUFFERED'] = 'y'
os.chdir(os.path.join(os.path.dirname(os.path.realpath(__file__)), '..'))
subprocess.check_call('tools/setup/generate-test-credentials')
@@ -72,7 +80,11 @@ def run_tests(realms_have_subdomains, files):
file = os.path.join(os.path.dirname(__file__), '../frontend_tests/casper_tests', file)
test_files.append(os.path.abspath(file))
subprocess.check_call('tools/setup/generate-fixtures')
generate_fixtures_command = ['tools/setup/generate-fixtures']
if not is_template_database_current():
generate_fixtures_command.append('--force')
subprocess.check_call(generate_fixtures_command)
remote_debug = ""
if options.remote_debug:

View File

@@ -2,24 +2,17 @@
set -e
function migration_status {
./manage.py migrate --list --settings=zproject.test_settings | sed 's/*/ /' > "$1"
./manage.py get_migration_status --settings=zproject.test_settings > $1
}
template_grep_error_code=$(echo "SELECT 1 from pg_database WHERE datname='zulip_test_template';" | python manage.py dbshell --settings=zproject.test_settings | grep -q "1 row"; echo $?)
if [ "$template_grep_error_code" == "0" ]; then
migration_status var/available-migrations
if [ -e var/migration-status ] &&
cmp -s var/available-migrations var/migration-status &&
[ "$1" != "--force" ]; then
"$(dirname "$0")/../../scripts/setup/terminate-psql-sessions" zulip zulip_test zulip_test_base zulip_test_template
psql -h localhost postgres zulip_test << EOF
if [ "$1" != "--force" ]; then
"$(dirname "$0")/../../scripts/setup/terminate-psql-sessions" zulip zulip_test zulip_test_base zulip_test_template
psql -h localhost postgres zulip_test << EOF
DROP DATABASE IF EXISTS zulip_test;
CREATE DATABASE zulip_test TEMPLATE zulip_test_template;
EOF
sh "$(dirname "$0")/../../scripts/setup/flush-memcached"
exit 0
fi
sh "$(dirname "$0")/../../scripts/setup/flush-memcached"
exit 0
fi
mkdir -p zerver/fixtures

View File

@@ -23,6 +23,7 @@ if __name__ == "__main__":
TOOLS_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(os.path.dirname(TOOLS_DIR))
sys.path.insert(0, os.path.dirname(TOOLS_DIR))
from zerver.lib.test_fixtures import is_template_database_current
os.environ['DJANGO_SETTINGS_MODULE'] = 'zproject.test_settings'
# "-u" uses unbuffered IO, which is important when wrapping it in subprocess
os.environ['PYTHONUNBUFFERED'] = 'y'
@@ -136,7 +137,11 @@ if __name__ == "__main__":
django.setup()
if options.generate_fixtures:
subprocess.call(os.path.join(TOOLS_DIR, 'setup', 'generate-fixtures'))
generate_fixtures_command = [os.path.join(TOOLS_DIR, 'setup', 'generate-fixtures')]
if not is_template_database_current():
generate_fixtures_command.append('--force')
subprocess.call(generate_fixtures_command)
TestRunner = get_runner(settings)
test_runner = TestRunner()

View File

@@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
import os
import re
from typing import Any, Optional
from importlib import import_module
from six import text_type
from six.moves import cStringIO as StringIO
from django.db import connections, DEFAULT_DB_ALIAS
from django.apps import apps
from django.core.management import call_command
from django.utils.module_loading import module_has_submodule
def database_exists(database_name, **options):
# type: (text_type, **Any) -> bool
db = options.get('database', DEFAULT_DB_ALIAS)
connection = connections[db]
with connection.cursor() as cursor:
cursor.execute("SELECT 1 from pg_database WHERE datname='{}';".format(database_name))
return_value = bool(cursor.fetchone())
connections.close_all()
return return_value
def get_migration_status(**options):
# type: (**Any) -> str
verbosity = options.get('verbosity', 1)
for app_config in apps.get_app_configs():
if module_has_submodule(app_config.module, "management"):
import_module('.management', app_config.name)
app_labels = [options['app_label']] if options.get('app_label') else None
db = options.get('database', DEFAULT_DB_ALIAS)
out = StringIO()
call_command(
'showmigrations',
'--list',
app_labels=app_labels,
database=db,
no_color=options.get('no_color', False),
settings=options.get('settings', os.environ['DJANGO_SETTINGS_MODULE']),
stdout=out,
traceback=options.get('traceback', True),
verbosity=verbosity,
)
connections.close_all()
out.seek(0)
output = out.read()
return re.sub('\x1b\[(1|0)m', '', output)
def are_migrations_the_same(migration_file, **options):
# type: (text_type, **Any) -> bool
if not os.path.exists(migration_file):
return False
with open(migration_file) as f:
migration_content = f.read()
return migration_content == get_migration_status(**options)
def is_template_database_current(
database_name='zulip_test_template',
migration_status='var/migration-status',
settings='zproject.test_settings'
):
# type: (Optional[text_type], Optional[text_type], Optional[text_type]) -> bool
if database_exists(database_name):
return are_migrations_the_same(migration_status, settings=settings)
return False

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
import argparse
from typing import Any
from django.db import DEFAULT_DB_ALIAS
from django.core.management.base import BaseCommand
from zerver.lib.test_fixtures import get_migration_status
class Command(BaseCommand):
help = "Get status of migrations."
def add_arguments(self, parser):
# type: (argparse.ArgumentParser) -> None
parser.add_argument('app_label', nargs='?',
help='App label of an application to synchronize the state.')
parser.add_argument('--database', action='store', dest='database',
default=DEFAULT_DB_ALIAS, help='Nominates a database to synchronize. '
'Defaults to the "default" database.')
def handle(self, *args, **options):
# type: (*Any, **Any) -> None
self.stdout.write(get_migration_status(**options))