mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 05:23:35 +00:00
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:
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
69
zerver/lib/test_fixtures.py
Normal file
69
zerver/lib/test_fixtures.py
Normal 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
|
||||
24
zerver/management/commands/get_migration_status.py
Normal file
24
zerver/management/commands/get_migration_status.py
Normal 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))
|
||||
Reference in New Issue
Block a user