test(ci): сторож пропусков кричит ::error:: под Actions (#2871) #2876
2 changed files with 55 additions and 2 deletions
|
|
@ -96,15 +96,24 @@ def pytest_sessionfinish(session, exitstatus) -> None:
|
||||||
unlisted = sorted(_observed_skips - _allowed_skips())
|
unlisted = sorted(_observed_skips - _allowed_skips())
|
||||||
if not unlisted:
|
if not unlisted:
|
||||||
return
|
return
|
||||||
print(
|
head = (
|
||||||
f"\nНЕУЧТЁННЫЙ ПРОПУСК ({len(unlisted)}): проверка не исполнилась и не "
|
f"НЕУЧТЁННЫЙ ПРОПУСК ({len(unlisted)}): проверка не исполнилась и не "
|
||||||
f"объявлена в {_SKIP_ALLOWLIST_PATH.name}:"
|
f"объявлена в {_SKIP_ALLOWLIST_PATH.name}:"
|
||||||
)
|
)
|
||||||
|
print(f"\n{head}")
|
||||||
for nodeid in unlisted:
|
for nodeid in unlisted:
|
||||||
print(f" - {nodeid}")
|
print(f" - {nodeid}")
|
||||||
print(
|
print(
|
||||||
"Почини тест либо внеси его в skip_allowlist.txt с причиной — "
|
"Почини тест либо внеси его в skip_allowlist.txt с причиной — "
|
||||||
"пропуск без записи неотличим от пройденной проверки."
|
"пропуск без записи неотличим от пройденной проверки."
|
||||||
)
|
)
|
||||||
|
# #2871: под Actions дублируем в ::error:: — иначе сообщение тонет.
|
||||||
|
# 13.08 этот сторож четыре прогона подряд ронял job'у совершенно правильно,
|
||||||
|
# а его строка лежала посреди тысячи других (обычный print, по-русски) —
|
||||||
|
# и поиск по «FAILED / ERROR» её не находил. Причину искали три часа
|
||||||
|
# в диске, раннере, покрытии и кэше. Сторож, который роняет прогон,
|
||||||
|
# обязан кричать так, чтобы его нашли.
|
||||||
|
if os.environ.get("GITHUB_ACTIONS") or os.environ.get("CI"):
|
||||||
|
print(f"::error::{head} " + "; ".join(unlisted))
|
||||||
if exitstatus == 0:
|
if exitstatus == 0:
|
||||||
session.exitstatus = 1
|
session.exitstatus = 1
|
||||||
|
|
|
||||||
44
backend/tests/test_skip_guard_loud.py
Normal file
44
backend/tests/test_skip_guard_loud.py
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
"""Проверка, что сторож пропусков кричит под Actions (#2871)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import types
|
||||||
|
|
||||||
|
import tests.conftest as ct
|
||||||
|
|
||||||
|
|
||||||
|
def _run_guard(monkeypatch, capsys, *, ci: bool, observed: set[str]) -> str:
|
||||||
|
monkeypatch.setattr(ct, "_observed_skips", observed)
|
||||||
|
monkeypatch.setattr(ct, "_allowed_skips", lambda: set())
|
||||||
|
monkeypatch.delenv("GITHUB_ACTIONS", raising=False)
|
||||||
|
monkeypatch.delenv("CI", raising=False)
|
||||||
|
if ci:
|
||||||
|
monkeypatch.setenv("GITHUB_ACTIONS", "true")
|
||||||
|
session = types.SimpleNamespace(exitstatus=0)
|
||||||
|
ct.pytest_sessionfinish(session, 0)
|
||||||
|
return capsys.readouterr().out, session.exitstatus
|
||||||
|
|
||||||
|
|
||||||
|
def test_guard_emits_error_annotation_under_actions(monkeypatch, capsys) -> None:
|
||||||
|
out, rc = _run_guard(monkeypatch, capsys, ci=True, observed={"tests/x.py::test_y"})
|
||||||
|
assert "::error::" in out, "под Actions сторож обязан подниматься в аннотации"
|
||||||
|
assert "tests/x.py::test_y" in out
|
||||||
|
assert rc == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_guard_stays_quiet_locally(monkeypatch, capsys) -> None:
|
||||||
|
"""Контроль: локально ::error:: не нужен, человеческое сообщение остаётся."""
|
||||||
|
out, rc = _run_guard(monkeypatch, capsys, ci=False, observed={"tests/x.py::test_y"})
|
||||||
|
assert "::error::" not in out
|
||||||
|
assert "НЕУЧТЁННЫЙ ПРОПУСК" in out
|
||||||
|
assert rc == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_guard_silent_when_all_skips_declared(monkeypatch, capsys) -> None:
|
||||||
|
"""Контроль: без незадекларированных пропусков сторож молчит и не роняет."""
|
||||||
|
monkeypatch.setattr(ct, "_observed_skips", set())
|
||||||
|
monkeypatch.setattr(ct, "_allowed_skips", lambda: set())
|
||||||
|
session = types.SimpleNamespace(exitstatus=0)
|
||||||
|
ct.pytest_sessionfinish(session, 0)
|
||||||
|
assert capsys.readouterr().out == ""
|
||||||
|
assert session.exitstatus == 0
|
||||||
Loading…
Add table
Reference in a new issue