nti.testing 4.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,693 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Support for using :mod:`testgres` to create and use a Postgres
4
+ instance as a layer.
5
+
6
+ There is also support for benchmarking and saving databeses for
7
+ later examination.
8
+
9
+ .. versionadded:: 4.0.0
10
+
11
+ The APIs are preliminary and may change.
12
+
13
+ This is only supported on platforms that can install ``psycopg2``.
14
+
15
+ """
16
+ from contextlib import contextmanager
17
+ import functools
18
+ import os
19
+ import sys
20
+ import unittest
21
+ from unittest.mock import patch
22
+
23
+ #import psycopg2
24
+ #import psycopg2.extras
25
+ #import psycopg2.pool
26
+
27
+ try:
28
+ from psycopg2 import ProgrammingError
29
+ except ImportError:
30
+ ThreadedConnectionPool = None
31
+ DictCursor = None
32
+ class IntegrityError(Exception):
33
+ """Never thrown"""
34
+ ProgrammingError = InternalError = IntegrityError
35
+ else:
36
+ from psycopg2.pool import ThreadedConnectionPool
37
+ from psycopg2.extras import DictCursor
38
+ from psycopg2 import IntegrityError
39
+ from psycopg2 import InternalError
40
+
41
+ import testgres
42
+
43
+
44
+ if 'PG_CONFIG' not in os.environ:
45
+ # Set up for macports and fedora, using files that exist.
46
+ # Otherwise, don't set it, assume things are on the path.
47
+ for option in (
48
+ '/opt/local/lib/postgresql11/bin/pg_config',
49
+ '/usr/pgsql-11/bin/pg_config',
50
+ ):
51
+ if os.path.isfile(option):
52
+ # TODO: Check exec bit
53
+ os.environ['PG_CONFIG'] = option
54
+ break
55
+
56
+ # If True, save the database to a pg_dump
57
+ # file on teardown. The file name will be printed.''
58
+ SAVE_DATABASE_ON_TEARDOWN = False
59
+ SAVE_DATABASE_FILENAME = None
60
+
61
+ # If the path to a database dump file that exists, the database
62
+ # will be restored from this file on setUp.
63
+ LOAD_DATABASE_ON_SETUP = None
64
+
65
+ if 'NTI_SAVE_DB' in os.environ:
66
+ # NTI_SAVE_DB is either 1/on/true (case-insensitive)
67
+ # or a file name.
68
+ val = os.environ['NTI_SAVE_DB']
69
+ if val.lower() in {'0', 'off', 'false', 'no'}:
70
+ SAVE_DATABASE_ON_TEARDOWN = False
71
+ else:
72
+ SAVE_DATABASE_ON_TEARDOWN = True
73
+ if val.lower() not in {'1', 'on', 'true', 'yes'}:
74
+ SAVE_DATABASE_FILENAME = val
75
+
76
+
77
+ if 'NTI_LOAD_DB_FILE' in os.environ:
78
+ LOAD_DATABASE_ON_SETUP = os.environ['NTI_LOAD_DB_FILE']
79
+
80
+
81
+ def patched_get_pg_version(*args, **kwargs):
82
+ # We patch this in testgres.node, so its ok to import
83
+ # the original. In version 1.10, they changed the signature
84
+ # of this function, so be sure to accept whatever it does and
85
+ # pass it on.
86
+ from testgres.utils import get_pg_version
87
+ from testgres.node import PgVer
88
+ from packaging.version import InvalidVersion
89
+
90
+ # Some installs of postgres return
91
+ # strings that the version parser doesn't like;
92
+ # notably the Python images get a debian build that says
93
+ # "15.3-0+deb12u1" which get_pg_version() chops down to
94
+ # "15.3-0+". If it can't be parsed, then return a fake.
95
+
96
+ try:
97
+ version = get_pg_version(*args, **kwargs)
98
+ PgVer(version)
99
+ except InvalidVersion:
100
+ print('testgres: Got invalid postgres version', version)
101
+ # The actual version string looks like "postgres (PostgreSQL) 15.4",
102
+ # and get_pg_version() processes that down to this
103
+ version = "15.4"
104
+ print('testgres: Substituting version', version)
105
+
106
+ return version
107
+
108
+ class DatabaseLayer(object):
109
+ """
110
+ A test layer that creates the database, and sets each
111
+ test up in its own connection, aborting the transaction when
112
+ done.
113
+ """
114
+
115
+ #: The name of the database within the node. We only create
116
+ #: the default databases, so this should be 'postgres'
117
+ DATABASE_NAME = 'postgres'
118
+
119
+ #: A `testgres.node.PostgresNode`, created for the layer.
120
+ #: A psycopg2 connection to it is located in the :attr:`connection`
121
+ #: attribute (similarly for :attr:`connection_pool`), while
122
+ #: a DSN connection string is in :attr:`postgres_dsn`
123
+ postgres_node = None
124
+
125
+ #: A string you can use to connect to Postgres.
126
+ postgres_dsn = None
127
+
128
+ #: A string you can pass to SQLAlchemy
129
+ postgres_uri = None
130
+
131
+ #: Set for each test.
132
+ connection = None
133
+
134
+ #: Set for each test.
135
+ cursor = None
136
+
137
+ connection_pool = None
138
+
139
+
140
+ connection_pool_klass = ThreadedConnectionPool
141
+ connection_pool_minconn = 1
142
+ connection_pool_maxconn = 51
143
+
144
+ @classmethod
145
+ def setUp(cls):
146
+ testgres.configure_testgres()
147
+
148
+ with patch('testgres.node.get_pg_version', new=patched_get_pg_version):
149
+ node = cls.postgres_node = testgres.get_new_node()
150
+
151
+ # init takes about about 2 -- 3 seconds
152
+ node.init(
153
+ # Use the encoding as UTF-8. Set the locale as POSIX
154
+ # instead of inheriting it (in JAM's environment, the locale
155
+ # and thus collation and ctype is en_US.UTF8; this turns out to be
156
+ # up to 40% slower than POSIX).
157
+ # We could explicitly specify 'en-x-icu' on each column, if we required
158
+ # ICU support, but it cannot be used as a default collation.
159
+ initdb_params=[
160
+ "-E", "UTF8",
161
+ '--locale', 'POSIX',
162
+ # Don't force to disk; this may save some minor init time.
163
+ '--no-sync',
164
+ ],
165
+ log_statement='none',
166
+ # Disable unix sockets. Some platforms might try to put this
167
+ # in a directory we can't write to
168
+ unix_sockets=False
169
+ )
170
+ # Speed up bulk inserts
171
+ # These settings appeared to make no difference for the
172
+ # 2 million security insert or the 500K security mapping insert;
173
+ # likely because the final table sizes are < 300MB, so the default max size of
174
+ # 1GB is more than enough.
175
+ node.append_conf('fsync = off')
176
+ node.append_conf('full_page_writes = off')
177
+ node.append_conf('min_wal_size = 500MB')
178
+ node.append_conf('max_wal_size = 2GB')
179
+ # 'replica' is the default. If we use 'minimal' we could be
180
+ # a bit faster, but that's not exactly realistic. Plus,
181
+ # using 'minimal' disables the WAL backup functionality.
182
+ # If we set to 'minimal', we must also set 'max_wal_senders' to 0.
183
+ node.append_conf('wal_level = replica')
184
+ node.append_conf('wal_compression = on')
185
+ node.append_conf('wal_writer_delay = 10000ms')
186
+ node.append_conf('wal_writer_flush_after = 10MB')
187
+
188
+ node.append_conf('temp_buffers = 500MB')
189
+ node.append_conf('work_mem = 500MB')
190
+ node.append_conf('maintenance_work_mem = 500MB')
191
+ node.append_conf('shared_buffers = 500MB')
192
+
193
+ node.append_conf('max_connections = 100')
194
+
195
+ # auto-explain for slow queries
196
+ if 'benchmark' in ' '.join(sys.argv):
197
+ print("Enabling BENCHMARK SETTINGS")
198
+ node.append_conf('shared_preload_libraries = auto_explain')
199
+ node.append_conf('auto_explain.log_min_duration = 40ms')
200
+ node.append_conf('auto_explain.log_nested_statements = on')
201
+ node.append_conf('auto_explain.log_analyze = on')
202
+ node.append_conf('auto_explain.log_timing = on')
203
+ node.append_conf('auto_explain.log_triggers = on')
204
+
205
+ # PG 11 only, when --with-llvm was used to compile.
206
+ # It seems if it can't be used, it's ignored? It errors on 10 though,
207
+ # but we only support 11
208
+ node.append_conf('jit = on')
209
+
210
+
211
+ node.start()
212
+ cls.connection_pool = cls.connection_pool_klass(
213
+ cls.connection_pool_minconn,
214
+ cls.connection_pool_maxconn,
215
+ dbname=cls.DATABASE_NAME,
216
+ host='localhost',
217
+ port=cls.postgres_node.port,
218
+ cursor_factory=DictCursor,
219
+ )
220
+
221
+ cls.postgres_dsn = "host=%s dbname=%s port=%s" % (
222
+ node.host, cls.DATABASE_NAME, node.port
223
+ )
224
+ cls.postgres_uri = "postgresql://%s:%s/%s" % (
225
+ cls.postgres_node.host,
226
+ cls.postgres_node.port,
227
+ cls.DATABASE_NAME
228
+ )
229
+
230
+ with cls.borrowed_connection() as conn:
231
+ with conn.cursor() as cur:
232
+ i = cls.__get_db_info(cur)
233
+ print(f"({i['version']} {i['current_database']}/{i['current_schema']} "
234
+ f"{i['Encoding']}-{i['Collate']}) ", end="")
235
+
236
+ @classmethod
237
+ def tearDown(cls):
238
+ cls.connection_pool.closeall()
239
+ cls.connection_pool = None
240
+
241
+ cls.postgres_node.__exit__(None, None, None)
242
+ cls.postgres_node = None
243
+
244
+ @classmethod
245
+ def testSetUp(cls):
246
+ # XXX: Errors here cause the tearDown method to not get called.
247
+ cls.connection = cls.connection_pool.getconn()
248
+ cls.cursor = cls.connection.cursor()
249
+
250
+ @classmethod
251
+ def testTearDown(cls):
252
+ cls.connection.rollback() # Make sure we're able to execute
253
+ cls.cursor.execute('UNLISTEN *')
254
+ cls.cursor.close()
255
+ cls.cursor = None
256
+ cls.connection_pool.putconn(cls.connection)
257
+ cls.connection = None
258
+
259
+ @classmethod
260
+ def __get_db_info(cls, cur):
261
+ query = """
262
+ SELECT version() as version,
263
+ d.datname as "Name",
264
+ pg_catalog.pg_get_userbyid(d.datdba) as "Owner",
265
+ pg_catalog.pg_encoding_to_char(d.encoding) as "Encoding",
266
+ d.datcollate as "Collate",
267
+ d.datctype as "Ctype",
268
+ pg_catalog.array_to_string(d.datacl, E'\n') AS "Access privileges",
269
+ current_database() as "current_database",
270
+ current_schema() as "current_schema"
271
+ FROM pg_catalog.pg_database d
272
+ WHERE d.datname = %s
273
+ """
274
+ cur.execute(query, (cls.DATABASE_NAME,))
275
+ row = cur.fetchone()
276
+ return dict(row)
277
+
278
+ @classmethod
279
+ @contextmanager
280
+ def borrowed_connection(cls):
281
+ """
282
+ Context manager that returns a connection from the connection
283
+ pool.
284
+ """
285
+ conn = cls.connection_pool.getconn()
286
+ try:
287
+ yield conn
288
+ finally:
289
+ cls.connection_pool.putconn(conn)
290
+
291
+
292
+ @classmethod
293
+ def truncate_table(cls, conn, table_name):
294
+ """Transactionally truncate the given *table_name* using *conn*"""
295
+ try:
296
+
297
+ with conn.cursor() as cur:
298
+ cur.execute(
299
+ 'TRUNCATE TABLE ' + table_name + ' CASCADE'
300
+ )
301
+ except (ProgrammingError, InternalError):
302
+ # Table doesn't exist, not a full schema,
303
+ # ignore.
304
+ # OR:
305
+ # Already aborted
306
+ import traceback
307
+ traceback.print_exc()
308
+ conn.rollback()
309
+ else:
310
+ # Is PostgreSQL, TRUNCATE is transactional!
311
+ # Awesome!
312
+ conn.commit()
313
+
314
+ @classmethod
315
+ def drop_relation(cls, relation, kind='TABLE', idempotent=False):
316
+ """Drops the *relation* of type *kind* (default table), in new transaction."""
317
+ with cls.borrowed_connection() as conn:
318
+ with conn.cursor() as cur:
319
+ if idempotent:
320
+ cur.execute(f"DROP {kind} IF EXISTS {relation}")
321
+ else:
322
+ cur.execute(f"DROP {kind} {relation}")
323
+ conn.commit()
324
+
325
+ @classmethod
326
+ def vacuum(cls, *tables, **kwargs):
327
+ verbose = ''
328
+ if kwargs.pop('verbose', False):
329
+ verbose = ', VERBOSE'
330
+
331
+ with cls.borrowed_connection() as conn:
332
+ conn.autocommit = True
333
+ # FULL rewrites all tables and takes forever.
334
+ # FREEZE is simpler and compacts tables
335
+ stmt = f'VACUUM (FREEZE, ANALYZE {verbose}) '
336
+ tables = tables or ('',)
337
+ with conn.cursor() as cur:
338
+ # VACUUM cannot run inside a transaction block...
339
+ for t in tables:
340
+ cur.execute(stmt + t)
341
+ conn.autocommit = False
342
+ if verbose:
343
+ for n in conn.notices:
344
+ print(n)
345
+ del conn.notices[:]
346
+ if kwargs.pop('size_report', True):
347
+ cls.print_size_report()
348
+
349
+ ONLY_PRINT_SIZE_OF_TABLES = None
350
+
351
+ @classmethod
352
+ def print_size_report(cls):
353
+ extra_query = ''
354
+ if cls.ONLY_PRINT_SIZE_OF_TABLES:
355
+ t = cls.ONLY_PRINT_SIZE_OF_TABLES
356
+ extra_query = f"AND table_name = '{t}'"
357
+ query = f"""
358
+ SELECT table_name,
359
+ pg_size_pretty(total_bytes) AS total,
360
+ pg_size_pretty(index_bytes) AS INDEX,
361
+ pg_size_pretty(toast_bytes) AS toast,
362
+ pg_size_pretty(table_bytes) AS TABLE
363
+ FROM (
364
+ SELECT *, total_bytes-index_bytes-COALESCE(toast_bytes,0) AS table_bytes FROM (
365
+ SELECT c.oid,nspname AS table_schema, relname AS TABLE_NAME
366
+ , c.reltuples AS row_estimate
367
+ , pg_total_relation_size(c.oid) AS total_bytes
368
+ , pg_indexes_size(c.oid) AS index_bytes
369
+ , pg_total_relation_size(reltoastrelid) AS toast_bytes
370
+ FROM pg_class c
371
+ LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
372
+ WHERE relkind = 'r'
373
+ ) a
374
+ ) a
375
+ WHERE (table_name NOT LIKE 'pg_%' and table_name not like 'abstract_%'
376
+ {extra_query}
377
+ )
378
+ AND table_schema <> 'pg_catalog' and table_schema <> 'information_schema'
379
+ ORDER BY total_bytes DESC
380
+ """
381
+
382
+ with cls.borrowed_connection() as conn:
383
+ with conn.cursor() as cur:
384
+ cur.execute(query)
385
+
386
+ rows = [dict(row) for row in cur]
387
+
388
+ keys = ['table_name', 'total', 'index', 'toast', 'table']
389
+
390
+ rows.insert(0, {k: k for k in keys})
391
+ print()
392
+ fmt = "| {table_name:35s} | {total:10s} | {index:10s} | {toast:10s} | {table:10s}"
393
+ for row in rows:
394
+ if not extra_query and row['total'] in {
395
+ '72 kB', '32 kB', '24 kB', '16 kB', '8192 bytes'
396
+ }:
397
+ continue
398
+ print(fmt.format(
399
+ **{k: v if v else '<null>' for k, v in row.items()}
400
+ ))
401
+
402
+
403
+ class SchemaDatabaseLayer(DatabaseLayer):
404
+ """
405
+ A test layer that adds our schema.
406
+ """
407
+
408
+ SCHEMA_FILE = os.path.abspath(os.path.join(
409
+ os.path.dirname(os.path.abspath(__file__)),
410
+ '..', '..', '..', '..', '..',
411
+ 'full_schema.sql'
412
+ ))
413
+
414
+ @classmethod
415
+ def run_files(cls, *files):
416
+ for fname in files:
417
+ code, stdout, stderr = cls.postgres_node.psql(
418
+ filename=fname,
419
+ ON_ERROR_STOP=1
420
+ )
421
+ if code:
422
+ break
423
+
424
+ if code:
425
+ import subprocess
426
+ stdout = stdout.decode("utf-8")
427
+ stderr = stderr.decode('utf-8')
428
+ print(stdout)
429
+ print(stderr)
430
+ raise subprocess.CalledProcessError(
431
+ code,
432
+ 'psql',
433
+ stdout,
434
+ stderr
435
+ )
436
+
437
+ @classmethod
438
+ def _tangle_schema_if_needed(cls):
439
+ # If the schema files do not exist, or db.org is newer
440
+ # than they are, run emacs to weave the files together.
441
+ # This requires a working emacs with org-mode available.
442
+ from pathlib import Path
443
+ import subprocess
444
+
445
+ cwd = Path(".")
446
+ # Each org file tangles to at least one sql file.
447
+ org_to_sql = {
448
+ org: org.with_suffix('.sql')
449
+ for org in cwd.glob("*.org")
450
+ }
451
+ org_to_sql[Path("db.org")] = Path("full_schema.sql")
452
+
453
+ for org, sql in org_to_sql.items():
454
+ if not org.exists():
455
+ continue
456
+ if sql.exists() and sql.stat().st_mtime >= org.stat().st_mtime:
457
+ continue
458
+ print(f"\nDatabase schema files outdated; tangling {org}")
459
+ ex = None
460
+ try:
461
+ output = subprocess.check_output([
462
+ "emacs",
463
+ "--batch",
464
+ "--eval",
465
+ f'''(progn
466
+ (package-initialize)
467
+ (require 'org)
468
+ (org-babel-tangle-file "{org}")
469
+ )'''
470
+ ], stderr=subprocess.STDOUT)
471
+ except FileNotFoundError as e:
472
+ output = str(e).encode('utf-8')
473
+ ex = e
474
+ except subprocess.CalledProcessError as e:
475
+ output = ex.output
476
+ ex = e # pylint:disable=redefined-variable-type
477
+
478
+ output = output.decode('utf-8')
479
+
480
+ if ex is not None or 'Tangled 0' in output:
481
+ print("Failed to tangle database schema; "
482
+ "(check file paths):\n",
483
+ output,
484
+ file=sys.stderr)
485
+ sys.exit(1)
486
+
487
+ @classmethod
488
+ def setUp(cls):
489
+ cwd = os.getcwd()
490
+ try:
491
+ os.chdir(os.path.dirname(cls.SCHEMA_FILE))
492
+ # XXX: Do exceptions here prevent the super tearDown()
493
+ # from being called?
494
+ cls._tangle_schema_if_needed()
495
+
496
+ to_run = [cls.SCHEMA_FILE]
497
+ if os.path.exists("prereq.sql"):
498
+ to_run.insert(0, "prereq.sql")
499
+ cls.run_files(*to_run)
500
+ finally:
501
+ os.chdir(cwd)
502
+
503
+ @classmethod
504
+ def tearDown(cls):
505
+ pass
506
+
507
+ @classmethod
508
+ def testSetUp(cls):
509
+ pass
510
+
511
+ @classmethod
512
+ def testTearDown(cls):
513
+ pass
514
+
515
+ class DatabaseBackupLayerHelper:
516
+ """
517
+ A layer helper that works with another layer to
518
+
519
+ * create a backup of the current database on `push`;
520
+ * make that backup active;
521
+ * switch the connection pool to that backup
522
+ * reverse all of that on layer `pop`
523
+
524
+ Note that this consists of modifying values in the `DatabaseLayer`,
525
+ so the *layer* parameter must extend that.
526
+ """
527
+
528
+ _nodes = []
529
+ _pools = []
530
+
531
+ @classmethod
532
+ def push(cls, layer):
533
+ current_node = DatabaseLayer.postgres_node
534
+ cls._nodes.append(current_node)
535
+ cls._pools.append(DatabaseLayer.connection_pool)
536
+
537
+ with layer.borrowed_connection() as conn:
538
+ with conn.cursor() as cur:
539
+ # If we don't checkpoint here, then the backup waits
540
+ # for the next WAL checkpoint to happen. We may not have
541
+ # written much to the WAL, so we could wait until a time limit
542
+ # expires, which is ofter 30+ seconds. We don't want to wait.
543
+ cur.execute('CHECKPOINT')
544
+
545
+ # A streaming backup uses a replication slot, but it
546
+ # does the copy in parallel.
547
+ backup = current_node.backup(xlog_method='stream')
548
+ DatabaseLayer.postgres_node = new_node = backup.spawn_primary()
549
+ new_node.start()
550
+ DatabaseLayer.connection_pool = layer.connection_pool_klass(
551
+ layer.connection_pool_minconn,
552
+ layer.connection_pool_maxconn,
553
+ dbname=layer.DATABASE_NAME,
554
+ host='localhost',
555
+ port=new_node.port,
556
+ cursor_factory=DictCursor
557
+ )
558
+
559
+ @classmethod
560
+ def pop(cls, layer): # pylint:disable=unused-argument
561
+ DatabaseLayer.tearDown() # Closes the current node, and the connection pool
562
+ DatabaseLayer.postgres_node = cls._nodes.pop()
563
+ DatabaseLayer.connection_pool = cls._pools.pop()
564
+
565
+
566
+ _persistent_base = (
567
+ # If we're loading a file, it has the schema
568
+ # info.
569
+
570
+ SchemaDatabaseLayer
571
+ if not LOAD_DATABASE_ON_SETUP
572
+ else DatabaseLayer
573
+ )
574
+
575
+ class PersistentDatabaseLayer(_persistent_base):
576
+ """
577
+ A layer that establishes persistent data visible to
578
+ all of its tests (and all of its sub-layers).
579
+
580
+ Sub-layers need to check whether they should
581
+ clean up or not, because we may be saving the database file.
582
+
583
+ It's important to have a fairly linear layer
584
+ setup, or layers that don't interfere with each other.
585
+ """
586
+
587
+ @classmethod
588
+ def setUp(cls):
589
+ if LOAD_DATABASE_ON_SETUP:
590
+ print(f" (Loading database from {LOAD_DATABASE_ON_SETUP}) ",
591
+ end='',
592
+ flush=True)
593
+ cls.postgres_node.restore(LOAD_DATABASE_ON_SETUP)
594
+ cls.vacuum()
595
+
596
+ @classmethod
597
+ def testSetUp(cls):
598
+ pass
599
+
600
+ @classmethod
601
+ def testTearDown(cls):
602
+ pass
603
+
604
+ @classmethod
605
+ def persistent_layer_skip_teardown(cls):
606
+ """
607
+ Should persistent layers, that write data intended to be
608
+ visible between tests (and in sub-layers) tear down that data
609
+ when the layer is torn down? If we're saving the database, we
610
+ don't want to do that.
611
+
612
+ Raising NotImplementedError causes the testrunner to assume
613
+ it's python resources that are the problem and continue in a new
614
+ subprocess, which doesn't help (and may hurt?). So you must check this as a
615
+ boolean.
616
+ """
617
+ return SAVE_DATABASE_ON_TEARDOWN
618
+
619
+ @classmethod
620
+ def persistent_layer_skip_setup(cls):
621
+ """
622
+ Should persistent layers skip their setup because
623
+ we loaded a save file?
624
+ """
625
+ return LOAD_DATABASE_ON_SETUP
626
+
627
+ @classmethod
628
+ def tearDown(cls):
629
+ if SAVE_DATABASE_ON_TEARDOWN:
630
+ tmp_fname = cls.postgres_node.dump(format='custom')
631
+ result_fname = tmp_fname
632
+ if SAVE_DATABASE_FILENAME:
633
+ import shutil
634
+ result_fname = SAVE_DATABASE_FILENAME
635
+ while os.path.exists(result_fname):
636
+ result_fname += '.1'
637
+ shutil.move(tmp_fname, result_fname)
638
+ print(f" (Database dumped to {result_fname}) ", end='')
639
+
640
+ def persistent_skip_setup(func):
641
+
642
+ @functools.wraps(func)
643
+ def maybe_skip_setup(cls):
644
+ if cls.persistent_layer_skip_setup():
645
+ return
646
+ func(cls)
647
+ return maybe_skip_setup
648
+
649
+ def persistent_skip_teardown(func):
650
+ @functools.wraps(func)
651
+ def f(cls):
652
+ if cls.persistent_layer_skip_teardown():
653
+ return
654
+ func(cls)
655
+ return f
656
+
657
+ class DatabaseTestCase(unittest.TestCase):
658
+ """
659
+ A helper test base containing some functions useful for both
660
+ benchmarking and unit testing.
661
+ """
662
+ # pylint:disable=no-member
663
+
664
+ @contextmanager
665
+ def assertRaisesIntegrityError(self, match=None):
666
+ if match:
667
+ with self.assertRaisesRegex(IntegrityError, match) as exc:
668
+ yield exc
669
+ else:
670
+ with self.assertRaises(IntegrityError) as exc:
671
+ yield exc
672
+
673
+ # We can't do any queries after an error is raised
674
+ # until we rollback.
675
+ self.layer.connection.rollback()
676
+ return exc
677
+
678
+ def assert_row_count_in_query(self, expected_count, query):
679
+ cur = self.layer.cursor
680
+
681
+ cur.execute('SELECT COUNT(*) FROM ' + query)
682
+ row = cur.fetchone()
683
+ count = row[0]
684
+
685
+ self.assertEqual(expected_count, count, query)
686
+
687
+ def assert_row_count_in_table(self, expected_count, table_name):
688
+ __traceback_info__ = table_name
689
+ self.assert_row_count_in_query(expected_count, table_name)
690
+
691
+ def assert_row_count_in_cursor(self, rowcount, cursor=None):
692
+ cur = cursor if cursor is not None else self.layer.cursor
693
+ self.assertEqual(cur.rowcount, rowcount)
File without changes
@@ -0,0 +1,17 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Tests for postgres.py
4
+
5
+ """
6
+
7
+ import unittest
8
+
9
+
10
+ class TestBasic(unittest.TestCase):
11
+
12
+ def test_imports(self):
13
+ from .. import postgres
14
+ self.assertIsNotNone(postgres)
15
+
16
+ if __name__ == '__main__':
17
+ unittest.main()