|
28 | 28 | collection-time regressions where a test file fails to import and |
29 | 29 | pytest silently drops tens of tests. |
30 | 30 |
|
| 31 | +4. ``test_session_skipped_count_below_ceiling`` — per-lane skip-count |
| 32 | + ceiling. Catches the inverse of #3: collection size unchanged but |
| 33 | + tests transition pass→skip silently because a marker was applied |
| 34 | + too broadly. The conftest documents a prior incident of this kind |
| 35 | + (PR #1375 audit, 14 ``supervisor_mock`` tests silently skipping on |
| 36 | + every testcontainer run — see ``tests/src/e2e/conftest.py:158-166``). |
| 37 | +
|
31 | 38 | This file is placed under ``basic/`` (NOT ``haos_only/``) on purpose: the |
32 | 39 | auto-applied ``haos_only`` marker would skip these whenever |
33 | 40 | ``is_haos_backend_selected()`` returns False, which is exactly the |
|
42 | 49 |
|
43 | 50 | from ..utilities.assertions import safe_call_tool |
44 | 51 |
|
45 | | -# Floor for total collected tests across all lanes. As of 2026-05-22 each |
46 | | -# lane collects ~913 tests (just differing skip mix per mode). Set well |
47 | | -# below current value to allow normal test-add/remove churn while still |
48 | | -# catching the case where ~50+ tests vanish from collection. |
| 52 | +# Floor for total collected tests across all lanes. As of 2026-05-22 |
| 53 | +# container lanes collect 912 and HAOS lanes collect 915 (small per-mode |
| 54 | +# variance from parametrize/fixture-driven cases). Floor sits well below |
| 55 | +# all three to allow normal test-add/remove churn while still catching |
| 56 | +# the case where ~50+ tests vanish from collection. |
49 | 57 | _COLLECTION_FLOOR = 850 |
50 | 58 |
|
| 59 | +# Per-lane ceilings for the count of skip-marked tests. Set 5-9 above |
| 60 | +# current per-lane skip counts (as of 2026-05-22: container=46, |
| 61 | +# haos=14, haos_inaddon=22). A buffer of 5-9 absorbs PRs that |
| 62 | +# legitimately add a few new marker-gated tests, but catches a |
| 63 | +# mass-skip incident like PR #1375 (14 tests started skipping silently |
| 64 | +# because a marker was applied too broadly). |
| 65 | +_SKIP_CEILING_PER_LANE = { |
| 66 | + "container": 55, |
| 67 | + "haos": 20, |
| 68 | + "haos_inaddon": 30, |
| 69 | +} |
| 70 | + |
51 | 71 |
|
52 | 72 | def test_backend_dispatch_matches_workflow_env( |
53 | 73 | ha_container_with_fresh_config: dict[str, Any], |
54 | 74 | ) -> None: |
55 | 75 | """Conftest dispatch must pick the backend the workflow env implies. |
56 | 76 |
|
57 | | - Runs unconditionally on every lane — branches off env vars to mirror |
58 | | - conftest's own dispatch logic. Mismatch means the dispatch silently |
59 | | - picked a different backend than CI asked for. |
| 77 | + Runs unconditionally on every lane — branches off env vars to |
| 78 | + approximate conftest's dispatch logic. (Conftest's |
| 79 | + ``is_haos_backend_selected`` additionally requires the qcow2 file |
| 80 | + to exist on disk; this test only checks env-var truthiness. The |
| 81 | + test deliberately re-derives the expected backend independently |
| 82 | + rather than calling the same helper, so a helper-side regression |
| 83 | + can't make the test silently agree with the bug.) Mismatch means |
| 84 | + the dispatch silently picked a different backend than CI asked for. |
60 | 85 | """ |
61 | 86 | image_path = os.environ.get("HAOS_TEST_IMAGE_PATH") |
62 | 87 | mode = os.environ.get("HAOS_TEST_MODE", "") |
@@ -108,8 +133,12 @@ async def test_supervisor_addon_tool_behavior_matches_backend( |
108 | 133 |
|
109 | 134 | - HAOS external + inaddon: ``ha_get_addon`` returns a populated |
110 | 135 | addons list (the bake installs several addons). |
111 | | - - testcontainer: ``ha_get_addon`` returns ``success=False`` because |
112 | | - the Supervisor proxy endpoint is unreachable. |
| 136 | + - testcontainer: ``ha_get_addon`` raises ToolError |
| 137 | + (RESOURCE_NOT_FOUND from the ``supervisor/api`` WebSocket proxy |
| 138 | + because no Supervisor is running); ``safe_call_tool`` catches |
| 139 | + and decodes the structured error to ``{"success": False, ...}``. |
| 140 | + The dict conversion is load-bearing — a future maintainer should |
| 141 | + NOT switch to ``assert_mcp_failure`` or similar. |
113 | 142 |
|
114 | 143 | The asymmetry of this check makes it impossible for one backend to |
115 | 144 | impersonate the other while keeping this test green. |
@@ -145,12 +174,59 @@ def test_session_collected_test_count_above_floor(request: Any) -> None: |
145 | 174 |
|
146 | 175 | Catches collection-time regressions: a test file fails to import, |
147 | 176 | pytest collects tens of fewer tests, the suite stays green with |
148 | | - reduced coverage. Collection count is mode-independent (all lanes |
149 | | - collect the same items, mode only changes pass/skip mix). |
| 177 | + reduced coverage. Collection count varies by a handful across |
| 178 | + modes (parametrize/fixture-driven — currently 912 container vs |
| 179 | + 915 HAOS lanes); the floor sits well below all three lanes' actuals. |
150 | 180 | """ |
151 | 181 | total = len(request.session.items) |
152 | 182 | assert total >= _COLLECTION_FLOOR, ( |
153 | 183 | f"Only {total} tests collected, expected >= {_COLLECTION_FLOOR}. " |
154 | 184 | f"A test file likely failed to import, dropping coverage. Check " |
155 | 185 | f"for collection errors in the pytest output." |
156 | 186 | ) |
| 187 | + |
| 188 | + |
| 189 | +def test_session_skipped_count_below_ceiling( |
| 190 | + request: Any, |
| 191 | + ha_container_with_fresh_config: dict[str, Any], |
| 192 | +) -> None: |
| 193 | + """Per-lane skip-count must stay below ``_SKIP_CEILING_PER_LANE[backend]``. |
| 194 | +
|
| 195 | + Catches the inverse of the collection-floor check: the suite still |
| 196 | + collects the expected total, but tests transition pass→skip silently |
| 197 | + because a marker was applied too broadly in conftest's |
| 198 | + ``pytest_collection_modifyitems`` hook. |
| 199 | +
|
| 200 | + The conftest itself documents a real prior incident of this kind |
| 201 | + (``tests/src/e2e/conftest.py:158-166`` — PR #1375 audit, 14 |
| 202 | + ``supervisor_mock`` tests silently skipping on every testcontainer |
| 203 | + run because an ``external_only`` skip was scoped wrong). A |
| 204 | + skip-count ceiling per lane catches that whole class of bug. |
| 205 | +
|
| 206 | + Ceilings sit 5-9 above current per-lane skip counts; updates are |
| 207 | + only required when a PR legitimately introduces enough new |
| 208 | + marker-gated tests to cross the threshold (uncommon). |
| 209 | + """ |
| 210 | + backend = ha_container_with_fresh_config["backend"] |
| 211 | + ceiling = _SKIP_CEILING_PER_LANE.get(backend) |
| 212 | + assert ceiling is not None, ( |
| 213 | + f"Unknown backend {backend!r} — add to _SKIP_CEILING_PER_LANE " |
| 214 | + f"at the top of this file" |
| 215 | + ) |
| 216 | + # Items in request.session.items already have skip markers applied |
| 217 | + # by pytest_collection_modifyitems (which ran before any test). |
| 218 | + # Under pytest-xdist with --dist loadscope, each worker collects |
| 219 | + # the full session, so this count is consistent per worker. |
| 220 | + skipped = sum( |
| 221 | + 1 |
| 222 | + for item in request.session.items |
| 223 | + if any(m.name == "skip" for m in item.iter_markers()) |
| 224 | + ) |
| 225 | + assert skipped <= ceiling, ( |
| 226 | + f"{skipped} tests have skip markers on the {backend} lane, " |
| 227 | + f"which exceeds the ceiling of {ceiling}. A marker may be " |
| 228 | + f"applied too broadly in pytest_collection_modifyitems — " |
| 229 | + f"check tests/src/e2e/conftest.py:115-168 for recent changes. " |
| 230 | + f"If the increase is intentional (legitimate new marker-gated " |
| 231 | + f"tests), bump _SKIP_CEILING_PER_LANE[{backend!r}] in this file." |
| 232 | + ) |
0 commit comments