Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 11 additions & 6 deletions docs/adr/0006-docker-rejection-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,15 @@ named volume) ([#114](https://github.com/modern-python/compose2pod/issues/114)),
`mode` is refused at the other end of the range, where podman 6.0.1's `crun` will not mount it.
The `integration` job pins `ubuntu-24.04` for the same reason: it is the runner that ships the
floor, and on a newer one the job would measure a podman no user of the floor has.
Two residuals are open by design: `depends_on` errors among services outside the
target's closure are accepted here and rejected by Docker
([#87](https://github.com/modern-python/compose2pod/issues/87)), and the drive-qualified *bind*
(`C:\data:/var`) is a limitation rather than rule two, since podman mounts that source through
`--mount` and only the short `-v` spec cannot spell it -- the long form already emits `--mount`, so
the capability is reachable today and only the short spelling is missing
Two residuals are open by design. `depends_on` errors among services outside the target's closure
are accepted here and rejected by Docker
([#87](https://github.com/modern-python/compose2pod/issues/87)): the one place the hard rule is
knowingly broken, so it is executed rather than described. `tests/conformance/corpus_residual/`
holds both documents, the summary prints them, and the test fails when a residual *closes*, since a
catalogue nobody re-runs goes stale in the direction that looks green. `assert_rule` still raises
for every document outside that directory, so the rule stays hard everywhere it is not deliberately
suspended. The other residual is the drive-qualified *bind* (`C:\data:/var`), a limitation rather
than rule two, since podman mounts that source through `--mount` and only the short `-v` spec
cannot spell it -- the long form already emits `--mount`, so the capability is reachable today and
only the short spelling is missing
([#111](https://github.com/modern-python/compose2pod/issues/111)).
39 changes: 38 additions & 1 deletion tests/conformance/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
# global so it is unambiguously one collector per pytest run, not one per import.
_OVER_REJECTIONS: pytest.StashKey[list[str]] = pytest.StashKey()

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


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


def pytest_terminal_summary(terminalreporter: pytest.TerminalReporter) -> None:
"""Print every over-reject verdict collected this run.
"""Print every over-reject verdict and every confirmed residual collected this run.

Over-rejections never fail the build (see `assert_rule`); this is the harness's
only way of keeping them visible, which is what the tracked-limitation issues
promise. Silent when nothing was collected, which is the normal
case for `just test-ci` (the conformance suite is deselected there and this hook
never runs a probe, so the list stays empty).
"""
residuals = terminalreporter.config.stash.get(_RESIDUALS, [])
if residuals:
terminalreporter.section("conformance: rule-one residuals (docker rejects, compose2pod accepts)")
for label in residuals:
terminalreporter.write_line(label)
terminalreporter.write_line(
f"{len(residuals)} residual(s) -- the hard rule, knowingly broken; "
"https://github.com/modern-python/compose2pod/issues/87"
)
over_rejections = terminalreporter.config.stash.get(_OVER_REJECTIONS, [])
if not over_rejections:
return
Expand Down Expand Up @@ -145,3 +161,24 @@ def _assert(compose: dict[str, Any]) -> str:
return "over-reject"

return _assert


@pytest.fixture
def assert_residual(tmp_path: Path, request: pytest.FixtureRequest) -> Callable[[dict[str, Any]], None]:
"""Assert one document still breaks rule one, and record it for the run's summary.

The inverse of `assert_rule`, which raises on this combination: here it is the
expected result, and either half changing is what fails. A residual that closed
leaves a file claiming a breach that no longer exists, which is worse than no
catalogue at all.
"""

def _assert(compose: dict[str, Any]) -> None:
text = yaml.safe_dump(compose, sort_keys=False)
assert not _docker_accepts(text, tmp_path), "docker now accepts this document, so it documents no residual"
assert _compose2pod_accepts(text, tmp_path), (
"compose2pod now rejects this document -- the residual is closed, delete the file"
)
request.config.stash[_RESIDUALS].append(request.node.nodeid)

return _assert
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Docker rejects the document ("dependency cycle detected"); compose2pod accepts it,
# because `app` is the target and its closure never reaches `a` or `b`. Residual, issue 87.
services:
app:
image: nginx
a:
image: nginx
depends_on:
- b
b:
image: nginx
depends_on:
- a
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Docker rejects the document ("depends on undefined service"); compose2pod accepts it,
# because `app` is the target and its closure never reaches `other`. Residual, issue 87.
services:
app:
image: nginx
other:
image: nginx
depends_on:
- ghost
33 changes: 33 additions & 0 deletions tests/conformance/test_residuals.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""The documents where rule one is knowingly broken, probed instead of described.

ADR-0006 calls `accepted(compose2pod) ⊆ accepted(docker)` hard, and issue 87 records two
exceptions as a deliberate ruling: a `depends_on` naming an undefined service, and a
dependency cycle, both on services outside the `--target`'s closure, which `startup_order`
never walks. `assert_rule` raises on exactly that combination, so neither could live in
`corpus/` -- and so neither was measured anywhere, which left the one hard rule's known
breach resting on prose. These files are that breach, executed.
"""

from collections.abc import Callable
from pathlib import Path
from typing import Any

import pytest
import yaml


_RESIDUAL_CORPUS = sorted((Path(__file__).parent / "corpus_residual").glob("*.yaml"))


@pytest.mark.parametrize("path", _RESIDUAL_CORPUS, ids=lambda p: p.stem)
def test_a_catalogued_residual_still_breaks_rule_one(
path: Path, assert_residual: Callable[[dict[str, Any]], None]
) -> None:
"""A file here fails when the residual *closes*, which is when it should be deleted.

Tolerating a catalogued exception is not the point -- an entry nobody re-runs goes
stale in the direction that looks green, which is how issue 86's unmeasured claim
survived long enough to ship. Cataloguing it is only worth anything if the catalogue
is wrong when the world changes.
"""
assert_residual(yaml.safe_load(path.read_text()))
Loading