v0.166.0
 1"""
 2The extension point for the test runner.
 3
 4Packages that participate in testing subclass TestLifecycle and register it
 5under the `plain.testing` entry point group:
 6
 7    [project.entry-points."plain.testing"]
 8    postgres = "plain.postgres.testing.lifecycle:PostgresTestLifecycle"
 9
10The runner discovers and drives lifecycles; packages never import the runner.
11"""
12
13from collections.abc import Generator
14from contextlib import contextmanager
15from dataclasses import dataclass
16
17__all__ = ["CollectedTest", "TestLifecycle"]
18
19
20@dataclass(frozen=True, kw_only=True)
21class CollectedTest:
22    """
23    One test the runner is about to run, as `around_test(test)` receives it.
24    """
25
26    # Where the test is and what it is called, as the runner prints it:
27    # "tests/test_signup.py::test_welcome", or "...::test_price[annual]" for
28    # one case of a test with `@cases`.
29    id: str
30    # The names given to `@tag(...)`.
31    tags: tuple[str, ...] = ()
32
33    @property
34    def name(self) -> str:
35        """The test's name within its file (e.g. "test_price[annual]")."""
36        return self.id.partition("::")[2]
37
38
39class TestLifecycle:
40    # When set, the lifecycle only loads if this package is in the app's
41    # INSTALLED_PACKAGES — the entry point is importable whenever the package
42    # is in the environment, which is a wider net than "the app uses it".
43    required_package: str | None = None
44
45    def setup_worker(self) -> None:
46        """Called once per run, before the first test."""
47
48    def teardown_worker(self) -> None:
49        """Called once per run, after the last test."""
50
51    def describe_setup(self) -> tuple[tuple[str, float], ...]:
52        """
53        What `setup_worker()` spent its time on, for the run's report of
54        where its time went: `(("built template (49 migrations)", 0.54),)`.
55        Each is a name and seconds, printed under the lifecycle's own line.
56        Empty when there is nothing worth a line.
57        """
58        return ()
59
60    @contextmanager
61    def around_test(self, test: CollectedTest) -> Generator[None]:
62        """
63        Wrap a single test. `test.tags` carries any `@tag(...)` labels, which
64        lifecycles can use to vary behavior per-test.
65        """
66        yield
67
68    def describe_value(self, value: object) -> str | None:
69        """
70        What a failure report prints for a value this package owns, or None
71        for a value that isn't this package's to describe.
72
73        A report prints the values a failed test had in hand, by their
74        `repr`. This is for a value whose `repr` says too little to fix a
75        test by (a model instance that prints as its class and its id).
76        The text is printed as it is, on one line or several.
77
78        It is called while the test's lifecycles are still in place. It
79        must not change anything the test did, and must not do anything the
80        test didn't: a description that runs a query is a query the failing
81        test never ran.
82        """
83        return None