-
Notifications
You must be signed in to change notification settings - Fork 607
Expand file tree
/
Copy pathconftest.py
More file actions
145 lines (120 loc) · 6.36 KB
/
Copy pathconftest.py
File metadata and controls
145 lines (120 loc) · 6.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
"""Shared pytest configuration for RedAmon (auto-tiering + isolation rules).
This file is intentionally near-identical at every in-container test root
(agentic/, recon/, recon_orchestrator/, scanners/ai_attack_surface_scan/,
scanners/capture_proxy/, services/docker_broker/, and the repo root). Each
container mounts only its own subtree,
so the config must be visible at each in-container rootdir; keep the copies in
sync.
TIERS (Part B) — every collected test is auto-marked by filename so we do not
have to edit ~350 files. Selection is then `pytest -m unit` (the fast, hermetic
gate), `-m 'unit or integration'` (coverage), or `-m live` (needs a stack):
* ``integration`` — ``*_integration.py``, ``*_skill.py``, ``*_e2e*``
* ``live`` — ``live_*``, ``*_live*``, ``*_smoke*`` (self-skip when a
stack/service is down; never hard-fail on a missing prereq)
* ``unit`` — everything else (the default gate tier)
A file can override the auto-mark with an explicit ``@pytest.mark.{unit,
integration,live}`` marker (respected below).
ISOLATION RULE (Part C) — the make-or-break of a deterministic AI-run suite:
NEVER mutate global process state at import time. Do not overwrite
``sys.modules``, ``os.environ``, or the cwd at module scope and leave it
mutated; a later-collected test then imports the polluted module and silently
drops to 0% coverage (this is exactly the bug that hid `tradecraft_lookup.py`).
Use fixtures or ``mock.patch.dict(sys.modules, {...})`` / ``mock.patch.dict(
os.environ, {...})`` scoped to a single test, and restore in ``tearDownModule``
if you must shim at import time. See ``docs/readmes/README.TESTING.md``.
DETERMINISM AT THE GATE — a large body of these tests was written for the old
per-file ``python -m unittest tests.test_x`` runner, where every file was its
own process, so many stub langchain/langgraph into ``sys.modules`` at import
time and (worse) bake real tool objects against a fake decorator during import.
That is order-dependent in a single pytest process. The canonical gate
(``redamon.sh test`` / ``run_tests.sh``) therefore runs each test FILE in its
own pytest subprocess (parallelized), which reproduces the isolation these tests
assume and makes the gate deterministic. Running ``pytest -m unit`` over the
whole tree in one process is intentionally NOT the gate for that reason — use it
only for a single file / node id while iterating. The ``-m unit`` path-scope
(``pytest_ignore_collect`` below) additionally keeps such a single-process run
from even importing the heavier integration/live files.
"""
import asyncio
import os
import sys
import pytest
@pytest.fixture(autouse=True)
def _ensure_event_loop():
"""Guarantee a usable current event loop for each test.
Many suites drive coroutines with the classic
``asyncio.get_event_loop().run_until_complete(...)`` helper. Python's
``unittest.IsolatedAsyncioTestCase`` calls ``set_event_loop(None)`` on
teardown, so any such test that runs AFTER an IsolatedAsyncioTestCase would
otherwise hit "There is no current event loop in thread 'MainThread'". Under
the old per-file `python -m unittest` runner each file was its own process,
hiding this cross-file interaction; a single pytest process exposes it. This
autouse fixture restores a loop when the previous test cleared it — a
determinism fix, not a behavior change.
"""
try:
loop = asyncio.get_event_loop()
if loop.is_closed():
raise RuntimeError
except RuntimeError:
asyncio.set_event_loop(asyncio.new_event_loop())
yield
@pytest.fixture(autouse=True)
def _reset_circuit_breakers():
"""Forget recon circuit-breaker state before and after every test.
recon/helpers/circuit_breaker.py keeps process-wide state by design (one
scan is one process), so a breaker a test opened would otherwise skip the
next test's calls. Only resets when the module is already loaded, so an
unrelated test never pulls in the recon.helpers package.
"""
mod = sys.modules.get("recon.helpers.circuit_breaker")
if mod is not None:
mod.reset_registry()
yield
mod = sys.modules.get("recon.helpers.circuit_breaker")
if mod is not None:
mod.reset_registry()
# Filename fragments that route a test file to a non-unit tier. Matched against
# the lowercased basename so this stays OS/path independent.
_INTEGRATION_MARKERS = ("_integration.py", "_skill.py", "_e2e")
_LIVE_MARKERS = ("live_", "_live", "_smoke", "smoke_")
_TIER_NAMES = {"unit", "integration", "live"}
def _auto_tier(basename: str) -> str:
name = basename.lower()
if any(frag in name for frag in _LIVE_MARKERS):
return "live"
if any(frag in name for frag in _INTEGRATION_MARKERS):
return "integration"
return "unit"
def pytest_ignore_collect(collection_path, config):
"""Make the `-m unit` gate hermetic by NOT importing non-unit files at all.
pytest imports every *collected* module during collection — even under
`-m unit`, because the tier marker is read only after import. Many
integration/skill files stub or mutate shared modules at import time (e.g.
they replace the real ``langchain_core.tools.tool`` decorator with a fake),
which then breaks unit tests that build real tools. Marker selection alone
cannot prevent that. So when the marker expression is exactly ``unit`` we
skip collecting non-unit files entirely — the plan's path-scoped-collection
fallback. Broader runs (`-m 'unit or integration'`, coverage) collect
everything as usual (and run the heavier tiers with process isolation).
"""
markexpr = (config.getoption("markexpr", default="") or "").replace(" ", "")
if markexpr != "unit":
return None
p = collection_path
if p.is_file() and p.suffix == ".py" and (
p.name.startswith("test_") or p.name.endswith("_test.py")
):
if _auto_tier(p.name) != "unit":
return True
return None
def pytest_collection_modifyitems(config, items):
"""Auto-mark each collected item into exactly one tier by filename.
An explicit ``@pytest.mark.{unit,integration,live}`` in the file wins; only
files with no tier marker get an inferred one.
"""
for item in items:
if _TIER_NAMES & {m.name for m in item.iter_markers()}:
continue
tier = _auto_tier(os.path.basename(str(item.fspath)))
item.add_marker(tier)