Files
hermes-agent/tests/test_platforms_marker.py
ethernet 9b8ad35ee6 refactor(tests): platforms() fully replaces the linux_only/macos_only/windows_only trio
No shims, one marker system. The full mechanical sweep:

- 165 test files converted: every @pytest.mark.<os>_only decorator and
  pytestmark assignment is now platforms("<os>"). Docstring/comment
  prose mentioning the trio rewritten to the platforms() vocabulary.
- conftest: _OS_MARKS, the legacy skip loop, the double-mark reject for
  the trio, and the selector-tagging shim are deleted. The single
  platforms() gate does all host gating; _reject_contradictory_platform_
  marks replaces the old reject (one platforms() marker per test — a
  module-level pytestmark stacked on a per-test marker is the historic
  skipped-everywhere-green-everywhere failure and stays a hard error).
- pyproject: only the platforms marker is registered.
- list_os_marked_tests.py: rewritten for the single vocabulary — takes
  a platform name (linux/macos/windows), matches quoted platforms(...)
  specs including negated and any-of forms, exits nonzero on empty
  selection. Its test file rewritten to match (bare identifiers must
  not match; the spec must appear inside the string literal).
- tests.yml macOS lane: marker: macos feeds the lister; the pytest
  selection is -m "platforms and not integration" — the marker name is
  the selector, the conftest's per-test host skips are the gate.
- test_os_marker_gating.py rewritten for the new reject (the old file
  tested deleted machinery).
- AGENTS.md / CONTRIBUTING.md updated to the single vocabulary.

Verified: full-tree compile; marker unit tests (36); lister tests;
macOS-lane simulation (21 files, 180 collected / 429 deselected on a
windows host); broad xdist slices through scripts/run_tests.sh
(367 + 1674 passed, zero refactor-attributable failures — the 6 red
tests in the second slice fail identically with the changes stashed).
2026-08-31 17:53:44 -04:00

164 lines
6.0 KiB
Python

"""The composable ``platforms`` marker: spec evaluation + collection gating.
Behavior-contract tests for the gate introduced alongside the fixed
platforms("linux")/platforms("macos")/platforms("windows") trio: any-of semantics, negation,
POSIX grouping, arch filters, and the hard errors on unknown specs and
stray keyword arguments.
"""
from __future__ import annotations
import pytest
from tests.conftest import _host_matches_platforms
class TestSpecEvaluation:
"""Pure evaluation: takes the host as data, no host faking."""
# (specs, host_platform, expected_ok)
CASES = [
(("linux",), "linux", True),
(("linux",), "win32", False),
(("macos",), "darwin", True),
(("windows",), "win32", True),
(("windows",), "linux", False),
(("posix",), "linux", True),
(("posix",), "darwin", True),
(("posix",), "win32", False),
(("not macos",), "linux", True),
(("not macos",), "darwin", False),
(("not windows",), "win32", False),
(("not windows",), "linux", True),
(("linux", "win32host-mismatch"), "win32", False), # unknown spec never matches
(("any",), "linux", True),
((), "linux", True), # no specs = documentation form, matches all
]
@pytest.mark.parametrize(("specs", "host", "expected"), CASES)
def test_spec_matrix(self, specs, host, expected, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", host)
ok, _reason = _host_matches_platforms(specs)
assert ok is expected
def test_unknown_spec_is_reported_not_matched(self, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "linux")
ok, reason = _host_matches_platforms(("amiga",))
assert ok is False
assert "unknown spec" in reason
def test_negation_of_unknown_spec_is_rejected(self, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "linux")
ok, reason = _host_matches_platforms(("not amiga",))
assert ok is False
assert "unknown spec" in reason
def test_case_insensitive_specs(self, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "win32")
ok, _ = _host_matches_platforms(("WINDOWS",))
assert ok is True
class TestArchFilter:
@pytest.mark.parametrize(
("arch", "machine", "negate", "expected"),
[
("arm64", "arm64", False, True),
("arm64", "x86_64", False, False),
("aarch64", "arm64", False, True), # alias
("arm64", "arm64", True, False),
("arm64", "x86_64", True, True),
],
)
def test_arch_matrix(self, arch, machine, negate, expected, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "win32")
monkeypatch.setattr("tests.conftest._platform_machine", lambda: machine)
ok, reason = _host_matches_platforms(("windows",), arch=arch, arch_negate=negate)
assert ok is expected, reason
def test_arch_reason_names_the_machine(self, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "win32")
monkeypatch.setattr("tests.conftest._platform_machine", lambda: "x86_64")
ok, reason = _host_matches_platforms(("windows",), arch="arm64")
assert ok is False
assert "x86_64" in reason
class TestAnyOfSemantics:
def test_multiple_specs_are_any_of(self, monkeypatch):
monkeypatch.setattr("tests.conftest.sys.platform", "darwin")
ok, _ = _host_matches_platforms(("linux", "macos"))
assert ok is True
def test_first_matching_spec_wins_over_later_unknown(self, monkeypatch):
# any-of: a matching spec satisfies the gate even if a later spec
# is garbage — unknown specs only matter when nothing matched.
monkeypatch.setattr("tests.conftest.sys.platform", "linux")
ok, _ = _host_matches_platforms(("linux", "amiga"))
assert ok is True
class TestCollectionGating:
"""The marker must actually skip/gate collected items on this host."""
@pytest.mark.platforms("not " + __import__("sys").platform.split("_")[0])
def test_never_runs_on_this_host_shape(self):
# The spec is built to exclude whatever this host is (linux → "not
# linux", win32 → "not windows"); if it RUNS the gate is broken.
raise AssertionError("platforms() gate failed to skip this host")
@pytest.mark.platforms("any")
def test_any_spec_runs_everywhere(self):
assert True
@pytest.mark.skipif(
__import__("sys").platform == "win32",
reason="linux-host assertion; inverted on the linux lane below",
)
@pytest.mark.platforms("linux")
def test_runs_on_linux(self):
assert True
class TestHardErrors:
def test_stray_kwarg_is_a_usage_error(self):
# The gate raises UsageError (surfaced by pytest as a collection
# error) for keyword arguments it does not understand — evaluated
# directly because the raise happens inside the project conftest's
# collection hook.
import pytest as _pytest
from tests.conftest import _platforms_gate_reason
class _Item:
nodeid = "tests/x.py::test_x"
@staticmethod
def iter_markers(name):
yield _pytest.mark.platforms("linux", bogus=True).mark
with _pytest.raises(_pytest.UsageError, match="unexpected keyword"):
_platforms_gate_reason(_Item)
class TestMachineAliases:
"""_platform_machine normalizes the raw platform.machine() spellings."""
@pytest.mark.parametrize(
("raw", "normalized"),
[
("AMD64", "x86_64"),
("x86", "x86_64"),
("aarch64", "arm64"),
("arm64", "arm64"),
("x86_64", "x86_64"),
],
)
def test_alias_matrix(self, raw, normalized, monkeypatch):
import platform as _platform
monkeypatch.setattr(_platform, "machine", lambda: raw)
from tests.conftest import _platform_machine
assert _platform_machine() == normalized