From a1a4b6830677250dda58b1f23487354f4352214b Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Mon, 24 Apr 2017 15:52:50 -0400 Subject: [PATCH] Change fulldbtests to use py.test --- initdb.py | 11 +++++++- test/fixtures.py | 67 +++++++++++++++++++++++++++++++++++++++++----- test/fulldbtest.sh | 16 ++++++----- 3 files changed, 79 insertions(+), 15 deletions(-) diff --git a/initdb.py b/initdb.py index 28093dcfd..61491973a 100644 --- a/initdb.py +++ b/initdb.py @@ -20,7 +20,7 @@ from data.database import (db, all_models, beta_classes, Role, TeamRole, Visibil ExternalNotificationEvent, ExternalNotificationMethod, NotificationKind, QuayRegion, QuayService, UserRegion, OAuthAuthorizationCode, ServiceKeyApprovalType, MediaType, LabelSourceType, UserPromptKind, - RepositoryKind, TagKind, BlobPlacementLocation) + RepositoryKind, TagKind, BlobPlacementLocation, User) from data import model from data.queue import WorkQueue from app import app, storage as store, tf @@ -440,6 +440,15 @@ def wipe_database(): def populate_database(minimal=False, with_storage=False): logger.debug('Populating the DB with test data.') + # Check if the data already exists. If so, we skip. This can happen between calls from the + # "old style" tests and the new py.test's. + try: + User.get(username='devtable') + logger.debug('DB already populated') + return + except User.DoesNotExist: + pass + # Note: databases set up with "real" schema (via Alembic) will not have these types # type, so we it here it necessary. try: diff --git a/test/fixtures.py b/test/fixtures.py index 7477713b7..31e0f516e 100644 --- a/test/fixtures.py +++ b/test/fixtures.py @@ -1,10 +1,12 @@ import os +from cachetools import lru_cache + import pytest import shutil from flask import Flask, jsonify from flask_login import LoginManager -from peewee import SqliteDatabase +from peewee import SqliteDatabase, savepoint, InternalError from app import app as application from data import model @@ -14,30 +16,56 @@ from endpoints.api import api_bp from endpoints.web import web from initdb import initialize_database, populate_database + from path_converters import APIRepositoryPathConverter, RegexConverter, RepositoryPathConverter from test.testconfig import FakeTransaction - @pytest.fixture(scope="session") +@lru_cache(maxsize=1) # Important! pytest is calling this multiple times (despite it being session) def init_db_path(tmpdir_factory): """ Creates a new database and appropriate configuration. Note that the initial database is created *once* per session. In the non-full-db-test case, the database_uri fixture makes a copy of the SQLite database file on disk and passes a new copy to each test. """ - sqlitedb_file = str(tmpdir_factory.mktemp("data").join("test.db")) - sqlitedb = 'sqlite:///{0}'.format(sqlitedb_file) + if os.environ.get('TEST_DATABASE_URI'): + return _init_db_path_real_db(os.environ.get('TEST_DATABASE_URI')) + + return _init_db_path_sqlite(tmpdir_factory) + +def _init_db_path_real_db(db_uri): + """ Initializes a real database for testing by populating it from scratch. Note that this does + *not* add the tables (merely data). Callers must have migrated the database before calling + the test suite. + """ + configure({ + "DB_URI": db_uri, + "DB_CONNECTION_ARGS": { + 'threadlocals': True, + 'autorollback': True, + }, + "DB_TRANSACTION_FACTORY": _create_transaction, + }) + + populate_database() + return db_uri + +def _init_db_path_sqlite(tmpdir_factory): + """ Initializes a SQLite database for testing by populating it from scratch and placing it into + a temp directory file. + """ + sqlitedbfile = str(tmpdir_factory.mktemp("data").join("test.db")) + sqlitedb = 'sqlite:///{0}'.format(sqlitedbfile) conf = {"TESTING": True, "DEBUG": True, "DB_URI": sqlitedb} - os.environ['TEST_DATABASE_URI'] = str(sqlitedb) os.environ['DB_URI'] = str(sqlitedb) - db.initialize(SqliteDatabase(sqlitedb_file)) + db.initialize(SqliteDatabase(sqlitedbfile)) application.config.update(conf) application.config.update({"DB_URI": sqlitedb}) initialize_database() populate_database() close_db_filter(None) - return str(sqlitedb_file) + return str(sqlitedbfile) @pytest.fixture() @@ -47,6 +75,11 @@ def database_uri(monkeypatch, init_db_path, sqlitedb_file): on a per-test basis. In the non-SQLite case, a reference to the existing database URI is returned. """ + if os.environ.get('TEST_DATABASE_URI'): + db_uri = os.environ['TEST_DATABASE_URI'] + monkeypatch.setenv("DB_URI", db_uri) + return db_uri + # Copy the golden database file to a new path. shutil.copy2(init_db_path, sqlitedb_file) @@ -84,8 +117,28 @@ def appconfig(database_uri): @pytest.fixture() def initialized_db(appconfig): """ Configures the database for the database found in the appconfig. """ + + # Configure the database. configure(appconfig) + # If under a test *real* database, setup a savepoint. + under_test_real_database = bool(os.environ.get('TEST_DATABASE_URI')) + if under_test_real_database: + test_savepoint = savepoint(db) + test_savepoint.__enter__() + + yield # Run the test. + + try: + test_savepoint.__exit__(None, None, None) + except InternalError: + # If postgres fails with an exception (like IntegrityError) mid-transaction, it terminates + # it immediately, so when we go to remove the savepoint, it complains. We can safely ignore + # this case. + pass + else: + yield + @pytest.fixture() def app(appconfig, initialized_db): """ Used by pytest-flask plugin to inject a custom app instance for testing. """ diff --git a/test/fulldbtest.sh b/test/fulldbtest.sh index a3de55009..63faba726 100755 --- a/test/fulldbtest.sh +++ b/test/fulldbtest.sh @@ -13,8 +13,8 @@ up_mysql() { } down_mysql() { - docker kill mysql - docker rm -v mysql + docker kill mysql || true + docker rm -v mysql || true } up_postgres() { @@ -30,8 +30,8 @@ up_postgres() { } down_postgres() { - docker kill postgres - docker rm -v postgres + docker kill postgres || true + docker rm -v postgres || true } run_tests() { @@ -39,7 +39,7 @@ run_tests() { PYTHONPATH=. TEST_DATABASE_URI=$1 TEST=true alembic upgrade head # Run the full test suite. - SKIP_DB_SCHEMA=true TEST_DATABASE_URI=$1 TEST=true python -m unittest discover -f + PYTHONPATH=. SKIP_DB_SCHEMA=true TEST_DATABASE_URI=$1 TEST=true py.test ${2:-.} --ignore=endpoints/appr/test/ } CIP=${CONTAINERIP-'127.0.0.1'} @@ -48,20 +48,22 @@ echo "> Using container IP address $CIP" # NOTE: MySQL is currently broken on setup. # Test (and generate, if requested) via MySQL. echo '> Starting MySQL' +down_mysql up_mysql echo '> Running Full Test Suite (mysql)' set +e -run_tests "mysql+pymysql://root:password@$CIP/genschema" +run_tests "mysql+pymysql://root:password@$CIP/genschema" $1 set -e down_mysql # Test via Postgres. echo '> Starting Postgres' +down_postgres up_postgres echo '> Running Full Test Suite (postgres)' set +e -run_tests "postgresql://postgres@$CIP/genschema" +run_tests "postgresql://postgres@$CIP/genschema" $1 set -e down_postgres