← Back to team overview

sts-sponsors team mailing list archive

[Merge] ~bjornt/maas:pytest-django-setup into maas:master

 

Björn Tillenius has proposed merging ~bjornt/maas:pytest-django-setup into maas:master.

Commit message:
Add a pytest DB fixture to set up the database for the tests.

You now no longer have to run the pytest tests that require a DB using
bin/database. Instead you run bin/pytest and it will set up the DB
automatically. It still uses the bin/database infrastructure under the hood,
and it will re-use the DB by default. So the time it takes to run the tests is
the same as before.

For the performance tests, it's possible to pass in a DB dump to use as well.
I removed pytest-django, since we want to control how the DB is
created and cleaned up.



Requested reviews:
  MAAS Maintainers (maas-maintainers)

For more details, see:
https://code.launchpad.net/~bjornt/maas/+git/maas/+merge/433712
-- 
Your team MAAS Maintainers is requested to review the proposed merge of ~bjornt/maas:pytest-django-setup into maas:master.
diff --git a/pyproject.toml b/pyproject.toml
index abafc45..edf51b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -35,7 +35,6 @@ snippets
 order_by_type = false
 
 [tool.pytest.ini_options]
-DJANGO_SETTINGS_MODULE = "maasserver.djangosettings.development"
 filterwarnings = "error::BytesWarning"
 testpaths = [
   "src/apiclient",
@@ -54,7 +53,3 @@ testpaths = [
   "src/provisioningserver/utils/pytest_tests",
   # [[[end]]]
 ]
-markers = [
-    "perftest: marks tests for performance testing"
-]
-addopts = "--reuse-db"
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 1dbc728..0b72d0f 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -4,7 +4,6 @@ ipdb==0.13.9
 junitxml==0.7
 postgresfixture==0.4.2
 pytest==7.1.2
-pytest-django==4.5.2
 pytest-mock==3.7.0
 pytest-xdist==2.5.0
 python-subunit==1.4.0
diff --git a/setup.cfg b/setup.cfg
index 402ce94..cb3e5db 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -37,6 +37,10 @@ console_scripts =
   test.region.legacy = maastesting.scripts:run_region_legacy
   test.rack = maastesting.scripts:run_rack
   test.parallel = maastesting.scripts:run_parallel
+pytest11 =
+  maas-django = maastesting.pytest.django
+  maas-seeds = maastesting.pytest.seeds
+  maas-perftest = maastesting.pytest.perftest
 
 [options.packages.find]
 where = src
diff --git a/src/maasperf/tests/cli/conftest.py b/src/maasperf/tests/cli/conftest.py
index 450955d..79aeba8 100644
--- a/src/maasperf/tests/cli/conftest.py
+++ b/src/maasperf/tests/cli/conftest.py
@@ -7,19 +7,6 @@ from django.core.serializers import serialize
 from django.http import HttpResponse
 from pytest import fixture
 
-from maasserver.testing.factory import factory as maasserver_factory
-
-
-# override pytest-django's db setup
-@fixture(scope="session")
-def django_db_setup():
-    pass
-
-
-@fixture(scope="session")
-def factory():
-    return maasserver_factory
-
 
 @fixture()
 def maas_user(factory):
diff --git a/src/maasperf/tests/cli/test_machines.py b/src/maasperf/tests/cli/test_machines.py
index 95d09bc..94c3af3 100644
--- a/src/maasperf/tests/cli/test_machines.py
+++ b/src/maasperf/tests/cli/test_machines.py
@@ -11,7 +11,7 @@ from maascli.config import ProfileConfig
 from maascli.parser import get_deepest_subparser, prepare_parser
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 def test_perf_list_machines_CLI(
     perf, cli_profile, monkeypatch, cli_machines_api_response
 ):
diff --git a/src/maasperf/tests/conftest.py b/src/maasperf/tests/conftest.py
deleted file mode 100644
index 3850845..0000000
--- a/src/maasperf/tests/conftest.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright 2022 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-from pytest import fixture
-
-__all__ = [
-    "admin_api_client",
-    "api_client",
-    "django_db_setup",
-    "factory",
-    "maas_user",
-]
-
-
-pytest_plugins = "maastesting.pytest.perftest,maastesting.pytest.seeds"
-
-
-# override pytest-django's db setup
-@fixture(scope="session")
-def django_db_setup():
-    pass
-
-
-@fixture(scope="session")
-def factory():
-    # Local imports from maasserver so that pytest --help works
-    from maasserver.testing.factory import factory as maasserver_factory
-
-    return maasserver_factory
-
-
-@fixture()
-def admin(factory):
-    return factory.make_admin()
-
-
-@fixture()
-def maas_user(factory):
-    return factory.make_User()
-
-
-@fixture()
-def api_client(maas_user):
-    # Local imports from maasserver so that pytest --help works
-    from maasserver.models.user import get_auth_tokens
-    from maasserver.testing.testclient import MAASSensibleOAuthClient
-
-    return MAASSensibleOAuthClient(
-        user=maas_user, token=get_auth_tokens(maas_user)[0]
-    )
-
-
-@fixture()
-def admin_api_client(admin):
-    # Local imports from maasserver so that pytest --help works
-    from maasserver.models.user import get_auth_tokens
-    from maasserver.testing.testclient import MAASSensibleOAuthClient
-
-    return MAASSensibleOAuthClient(user=admin, token=get_auth_tokens(admin)[0])
diff --git a/src/maasperf/tests/maasserver/api/test_machines.py b/src/maasperf/tests/maasserver/api/test_machines.py
index a823f68..157acc7 100644
--- a/src/maasperf/tests/maasserver/api/test_machines.py
+++ b/src/maasperf/tests/maasserver/api/test_machines.py
@@ -4,7 +4,6 @@
 from django.urls import reverse
 from piston3.emitters import Emitter
 from piston3.handler import typemapper
-import pytest
 
 from maasserver.api.machines import MachinesHandler
 from maastesting.http import make_HttpRequest
@@ -15,7 +14,6 @@ class DummyEmitter(Emitter):
         self.construct()
 
 
-@pytest.mark.django_db
 def test_perf_list_machines_MachineHandler_api_endpoint(
     perf, admin_api_client
 ):
@@ -23,7 +21,6 @@ def test_perf_list_machines_MachineHandler_api_endpoint(
         admin_api_client.get(reverse("machines_handler"))
 
 
-@pytest.mark.django_db
 def test_perf_list_machines_MachinesHander_direct_call(perf, admin):
     handler = MachinesHandler()
     request = make_HttpRequest()
@@ -40,7 +37,6 @@ def test_perf_list_machines_MachinesHander_direct_call(perf, admin):
         emitter.render(request)
 
 
-@pytest.mark.django_db
 def test_perf_list_machines_MachinesHander_only_objects(perf, admin):
     handler = MachinesHandler()
     request = make_HttpRequest()
diff --git a/src/maasperf/tests/maasserver/models/conftest.py b/src/maasperf/tests/maasserver/models/conftest.py
deleted file mode 100644
index f3830e8..0000000
--- a/src/maasperf/tests/maasserver/models/conftest.py
+++ /dev/null
@@ -1,17 +0,0 @@
-# Copyright 2022 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-from pytest import fixture
-
-from maasserver.testing.factory import factory as maasserver_factory
-
-
-# override pytest-django's db setup
-@fixture(scope="session")
-def django_db_setup():
-    pass
-
-
-@fixture(scope="session")
-def factory():
-    return maasserver_factory
diff --git a/src/maasperf/tests/maasserver/models/test_machine.py b/src/maasperf/tests/maasserver/models/test_machine.py
index 2273294..41b2248 100644
--- a/src/maasperf/tests/maasserver/models/test_machine.py
+++ b/src/maasperf/tests/maasserver/models/test_machine.py
@@ -7,16 +7,16 @@ import pytest
 from maasserver.models import Machine
 
 
-@pytest.mark.django_db
+@pytest.mark.allow_transactions
 def test_perf_create_machines(perf, factory):
     # TODO use create machines script
     with perf.record("test_perf_create_machines"):
-        with transaction.atomic():
-            for _ in range(30):
-                factory.make_Machine()
+        for _ in range(30):
+            factory.make_Machine()
+        transaction.commit()
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 def test_perf_list_machines(perf):
     with perf.record("test_perf_list_machines"):
         list(Machine.objects.all())
diff --git a/src/maasperf/tests/maasserver/websockets/test_machines.py b/src/maasperf/tests/maasserver/websockets/test_machines.py
index a182a22..f91ecc9 100644
--- a/src/maasperf/tests/maasserver/websockets/test_machines.py
+++ b/src/maasperf/tests/maasserver/websockets/test_machines.py
@@ -1,13 +1,10 @@
 # Copyright 2022 Canonical Ltd.  This software is licensed under the
 # GNU Affero General Public License version 3 (see the file LICENSE).
 
-import pytest
-
 from maasserver.models import Machine
 from maasserver.websockets.handlers.machine import MachineHandler
 
 
-@pytest.mark.django_db
 def test_perf_list_machines_Websocket_endpoint(perf, admin):
     # This should test the websocket calls that are used to load
     # the machine listing page on the initial page load.
@@ -27,7 +24,6 @@ def test_perf_list_machines_Websocket_endpoint(perf, admin):
         ws_handler.list(params)
 
 
-@pytest.mark.django_db
 def test_perf_list_machines_Websocket_endpoint_all(perf, admin):
     # How long would it take to list all the machines using the
     # websocket without any pagination.
diff --git a/src/maasserver/conftest.py b/src/maasserver/conftest.py
index b50be82..063bfe9 100644
--- a/src/maasserver/conftest.py
+++ b/src/maasserver/conftest.py
@@ -9,7 +9,6 @@ import pytest
 
 from maasserver import vault
 from maasserver.config import RegionConfiguration
-from maasserver.testing.factory import factory as maasserver_factory
 from maasserver.vault import (
     get_region_vault_client,
     get_region_vault_client_if_enabled,
@@ -23,11 +22,6 @@ def clean_globals(clean_globals):
     yield
 
 
-@pytest.fixture(scope="session")
-def factory():
-    return maasserver_factory
-
-
 @pytest.fixture
 def vault_regionconfig(mocker):
     store = {}
diff --git a/src/maasserver/management/commands/pytest_tests/test_config_vault.py b/src/maasserver/management/commands/pytest_tests/test_config_vault.py
index 23333ea..f75908c 100644
--- a/src/maasserver/management/commands/pytest_tests/test_config_vault.py
+++ b/src/maasserver/management/commands/pytest_tests/test_config_vault.py
@@ -25,6 +25,7 @@ def configure_mock(mocker):
     yield mocker.patch.object(config_vault, "configure_region_with_vault")
 
 
+@pytest.mark.usefixtures("maasdb")
 class TestConfigVaultConfigurateCommand:
     def _configure_kwargs(
         self,
@@ -125,7 +126,7 @@ class TestConfigVaultConfigurateCommand:
         )
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 class TestSetVaultConfiguredDbCommand:
     def test_does_nothing_when_no_maas_id(self):
         assert MAAS_ID.get() is None
@@ -149,7 +150,7 @@ class TestSetVaultConfiguredDbCommand:
         assert ControllerInfo.objects.get(node_id=node.id).vault_configured
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 class TestConfigVaultMigrateCommand:
     def test_raises_when_vault_already_enabled(self):
         Config.objects.set_config("vault_enabled", True)
@@ -198,7 +199,7 @@ class TestConfigVaultMigrateCommand:
         migrate_mock.assert_called_once_with(client)
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 class TestMigrateSecrets:
     def test_migrate_secrets_enables_vault(self, mocker):
         assert not Config.objects.get_config("vault_enabled", False)
@@ -334,7 +335,7 @@ class TestMigrateSecrets:
         assert notify_mock.call_count == 1
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 class TestStatus:
     def test_status_not_enabled(self, capsys):
         region_one = factory.make_RegionController(hostname="one")
diff --git a/src/maasserver/models/pytest_tests/test_node.py b/src/maasserver/models/pytest_tests/test_node.py
index ca5fb9f..72f89b2 100644
--- a/src/maasserver/models/pytest_tests/test_node.py
+++ b/src/maasserver/models/pytest_tests/test_node.py
@@ -1,11 +1,8 @@
 import logging
 
-import pytest
-
 from maasserver.enum import NODE_STATUS
 
 
-@pytest.mark.django_db
 def test_node_mark_failed_deployment_logs_failure(factory, caplog):
     node = factory.make_Node(
         status=NODE_STATUS.DEPLOYING, with_boot_disk=False
diff --git a/src/maasserver/pytest_tests/test_certificates.py b/src/maasserver/pytest_tests/test_certificates.py
index 2e59d3d..2ff100d 100644
--- a/src/maasserver/pytest_tests/test_certificates.py
+++ b/src/maasserver/pytest_tests/test_certificates.py
@@ -7,7 +7,7 @@ from provisioningserver.testing.certificates import (
 )
 
 
-@pytest.mark.django_db
+@pytest.mark.usefixtures("maasdb")
 class TestGetMAASCertificate:
     def test_no_secret(self):
         assert (
diff --git a/src/maasserver/pytest_tests/test_secrets.py b/src/maasserver/pytest_tests/test_secrets.py
index 4428151..16964f8 100644
--- a/src/maasserver/pytest_tests/test_secrets.py
+++ b/src/maasserver/pytest_tests/test_secrets.py
@@ -15,8 +15,8 @@ def vault_client(request):
         yield None
 
 
-@pytest.mark.django_db
 @pytest.mark.parametrize("vault_client", [True, False], indirect=True)
+@pytest.mark.usefixtures("maasdb")
 class TestSecretManager:
     def set_secret(self, vault_client, path, value):
         if vault_client:
diff --git a/src/maasserver/pytest_tests/test_vault.py b/src/maasserver/pytest_tests/test_vault.py
index d050999..c6cc302 100644
--- a/src/maasserver/pytest_tests/test_vault.py
+++ b/src/maasserver/pytest_tests/test_vault.py
@@ -141,7 +141,6 @@ class TestVaultClient:
         ensure_auth.assert_called_once()
 
 
-@pytest.mark.django_db
 class TestGetRegionVaultClient:
     def test_cached(self, mocker):
         mock_get_client = mocker.patch.object(
diff --git a/src/maasserver/testing/resources.py b/src/maasserver/testing/resources.py
index d628ba9..e2ba9f9 100644
--- a/src/maasserver/testing/resources.py
+++ b/src/maasserver/testing/resources.py
@@ -59,6 +59,28 @@ def connect_no_transaction(cluster):
             conn.close()
 
 
+def create_postgres_cluster():
+    cluster = ClusterFixture("db", preserve=True)
+    cluster.create()
+    postgres_path = Path(cluster.datadir)
+    postgres_conf = postgres_path / "postgresql.conf"
+    postgres_speed_conf = postgres_path / "postgresql.conf.speed"
+    if "postgresql.conf.speed" not in postgres_conf.read_text():
+        with postgres_conf.open("a") as fh:
+            fh.write("include = 'postgresql.conf.speed'\n")
+        with postgres_speed_conf.open("w") as fh:
+            fh.write(
+                dedent(
+                    """\
+                fsync = off
+                full_page_writes = off
+                synchronous_commit = off
+                """
+                )
+            )
+    return cluster
+
+
 class DatabaseClusterManager(TestResourceManager):
     """Resource manager for a PostgreSQL cluster."""
 
@@ -66,24 +88,7 @@ class DatabaseClusterManager(TestResourceManager):
     testDownCost = 2
 
     def make(self, dependencies):
-        cluster = ClusterFixture("db", preserve=True)
-        cluster.create()
-        postgres_path = Path(cluster.datadir)
-        postgres_conf = postgres_path / "postgresql.conf"
-        postgres_speed_conf = postgres_path / "postgresql.conf.speed"
-        if "postgresql.conf.speed" not in postgres_conf.read_text():
-            with postgres_conf.open("a") as fh:
-                fh.write("include = 'postgresql.conf.speed'\n")
-            with postgres_speed_conf.open("w") as fh:
-                fh.write(
-                    dedent(
-                        """\
-                    fsync = off
-                    full_page_writes = off
-                    synchronous_commit = off
-                    """
-                    )
-                )
+        cluster = create_postgres_cluster()
         cluster.setUp()
         return cluster
 
diff --git a/src/maastesting/pytest/__init__.py b/src/maastesting/pytest/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maastesting/pytest/__init__.py
diff --git a/src/maastesting/pytest/django.py b/src/maastesting/pytest/django.py
new file mode 100644
index 0000000..6129d15
--- /dev/null
+++ b/src/maastesting/pytest/django.py
@@ -0,0 +1,245 @@
+from contextlib import contextmanager
+import os
+from pathlib import Path
+
+from django.db import transaction
+from postgresfixture import ClusterFixture
+import pytest
+
+from maasserver.djangosettings import development
+import maasserver.testing
+from maasserver.testing.resources import (
+    close_all_connections,
+    create_postgres_cluster,
+)
+from maasserver.utils.orm import enable_all_database_connections
+
+cluster_stash = pytest.StashKey[ClusterFixture]
+db_template_stash = pytest.StashKey[str]
+
+
+def pytest_addoption(parser):
+    default_initial_db = (
+        Path(maasserver.testing.__file__).parent / "initial.maas_test.sql"
+    )
+    maas_parser = parser.getgroup("maas", description="MAAS")
+    maas_parser.addoption(
+        "--maas-recreate-initial-db",
+        help="Recreate the DB template that's used to speed up tests",
+        action="store_true",
+    )
+    maas_parser.addoption(
+        "--maas-initial-db",
+        help="The initial DB dump that's used to create the DB template.",
+        default=str(default_initial_db),
+    )
+
+
+def load_initial_db_file(cluster, template_name, path):
+    if path.suffix == ".sql":
+        with connect(cluster) as conn:
+            with conn.cursor() as cursor:
+                cursor.execute(f'CREATE DATABASE "{template_name}"')
+        cluster.execute(
+            "psql",
+            "--quiet",
+            "--single-transaction",
+            "--set=ON_ERROR_STOP=1",
+            "--dbname",
+            template_name,
+            "--output",
+            os.devnull,
+            "--file",
+            str(path),
+        )
+    else:
+        # Assume it's a DB dump of the "maas" database.
+        cluster.execute(
+            "pg_restore",
+            "-O",
+            "-x",
+            "--disable-triggers",
+            "--create",
+            "--clean",
+            "--if-exists",
+            "-d",
+            "postgres",
+            str(path),
+        )
+        with connect(cluster) as conn:
+            with conn.cursor() as cursor:
+                cursor.execute(
+                    f'ALTER DATABASE "maas" RENAME TO "{template_name}"'
+                )
+
+
+@contextmanager
+def connect(cluster):
+    conn = cluster.connect()
+    conn.autocommit = True
+    yield conn
+    conn.close()
+
+
+@pytest.hookimpl(tryfirst=False)
+def pytest_configure(config):
+    config.addinivalue_line(
+        "markers",
+        "allow_transactions: Allow a test to use transaction.commit()",
+    )
+
+
+@pytest.hookimpl(tryfirst=True)
+def pytest_load_initial_conftests(early_config, parser, args):
+    cluster = create_postgres_cluster()
+    cluster.setUp()
+    early_config.stash[cluster_stash] = cluster
+    os.environ[
+        "DJANGO_SETTINGS_MODULE"
+    ] = "maasserver.djangosettings.development"
+    import django
+
+    from maasserver.djangosettings import development
+
+    database = development.DATABASES["default"]
+    template = f"{database['NAME']}_test"
+    early_config.stash[db_template_stash] = template
+    database["NAME"] = "no_such_db"
+    django.setup()
+
+
+@pytest.hookimpl
+def pytest_unconfigure(config):
+    cluster = config.stash[cluster_stash]
+    cluster.cleanUp()
+
+
+def _set_up_template_db(
+    cluster, template_name, template_path, force_recreate=False
+):
+    if force_recreate:
+        with connect(cluster) as conn:
+            with conn.cursor() as cursor:
+                cursor.execute(f'DROP DATABASE IF EXISTS "{template_name}"')
+
+    if template_name not in cluster.databases:
+        load_initial_db_file(cluster, template_name, template_path)
+
+    from django.core.management import call_command
+
+    from maasserver import dbviews, triggers
+    from maasserver.djangosettings import development
+
+    old_name = development.DATABASES["default"]["NAME"]
+    development.DATABASES["default"]["NAME"] = template_name
+
+    import django
+
+    django.setup()
+    enable_all_database_connections()
+    dbviews.drop_all_views()
+    call_command("migrate", interactive=False)
+
+    triggers.register_all_triggers()
+    dbviews.register_all_views()
+
+    close_all_connections()
+    development.DATABASES["default"]["NAME"] = old_name
+
+
+@pytest.fixture(scope="session")
+def templatemaasdb(pytestconfig):
+
+    cluster = pytestconfig.stash[cluster_stash]
+    force_recreate = pytestconfig.option.maas_recreate_initial_db
+    template_path = Path(pytestconfig.option.maas_initial_db)
+    with cluster.lock.exclusive:
+        template_name = pytestconfig.stash[db_template_stash]
+        _set_up_template_db(
+            cluster, template_name, template_path, force_recreate
+        )
+
+
+@pytest.fixture
+def ensuremaasdb(templatemaasdb, pytestconfig, worker_id):
+    from maasserver.djangosettings import development
+
+    template = pytestconfig.stash[db_template_stash]
+    dbname = f"{template}_{worker_id}"
+    database = development.DATABASES["default"]
+    database["NAME"] = dbname
+    cluster = pytestconfig.stash[cluster_stash]
+    if dbname not in cluster.databases:
+        template = pytestconfig.stash[db_template_stash]
+        with cluster.lock.exclusive:
+            with connect(cluster) as conn:
+                with conn.cursor() as cursor:
+                    cursor.execute(
+                        f'CREATE DATABASE "{dbname}" WITH TEMPLATE "{template}"'
+                    )
+    yield
+    database["NAME"] = "no_such_db"
+
+
+@pytest.fixture
+def maasdb(ensuremaasdb, request, pytestconfig):
+    enable_all_database_connections()
+    # Start a transaction.
+    transaction.set_autocommit(False)
+    allow_transactions = (
+        request.node.get_closest_marker("allow_transactions") is not None
+    )
+    if allow_transactions:
+        yield
+        close_all_connections()
+        # Since transactions are allowed, we assume a commit has been
+        # made, so we can't simply do rollback to clean up the DB.
+        dbname = development.DATABASES["default"]["NAME"]
+        cluster = pytestconfig.stash[cluster_stash]
+        cluster.dropdb(dbname)
+    else:
+        # Wrap the test in an atomic() block in order to prevent commits.
+        with transaction.atomic():
+            yield
+        # Since we don't allow commits, we can safely rollback and don't
+        # have to recreate the DB.
+        transaction.rollback()
+        close_all_connections()
+
+
+@pytest.fixture
+def factory(maasdb):
+    # Local imports from maasserver so that pytest --help works
+    from maasserver.testing.factory import factory as maasserver_factory
+
+    return maasserver_factory
+
+
+@pytest.fixture
+def admin(factory):
+    return factory.make_admin()
+
+
+@pytest.fixture
+def maas_user(factory):
+    return factory.make_User()
+
+
+@pytest.fixture
+def api_client(maas_user):
+    # Local imports from maasserver so that pytest --help works
+    from maasserver.models.user import get_auth_tokens
+    from maasserver.testing.testclient import MAASSensibleOAuthClient
+
+    return MAASSensibleOAuthClient(
+        user=maas_user, token=get_auth_tokens(maas_user)[0]
+    )
+
+
+@pytest.fixture
+def admin_api_client(admin):
+    # Local imports from maasserver so that pytest --help works
+    from maasserver.models.user import get_auth_tokens
+    from maasserver.testing.testclient import MAASSensibleOAuthClient
+
+    return MAASSensibleOAuthClient(user=admin, token=get_auth_tokens(admin)[0])
diff --git a/src/maastesting/pytest/tests/__init__.py b/src/maastesting/pytest/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/maastesting/pytest/tests/__init__.py
diff --git a/src/provisioningserver/conftest.py b/src/provisioningserver/conftest.py
deleted file mode 100644
index 57b9e3a..0000000
--- a/src/provisioningserver/conftest.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright 2022 Canonical Ltd.  This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-import pytest
-
-from maastesting.factory import factory as maastesting_factory
-
-
-@pytest.fixture(scope="session")
-def factory():
-    return maastesting_factory
diff --git a/utilities/check-imports b/utilities/check-imports
index 2e3cf92..fb59c92 100755
--- a/utilities/check-imports
+++ b/utilities/check-imports
@@ -341,6 +341,7 @@ TestHelpers = (
 TestHelpersRule = Rule(
     Allow("crochet|crochet.**"),
     Allow("django|django.**"),
+    Allow("maasserver|maasserver.**"),
     Allow("netaddr|netaddr.**"),
     Allow("twisted.**"),
     Allow("wrapt"),
diff --git a/utilities/run-perf-tests-ci b/utilities/run-perf-tests-ci
index b312abb..8dbd6cd 100755
--- a/utilities/run-perf-tests-ci
+++ b/utilities/run-perf-tests-ci
@@ -13,20 +13,28 @@ GIT_HASH="$(git rev-parse HEAD)"
 PYTHONHASHSEED="${PYTHONHASHSEED:-$(shuf -i 0-4294967295 -n 1)}"
 MAAS_RAND_SEED="${MAAS_RAND_SEED:-$(od -vAn -N8 -tx8 < /dev/urandom | tr -d ' ')}"
 
+DB_DUMP=$1
 OUTPUT_FILE="${OUTPUT_FILE:-maas-perf-results.json}"
 
+if [ -z "$1" ]
+then
+  echo "Usage: $0 <maas_db_dump>"
+  exit 1
+fi
+
 export MAAS_RAND_SEED PYTHONHASHSEED GIT_HASH GIT_BRANCH
 
 echo "MAAS_RAND_SEED=${MAAS_RAND_SEED}"
 echo "PYTHONHASHSEED=${PYTHONHASHSEED}"
 
-bin/database --preserve run make syncdb || exit 1
-exec bin/database --preserve run -- bin/pytest \
+exec bin/pytest \
     -q \
     --disable-warnings \
     --show-capture=no \
     --no-header \
     --no-summary \
     --junit-xml=junit-perf.xml \
+    --maas-recreate-initial-db \
+    --maas-initial-db ${DB_DUMP} \
     ./src/maasperf/ \
     --perf-output-file ${OUTPUT_FILE}
diff --git a/utilities/run-py-tests-ci b/utilities/run-py-tests-ci
index 67c9f96..5113d55 100755
--- a/utilities/run-py-tests-ci
+++ b/utilities/run-py-tests-ci
@@ -19,7 +19,6 @@ bin/test.parallel --emit-subunit | \
     bin/subunit2junitxml --no-passthrough -f -o junit.xml | \
     bin/subunit2pyunit --no-passthrough
 res1=$?
-DBUPGRADE_ARGS='-v 0' bin/database --preserve run -- make syncdb
-bin/database run -- bin/pytest -n auto --maxprocesses=6 --dist=loadscope --junit-xml=junit-pytest.xml
+bin/pytest -n auto --maxprocesses=6 --dist=loadscope --junit-xml=junit-pytest.xml
 res2=$?
 exit $((res1 + res2))

Follow ups