1+ import json
2+ import importlib
3+ import sys
4+ from pathlib import Path
5+
6+ import pytest
7+
8+
9+ REPO_ROOT = Path (__file__ ).resolve ().parents [1 ]
10+ sys .path .insert (0 , str (REPO_ROOT ))
11+ sys .modules .pop ("oracletrace.cli" , None )
12+ sys .modules .pop ("oracletrace" , None )
13+ cli = importlib .import_module ("oracletrace.cli" )
14+ assert str (REPO_ROOT / "oracletrace" ) in str (Path (cli .__file__ ).resolve ())
15+
16+
17+ @pytest .fixture
18+ def trace_data ():
19+ return {
20+ "functions" : [
21+ {
22+ "name" : "foo" ,
23+ "total_time" : 1.5 ,
24+ "call_count" : 3 ,
25+ "avg_time" : 0.5 ,
26+ },
27+ {
28+ "name" : "bar" ,
29+ "total_time" : 2.0 ,
30+ "call_count" : 2 ,
31+ "avg_time" : 1.0 ,
32+ },
33+ ]
34+ }
35+
36+
37+ class FakeTracer :
38+ def __init__ (self , root , ignore_patterns , data ):
39+ self .root = root
40+ self .ignore_patterns = ignore_patterns
41+ self .data = data
42+ self .started = False
43+ self .stopped = False
44+ self .show_results_calls = []
45+
46+ def start (self ):
47+ self .started = True
48+
49+ def stop (self ):
50+ self .stopped = True
51+
52+ def get_trace_data (self ):
53+ return self .data
54+
55+ def show_results (self , top ):
56+ self .show_results_calls .append (top )
57+
58+
59+ def _run_cli (monkeypatch , argv ):
60+ monkeypatch .setattr (sys , "argv" , argv )
61+ return cli .main ()
62+
63+
64+ def test_main_returns_1_when_target_not_found (monkeypatch , capsys ):
65+ exit_code = _run_cli (monkeypatch , ["oracletrace" , "missing_script.py" ])
66+
67+ captured = capsys .readouterr ()
68+ assert exit_code == 1
69+ assert "Target not found: missing_script.py" in captured .out
70+
71+
72+ def test_main_returns_1_when_ignore_regex_is_invalid (monkeypatch , tmp_path , capsys ):
73+ target = tmp_path / "target.py"
74+ target .write_text ("print('ok')\n " , encoding = "utf-8" )
75+
76+ exit_code = _run_cli (monkeypatch , ["oracletrace" , str (target ), "--ignore" , "[" ])
77+
78+ captured = capsys .readouterr ()
79+ assert exit_code == 1
80+ assert "Regex error: [" in captured .out
81+
82+
83+ def test_main_runs_trace_and_exports_json_and_csv (monkeypatch , tmp_path , trace_data ):
84+ target = tmp_path / "target.py"
85+ target .write_text ("print('hello')\n " , encoding = "utf-8" )
86+ json_output = tmp_path / "trace.json"
87+ csv_output = tmp_path / "trace.csv"
88+
89+ fake_tracer_holder = {}
90+
91+ def tracer_factory (root , ignore_patterns ):
92+ fake = FakeTracer (root , ignore_patterns , trace_data )
93+ fake_tracer_holder ["instance" ] = fake
94+ return fake
95+
96+ run_path_calls = []
97+
98+ def fake_run_path (path , run_name ):
99+ run_path_calls .append ((path , run_name ))
100+
101+ monkeypatch .setattr (cli , "Tracer" , tracer_factory )
102+ monkeypatch .setattr (cli .runpy , "run_path" , fake_run_path )
103+ monkeypatch .setattr (sys , "path" , sys .path .copy ())
104+
105+ exit_code = _run_cli (
106+ monkeypatch ,
107+ [
108+ "oracletrace" ,
109+ str (target ),
110+ "--json" ,
111+ str (json_output ),
112+ "--csv" ,
113+ str (csv_output ),
114+ "--ignore" ,
115+ r".*target.py:foo" ,
116+ ],
117+ )
118+
119+ fake_tracer = fake_tracer_holder ["instance" ]
120+
121+ assert exit_code == 0
122+ assert fake_tracer .started is True
123+ assert fake_tracer .stopped is True
124+ assert fake_tracer .show_results_calls == [None ]
125+ assert run_path_calls == [(str (target .resolve ()), "__main__" )]
126+
127+ loaded_json = json .loads (json_output .read_text (encoding = "utf-8" ))
128+ assert loaded_json == trace_data
129+
130+ csv_text = csv_output .read_text (encoding = "utf-8" )
131+ assert "function,total_time,calls,avg_time" in csv_text
132+ assert "foo,1.5,3,0.5" in csv_text
133+ assert "bar,2.0,2,1.0" in csv_text
134+
135+ assert sys .path [0 ] == str (target .parent .resolve ())
136+
137+
138+ def test_main_passes_top_value_to_show_results (monkeypatch , tmp_path , trace_data ):
139+ target = tmp_path / "target.py"
140+ target .write_text ("print('hello')\n " , encoding = "utf-8" )
141+
142+ fake_tracer_holder = {}
143+
144+ def tracer_factory (root , ignore_patterns ):
145+ fake = FakeTracer (root , ignore_patterns , trace_data )
146+ fake_tracer_holder ["instance" ] = fake
147+ return fake
148+
149+ monkeypatch .setattr (cli , "Tracer" , tracer_factory )
150+ monkeypatch .setattr (cli .runpy , "run_path" , lambda * args , ** kwargs : None )
151+
152+ exit_code = _run_cli (monkeypatch , ["oracletrace" , str (target ), "--top" , "5" ])
153+
154+ fake_tracer = fake_tracer_holder ["instance" ]
155+ assert exit_code == 0
156+ assert fake_tracer .show_results_calls == [5 ]
157+
158+
159+ def test_main_returns_1_when_compare_file_not_found (monkeypatch , tmp_path , trace_data , capsys ):
160+ target = tmp_path / "target.py"
161+ target .write_text ("print('hello')\n " , encoding = "utf-8" )
162+
163+ monkeypatch .setattr (cli , "Tracer" , lambda root , ignore_patterns : FakeTracer (root , ignore_patterns , trace_data ))
164+ monkeypatch .setattr (cli .runpy , "run_path" , lambda * args , ** kwargs : None )
165+
166+ exit_code = _run_cli (
167+ monkeypatch ,
168+ ["oracletrace" , str (target ), "--compare" , str (tmp_path / "missing_compare.json" )],
169+ )
170+
171+ captured = capsys .readouterr ()
172+ assert exit_code == 1
173+ assert "Compare file not found:" in captured .out
174+
175+
176+ def test_main_fails_with_exit_2_on_regression (monkeypatch , tmp_path , trace_data , capsys ):
177+ target = tmp_path / "target.py"
178+ target .write_text ("print('hello')\n " , encoding = "utf-8" )
179+ compare_file = tmp_path / "baseline.json"
180+ compare_file .write_text (json .dumps ({"functions" : []}), encoding = "utf-8" )
181+
182+ monkeypatch .setattr (cli , "Tracer" , lambda root , ignore_patterns : FakeTracer (root , ignore_patterns , trace_data ))
183+ monkeypatch .setattr (cli .runpy , "run_path" , lambda * args , ** kwargs : None )
184+
185+ compare_calls = []
186+
187+ def fake_compare_traces (old_data , new_data , threshold ):
188+ compare_calls .append ((old_data , new_data , threshold ))
189+ return {"has_regression" : True }
190+
191+ monkeypatch .setattr (cli , "compare_traces" , fake_compare_traces )
192+
193+ exit_code = _run_cli (
194+ monkeypatch ,
195+ [
196+ "oracletrace" ,
197+ str (target ),
198+ "--compare" ,
199+ str (compare_file ),
200+ "--fail-on-regression" ,
201+ "--threshold" ,
202+ "7.5" ,
203+ ],
204+ )
205+
206+ captured = capsys .readouterr ()
207+ assert exit_code == 2
208+ assert "Build failed: performance regression above 7.50% detected." in captured .out
209+ assert compare_calls [0 ][2 ] == 7.5
210+
211+
212+ def test_main_returns_0_when_no_regression (monkeypatch , tmp_path , trace_data ):
213+ target = tmp_path / "target.py"
214+ target .write_text ("print('hello')\n " , encoding = "utf-8" )
215+ compare_file = tmp_path / "baseline.json"
216+ compare_file .write_text (json .dumps ({"functions" : []}), encoding = "utf-8" )
217+
218+ monkeypatch .setattr (cli , "Tracer" , lambda root , ignore_patterns : FakeTracer (root , ignore_patterns , trace_data ))
219+ monkeypatch .setattr (cli .runpy , "run_path" , lambda * args , ** kwargs : None )
220+ monkeypatch .setattr (cli , "compare_traces" , lambda old_data , new_data , threshold : {"has_regression" : False })
221+
222+ exit_code = _run_cli (
223+ monkeypatch ,
224+ [
225+ "oracletrace" ,
226+ str (target ),
227+ "--compare" ,
228+ str (compare_file ),
229+ "--fail-on-regression" ,
230+ ],
231+ )
232+
233+ assert exit_code == 0
0 commit comments