1- """Tests for --test-mode feature (Phase 1: Basic functionality ).
1+ """Tests for --test-mode feature (Phase 2: JSON Failure Reports ).
22
3- Tests the essential test mode functionality:
3+ Tests the test mode functionality:
44- Bootstrapper initialization with test_mode flag
55- Exception handling: catch errors, log, continue
66- Bootstrapper.finalize() exit codes
7+ - JSON failure report generation
8+ - failure_type field for categorizing failures
79"""
810
11+ import json
912import pathlib
1013import tempfile
1114import typing
@@ -85,7 +88,15 @@ def test_finalize_with_failures_returns_one(
8588 ) -> None :
8689 """Test finalize returns 1 when there are failures in test mode."""
8790 bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
88- bt .failed_packages .append ("failing-pkg" )
91+ bt .failed_packages .append (
92+ {
93+ "package" : "failing-pkg" ,
94+ "version" : "1.0.0" ,
95+ "exception_type" : "RuntimeError" ,
96+ "exception_message" : "Build failed" ,
97+ "failure_type" : "bootstrap" ,
98+ }
99+ )
89100 assert bt .finalize () == 1
90101
91102 def test_finalize_not_in_test_mode_returns_zero (
@@ -94,15 +105,47 @@ def test_finalize_not_in_test_mode_returns_zero(
94105 """Test finalize returns 0 when not in test mode (regardless of failures)."""
95106 bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = False )
96107 # Even if we manually add failures (shouldn't happen), it returns 0
97- bt .failed_packages .append ("some-pkg" )
108+ bt .failed_packages .append (
109+ {
110+ "package" : "some-pkg" ,
111+ "version" : "1.0.0" ,
112+ "exception_type" : "RuntimeError" ,
113+ "exception_message" : "Error" ,
114+ "failure_type" : "bootstrap" ,
115+ }
116+ )
98117 assert bt .finalize () == 0
99118
100119 def test_finalize_logs_failed_packages (
101120 self , mock_context : context .WorkContext , caplog : pytest .LogCaptureFixture
102121 ) -> None :
103122 """Test finalize logs the list of failed packages."""
104123 bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
105- bt .failed_packages .extend (["pkg-a" , "pkg-b" , "pkg-c" ])
124+ bt .failed_packages .extend (
125+ [
126+ {
127+ "package" : "pkg-a" ,
128+ "version" : "1.0" ,
129+ "exception_type" : "E" ,
130+ "exception_message" : "m" ,
131+ "failure_type" : "bootstrap" ,
132+ },
133+ {
134+ "package" : "pkg-b" ,
135+ "version" : "2.0" ,
136+ "exception_type" : "E" ,
137+ "exception_message" : "m" ,
138+ "failure_type" : "hook" ,
139+ },
140+ {
141+ "package" : "pkg-c" ,
142+ "version" : "3.0" ,
143+ "exception_type" : "E" ,
144+ "exception_message" : "m" ,
145+ "failure_type" : "dependency_extraction" ,
146+ },
147+ ]
148+ )
106149
107150 exit_code = bt .finalize ()
108151
@@ -111,3 +154,126 @@ def test_finalize_logs_failed_packages(
111154 assert "pkg-a" in caplog .text
112155 assert "pkg-b" in caplog .text
113156 assert "pkg-c" in caplog .text
157+
158+
159+ def _find_failure_report (work_dir : pathlib .Path ) -> pathlib .Path | None :
160+ """Find the test-mode-failures-*.json file in work_dir."""
161+ reports = list (work_dir .glob ("test-mode-failures-*.json" ))
162+ return reports [0 ] if reports else None
163+
164+
165+ class TestJsonFailureReport :
166+ """Test JSON failure report generation."""
167+
168+ def test_finalize_writes_json_report (
169+ self , mock_context : context .WorkContext
170+ ) -> None :
171+ """Test finalize writes test-mode-failures-<timestamp>.json with failure details."""
172+ bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
173+ bt .failed_packages .append (
174+ {
175+ "package" : "failing-pkg" ,
176+ "version" : "1.0.0" ,
177+ "exception_type" : "CalledProcessError" ,
178+ "exception_message" : "Compilation failed" ,
179+ "failure_type" : "bootstrap" ,
180+ }
181+ )
182+
183+ bt .finalize ()
184+
185+ report_path = _find_failure_report (mock_context .work_dir )
186+ assert report_path is not None
187+ assert report_path .name .startswith ("test-mode-failures-" )
188+ assert report_path .name .endswith (".json" )
189+
190+ with open (report_path ) as f :
191+ report = json .load (f )
192+
193+ assert "failures" in report
194+ assert len (report ["failures" ]) == 1
195+ assert report ["failures" ][0 ]["package" ] == "failing-pkg"
196+ assert report ["failures" ][0 ]["version" ] == "1.0.0"
197+ assert report ["failures" ][0 ]["exception_type" ] == "CalledProcessError"
198+ assert report ["failures" ][0 ]["exception_message" ] == "Compilation failed"
199+ assert report ["failures" ][0 ]["failure_type" ] == "bootstrap"
200+
201+ def test_finalize_no_report_when_no_failures (
202+ self , mock_context : context .WorkContext
203+ ) -> None :
204+ """Test finalize does not write report when there are no failures."""
205+ bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
206+
207+ bt .finalize ()
208+
209+ report_path = _find_failure_report (mock_context .work_dir )
210+ assert report_path is None
211+
212+ def test_finalize_report_with_null_version (
213+ self , mock_context : context .WorkContext
214+ ) -> None :
215+ """Test finalize handles failures where version is None (resolution failure)."""
216+ bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
217+ bt .failed_packages .append (
218+ {
219+ "package" : "failed-to-resolve" ,
220+ "version" : None ,
221+ "exception_type" : "ResolutionError" ,
222+ "exception_message" : "Could not resolve version" ,
223+ "failure_type" : "resolution" ,
224+ }
225+ )
226+
227+ bt .finalize ()
228+
229+ report_path = _find_failure_report (mock_context .work_dir )
230+ assert report_path is not None
231+ with open (report_path ) as f :
232+ report = json .load (f )
233+
234+ assert report ["failures" ][0 ]["version" ] is None
235+ assert report ["failures" ][0 ]["failure_type" ] == "resolution"
236+
237+ def test_finalize_report_multiple_failure_types (
238+ self , mock_context : context .WorkContext
239+ ) -> None :
240+ """Test finalize correctly reports multiple failures with different types."""
241+ bt = bootstrapper .Bootstrapper (ctx = mock_context , test_mode = True )
242+ bt .failed_packages .extend (
243+ [
244+ {
245+ "package" : "pkg-a" ,
246+ "version" : "1.0.0" ,
247+ "exception_type" : "BuildError" ,
248+ "exception_message" : "Failed to compile" ,
249+ "failure_type" : "bootstrap" ,
250+ },
251+ {
252+ "package" : "pkg-b" ,
253+ "version" : "2.0.0" ,
254+ "exception_type" : "HookError" ,
255+ "exception_message" : "Validation failed" ,
256+ "failure_type" : "hook" ,
257+ },
258+ {
259+ "package" : "pkg-c" ,
260+ "version" : "3.0.0" ,
261+ "exception_type" : "MetadataError" ,
262+ "exception_message" : "Could not read metadata" ,
263+ "failure_type" : "dependency_extraction" ,
264+ },
265+ ]
266+ )
267+
268+ bt .finalize ()
269+
270+ report_path = _find_failure_report (mock_context .work_dir )
271+ assert report_path is not None
272+ with open (report_path ) as f :
273+ report = json .load (f )
274+
275+ assert len (report ["failures" ]) == 3
276+ failure_types = [f ["failure_type" ] for f in report ["failures" ]]
277+ assert "bootstrap" in failure_types
278+ assert "hook" in failure_types
279+ assert "dependency_extraction" in failure_types
0 commit comments