Skip to content

Commit 696777d

Browse files
committed
tests/interaction: add compute_cells/cell_id helpers and __post_init__ self-tests
compute_cells() expands a test's stacked @requirement marks into the (transport, spec_version) pytest.param list with intersection semantics: a cell is dropped if any requirement's [added_in, removed_in) window or arm_exclusions excludes it, and marked xfail-strict if any requirement's known_failures matches it. Requirement.transports is intentionally not consulted (descriptive metadata only). cell_id() keeps node-id suffixes as bare transport names while SPEC_VERSIONS has a single entry. test_coverage.py gains pytest.raises self-tests for every new __post_init__ validation branch on ArmExclusion / KnownFailure / Requirement. 12 passed; pyright/ruff clean; conftest not yet wired. Claude-Session: https://claude.ai/code/session_01NF95tmzF6RhVPktJdL6fK5
1 parent 118587b commit 696777d

2 files changed

Lines changed: 125 additions & 3 deletions

File tree

tests/interaction/_requirements.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@
3333
"""
3434

3535
import re
36-
from collections.abc import Callable
36+
from collections.abc import Callable, Sequence
3737
from dataclasses import dataclass
38-
from typing import Literal, TypeVar
38+
from typing import Any, Literal, TypeVar
3939

4040
import pytest
4141

@@ -2889,3 +2889,74 @@ def apply(test_fn: _TestFn) -> _TestFn:
28892889
def covered_by(requirement_id: str) -> list[str]:
28902890
"""Return the (mutable) list of test names recorded as exercising `requirement_id`."""
28912891
return _COVERAGE.setdefault(requirement_id, [])
2892+
2893+
2894+
def cell_id(transport: Transport, version: SpecVersion, *, spec_versions: Sequence[SpecVersion] = SPEC_VERSIONS) -> str:
2895+
"""Return the pytest node-id suffix for a (transport, spec_version) cell.
2896+
2897+
While the active matrix has a single spec version, the suffix is just the transport name so
2898+
existing node ids stay byte-identical; once a second version is on the axis the suffix becomes
2899+
``transport-version``.
2900+
"""
2901+
return transport if len(spec_versions) == 1 else f"{transport}-{version}"
2902+
2903+
2904+
def compute_cells(
2905+
requirements: Sequence[Requirement],
2906+
*,
2907+
spec_versions: Sequence[SpecVersion] = SPEC_VERSIONS,
2908+
transports: Sequence[Transport] = CONNECTABLE_TRANSPORTS,
2909+
) -> list[Any]:
2910+
"""Compute the (transport, spec_version) parametrization cells for a test.
2911+
2912+
Stacked ``@requirement`` decorators contribute multiple entries; the cells emitted are the
2913+
INTERSECTION across all of them: a cell is dropped if it falls outside any requirement's
2914+
``[added_in, removed_in)`` window or matches any requirement's ``arm_exclusions``. An empty
2915+
``requirements`` sequence yields the full transport x spec-version grid.
2916+
2917+
``Requirement.transports`` is intentionally NOT consulted -- it is descriptive metadata about
2918+
where a behaviour is observable, not a cell filter (only ``arm_exclusions`` / ``added_in`` /
2919+
``removed_in`` drive cell generation).
2920+
2921+
Returns a list of ``pytest.param((transport, version), id=..., marks=...)`` values for use as
2922+
``metafunc.parametrize`` argvalues.
2923+
"""
2924+
cells: list[Any] = []
2925+
for version in spec_versions:
2926+
version_ordinal = KNOWN_PROTOCOL_VERSIONS.index(version)
2927+
for transport in sorted(transports):
2928+
if transport in TRANSPORT_SPEC_VERSIONS and version not in TRANSPORT_SPEC_VERSIONS[transport]:
2929+
continue
2930+
# Requirement.transports is descriptive metadata only and does not filter cells.
2931+
if any(
2932+
(req.added_in is not None and version_ordinal < KNOWN_PROTOCOL_VERSIONS.index(req.added_in))
2933+
or (req.removed_in is not None and version_ordinal >= KNOWN_PROTOCOL_VERSIONS.index(req.removed_in))
2934+
for req in requirements
2935+
):
2936+
continue
2937+
if any(
2938+
(ex.transport is None or ex.transport == transport)
2939+
and (ex.spec_version is None or ex.spec_version == version)
2940+
for req in requirements
2941+
for ex in req.arm_exclusions
2942+
):
2943+
continue
2944+
matched_failure = next(
2945+
(
2946+
kf
2947+
for req in requirements
2948+
for kf in req.known_failures
2949+
if (kf.transport is None or kf.transport == transport)
2950+
and (kf.spec_version is None or kf.spec_version == version)
2951+
),
2952+
None,
2953+
)
2954+
marks = [pytest.mark.xfail(reason=matched_failure.note, strict=True)] if matched_failure else ()
2955+
cells.append(
2956+
pytest.param(
2957+
(transport, version),
2958+
id=cell_id(transport, version, spec_versions=spec_versions),
2959+
marks=marks,
2960+
)
2961+
)
2962+
return cells

tests/interaction/test_coverage.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,19 @@
1111
import re
1212
from pathlib import Path
1313
from types import ModuleType
14+
from typing import cast
1415

1516
import pytest
1617

17-
from tests.interaction._requirements import REQUIREMENTS, Requirement, covered_by, requirement
18+
from tests.interaction._requirements import (
19+
REQUIREMENTS,
20+
ArmExclusion,
21+
KnownFailure,
22+
Requirement,
23+
SpecVersion,
24+
covered_by,
25+
requirement,
26+
)
1827

1928
_SUITE_ROOT = Path(__file__).parent
2029
_REPO_ROOT = _SUITE_ROOT.parent.parent
@@ -103,3 +112,45 @@ def test_invalid_requirement_source_is_rejected() -> None:
103112
"""A requirement whose source is not a spec URL, 'sdk', or an issue reference fails at construction."""
104113
with pytest.raises(ValueError, match="source must be a specification URL"):
105114
Requirement(source="https://example.com/not-the-spec", behavior="Never constructed.")
115+
116+
117+
def test_arm_exclusion_with_unknown_spec_version_is_rejected() -> None:
118+
"""An arm exclusion naming a spec version outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
119+
with pytest.raises(ValueError, match="is not in KNOWN_PROTOCOL_VERSIONS"):
120+
ArmExclusion(reason="requires-session", spec_version=cast("SpecVersion", "2099-01-01"))
121+
122+
123+
def test_known_failure_with_empty_note_is_rejected() -> None:
124+
"""A known failure with a blank note fails at construction."""
125+
with pytest.raises(ValueError, match="note must be non-empty"):
126+
KnownFailure(note=" ")
127+
128+
129+
def test_known_failure_with_unknown_spec_version_is_rejected() -> None:
130+
"""A known failure naming a spec version outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
131+
with pytest.raises(ValueError, match="is not in KNOWN_PROTOCOL_VERSIONS"):
132+
KnownFailure(note="x", spec_version=cast("SpecVersion", "2099-01-01"))
133+
134+
135+
def test_known_failure_with_malformed_issue_is_rejected() -> None:
136+
"""A known failure whose issue reference is neither '#<n>' nor a GitHub URL fails at construction."""
137+
with pytest.raises(ValueError, match="must be '#<n>' or a GitHub URL"):
138+
KnownFailure(note="x", issue="not-a-link")
139+
140+
141+
def test_requirement_with_unknown_added_in_is_rejected() -> None:
142+
"""A requirement whose added_in is outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
143+
with pytest.raises(ValueError, match="added_in .* is not in KNOWN_PROTOCOL_VERSIONS"):
144+
Requirement(source="sdk", behavior="x", added_in=cast("SpecVersion", "2099-01-01"))
145+
146+
147+
def test_requirement_with_unknown_removed_in_is_rejected() -> None:
148+
"""A requirement whose removed_in is outside KNOWN_PROTOCOL_VERSIONS fails at construction."""
149+
with pytest.raises(ValueError, match="removed_in .* is not in KNOWN_PROTOCOL_VERSIONS"):
150+
Requirement(source="sdk", behavior="x", removed_in=cast("SpecVersion", "2099-01-01"))
151+
152+
153+
def test_requirement_with_empty_version_range_is_rejected() -> None:
154+
"""A requirement whose added_in is not strictly earlier than its removed_in fails at construction."""
155+
with pytest.raises(ValueError, match="must be earlier than"):
156+
Requirement(source="sdk", behavior="x", added_in="2025-11-25", removed_in="2025-11-25")

0 commit comments

Comments
 (0)