41 lines
2.0 KiB
Python
41 lines
2.0 KiB
Python
"""Merge-order-safe expected failures for live gaps whose fix is an open PR.
|
|
|
|
A plain ``xfail(strict=True)`` turns main red the moment its fix merges (XPASS), and a
|
|
non-strict one guards nothing. ``known_failure`` is a run-time xfail keyed on the gap's own
|
|
failure message: the cell XFAILs only while it fails exactly that way, fails loudly on any other
|
|
failure (a timeout, a lost reply, a boot failure), and simply passes once the fix lands, whichever
|
|
merges first. Wrap only the final assertions. When a fix has landed, delete its ``known_failure``.
|
|
Probe-gated gaps live in ``tests/e2e/core/delivery/_pending_fixes.py``.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import re
|
|
from typing import ContextManager, Iterator, Mapping, Tuple, Type
|
|
|
|
import pytest
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def known_failure(pattern: str, reason: str,
|
|
raises: Type[BaseException] | Tuple[Type[BaseException], ...] = AssertionError) -> Iterator[None]:
|
|
"""Run-time xfail for a live gap: an exception of type ``raises`` raised inside the block whose
|
|
message matches ``pattern`` (``re.search``) XFAILs the cell; any other failure propagates, and a
|
|
clean pass stays a pass. Wrap only the final assertions, after every wait has settled, so a lost
|
|
reply, a failed boot or a timeout can never be mistaken for the gap."""
|
|
try:
|
|
yield
|
|
except raises as exc:
|
|
if not re.search(pattern, str(exc)):
|
|
raise
|
|
pytest.xfail(f"{reason} [observed: {str(exc).splitlines()[0][:240]}]")
|
|
|
|
|
|
def known_gate(known: Mapping[str, Tuple[str, str]], key: str,
|
|
raises: Type[BaseException] | Tuple[Type[BaseException], ...] = AssertionError) -> ContextManager[None]:
|
|
""":func:`known_failure` for a cell a file's ``KNOWN`` table (key -> ``(pattern, reason)``)
|
|
names, a no-op for every other cell, so one wrapped block serves gated and plain cells."""
|
|
entry = known.get(key)
|
|
return known_failure(*entry, raises=raises) if entry else contextlib.nullcontext()
|