2121from __future__ import absolute_import
2222from __future__ import print_function
2323
24+ import datetime
2425import logging
26+ import multiprocessing
2527import os
2628import subprocess
29+ import sys
2730import tempfile
2831
32+ import posix_ipc
2933import psycopg2
3034import psycopg2 .extensions
3135import pytest
4953D1_SKIP_LIST = 'skip_passed/list'
5054D1_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
5771def 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
116143def 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' )
324358def 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
427494def 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
446513def 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
454523def 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 ''
0 commit comments