diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index f693282a..73e57a47 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -153,6 +153,8 @@ jobs: sudo service postgresql restart; sleep 10 # set up postgresql database sudo -u postgres psql -c "CREATE ROLE rounduptest WITH CREATEDB LOGIN PASSWORD 'rounduptest';" -U postgres + sudo -u postgres psql -c "CREATE ROLE rounduptest_schema LOGIN PASSWORD 'rounduptest';" -U postgres + sudo -u postgres psql -c "CREATE DATABASE rounduptest_schema;GRANT CREATE ON DATABASE rounduptest_schema TO rounduptest_schema;" -U postgres - name: install redis run: | diff --git a/.travis.yml b/.travis.yml index f3116772..8ed95e87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -155,6 +155,8 @@ before_script: - sudo service postgresql restart; sleep 30 # set up postgresql database - psql -c "CREATE ROLE rounduptest WITH CREATEDB LOGIN PASSWORD 'rounduptest';" -U postgres + - psql -c "CREATE ROLE rounduptest_schema LOGIN PASSWORD 'rounduptest';" -U postgres + - psql -c "CREATE DATABASE rounduptest_schema;GRANT CREATE ON DATABASE rounduptest_schema TO rounduptest_schema;" -U postgres # build the .mo translation files and install them into a tree # (locale/locale under roundup directory root) diff --git a/CHANGES.txt b/CHANGES.txt index 8b25f732..0023e27d 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -104,6 +104,9 @@ Features: configuring database on a per-tracker basis. Also replaces use of PGSERVICE env variable for single instance trackers. (From ML question by ivanov. John Rouillard) +- issue2550852 - support for specifying a PostgreSQL schema to use for + the Roundup database. (Patch by Stuart McGraw; slight modifications, + tests, docs: John Rouillard). 2023-07-13 2.3.0 diff --git a/doc/installation.txt b/doc/installation.txt index c0679c25..61f8622f 100644 --- a/doc/installation.txt +++ b/doc/installation.txt @@ -478,9 +478,10 @@ in the following steps. Admin Password: Confirm: - Note: running this command will *destroy any existing data in the - database*. In the case of MySQL and PostgreSQL, any existing database - will be dropped and re-created. + Note: running this command will *destroy any existing data in + the database*. In the case of MySQL and PostgreSQL, any existing + database (or optionally database schema for PostgreSQL) will be + dropped and re-created. Once this is done, the tracker has been created. See the note in the `administration guide`_ on how to :ref:`initialise a diff --git a/doc/mysql.txt b/doc/mysql.txt index c0ce0131..71edee8c 100644 --- a/doc/mysql.txt +++ b/doc/mysql.txt @@ -21,6 +21,32 @@ to install: 2. Python MySQL interface - https://pypi.org/project/mysqlclient/ +Preparing the Database +====================== + +The Roundup user expects to be able to create and drop its database +when using ``roundup_admin init``. + +In the examples below, replace ``roundupuser``, ``rounduppw`` and +``roundupdb`` with suitable values. + +This assumes you are running MySQL on the same host as you are +running Roundup. If this is not the case, setting up remote +credentials, SSL/TLS etc. is beyond the scope of this documentation. +However examples are welcome on the wiki or mailing list. + +These references may be helpful: +https://dev.mysql.com/doc/refman/8.0/en/create-user.html and +https://dev.mysql.com/doc/refman/8.0/en/grant.html. + +Creating a Role/User +-------------------- + +The following command will create a ``roundupuser`` with the ability +to create the database:: + + mysql -u root -e 'CREATE USER "roundupuser"@"localhost" IDENTIFIED WITH mysql_native_password BY "rounduppw"; GRANT ALL on roundupuser.* TO "roundupuser"@"localhost";' + Other Configuration =================== diff --git a/doc/postgresql.txt b/doc/postgresql.txt index 36510415..126cdb7c 100644 --- a/doc/postgresql.txt +++ b/doc/postgresql.txt @@ -27,30 +27,95 @@ suggest that you install into a python virtual environment. .. _PostgreSQL: https://www.postgresql.org/ +Preparing the Database +====================== + +Roundup can use Postgres in one of two ways: + + 1. Roundup creates and uses a database + 2. Roundup uses a pre-created database and creates and uses a schema + under the database. + +In the examples below, replace ``roundupuser``, ``rounduppw`` and +``roundupdb`` with suitable values. + +This assumes that you are running Postgres on the same machine with +Roundup. Using a remote database, setting up SSL/TLS and other +authentication methods is beyond the scope of this +documentation. However examples are welcome on the wiki or mailing +list. + +Creating a Role/User +-------------------- + +For case 1 create a user using:: + + psql -c "CREATE ROLE roundupuser WITH CREATEDB LOGIN PASSWORD 'rounduppw';" -U postgres + +After running ``roundup-admin init`` to create your databases, you can +remove the CREATEDB permission using:: + + psql -c "ALTER ROLE roundupuser NOCREATEDB;" + +If needed (e.g. you want to deploy a new tracker) you can use ``ALTER +ROLE`` with ``CREATEDB`` to add the permission back. + +For case 2 you need to create the user:: + + psql -c "CREATE ROLE roundupuser LOGIN PASSWORD 'rounduppw';" -U postgres + +This psql command connects as the postgres database superuser. You may +need to run this under sudo as the postgres user or provide a password +to become an admin on the postgres db process. + + +Creating a Database +------------------- + +For case 1, roundup will create the database on demand using the +``roundup_admin init`` command. So there is nothing to do here. + +For case 2, run:: + + psql -c "CREATE DATABASE roundupdb;GRANT CREATE ON DATABASE roundupdb TO roundupuser;" -U postgres + +This creates the database and allows the roundup user to create a new +schema when running ``roundup_admin init``. + + Running the PostgreSQL unit tests ================================= The user that you're running the tests as will need to be able to access the postgresql database on the local machine and create and drop -databases. See the config values in 'test/db_test_base.py' +databases and schemas. See the config values in 'test/db_test_base.py' about which database connection, name and user will be used. -At this time the following command will setup the user:: +At this time the following commands will setup the users and required +databases:: sudo -u postgres psql -c "CREATE ROLE rounduptest WITH CREATEDB LOGIN PASSWORD 'rounduptest';" -U postgres -Note ``rounduptest`` is a well known account, so you should -remove/disable the account after testing and set up a suitable -production account. You need to remove any database owned by -``rounduptest`` first. So something like this should work:: + sudo -u postgres psql -c "CREATE ROLE rounduptest_schema LOGIN PASSWORD 'rounduptest';" -U postgres + sudo -u postgres psql -c "CREATE DATABASE rounduptest_schema;GRANT CREATE ON DATABASE rounduptest_schema TO rounduptest_schema;" -U postgres + +Note ``rounduptest`` and ``rounduptest_schema`` are well known +accounts, so you should remove/disable the accounts after testing and +set up a suitable production account. You need to remove any database +owned by ``rounduptest`` first. To clean everything up, something like +this should work:: sudo -u postgres psql -c "DROP DATABASE rounduptest;" -U postgres sudo -u postgres psql -c "DROP ROLE rounduptest;" -U postgres + sudo -u postgres psql -c "DROP DATABASE rounduptest_schema;" -U postgres + sudo -u postgres psql -c "DROP ROLE rounduptest_schema;" -U postgres If the ``rounduptest`` database is left in a broken state (e.g. because of a crash during testing) dropping the database and -restarting the tests should fix it. +restarting the tests should fix it. If you have issues while running +the schema test, you can drop the ``rounduptest` schema in the +``rounduptest_schema`` database. Credit ====== diff --git a/doc/reference.txt b/doc/reference.txt index 7e10d8b7..d8cb44e1 100644 --- a/doc/reference.txt +++ b/doc/reference.txt @@ -277,7 +277,9 @@ Section **rdbms** The database backend such as anydbm, sqlite, mysql or postgres. name -- ``roundup`` - Name of the database to use. + Name of the database to use. For Postgresql, this can + be database.schema to use a specific schema within + a Postgres database. host -- ``localhost`` Database server host. diff --git a/doc/upgrading.txt b/doc/upgrading.txt index adf6fa6a..f3d907d1 100644 --- a/doc/upgrading.txt +++ b/doc/upgrading.txt @@ -153,6 +153,39 @@ The removed columns are: severity, versions, keywords, dependencies. It is also missing the ``solves`` field which is added to match the schema. +New PostgreSQL Settings (optional) +---------------------------------- + +With this release, you can specify a Postgresql database +schema to use. By default Roundup creates a database when +using ``roundup-admin init``. Setting the rdbms ``name`` +keyword to ``roundup_database.roundup_schema`` will create +and use the ``roundup_schema`` in the pre-created +``roundup_database``. + +Also there is a new configuration keyword in the rdbms section of +``config.ini``. The ``service`` keyword allows you to define the +service name for Postgres that will be looked up in the Postgres +`Connection Service File`_. Setting service to `roundup` with the +following in the service file:: + + [roundup_roundup] + host=127.0.0.1 + port=5432 + user=roundup + password=roundup + dbname=roundup + +would use the roundup database with the specified credentials. + +It is possible to define a service that connects to a specific +schema. However this will require a little fiddling to get things +working. A future enhancement may make using a schema via this +mechanism easier. See https://issues.roundup-tracker.org/issue2551299 +for details. + +.. _`Connection Service File`: https://www.postgresql.org/docs/current/libpq-pgservice.html + Bad Login Rate Limiting and Locking (info) ------------------------------------------ diff --git a/roundup/backends/back_postgresql.py b/roundup/backends/back_postgresql.py index e2678b3a..56f314b5 100644 --- a/roundup/backends/back_postgresql.py +++ b/roundup/backends/back_postgresql.py @@ -9,6 +9,7 @@ import logging import os +import re import shutil import time @@ -47,28 +48,40 @@ def connection_dict(config, dbnamestr=None): del d['read_default_file'] return d +def db_schema_split(database_name): + ''' Split database_name into database and schema parts''' + if '.' in database_name: + return database_name.split ('.') + return [database_name, ''] def db_create(config): """Clear all database contents and drop database itself""" - command = ("CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'" % - get_database_name(config)) - if config.RDBMS_TEMPLATE: - command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE - logging.getLogger('roundup.hyperdb').info(command) - db_command(config, command) - + db_name, schema_name = db_schema_split(config.RDBMS_NAME) + if not schema_name: + command = "CREATE DATABASE \"%s\" WITH ENCODING='UNICODE'" % db_name + if config.RDBMS_TEMPLATE: + command = command + " TEMPLATE=%s" % config.RDBMS_TEMPLATE + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command) + else: + command = "CREATE SCHEMA \"%s\" AUTHORIZATION \"%s\"" % (schema_name, config.RDBMS_USER) + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command, db_name) def db_nuke(config): - """Clear all database contents and drop database itself""" - command = 'DROP DATABASE "%s"' % get_database_name(config) - - logging.getLogger('roundup.hyperdb').info(command) - db_command(config, command) - + """Drop the database (and all its contents) or the schema.""" + db_name, schema_name = db_schema_split(config.RDBMS_NAME) + if not schema_name: + command = 'DROP DATABASE "%s"'% db_name + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command) + else: + command = 'DROP SCHEMA "%s" CASCADE' % schema_name + logging.getLogger('roundup.hyperdb').info(command) + db_command(config, command, db_name) if os.path.exists(config.DATABASE): shutil.rmtree(config.DATABASE) - def get_database_name(config): '''Get database name using config.RDBMS_NAME or config.RDBMS_SERVICE. @@ -124,14 +137,16 @@ def db_command(config, command, database='postgres'): before "template1" seems to have been used, so we fall back to it. Compare to issue2550543. ''' - template1 = connection_dict(config) + template1 = connection_dict(config, 'database') + db_name, schema_name = db_schema_split(template1['database']) template1['database'] = database try: conn = psycopg2.connect(**template1) except psycopg2.OperationalError as message: - if str(message).find('database "postgres" does not exist') >= 0: - return db_command(config, command, database='template1') + if not schema_name: + if re.search(r'database ".+" does not exist', str(message)): + return db_command(config, command, database='template1') raise hyperdb.DatabaseError(message) conn.set_isolation_level(0) @@ -142,17 +157,17 @@ def db_command(config, command, database='postgres'): return finally: conn.close() - raise RuntimeError('10 attempts to create database failed when running: %s' % command) + raise RuntimeError('10 attempts to create database or schema failed when running: %s' % command) -def pg_command(cursor, command): +def pg_command(cursor, command, args=()): '''Execute the postgresql command, which may be blocked by some other user connecting to the database, and return a true value if it succeeds. If there is a concurrent update, retry the command. ''' try: - cursor.execute(command) + cursor.execute(command, args) except psycopg2.DatabaseError as err: response = str(err).split('\n')[0] if "FATAL" not in response: @@ -164,19 +179,32 @@ def pg_command(cursor, command): if msg in response: time.sleep(0.1) return 0 - raise RuntimeError(response) + raise RuntimeError(response, command, args) return 1 def db_exists(config): - """Check if database already exists""" + """Check if database or schema already exists""" db = connection_dict(config, 'database') + db_name, schema_name = db_schema_split(db['database']) + if schema_name: + db['database'] = db_name try: conn = psycopg2.connect(**db) - conn.close() - return 1 + if not schema_name: + conn.close() + return 1 except Exception: return 0 + # will have a non-false value here; otherwise one + # of the above returns would have returned. + # Get a count of the number of schemas named (either 0 or 1). + command = "SELECT COUNT(*) FROM information_schema.schemata WHERE schema_name = %s" + cursor = conn.cursor() + pg_command(cursor, command, (schema_name,)) + count = cursor.fetchall()[0][0] + conn.close() + return count # 'count' will be 0 or 1. class Sessions(sessions_rdbms.Sessions): @@ -225,6 +253,10 @@ class Database(rdbms_common.Database): def sql_open_connection(self): db = connection_dict(self.config, 'database') + db_name, schema_name = db_schema_split (db['database']) + if schema_name: + db['database'] = db_name + # database option always present: log it if not null if db['database']: logging.getLogger('roundup.hyperdb').info( @@ -242,6 +274,11 @@ def sql_open_connection(self): lvl = isolation_levels[self.config.RDBMS_ISOLATION_LEVEL] conn.set_isolation_level(lvl) + if schema_name: + self.sql ('SET search_path TO %s' % schema_name, cursor=cursor) + # Commit is required so that a subsequent rollback + # will not also rollback the search_path change. + self.sql ('COMMIT', cursor=cursor) return (conn, cursor) def sql_new_cursor(self, name='default', conn=None, *args, **kw): diff --git a/roundup/configuration.py b/roundup/configuration.py index 57c9561f..8a047159 100644 --- a/roundup/configuration.py +++ b/roundup/configuration.py @@ -1435,7 +1435,9 @@ def str2value(self, value): (DatabaseBackend, 'backend', NODEFAULT, "Database backend."), (Option, 'name', 'roundup', - "Name of the database to use.", + "Name of the database to use. For Postgresql, this can\n" + "be database.schema to use a specific schema within\n" + "a Postgres database.", ['MYSQL_DBNAME']), (NullableOption, 'host', 'localhost', "Database server host.", diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 00000000..d3aa5a53 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,14 @@ +# simple way to see if there are order dependencies in tests +# can use if pytest-random-order --random-order mode isn't +# usable (e.g. python2). + +#def pytest_collection_modifyitems(items): +# items.reverse() + +# Add a marker for pg_schema tests. +# They duplicate the postgresql tests exactly but uses a named +# schema rather than the default 'public' schema. +def pytest_configure(config): + config.addinivalue_line( + "markers", "pg_schema: tests using schema for postgres" + ) diff --git a/test/test_postgresql.py b/test/test_postgresql.py index 44827aa0..46bc3d69 100644 --- a/test/test_postgresql.py +++ b/test/test_postgresql.py @@ -20,6 +20,7 @@ import pytest from roundup.hyperdb import DatabaseError from roundup.backends import get_backend, have_backend +from roundup.backends.back_postgresql import db_command, db_schema_split from .db_test_base import DBTest, ROTest, config, SchemaTest, ClassicInitTest from .db_test_base import ConcurrentDBTest, HTMLItemTest, FilterCacheTest @@ -52,10 +53,15 @@ def setup_class(cls): # where an aborted test run (^C during setUp for example) # leaves the database in an unusable, partly configured state. try: - cls.nuke_database(cls) - except: - # ignore failure to nuke the database. - pass + cls.nuke_database() + except Exception as m: + # ignore failure to nuke the database if it doesn't exist. + # otherwise abort + if str(m.args[0]) == ( + 'database "%s" does not exist' % config.RDBMS_NAME): + pass + else: + raise def setUp(self): pass @@ -63,32 +69,95 @@ def setUp(self): def tearDown(self): self.nuke_database() + @classmethod def nuke_database(self): # clear out the database - easiest way is to nuke and re-create it self.module.db_nuke(config) +@skip_postgresql +class postgresqlSchemaOpener: + + RDBMS_NAME="rounduptest_schema.rounduptest" + RDBMS_USER="rounduptest_schema" + + if have_backend('postgresql'): + module = get_backend('postgresql') + + def setup_class(cls): + # nuke the schema for the class. Handles the case + # where an aborted test run (^C during setUp for example) + # leaves the database in an unusable, partly configured state. + config.RDBMS_NAME=cls.RDBMS_NAME + config.RDBMS_USER=cls.RDBMS_USER + + database, schema = db_schema_split(config.RDBMS_NAME) + + try: + cls.nuke_database() + except Exception as m: + # ignore failure to nuke the database if it doesn't exist. + # otherwise abort + if str(m.args[0]) == ( + 'schema "%s" does not exist' % schema): + pass + else: + raise + + def setUp(self): + # make sure to override the rdbms settings. + # before every test. + config.RDBMS_NAME=self.RDBMS_NAME + config.RDBMS_USER=self.RDBMS_USER + + def tearDown(self): + self.nuke_database() + config.RDBMS_NAME="rounduptest" + config.RDBMS_USER="rounduptest" + + @classmethod + def nuke_database(self): + # clear out the database - easiest way is to nuke and re-create it + self.module.db_nuke(config) @skip_postgresql -class postgresqlDBTest(postgresqlOpener, DBTest, unittest.TestCase): +class postgresqlPrecreatedSchemaDbOpener: + """Open the db where the user has only schema create rights. + The test tries to nuke the db and should result in an exception. + + RDBMS_NAME should not have the .schema on it as we want to + operate on the db itself with db_nuke. + """ + + RDBMS_NAME="rounduptest_schema" + RDBMS_USER="rounduptest_schema" + + if have_backend('postgresql'): + module = get_backend('postgresql') + + def setup_class(cls): + # nuke the schema for the class. Handles the case + # where an aborted test run (^C during setUp for example) + # leaves the database in an unusable, partly configured state. + config.RDBMS_NAME=cls.RDBMS_NAME + config.RDBMS_USER=cls.RDBMS_USER + def setUp(self): - # set for manual integration testing of 'native-fts' - # It is unset in tearDown so it doesn't leak into other tests. - # FIXME extract test methods in DBTest that hit the indexer - # into a new class (DBTestIndexer). Add DBTestIndexer - # to this class. - # Then create a new class in this file: - # postgresqlDBTestIndexerNative_FTS - # that imports from DBestIndexer to test native-fts. - # config['INDEXER'] = 'native-fts' - postgresqlOpener.setUp(self) - DBTest.setUp(self) + # make sure to override the rdbms settings. + # before every test. + config.RDBMS_NAME=self.RDBMS_NAME + config.RDBMS_USER=self.RDBMS_USER def tearDown(self): - # clean up config to prevent leak if native-fts is tested - config['INDEXER'] = '' - DBTest.tearDown(self) - postgresqlOpener.tearDown(self) + config.RDBMS_NAME="rounduptest" + config.RDBMS_USER="rounduptest" + + @classmethod + def nuke_database(self): + # clear out the database - easiest way is to nuke and re-create it + self.module.db_nuke(config) +@skip_postgresql +class postgresqlAdditionalDBTest(): def testUpgrade_6_to_7(self): # load the database @@ -224,6 +293,52 @@ def testUpgrade_7_to_8(self): self.assertEqual(self.db.database_schema['version'], 8) +@skip_postgresql +class postgresqlDBTest(postgresqlOpener, DBTest, + postgresqlAdditionalDBTest, unittest.TestCase): + def setUp(self): + # set for manual integration testing of 'native-fts' + # It is unset in tearDown so it doesn't leak into other tests. + # FIXME extract test methods in DBTest that hit the indexer + # into a new class (DBTestIndexer). Add DBTestIndexer + # to this class. + # Then create a new class in this file: + # postgresqlDBTestIndexerNative_FTS + # that imports from DBestIndexer to test native-fts. + # config['INDEXER'] = 'native-fts' + postgresqlOpener.setUp(self) + DBTest.setUp(self) + + def tearDown(self): + # clean up config to prevent leak if native-fts is tested + config['INDEXER'] = '' + DBTest.tearDown(self) + postgresqlOpener.tearDown(self) + +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlDBTestSchema(postgresqlSchemaOpener, DBTest, + postgresqlAdditionalDBTest, unittest.TestCase): + def setUp(self): + # set for manual integration testing of 'native-fts' + # It is unset in tearDown so it doesn't leak into other tests. + # FIXME extract test methods in DBTest that hit the indexer + # into a new class (DBTestIndexer). Add DBTestIndexer + # to this class. + # Then create a new class in this file: + # postgresqlDBTestIndexerNative_FTS + # that imports from DBestIndexer to test native-fts. + # config['INDEXER'] = 'native-fts' + postgresqlSchemaOpener.setUp(self) + DBTest.setUp(self) + + def tearDown(self): + # clean up config to prevent leak if native-fts is tested + config['INDEXER'] = '' + DBTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + @skip_postgresql class postgresqlROTest(postgresqlOpener, ROTest, unittest.TestCase): def setUp(self): @@ -234,6 +349,18 @@ def tearDown(self): ROTest.tearDown(self) postgresqlOpener.tearDown(self) +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlROTestSchema(postgresqlSchemaOpener, ROTest, + unittest.TestCase): + def setUp(self): + postgresqlSchemaOpener.setUp(self) + ROTest.setUp(self) + + def tearDown(self): + ROTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + @skip_postgresql class postgresqlConcurrencyTest(postgresqlOpener, ConcurrentDBTest, @@ -247,29 +374,23 @@ def tearDown(self): ConcurrentDBTest.tearDown(self) postgresqlOpener.tearDown(self) - @skip_postgresql -class postgresqlJournalTest(postgresqlOpener, ClassicInitBase, - unittest.TestCase): +@pytest.mark.pg_schema +class postgresqlConcurrencyTestSchema(postgresqlSchemaOpener, ConcurrentDBTest, + unittest.TestCase): backend = 'postgresql' def setUp(self): - postgresqlOpener.setUp(self) - ClassicInitBase.setUp(self) - self.tracker = setupTracker(self.dirname, self.backend) - db = self.tracker.open('admin') - self.id = db.issue.create(title='initial value') - db.commit() - db.close() + postgresqlSchemaOpener.setUp(self) + ConcurrentDBTest.setUp(self) def tearDown(self): - try: - self.db1.close() - self.db2.close() - except psycopg2.InterfaceError as exc: - if 'connection already closed' in str(exc): pass - else: raise - ClassicInitBase.tearDown(self) - postgresqlOpener.tearDown(self) + ConcurrentDBTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + + +@skip_postgresql +class postgresqlAdditionalJournalTest(): def _test_journal(self, expected_journal): id = self.id @@ -308,6 +429,56 @@ def testConcurrentRepeatableRead(self): exc = self.module.TransactionRollbackError self.assertRaises(exc, self._test_journal, []) +@skip_postgresql +class postgresqlJournalTest(postgresqlOpener, ClassicInitBase, + postgresqlAdditionalJournalTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlOpener.setUp(self) + ClassicInitBase.setUp(self) + self.tracker = setupTracker(self.dirname, self.backend) + db = self.tracker.open('admin') + self.id = db.issue.create(title='initial value') + db.commit() + db.close() + + def tearDown(self): + try: + self.db1.close() + self.db2.close() + except psycopg2.InterfaceError as exc: + if 'connection already closed' in str(exc): pass + else: raise + ClassicInitBase.tearDown(self) + postgresqlOpener.tearDown(self) + + +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlJournalTestSchema(postgresqlSchemaOpener, ClassicInitBase, + postgresqlAdditionalJournalTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + ClassicInitBase.setUp(self) + self.tracker = setupTracker(self.dirname, self.backend) + db = self.tracker.open('admin') + self.id = db.issue.create(title='initial value') + db.commit() + db.close() + + def tearDown(self): + try: + self.db1.close() + self.db2.close() + except psycopg2.InterfaceError as exc: + if 'connection already closed' in str(exc): pass + else: raise + ClassicInitBase.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + @skip_postgresql class postgresqlHTMLItemTest(postgresqlOpener, HTMLItemTest, @@ -322,6 +493,20 @@ def tearDown(self): postgresqlOpener.tearDown(self) +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlHTMLItemTestSchema(postgresqlSchemaOpener, HTMLItemTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + HTMLItemTest.setUp(self) + + def tearDown(self): + HTMLItemTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + @skip_postgresql class postgresqlFilterCacheTest(postgresqlOpener, FilterCacheTest, unittest.TestCase): @@ -334,6 +519,19 @@ def tearDown(self): FilterCacheTest.tearDown(self) postgresqlOpener.tearDown(self) +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlFilterCacheTestSchema(postgresqlSchemaOpener, FilterCacheTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + FilterCacheTest.setUp(self) + + def tearDown(self): + FilterCacheTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + @skip_postgresql class postgresqlSchemaTest(postgresqlOpener, SchemaTest, unittest.TestCase): @@ -346,6 +544,19 @@ def tearDown(self): postgresqlOpener.tearDown(self) +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlSchemaTestSchema(postgresqlSchemaOpener, SchemaTest, + unittest.TestCase): + def setUp(self): + postgresqlSchemaOpener.setUp(self) + SchemaTest.setUp(self) + + def tearDown(self): + SchemaTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + @skip_postgresql class postgresqlClassicInitTest(postgresqlOpener, ClassicInitTest, unittest.TestCase): @@ -359,6 +570,20 @@ def tearDown(self): postgresqlOpener.tearDown(self) +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlClassicInitTestSchema(postgresqlSchemaOpener, ClassicInitTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + ClassicInitTest.setUp(self) + + def tearDown(self): + ClassicInitTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + from .session_common import SessionTest @skip_postgresql class postgresqlSessionTest(postgresqlOpener, SessionTest, unittest.TestCase): @@ -371,6 +596,21 @@ def tearDown(self): SessionTest.tearDown(self) postgresqlOpener.tearDown(self) + +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlSessionTestSchema(postgresqlSchemaOpener, SessionTest, + unittest.TestCase): + s2b = lambda x,y : y + + def setUp(self): + postgresqlSchemaOpener.setUp(self) + SessionTest.setUp(self) + def tearDown(self): + SessionTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + @skip_postgresql class postgresqlSpecialActionTestCase(postgresqlOpener, SpecialActionTest, unittest.TestCase): @@ -384,7 +624,60 @@ def tearDown(self): postgresqlOpener.tearDown(self) @skip_postgresql -class postgresqlRestTest (RestTestCase, unittest.TestCase): +@pytest.mark.pg_schema +class postgresqlSpecialActionTestCaseSchema(postgresqlSchemaOpener, + SpecialActionTest, + unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + SpecialActionTest.setUp(self) + + def tearDown(self): + SpecialActionTest.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + +@skip_postgresql +class postgresqlRestTest (postgresqlOpener, RestTestCase, unittest.TestCase): + backend = 'postgresql' + def setUp(self): + postgresqlOpener.setUp(self) + RestTestCase.setUp(self) + + def tearDown(self): + RestTestCase.tearDown(self) + postgresqlOpener.tearDown(self) + + +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlRestTestSchema(postgresqlSchemaOpener, RestTestCase, + unittest.TestCase): backend = 'postgresql' + def setUp(self): + postgresqlSchemaOpener.setUp(self) + RestTestCase.setUp(self) + + def tearDown(self): + RestTestCase.tearDown(self) + postgresqlSchemaOpener.tearDown(self) + + +@skip_postgresql +@pytest.mark.pg_schema +class postgresqlDbDropFailureTestSchema(postgresqlPrecreatedSchemaDbOpener, + unittest.TestCase): + + def test_drop(self): + """Verify that the schema test database can not be dropped.""" + + with self.assertRaises(RuntimeError) as m: + self.module.db_nuke(config) + + + self.assertEqual(m.exception.args[0], + 'must be owner of database rounduptest_schema') + + # vim: set et sts=4 sw=4 :