Skip to content

Commit 103dcb5

Browse files
committed
test: add pipeline orchestration integration tests
New test/test_pipeline_integration.py drives AnalysisPipeline.execute with lightweight fake AnalysisStep instances -- no LLM, CodeQL, or network. Locks the orchestration contract that the big-file refactors touched around: - steps run in declared order; results map to the right AnalysisResult fields - a step returning success=False short-circuits the remaining steps and surfaces the failing step name + error - an exception in a step is caught, not propagated, and recorded on the result - consolidation in the finally block runs even when a step raises - executed step results are recorded in the context - a real-IO case writes summary.md into a tmp output dir Suite: 97 passed / 2 skipped (was 89 / 2).
1 parent 7411628 commit 103dcb5

1 file changed

Lines changed: 211 additions & 0 deletions

File tree

test/test_pipeline_integration.py

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
"""Integration tests for AnalysisPipeline.execute orchestration.
2+
3+
These drive the real pipeline with lightweight fake AnalysisStep instances --
4+
no LLM, no CodeQL, no network. They lock the orchestration contract:
5+
ordered execution, result mapping, short-circuit on failure, exception
6+
containment, and the always-run consolidation in the finally block.
7+
"""
8+
9+
import pytest
10+
11+
from pure_auto_codeql.core.context import AnalysisConfig, AnalysisContext
12+
from pure_auto_codeql.core.pipeline import AnalysisPipeline, AnalysisStep
13+
from pure_auto_codeql.services.llm_service import AgentResult
14+
15+
STEP_NAMES = [
16+
"cve_analysis",
17+
"sink_analysis",
18+
"source_analysis",
19+
"path_analysis",
20+
"codeql_generation",
21+
]
22+
23+
24+
class RecordingStep(AnalysisStep):
25+
"""Fake step that records execution order and returns a canned result."""
26+
27+
def __init__(self, name, order, *, success=True, error=None, raises=None,
28+
set_exec_result=None):
29+
super().__init__(name)
30+
self._order = order
31+
self._success = success
32+
self._error = error
33+
self._raises = raises
34+
self._set_exec_result = set_exec_result
35+
36+
async def execute(self, context):
37+
self._order.append(self.name)
38+
if self._raises is not None:
39+
raise self._raises
40+
if self._set_exec_result is not None:
41+
context.data["codeql_execution_result"] = self._set_exec_result
42+
return AgentResult(content=f"{self.name}-content",
43+
success=self._success, error=self._error)
44+
45+
46+
def make_context():
47+
# case_paths / cve_assets are unused by the orchestration path (and are
48+
# only touched by consolidation, which the orchestration tests stub out).
49+
return AnalysisContext(
50+
case_id="TEST-CASE-1",
51+
case_paths=None,
52+
cve_assets=None,
53+
language="java",
54+
)
55+
56+
57+
def stub_consolidation(pipeline, calls):
58+
"""Replace _consolidate_output_files with an async spy (no IO)."""
59+
60+
async def _spy(context, result, config):
61+
calls.append((context, result, config))
62+
63+
pipeline._consolidate_output_files = _spy
64+
65+
66+
# --------------------------------------------------------------------------- #
67+
# Orchestration (consolidation stubbed out)
68+
# --------------------------------------------------------------------------- #
69+
70+
@pytest.mark.asyncio
71+
async def test_steps_execute_in_order_and_map_to_result():
72+
order = []
73+
steps = [
74+
RecordingStep("cve_analysis", order),
75+
RecordingStep("sink_analysis", order),
76+
RecordingStep("source_analysis", order),
77+
RecordingStep("path_analysis", order),
78+
RecordingStep("codeql_generation", order,
79+
set_exec_result={"sentinel": "exec"}),
80+
]
81+
pipeline = AnalysisPipeline(steps)
82+
stub_consolidation(pipeline, [])
83+
context = make_context()
84+
85+
result = await pipeline.execute(context, AnalysisConfig())
86+
87+
assert order == STEP_NAMES
88+
assert result.success is True
89+
assert result.cve_result.content == "cve_analysis-content"
90+
assert result.sink_result.content == "sink_analysis-content"
91+
assert result.source_result.content == "source_analysis-content"
92+
assert result.path_analysis_result.content == "path_analysis-content"
93+
assert result.codeql_result.content == "codeql_generation-content"
94+
assert result.codeql_execution_result == {"sentinel": "exec"}
95+
assert result.execution_time is not None and result.execution_time >= 0
96+
97+
98+
@pytest.mark.asyncio
99+
async def test_step_failure_short_circuits_remaining_steps():
100+
order = []
101+
steps = [
102+
RecordingStep("cve_analysis", order),
103+
RecordingStep("sink_analysis", order, success=False, error="boom"),
104+
RecordingStep("source_analysis", order),
105+
RecordingStep("path_analysis", order),
106+
RecordingStep("codeql_generation", order),
107+
]
108+
pipeline = AnalysisPipeline(steps)
109+
stub_consolidation(pipeline, [])
110+
111+
result = await pipeline.execute(make_context(), AnalysisConfig())
112+
113+
# sink_analysis ran and failed; nothing after it executed.
114+
assert order == ["cve_analysis", "sink_analysis"]
115+
assert result.success is False
116+
assert "sink_analysis" in result.error_message
117+
assert "boom" in result.error_message
118+
# Result carried the successful steps up to the failure.
119+
assert result.cve_result.content == "cve_analysis-content"
120+
assert result.source_result is None
121+
122+
123+
@pytest.mark.asyncio
124+
async def test_step_exception_is_caught_and_not_propagated():
125+
order = []
126+
steps = [
127+
RecordingStep("cve_analysis", order),
128+
RecordingStep("sink_analysis", order, raises=RuntimeError("kaboom")),
129+
RecordingStep("source_analysis", order),
130+
]
131+
pipeline = AnalysisPipeline(steps)
132+
stub_consolidation(pipeline, [])
133+
134+
result = await pipeline.execute(make_context(), AnalysisConfig())
135+
136+
assert order == ["cve_analysis", "sink_analysis"]
137+
assert result.success is False
138+
assert result.error_message == "kaboom"
139+
140+
141+
@pytest.mark.asyncio
142+
async def test_consolidation_runs_even_when_a_step_raises():
143+
order = []
144+
calls = []
145+
steps = [RecordingStep("cve_analysis", order, raises=ValueError("x"))]
146+
pipeline = AnalysisPipeline(steps)
147+
stub_consolidation(pipeline, calls)
148+
149+
result = await pipeline.execute(make_context(), AnalysisConfig())
150+
151+
# finally-block consolidation fired exactly once despite the exception.
152+
assert len(calls) == 1
153+
assert calls[0][1] is result
154+
155+
156+
@pytest.mark.asyncio
157+
async def test_executed_step_results_recorded_in_context():
158+
order = []
159+
steps = [
160+
RecordingStep("cve_analysis", order),
161+
RecordingStep("sink_analysis", order),
162+
]
163+
pipeline = AnalysisPipeline(steps)
164+
stub_consolidation(pipeline, [])
165+
context = make_context()
166+
167+
await pipeline.execute(context, AnalysisConfig())
168+
169+
assert context.has_result("cve_analysis")
170+
assert context.has_result("sink_analysis")
171+
assert context.get_result("cve_analysis").content == "cve_analysis-content"
172+
173+
174+
@pytest.mark.asyncio
175+
async def test_default_config_used_when_none_passed():
176+
order = []
177+
pipeline = AnalysisPipeline([RecordingStep("cve_analysis", order)])
178+
calls = []
179+
stub_consolidation(pipeline, calls)
180+
181+
await pipeline.execute(make_context(), config=None)
182+
183+
# A default AnalysisConfig was constructed and threaded into consolidation.
184+
assert isinstance(calls[0][2], AnalysisConfig)
185+
186+
187+
def test_create_default_pipeline_has_five_named_steps():
188+
pipeline = AnalysisPipeline.create_default_pipeline()
189+
assert [s.name for s in pipeline.steps] == STEP_NAMES
190+
191+
192+
# --------------------------------------------------------------------------- #
193+
# Consolidation finally-block (real IO into a tmp dir)
194+
# --------------------------------------------------------------------------- #
195+
196+
@pytest.mark.asyncio
197+
async def test_consolidation_writes_summary_into_output_dir(tmp_path):
198+
order = []
199+
steps = [RecordingStep(name, order) for name in STEP_NAMES]
200+
pipeline = AnalysisPipeline(steps)
201+
config = AnalysisConfig(output_base_dir=str(tmp_path), keep_output_dirs=0)
202+
203+
result = await pipeline.execute(make_context(), config)
204+
205+
assert result.success is True
206+
assert result.error_message is None
207+
assert result.output_directory is not None
208+
run_dir = tmp_path / "TEST-CASE-1"
209+
assert run_dir.exists()
210+
summary = next(run_dir.rglob("summary.md"), None)
211+
assert summary is not None and summary.is_file()

0 commit comments

Comments
 (0)