Skip to content

Commit cd931ef

Browse files
committed
test: hold the negation twins and the size table to the tree
1 parent ea15054 commit cd931ef

4 files changed

Lines changed: 28 additions & 18 deletions

File tree

‎ARCHITECTURE.md‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Most of it is not yours to edit:
2323
| `_engine/_builder_check_typing.py` | 1 | 315 |
2424
| `_engine/_capable_typing.py` | 10 | 324 |
2525
| `_engine/_poll_typing.py` | 2 | 739 |
26+
| `_engine/_negated_typing.py` | 18 | 862 |
2627

2728
One is written by hand. `tests/test_architecture_doc.py` recomputes this table. No line counts, which
2829
move on every edit to a generated file.
@@ -39,12 +40,13 @@ move on every edit to a generated file.
3940
| `_engine/_negated_typing.py` | `scripts/generate_poll_protocols.py` | `_typing.py` and `_engine/_operations.py` |
4041
| `_engine/_check_typing.py` | `scripts/generate_check_protocols.py` | `_typing.py`, `_engine/_operations.py` |
4142

42-
One script writes three of them. That is the part people miss.
43+
One script writes four of them. That is the part people miss.
4344

4445
- **polling twins** answer `eventually()` and `eventually_sync()`, so every assertion hands back the chain.
4546
- **verdict twins** answer `check()`, returning an `AssertionOutcome` instead of `Self`.
4647
- **the capability façade** stands in for the builder itself, so it reads the mixins rather than the views:
4748
reading the views would narrow it to what one value type answers. It skips its own hand-written list.
49+
- **negation twins** answer `.not_`, where one assertion hands back the view the chain had before it.
4850

4951
`_engine/_operations.py` decides what the first two leave out: `NOT_AN_OPERATION`, `WITHOUT_A_VERDICT`.
5052

@@ -111,7 +113,7 @@ import lines are templates inside the generators.
111113
| `test_typing_completeness.py` | can a checker name a type for every exported symbol |
112114
| `test_pyright_baseline.py` | has a new pyright diagnostic appeared in the package |
113115
| `test_typing_promises.py` | is the object at the end of a chain the type the checker was promised |
114-
| `test_negated_protocols.py` | does every reachable view have a negation twin that hands the view back |
116+
| `test_negated_protocols.py` | does every reachable view have a negation twin that hands the view back, and is the file what the generator produces |
115117
| `test_architecture_doc.py` | does this document still describe the tree, counts included |
116118
| `test_typing_negative.py` | do the checkers still refuse what they should |
117119
| `test_typing_from_a_wheel.py` | does the typed surface survive packaging |
@@ -126,9 +128,9 @@ Which one goes red tells you what you did:
126128

127129
- a declaration the runtime does not have: `test_protocol_parity.py`
128130
- a declaration whose parameters the runtime does not match: `test_typing_conformance.py`
129-
- a generated file edited by hand: that file's own gate, one of the three
130-
- a protocol changed without regenerating: the polling and verdict gates, never the facade one, which
131-
reads the mixins instead
131+
- a generated file edited by hand: that file's own gate, one of the four
132+
- a protocol changed without regenerating: the polling, verdict and negation gates, never the facade one,
133+
which reads the mixins instead
132134
- a subject resolving to the wrong protocol: `test_typing.py`, and only under whichever checker sees it
133135
- a public name added or moved: `test_api_compatibility.py`, which is a snapshot rather than a rule
134136

‎tests/test_architecture_doc.py‎

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,8 @@
2525
_SIZE_ROW = re.compile(
2626
r"^\| `(?P<file>_engine/[a-z_]+\.py)` \| (?P<protocols>\d+) \| (?P<declarations>\d+) \|", re.MULTILINE
2727
)
28-
_TYPED_MODULES = [
29-
"_engine/_typing.py",
30-
"_engine/_check_typing.py",
31-
"_engine/_builder_check_typing.py",
32-
"_engine/_capable_typing.py",
33-
"_engine/_poll_typing.py",
34-
]
35-
"""The five, in the order the table lists them. A sixth added to the package has to reach the table."""
28+
_TYPED_MODULES = sorted(f"_engine/{path.name}" for path in (_ROOT / "assertpy2" / "_engine").glob("*_typing.py"))
29+
"""Read off the tree rather than listed here, which let the negation twins go unrecorded."""
3630

3731

3832
def _named_paths() -> set[str]:
@@ -82,7 +76,8 @@ def test_the_table_of_sizes_is_what_the_files_hold() -> None:
8276
rows = _SIZE_ROW.findall(_DOCUMENT.read_text(encoding="utf-8"))
8377
listed = [module for module, _, _ in rows]
8478

85-
assert_that(listed).described_as("modules the table has a row for, one row each").is_equal_to(_TYPED_MODULES)
79+
assert_that(sorted(listed)).described_as("modules the table has a row for").is_equal_to(_TYPED_MODULES)
80+
assert_that(listed).described_as("rows per module").does_not_contain_duplicates()
8681
assert_that({module: (int(protocols), int(declarations)) for module, protocols, declarations in rows}).described_as(
8782
"protocols and declarations per module, which ARCHITECTURE.md states outright"
8883
).is_equal_to({module: _measured(module) for module in listed})

‎tests/test_capable_protocol.py‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -349,11 +349,11 @@ def _generator() -> ModuleType:
349349
return generate_poll_protocols
350350

351351

352-
def _formatted(source: str) -> str:
353-
"""Both steps the generator runs after writing, so the comparison is of the same thing twice."""
352+
def _formatted(source: str, path: str = _capable_typing.__file__) -> str:
353+
"""*source* after the two steps the generator runs once it has written *path*: ruff format, then ruff's fixes."""
354354
for command in (
355-
["ruff", "format", "--stdin-filename", _capable_typing.__file__, "-"],
356-
["ruff", "check", "--fix", "--quiet", "--stdin-filename", _capable_typing.__file__, "-"],
355+
["ruff", "format", "--stdin-filename", path, "-"],
356+
["ruff", "check", "--fix", "--quiet", "--stdin-filename", path, "-"],
357357
):
358358
result = subprocess.run(
359359
[sys.executable, "-m", *command],

‎tests/test_negated_protocols.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
TRANSFORMS,
2424
WITHOUT_A_VERDICT,
2525
)
26+
from tests.test_capable_protocol import _formatted, _generator
2627

2728
_ROOT = pathlib.Path(__file__).resolve().parent.parent
2829
_VIEWS = _ROOT / "assertpy2" / "_engine" / "_typing.py"
@@ -391,3 +392,15 @@ class _Wanted(_Middle[str], Protocol): ...
391392
assert_that(_bound("_Wanted", known)).described_as("the substitution down each path").is_equal_to(
392393
[("_Wanted", {}), ("_Middle", {"_Y": "str"}), ("_Held", {"_X": "list[str]"}), ("_Plain", {})]
393394
)
395+
396+
397+
def test_the_file_is_what_the_generator_writes() -> None:
398+
"""Every test above checks the rule, so a hand edit that keeps it passed them all and a rerun erased it.
399+
400+
The file as it stands against what the generator writes after ruff, so a formatting-only hand edit
401+
shows too: formatting both sides would even it out. Line endings are read as text on both sides,
402+
so a file differing only in those still passes.
403+
"""
404+
assert_that(_formatted(_generator().generate_negated(), str(_TWINS))).described_as(
405+
"the negation twins are out of step; run python scripts/generate_poll_protocols.py"
406+
).is_equal_to(_TWINS.read_text(encoding="utf-8"))

0 commit comments

Comments
 (0)