mirror of
https://gitlab.archlinux.org/archlinux/aurweb.git
synced 2025-02-03 10:43:03 +01:00
231 lines
6.9 KiB
Python
231 lines
6.9 KiB
Python
"""
|
|
pytest configuration.
|
|
|
|
The conftest.py file is used to define pytest-global fixtures
|
|
or actions run before tests.
|
|
|
|
Module scoped fixtures:
|
|
----------------------
|
|
- setup_database
|
|
- db_session (depends: setup_database)
|
|
|
|
Function scoped fixtures:
|
|
------------------------
|
|
- db_test (depends: db_session)
|
|
|
|
Tests in aurweb which access the database **must** use the `db_test`
|
|
function fixture. Most database tests simply require this fixture in
|
|
an autouse=True setup fixture, or for fixtures used in DB tests example:
|
|
|
|
# In scenarios which there are no other database fixtures
|
|
# or other database fixtures dependency paths don't always
|
|
# hit `db_test`.
|
|
@pytest.fixture(autouse=True)
|
|
def setup(db_test):
|
|
return
|
|
|
|
# In scenarios where we can embed the `db_test` fixture in
|
|
# specific fixtures that already exist.
|
|
@pytest.fixture
|
|
def user(db_test):
|
|
with db.begin():
|
|
user = db.create(User, ...)
|
|
yield user
|
|
|
|
The `db_test` fixture triggers our module-level database fixtures,
|
|
then clears the database for each test function run in that module.
|
|
It is done this way because migration has a large cost; migrating
|
|
ahead of each function takes too long when compared to this method.
|
|
"""
|
|
import os
|
|
import pathlib
|
|
|
|
from multiprocessing import Lock
|
|
|
|
import py
|
|
import pytest
|
|
|
|
from posix_ipc import O_CREAT, Semaphore
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.engine import URL
|
|
from sqlalchemy.engine.base import Engine
|
|
from sqlalchemy.exc import ProgrammingError
|
|
from sqlalchemy.orm import scoped_session
|
|
|
|
import aurweb.config
|
|
import aurweb.db
|
|
|
|
from aurweb import initdb, logging, testing
|
|
from aurweb.testing.email import Email
|
|
from aurweb.testing.filelock import FileLock
|
|
from aurweb.testing.git import GitRepository
|
|
|
|
logger = logging.get_logger(__name__)
|
|
|
|
# Synchronization lock for database setup.
|
|
setup_lock = Lock()
|
|
|
|
|
|
def test_engine() -> Engine:
|
|
"""
|
|
Return a privileged SQLAlchemy engine with no database.
|
|
|
|
This method is particularly useful for providing an engine that
|
|
can be used to create and drop databases from an SQL server.
|
|
|
|
:return: SQLAlchemy Engine instance (not connected to a database)
|
|
"""
|
|
unix_socket = aurweb.config.get_with_fallback("database", "socket", None)
|
|
kwargs = {
|
|
"username": aurweb.config.get("database", "user"),
|
|
"password": aurweb.config.get_with_fallback(
|
|
"database", "password", None),
|
|
"host": aurweb.config.get("database", "host"),
|
|
"port": aurweb.config.get_with_fallback("database", "port", None),
|
|
"query": {
|
|
"unix_socket": unix_socket
|
|
}
|
|
}
|
|
|
|
backend = aurweb.config.get("database", "backend")
|
|
driver = aurweb.db.DRIVERS.get(backend)
|
|
return create_engine(URL.create(driver, **kwargs))
|
|
|
|
|
|
class AlembicArgs:
|
|
"""
|
|
Masquerade an ArgumentParser like structure.
|
|
|
|
This structure is needed to pass conftest-specific arguments
|
|
to initdb.run duration database creation.
|
|
"""
|
|
verbose = False
|
|
use_alembic = True
|
|
|
|
|
|
def _create_database(engine: Engine, dbname: str) -> None:
|
|
"""
|
|
Create a test database.
|
|
|
|
:param engine: Engine returned by test_engine()
|
|
:param dbname: Database name to create
|
|
"""
|
|
conn = engine.connect()
|
|
try:
|
|
conn.execute(f"CREATE DATABASE {dbname}")
|
|
except ProgrammingError: # pragma: no cover
|
|
# The database most likely already existed if we hit
|
|
# a ProgrammingError. Just drop the database and try
|
|
# again. If at that point things still fail, any
|
|
# exception will be propogated up to the caller.
|
|
conn.execute(f"DROP DATABASE {dbname}")
|
|
conn.execute(f"CREATE DATABASE {dbname}")
|
|
conn.close()
|
|
initdb.run(AlembicArgs)
|
|
|
|
|
|
def _drop_database(engine: Engine, dbname: str) -> None:
|
|
"""
|
|
Drop a test database.
|
|
|
|
:param engine: Engine returned by test_engine()
|
|
:param dbname: Database name to drop
|
|
"""
|
|
aurweb.schema.metadata.drop_all(bind=engine)
|
|
conn = engine.connect()
|
|
conn.execute(f"DROP DATABASE {dbname}")
|
|
conn.close()
|
|
|
|
|
|
def setup_email():
|
|
# TODO: Fix this data race! This try/catch is ugly; why is it even
|
|
# racing here? Perhaps we need to multiproc + multithread lock
|
|
# inside of setup_database to block the check?
|
|
with Semaphore("/test-emails", flags=O_CREAT, initial_value=1):
|
|
if not os.path.exists(Email.TEST_DIR):
|
|
# Create the directory.
|
|
os.makedirs(Email.TEST_DIR)
|
|
|
|
# Cleanup all email files for this test suite.
|
|
prefix = Email.email_prefix(suite=True)
|
|
files = os.listdir(Email.TEST_DIR)
|
|
for file in files:
|
|
if file.startswith(prefix):
|
|
os.remove(os.path.join(Email.TEST_DIR, file))
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def setup_database(tmp_path_factory: pathlib.Path, worker_id: str) -> None:
|
|
""" Create and drop a database for the suite this fixture is used in. """
|
|
engine = test_engine()
|
|
dbname = aurweb.db.name()
|
|
|
|
if worker_id == "master": # pragma: no cover
|
|
# If we're not running tests through multiproc pytest-xdist.
|
|
setup_email()
|
|
yield _create_database(engine, dbname)
|
|
_drop_database(engine, dbname)
|
|
return
|
|
|
|
def setup(path):
|
|
setup_email()
|
|
_create_database(engine, dbname)
|
|
|
|
tmpdir = tmp_path_factory.getbasetemp().parent
|
|
file_lock = FileLock(tmpdir, dbname)
|
|
file_lock.lock(on_create=setup)
|
|
yield # Run the test function depending on this fixture.
|
|
_drop_database(engine, dbname) # Cleanup the database.
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def db_session(setup_database: None) -> scoped_session:
|
|
"""
|
|
Yield a database session based on aurweb.db.name().
|
|
|
|
The returned session is popped out of persistence after the test is run.
|
|
"""
|
|
# After the test runs, aurweb.db.name() ends up returning the
|
|
# configured database, because PYTEST_CURRENT_TEST is removed.
|
|
dbname = aurweb.db.name()
|
|
session = aurweb.db.get_session()
|
|
|
|
yield session
|
|
|
|
# Close the session and pop it.
|
|
session.close()
|
|
aurweb.db.pop_session(dbname)
|
|
|
|
|
|
@pytest.fixture
|
|
def db_test(db_session: scoped_session) -> None:
|
|
"""
|
|
Database test fixture.
|
|
|
|
This fixture should be included in any tests which access the
|
|
database. It ensures that a test database is created and
|
|
alembic migrated, takes care of dropping the database when
|
|
the module is complete, and runs setup_test_db() to clear out
|
|
tables for each test.
|
|
|
|
Tests using this fixture should access the database
|
|
session via aurweb.db.get_session().
|
|
"""
|
|
testing.setup_test_db()
|
|
|
|
|
|
@pytest.fixture
|
|
def git(tmpdir: py.path.local) -> GitRepository:
|
|
yield GitRepository(tmpdir)
|
|
|
|
|
|
@pytest.fixture
|
|
def email_test() -> None:
|
|
"""
|
|
A decoupled test email setup fixture.
|
|
|
|
When using the `db_test` fixture, this fixture is redundant. Otherwise,
|
|
email tests need to run through our `setup_email` function to ensure
|
|
that we set them up to be used via aurweb.testing.email.Email.
|
|
"""
|
|
setup_email()
|