v0.166.0
 1"""
 2Where a project keeps its tests.
 3"""
 4
 5import sys
 6from dataclasses import dataclass
 7from pathlib import Path
 8
 9__all__ = []
10
11# Test modules are named for where they are, under this name, so two files
12# called `test_views.py` in different directories are different modules.
13TEST_MODULES_PACKAGE = "plain_tests"
14
15
16@dataclass(frozen=True, kw_only=True)
17class Layout:
18    """Where one run's tests and helper modules are."""
19
20    root: Path
21    helper_directory: Path
22    # The top-level name a test module may not import through (`tests`).
23    refused_import_name: str | None
24
25    def shown(self, path: Path) -> str:
26        """A path the way the run's output writes it: relative to the root."""
27        return path_as_shown(path, root=self.root)
28
29
30def path_as_shown(path: Path, *, root: Path) -> str:
31    """
32    A path the way a run's output writes it: relative to `root` when it is
33    under it, and as it was given when it isn't.
34    """
35    if path.is_absolute() and path.is_relative_to(root):
36        return path.relative_to(root).as_posix()
37    return str(path)
38
39
40def find_tests_directory(root: Path) -> Path:
41    """
42    The tests directory for a run started in `root`: the one place helper
43    modules are imported from, and where `lifecycle.py` lives.
44
45    `root` is normally the project root, with `tests/` beside `app/`. A
46    package whose test app is `tests/app` runs from inside `tests/`, so there
47    the root is the tests directory.
48    """
49    if root.name == "tests":
50        return root
51    return root / "tests"
52
53
54def import_helper_modules_from(directory: Path) -> None:
55    """
56    Put the directory helper modules live in on `sys.path`, so a test file
57    and `lifecycle.py` both import `<directory>/helpers.py` as `helpers`.
58    """
59    if str(directory) not in sys.path:
60        sys.path.insert(0, str(directory))
61
62
63def without_test_module_names(text: str) -> str:
64    """
65    Text with the names test modules are loaded under taken out of it, so
66    that what a test file defines is called what the file calls it.
67
68    A test file is a module named for where it is
69    (`plain_tests.tests.billing.test_refunds`), and Python puts a module's
70    name in front of what it defines: `<plain_tests.tests.billing.
71    test_refunds.FakeGateway object at 0x...>`. Nobody wrote that name, and
72    a report already says which file it is about. This is `<FakeGateway
73    object at 0x...>`.
74    """
75    if TEST_MODULES_PACKAGE not in text:
76        return text
77    names = [
78        name
79        for name, module in sys.modules.items()
80        if name.startswith(f"{TEST_MODULES_PACKAGE}.")
81        and getattr(module, "__file__", None)
82    ]
83    # The longest first: `plain_tests.tests.test_a` is the start of
84    # `plain_tests.tests.test_a_b`.
85    for name in sorted(names, key=len, reverse=True):
86        text = text.replace(f"{name}.", "")
87    return text