Skip to content

Commit ffc41c1

Browse files
committed
Fix parallel build
1 parent 0b23951 commit ffc41c1

File tree

5 files changed

+194
-120
lines changed

5 files changed

+194
-120
lines changed

conftest.py

+148-80
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@
2121
from __future__ import absolute_import
2222
from __future__ import print_function
2323

24+
import datetime
2425
import logging
26+
import multiprocessing
2527
import os
2628
import subprocess
29+
import sys
2730
import tempfile
2831

32+
import posix_ipc
2933
import psycopg2
3034
import psycopg2.extensions
3135
import pytest
@@ -49,10 +53,20 @@
4953
D1_SKIP_LIST = 'skip_passed/list'
5054
D1_SKIP_COUNT = 'skip_passed/count'
5155

56+
TEMPLATE_DB_KEY = 'template'
57+
TEST_DB_KEY = 'default'
58+
5259
# Allow redefinition of functions. Pytest allows multiple hooks with the same
5360
# name.
5461
# flake8: noqa: F811
5562

63+
# template_db_lock = threading.Lock()
64+
template_db_lock = multiprocessing.Lock()
65+
66+
# Hack to get access to print and logging output when running under pytest-xdist
67+
# and pytest-catchlog. Without this, only output from failed tests is displayed.
68+
sys.stdout = sys.stderr
69+
5670

5771
def pytest_addoption(parser):
5872
"""Add command line switches for pytest customization. See README.md for
@@ -110,14 +124,35 @@ def pytest_addoption(parser):
110124
)
111125

112126

127+
def pytest_configure():
128+
logging.debug('pytest_configure()')
129+
tmp_store_path = os.path.join(
130+
tempfile.gettempdir(), 'gmn_test_obj_store_{}'.format(
131+
d1_test.instance_generator.random_data.
132+
random_lower_ascii(min_len=12, max_len=12)
133+
)
134+
)
135+
logging.debug('Setting OBJECT_STORE_PATH = {}'.format(tmp_store_path))
136+
django.conf.settings.OBJECT_STORE_PATH = tmp_store_path
137+
d1_gmn.app.sciobj_store.create_clean_tmp_store()
138+
139+
113140
# Hooks
114141

115142

116143
def pytest_sessionstart(session):
117-
"""Called by pytest before calling session.main()"""
144+
"""Called by pytest before calling session.main()
145+
When running in parallel with xdist, this is called once for each worker.
146+
By default, the number of workers is the same as the number of CPU cores.
147+
"""
118148
if pytest.config.getoption('--sample-tidy'):
119149
d1_test.sample.start_tidy()
120-
pytest.exit('Tidy started. Run complete to complete')
150+
pytest.exit('Tidy started')
151+
152+
if pytest.config.getoption('--fixture-refresh'):
153+
db_drop(TEMPLATE_DB_KEY)
154+
pytest.exit('Template refresh started')
155+
121156
if pytest.config.getoption('--skip-clear'):
122157
_clear_skip_list()
123158
if pytest.config.getoption('--skip-print'):
@@ -306,122 +341,154 @@ def mn_client_v1_v2(request):
306341

307342
# Settings
308343

344+
# @pytest.fixture(scope='session', autouse=True)
345+
# def set_unique_sciobj_store_path(request):
346+
# tmp_store_path = os.path.join(
347+
# tempfile.gettempdir(),
348+
# 'gmn_test_obj_store_{}'.format(get_xdist_unique_suffix(request))
349+
# )
350+
# logging.debug('Setting OBJECT_STORE_PATH = {}'.format(tmp_store_path))
351+
# django.conf.settings.OBJECT_STORE_PATH = tmp_store_path
352+
# d1_gmn.app.sciobj_store.create_clean_tmp_store()
309353

310-
@pytest.fixture(scope='session', autouse=True)
311-
def set_unique_sciobj_store_path(request):
312-
tmp_store_path = os.path.join(
313-
tempfile.gettempdir(),
314-
'gmn_test_obj_store_{}'.format(get_xdist_unique_suffix(request))
315-
)
316-
django.conf.settings.OBJECT_STORE_PATH = tmp_store_path
317-
d1_gmn.app.sciobj_store.create_clean_tmp_store()
318-
319-
320-
# DB fixtures
354+
# Database setup
321355

322356

323357
@pytest.yield_fixture(scope='session')
324358
def django_db_setup(request, django_db_blocker):
325359
"""Set up DB fixture
326360
When running in parallel with xdist, this is called once for each worker.
327-
By default, the number of workers is the same as the number of CPU cores.
328361
"""
329362
logging.info('Setting up DB fixture')
330363

331-
test_db_key = 'default'
332-
test_db_name = ''.join([
333-
django.conf.settings.DATABASES[test_db_key]['NAME'],
334-
get_xdist_unique_suffix(request),
335-
])
336-
django.conf.settings.DATABASES[test_db_key]['NAME'] = test_db_name
337-
338-
template_db_key = 'template'
339-
template_db_name = django.conf.settings.DATABASES[template_db_key]['NAME']
364+
db_set_unique_db_name(request)
340365

341366
with django_db_blocker.unblock():
342367

343-
if pytest.config.getoption('--fixture-regen'):
344-
drop_database(test_db_name)
345-
create_blank_db(test_db_key, test_db_name)
346-
django.db.connections[test_db_key].commit()
347-
pytest.exit('Database dropped and reinitialized. Now run mk_db_fixture')
348-
349-
# try:
350-
# load_template_fixture(template_db_key, template_db_name)
351-
# except psycopg2.DatabaseError as e:
352-
# logging.error(str(e))
353-
354-
drop_database(test_db_name)
355-
create_db_from_template(test_db_name, template_db_name)
368+
# if pytest.config.getoption('--fixture-regen'):
369+
# db_drop(test_db_name)
370+
# db_create_blank(test_db_key, test_db_name)
371+
# django.db.connections[test_db_key].commit()
372+
# pytest.exit('Database dropped and reinitialized. Now run mk_db_fixture')
373+
374+
# Regular multiprocessing.Lock() context manager did not work here. Also
375+
# tried creating the lock at module scope, and also directly calling
376+
# acquire() and release(). It's probably related to how the worker processes
377+
# relate to each other when launched by pytest-xdist as compared to what the
378+
# multiprocessing module expects.
379+
with posix_ipc.Semaphore(
380+
'/{}'.format(__name__), flags=posix_ipc.O_CREAT, initial_value=1
381+
):
382+
logging.warn(
383+
'LOCK BEGIN {} {}'.
384+
format(db_get_name_by_key(TEMPLATE_DB_KEY), datetime.datetime.now())
385+
)
386+
387+
if not db_exists(TEMPLATE_DB_KEY):
388+
db_create_blank(TEMPLATE_DB_KEY)
389+
db_migrate(TEMPLATE_DB_KEY)
390+
db_populate_by_json(TEMPLATE_DB_KEY)
391+
db_migrate(TEMPLATE_DB_KEY)
392+
393+
logging.warn(
394+
'LOCK END {} {}'.
395+
format(db_get_name_by_key(TEMPLATE_DB_KEY), datetime.datetime.now())
396+
)
397+
398+
db_drop(TEST_DB_KEY)
399+
db_create_from_template()
400+
# db_migrate(TEST_DB_KEY)
356401

357402
# # Haven't found out how to prevent transactions from being started, so
358403
# # closing the implicit transaction here so that template fixture remains
359404
# # available.
360405
# django.db.connections[test_db_key].commit()
361406

362-
migrate_db(test_db_key)
363-
364407
yield
365408

366-
for connection in django.db.connections.all():
367-
connection.close()
409+
db_drop(TEST_DB_KEY)
410+
368411

369-
drop_database(test_db_name)
412+
def db_get_name_by_key(db_key):
413+
logging.debug('db_get_name_by_key() {}'.format(db_key))
414+
return django.conf.settings.DATABASES[db_key]['NAME']
415+
416+
417+
def db_set_unique_db_name(request):
418+
logging.debug('db_set_unique_db_name()')
419+
db_name = '_'.join([
420+
db_get_name_by_key(TEST_DB_KEY),
421+
get_xdist_unique_suffix(request),
422+
])
423+
django.conf.settings.DATABASES[TEST_DB_KEY]['NAME'] = db_name
370424

371425

372-
def create_db_from_template(test_db_name, template_db_name):
426+
def db_create_from_template():
427+
logging.debug('db_create_from_template()')
428+
new_db_name = db_get_name_by_key(TEST_DB_KEY)
429+
template_db_name = db_get_name_by_key(TEMPLATE_DB_KEY)
373430
logging.info(
374-
'Creating test DB from template. test_db="{}" template_db="{}"'.
375-
format(test_db_name, template_db_name)
431+
'Creating new db from template. new_db="{}" template_db="{}"'.
432+
format(new_db_name, template_db_name)
376433
)
377434
run_sql(
378435
'postgres',
379-
'create database {} template {};'.format(test_db_name, template_db_name)
436+
'create database {} template {};'.format(new_db_name, template_db_name)
380437
)
381438

382439

383-
def load_template_fixture(template_db_key, template_db_name):
440+
def db_populate_by_json(db_key):
384441
"""Load DB fixture from compressed JSON file to template database"""
385-
logging.info('Loading template DB fixture')
442+
logging.debug('db_populate_by_json() {}'.format(db_key))
386443
fixture_file_path = d1_test.sample.get_path('db_fixture.json.bz2')
387-
if pytest.config.getoption('--fixture-refresh'):
388-
# django.core.management.call_command('flush', database=template_db_key)
389-
drop_database(template_db_name)
390-
create_blank_db(template_db_key, template_db_name)
391-
logging.debug('Populating tables with fixture data')
444+
# loaddata used to have a 'commit' arg, but it appears to have been removed.
392445
django.core.management.call_command(
393-
'loaddata', fixture_file_path, database=template_db_key, commit=True
446+
'loaddata', fixture_file_path, database=db_key, commit=True
394447
)
395-
django.db.connections[template_db_key].commit()
396-
for connection in django.db.connections.all():
397-
connection.close()
448+
db_commit_and_close(db_key)
398449

399450

400-
def migrate_db(test_db_key):
451+
def db_migrate(db_key):
452+
logging.debug('db_migrate() {}'.format(db_key))
401453
django.core.management.call_command(
402-
'migrate', database=test_db_key, commit=True
454+
'migrate', '--run-syncdb', database=db_key
403455
)
404-
django.db.connections[test_db_key].commit()
405-
for connection in django.db.connections.all():
406-
connection.close()
456+
db_commit_and_close(db_key)
407457

408458

409-
def drop_database(db_name):
459+
def db_drop(db_key):
460+
logging.debug('db_drop() {}'.format(db_key))
461+
db_name = db_get_name_by_key(db_key)
410462
logging.debug('Dropping database: {}'.format(db_name))
463+
db_commit_and_close(db_key)
464+
run_sql('postgres', 'drop database if exists {};'.format(db_name))
465+
411466

467+
def db_commit_and_close(db_key):
468+
logging.debug('db_commit_and_close() {}'.format(db_key))
469+
django.db.connections[db_key].commit()
412470
for connection in django.db.connections.all():
413471
connection.close()
414472

415-
run_sql('postgres', 'drop database if exists {};'.format(db_name))
416-
417473

418-
def create_blank_db(db_key, db_name):
419-
logging.debug('Creating blank DB: {}'.format(db_name))
474+
def db_create_blank(db_key):
475+
logging.debug('db_create_blank() {}'.format(db_key))
476+
db_name = db_get_name_by_key(db_key)
477+
logging.debug('Creating blank database: {}'.format(db_name))
420478
run_sql('postgres', "create database {} encoding 'utf-8';".format(db_name))
421-
logging.debug('Creating GMN tables')
422-
django.core.management.call_command(
423-
'migrate', '--run-syncdb', database=db_key
479+
480+
481+
def db_exists(db_key):
482+
logging.debug('db_exists() {}'.format(db_key))
483+
db_name = db_get_name_by_key(db_key)
484+
exists_bool = bool(
485+
run_sql(
486+
'postgres',
487+
"select 1 from pg_database WHERE datname='{}'".format(db_name)
488+
)
424489
)
490+
logging.debug('db_exists(): {}'.format(exists_bool))
491+
return exists_bool
425492

426493

427494
def run_sql(db, sql):
@@ -431,7 +498,7 @@ def run_sql(db, sql):
431498
cur = conn.cursor()
432499
cur.execute(sql)
433500
except psycopg2.DatabaseError as e:
434-
logging.debug('SQL query result="{}"'.format(str(e)))
501+
logging.debug('SQL query error: {}'.format(str(e)))
435502
raise
436503
try:
437504
return cur.fetchall()
@@ -444,17 +511,18 @@ def run_sql(db, sql):
444511

445512

446513
def get_xdist_unique_suffix(request):
447-
return ''.join([
448-
d1_test.instance_generator.random_data.random_lower_ascii(
449-
min_len=12, max_len=12
450-
), get_xdist_suffix(request)
451-
])
514+
return '_'.join([get_random_ascii_string(), get_xdist_suffix(request)])
515+
516+
517+
def get_random_ascii_string():
518+
return d1_test.instance_generator.random_data.random_lower_ascii(
519+
min_len=12, max_len=12
520+
)
452521

453522

454523
def get_xdist_suffix(request):
455-
"""When running in parallel with xdist, each thread gets a different suffix.
456-
- In parallel run, return '_gw1', etc.
457-
- In single run, return ''.
458-
"""
524+
"""Return a different string for each worker when running in parallel under
525+
pytest-xdist, else return an empty string. Returned strings are on the form,
526+
"gwN"."""
459527
s = getattr(request.config, 'slaveinput', {}).get('slaveid')
460-
return '_{}'.format(s) if s is not None else ''
528+
return s if s is not None else ''

gmn/src/d1_gmn/app/sciobj_store.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def assert_sciobj_store_exists():
149149
if not is_existing_store():
150150
raise d1_common.types.exceptions.ServiceFailure(
151151
0, u'Attempted to access non-existing filesystem science object store. '
152-
'store_path="{}"'.format(get_store_version_path())
152+
u'store_path="{}"'.format(get_store_version_path())
153153
)
154154

155155

0 commit comments

Comments
 (0)