21
21
from __future__ import absolute_import
22
22
from __future__ import print_function
23
23
24
+ import datetime
24
25
import logging
26
+ import multiprocessing
25
27
import os
26
28
import subprocess
29
+ import sys
27
30
import tempfile
28
31
32
+ import posix_ipc
29
33
import psycopg2
30
34
import psycopg2 .extensions
31
35
import pytest
49
53
D1_SKIP_LIST = 'skip_passed/list'
50
54
D1_SKIP_COUNT = 'skip_passed/count'
51
55
56
+ TEMPLATE_DB_KEY = 'template'
57
+ TEST_DB_KEY = 'default'
58
+
52
59
# Allow redefinition of functions. Pytest allows multiple hooks with the same
53
60
# name.
54
61
# flake8: noqa: F811
55
62
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
+
56
70
57
71
def pytest_addoption (parser ):
58
72
"""Add command line switches for pytest customization. See README.md for
@@ -110,14 +124,35 @@ def pytest_addoption(parser):
110
124
)
111
125
112
126
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
+
113
140
# Hooks
114
141
115
142
116
143
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
+ """
118
148
if pytest .config .getoption ('--sample-tidy' ):
119
149
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
+
121
156
if pytest .config .getoption ('--skip-clear' ):
122
157
_clear_skip_list ()
123
158
if pytest .config .getoption ('--skip-print' ):
@@ -306,122 +341,154 @@ def mn_client_v1_v2(request):
306
341
307
342
# Settings
308
343
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()
309
353
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
321
355
322
356
323
357
@pytest .yield_fixture (scope = 'session' )
324
358
def django_db_setup (request , django_db_blocker ):
325
359
"""Set up DB fixture
326
360
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.
328
361
"""
329
362
logging .info ('Setting up DB fixture' )
330
363
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 )
340
365
341
366
with django_db_blocker .unblock ():
342
367
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)
356
401
357
402
# # Haven't found out how to prevent transactions from being started, so
358
403
# # closing the implicit transaction here so that template fixture remains
359
404
# # available.
360
405
# django.db.connections[test_db_key].commit()
361
406
362
- migrate_db (test_db_key )
363
-
364
407
yield
365
408
366
- for connection in django . db . connections . all ():
367
- connection . close ()
409
+ db_drop ( TEST_DB_KEY )
410
+
368
411
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
370
424
371
425
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 )
373
430
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 )
376
433
)
377
434
run_sql (
378
435
'postgres' ,
379
- 'create database {} template {};' .format (test_db_name , template_db_name )
436
+ 'create database {} template {};' .format (new_db_name , template_db_name )
380
437
)
381
438
382
439
383
- def load_template_fixture ( template_db_key , template_db_name ):
440
+ def db_populate_by_json ( db_key ):
384
441
"""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 ) )
386
443
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.
392
445
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
394
447
)
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 )
398
449
399
450
400
- def migrate_db (test_db_key ):
451
+ def db_migrate (db_key ):
452
+ logging .debug ('db_migrate() {}' .format (db_key ))
401
453
django .core .management .call_command (
402
- 'migrate' , database = test_db_key , commit = True
454
+ 'migrate' , '--run-syncdb' , database = db_key
403
455
)
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 )
407
457
408
458
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 )
410
462
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
+
411
466
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 ()
412
470
for connection in django .db .connections .all ():
413
471
connection .close ()
414
472
415
- run_sql ('postgres' , 'drop database if exists {};' .format (db_name ))
416
-
417
473
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 ))
420
478
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
+ )
424
489
)
490
+ logging .debug ('db_exists(): {}' .format (exists_bool ))
491
+ return exists_bool
425
492
426
493
427
494
def run_sql (db , sql ):
@@ -431,7 +498,7 @@ def run_sql(db, sql):
431
498
cur = conn .cursor ()
432
499
cur .execute (sql )
433
500
except psycopg2 .DatabaseError as e :
434
- logging .debug ('SQL query result="{}" ' .format (str (e )))
501
+ logging .debug ('SQL query error: {} ' .format (str (e )))
435
502
raise
436
503
try :
437
504
return cur .fetchall ()
@@ -444,17 +511,18 @@ def run_sql(db, sql):
444
511
445
512
446
513
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
+ )
452
521
453
522
454
523
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"."""
459
527
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