Skip to content

Commit b8603da

Browse files
authored
test: execute the two rule-one residuals instead of describing them (#128)
`accepted(compose2pod) subset-of accepted(docker)` is the hard rule, and issue 87 records two exceptions to it. Neither could live in `corpus/`, because `assert_rule` raises on exactly that combination, so the one known breach of the hard rule rested on prose. `corpus_residual/` holds both documents and `assert_residual` asserts the breach is still real, printing it in the terminal summary. It fails when a residual closes: a catalogue nobody re-runs goes stale in the direction that looks green. Every document outside that directory still goes through `assert_rule`. Closes #127
1 parent 6f45803 commit b8603da

5 files changed

Lines changed: 104 additions & 7 deletions

File tree

‎docs/adr/0006-docker-rejection-parity.md‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,15 @@ named volume) ([#114](https://github.com/modern-python/compose2pod/issues/114)),
5252
`mode` is refused at the other end of the range, where podman 6.0.1's `crun` will not mount it.
5353
The `integration` job pins `ubuntu-24.04` for the same reason: it is the runner that ships the
5454
floor, and on a newer one the job would measure a podman no user of the floor has.
55-
Two residuals are open by design: `depends_on` errors among services outside the
56-
target's closure are accepted here and rejected by Docker
57-
([#87](https://github.com/modern-python/compose2pod/issues/87)), and the drive-qualified *bind*
58-
(`C:\data:/var`) is a limitation rather than rule two, since podman mounts that source through
59-
`--mount` and only the short `-v` spec cannot spell it -- the long form already emits `--mount`, so
60-
the capability is reachable today and only the short spelling is missing
55+
Two residuals are open by design. `depends_on` errors among services outside the target's closure
56+
are accepted here and rejected by Docker
57+
([#87](https://github.com/modern-python/compose2pod/issues/87)): the one place the hard rule is
58+
knowingly broken, so it is executed rather than described. `tests/conformance/corpus_residual/`
59+
holds both documents, the summary prints them, and the test fails when a residual *closes*, since a
60+
catalogue nobody re-runs goes stale in the direction that looks green. `assert_rule` still raises
61+
for every document outside that directory, so the rule stays hard everywhere it is not deliberately
62+
suspended. The other residual is the drive-qualified *bind* (`C:\data:/var`), a limitation rather
63+
than rule two, since podman mounts that source through `--mount` and only the short `-v` spec
64+
cannot spell it -- the long form already emits `--mount`, so the capability is reachable today and
65+
only the short spelling is missing
6166
([#111](https://github.com/modern-python/compose2pod/issues/111)).

‎tests/conformance/conftest.py‎

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
# global so it is unambiguously one collector per pytest run, not one per import.
3535
_OVER_REJECTIONS: pytest.StashKey[list[str]] = pytest.StashKey()
3636

37+
# Every catalogued rule-one residual confirmed this run, as `<corpus-stem>` labels.
38+
# Kept apart from the over-rejections: an over-rejection is allowed by the rule, while
39+
# a residual is the rule being broken on purpose (issue 87), and reading them in one
40+
# list would blur the two directions the whole harness exists to keep apart.
41+
_RESIDUALS: pytest.StashKey[list[str]] = pytest.StashKey()
42+
3743

3844
@pytest.hookimpl(tryfirst=True)
3945
def pytest_collection_modifyitems(items: "list[pytest.Item]") -> None:
@@ -46,17 +52,27 @@ def pytest_collection_modifyitems(items: "list[pytest.Item]") -> None:
4652
def pytest_configure(config: pytest.Config) -> None:
4753
"""Create this run's over-rejection collector before any conformance test executes."""
4854
config.stash[_OVER_REJECTIONS] = []
55+
config.stash[_RESIDUALS] = []
4956

5057

5158
def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter) -> None:
52-
"""Print every over-reject verdict collected this run.
59+
"""Print every over-reject verdict and every confirmed residual collected this run.
5360
5461
Over-rejections never fail the build (see `assert_rule`); this is the harness's
5562
only way of keeping them visible, which is what the tracked-limitation issues
5663
promise. Silent when nothing was collected, which is the normal
5764
case for `just test-ci` (the conformance suite is deselected there and this hook
5865
never runs a probe, so the list stays empty).
5966
"""
67+
residuals = terminalreporter.config.stash.get(_RESIDUALS, [])
68+
if residuals:
69+
terminalreporter.section("conformance: rule-one residuals (docker rejects, compose2pod accepts)")
70+
for label in residuals:
71+
terminalreporter.write_line(label)
72+
terminalreporter.write_line(
73+
f"{len(residuals)} residual(s) -- the hard rule, knowingly broken; "
74+
"https://github.com/modern-python/compose2pod/issues/87"
75+
)
6076
over_rejections = terminalreporter.config.stash.get(_OVER_REJECTIONS, [])
6177
if not over_rejections:
6278
return
@@ -145,3 +161,24 @@ def _assert(compose: dict[str, Any]) -> str:
145161
return "over-reject"
146162

147163
return _assert
164+
165+
166+
@pytest.fixture
167+
def assert_residual(tmp_path: Path, request: pytest.FixtureRequest) -> Callable[[dict[str, Any]], None]:
168+
"""Assert one document still breaks rule one, and record it for the run's summary.
169+
170+
The inverse of `assert_rule`, which raises on this combination: here it is the
171+
expected result, and either half changing is what fails. A residual that closed
172+
leaves a file claiming a breach that no longer exists, which is worse than no
173+
catalogue at all.
174+
"""
175+
176+
def _assert(compose: dict[str, Any]) -> None:
177+
text = yaml.safe_dump(compose, sort_keys=False)
178+
assert not _docker_accepts(text, tmp_path), "docker now accepts this document, so it documents no residual"
179+
assert _compose2pod_accepts(text, tmp_path), (
180+
"compose2pod now rejects this document -- the residual is closed, delete the file"
181+
)
182+
request.config.stash[_RESIDUALS].append(request.node.nodeid)
183+
184+
return _assert
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Docker rejects the document ("dependency cycle detected"); compose2pod accepts it,
2+
# because `app` is the target and its closure never reaches `a` or `b`. Residual, issue 87.
3+
services:
4+
app:
5+
image: nginx
6+
a:
7+
image: nginx
8+
depends_on:
9+
- b
10+
b:
11+
image: nginx
12+
depends_on:
13+
- a
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Docker rejects the document ("depends on undefined service"); compose2pod accepts it,
2+
# because `app` is the target and its closure never reaches `other`. Residual, issue 87.
3+
services:
4+
app:
5+
image: nginx
6+
other:
7+
image: nginx
8+
depends_on:
9+
- ghost
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
"""The documents where rule one is knowingly broken, probed instead of described.
2+
3+
ADR-0006 calls `accepted(compose2pod) ⊆ accepted(docker)` hard, and issue 87 records two
4+
exceptions as a deliberate ruling: a `depends_on` naming an undefined service, and a
5+
dependency cycle, both on services outside the `--target`'s closure, which `startup_order`
6+
never walks. `assert_rule` raises on exactly that combination, so neither could live in
7+
`corpus/` -- and so neither was measured anywhere, which left the one hard rule's known
8+
breach resting on prose. These files are that breach, executed.
9+
"""
10+
11+
from collections.abc import Callable
12+
from pathlib import Path
13+
from typing import Any
14+
15+
import pytest
16+
import yaml
17+
18+
19+
_RESIDUAL_CORPUS = sorted((Path(__file__).parent / "corpus_residual").glob("*.yaml"))
20+
21+
22+
@pytest.mark.parametrize("path", _RESIDUAL_CORPUS, ids=lambda p: p.stem)
23+
def test_a_catalogued_residual_still_breaks_rule_one(
24+
path: Path, assert_residual: Callable[[dict[str, Any]], None]
25+
) -> None:
26+
"""A file here fails when the residual *closes*, which is when it should be deleted.
27+
28+
Tolerating a catalogued exception is not the point -- an entry nobody re-runs goes
29+
stale in the direction that looks green, which is how issue 86's unmeasured claim
30+
survived long enough to ship. Cataloguing it is only worth anything if the catalogue
31+
is wrong when the world changes.
32+
"""
33+
assert_residual(yaml.safe_load(path.read_text()))

0 commit comments

Comments
 (0)