|
1 | | -"""Tests for --test-mode feature (Phase 1: Basic functionality). |
| 1 | +"""Tests for --test-mode feature (Phase 2: JSON Failure Reports). |
2 | 2 |
|
3 | | -Tests the essential test mode functionality: |
| 3 | +Tests the test mode functionality: |
4 | 4 | - Bootstrapper initialization with test_mode flag |
5 | 5 | - Exception handling: catch errors, log, continue |
6 | 6 | - Bootstrapper.finalize() exit codes |
| 7 | +- JSON failure report generation |
7 | 8 | """ |
8 | 9 |
|
| 10 | +import json |
9 | 11 | import pathlib |
10 | 12 | import tempfile |
11 | 13 | import typing |
@@ -78,36 +80,180 @@ def test_finalize_no_failures_returns_zero( |
78 | 80 | ) -> None: |
79 | 81 | """Test finalize returns 0 when no failures in test mode.""" |
80 | 82 | bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
81 | | - assert bt.finalize() == 0 |
| 83 | + assert bt.finalize(mock_context.work_dir) == 0 |
82 | 84 |
|
83 | 85 | def test_finalize_with_failures_returns_one( |
84 | 86 | self, mock_context: context.WorkContext |
85 | 87 | ) -> None: |
86 | 88 | """Test finalize returns 1 when there are failures in test mode.""" |
87 | 89 | bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
88 | | - bt.failed_packages.append("failing-pkg") |
89 | | - assert bt.finalize() == 1 |
| 90 | + bt.failed_packages.append( |
| 91 | + { |
| 92 | + "package": "failing-pkg", |
| 93 | + "version": "1.0.0", |
| 94 | + "exception_type": "RuntimeError", |
| 95 | + "exception_message": "Build failed", |
| 96 | + } |
| 97 | + ) |
| 98 | + assert bt.finalize(mock_context.work_dir) == 1 |
90 | 99 |
|
91 | 100 | def test_finalize_not_in_test_mode_returns_zero( |
92 | 101 | self, mock_context: context.WorkContext |
93 | 102 | ) -> None: |
94 | 103 | """Test finalize returns 0 when not in test mode (regardless of failures).""" |
95 | 104 | bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=False) |
96 | 105 | # Even if we manually add failures (shouldn't happen), it returns 0 |
97 | | - bt.failed_packages.append("some-pkg") |
98 | | - assert bt.finalize() == 0 |
| 106 | + bt.failed_packages.append( |
| 107 | + { |
| 108 | + "package": "some-pkg", |
| 109 | + "version": "1.0.0", |
| 110 | + "exception_type": "RuntimeError", |
| 111 | + "exception_message": "Error", |
| 112 | + } |
| 113 | + ) |
| 114 | + assert bt.finalize(mock_context.work_dir) == 0 |
99 | 115 |
|
100 | 116 | def test_finalize_logs_failed_packages( |
101 | 117 | self, mock_context: context.WorkContext, caplog: pytest.LogCaptureFixture |
102 | 118 | ) -> None: |
103 | 119 | """Test finalize logs the list of failed packages.""" |
104 | 120 | bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
105 | | - bt.failed_packages.extend(["pkg-a", "pkg-b", "pkg-c"]) |
| 121 | + bt.failed_packages.extend( |
| 122 | + [ |
| 123 | + { |
| 124 | + "package": "pkg-a", |
| 125 | + "version": "1.0", |
| 126 | + "exception_type": "E", |
| 127 | + "exception_message": "m", |
| 128 | + }, |
| 129 | + { |
| 130 | + "package": "pkg-b", |
| 131 | + "version": "2.0", |
| 132 | + "exception_type": "E", |
| 133 | + "exception_message": "m", |
| 134 | + }, |
| 135 | + { |
| 136 | + "package": "pkg-c", |
| 137 | + "version": "3.0", |
| 138 | + "exception_type": "E", |
| 139 | + "exception_message": "m", |
| 140 | + }, |
| 141 | + ] |
| 142 | + ) |
106 | 143 |
|
107 | | - exit_code = bt.finalize() |
| 144 | + exit_code = bt.finalize(mock_context.work_dir) |
108 | 145 |
|
109 | 146 | assert exit_code == 1 |
110 | 147 | assert "3 package(s) failed" in caplog.text |
111 | 148 | assert "pkg-a" in caplog.text |
112 | 149 | assert "pkg-b" in caplog.text |
113 | 150 | assert "pkg-c" in caplog.text |
| 151 | + |
| 152 | + |
| 153 | +def _find_failure_report(work_dir: pathlib.Path) -> pathlib.Path | None: |
| 154 | + """Find the test-mode-failures-*.json file in work_dir.""" |
| 155 | + reports = list(work_dir.glob("test-mode-failures-*.json")) |
| 156 | + return reports[0] if reports else None |
| 157 | + |
| 158 | + |
| 159 | +class TestJsonFailureReport: |
| 160 | + """Test JSON failure report generation.""" |
| 161 | + |
| 162 | + def test_finalize_writes_json_report( |
| 163 | + self, mock_context: context.WorkContext |
| 164 | + ) -> None: |
| 165 | + """Test finalize writes test-mode-failures-<timestamp>.json with failure details.""" |
| 166 | + bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
| 167 | + bt.failed_packages.append( |
| 168 | + { |
| 169 | + "package": "failing-pkg", |
| 170 | + "version": "1.0.0", |
| 171 | + "exception_type": "CalledProcessError", |
| 172 | + "exception_message": "Compilation failed", |
| 173 | + } |
| 174 | + ) |
| 175 | + |
| 176 | + bt.finalize(mock_context.work_dir) |
| 177 | + |
| 178 | + report_path = _find_failure_report(mock_context.work_dir) |
| 179 | + assert report_path is not None |
| 180 | + assert report_path.name.startswith("test-mode-failures-") |
| 181 | + assert report_path.name.endswith(".json") |
| 182 | + |
| 183 | + with open(report_path) as f: |
| 184 | + report = json.load(f) |
| 185 | + |
| 186 | + assert "failures" in report |
| 187 | + assert len(report["failures"]) == 1 |
| 188 | + assert report["failures"][0]["package"] == "failing-pkg" |
| 189 | + assert report["failures"][0]["version"] == "1.0.0" |
| 190 | + assert report["failures"][0]["exception_type"] == "CalledProcessError" |
| 191 | + assert report["failures"][0]["exception_message"] == "Compilation failed" |
| 192 | + |
| 193 | + def test_finalize_no_report_when_no_failures( |
| 194 | + self, mock_context: context.WorkContext |
| 195 | + ) -> None: |
| 196 | + """Test finalize does not write report when there are no failures.""" |
| 197 | + bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
| 198 | + |
| 199 | + bt.finalize(mock_context.work_dir) |
| 200 | + |
| 201 | + report_path = _find_failure_report(mock_context.work_dir) |
| 202 | + assert report_path is None |
| 203 | + |
| 204 | + def test_finalize_report_with_null_version( |
| 205 | + self, mock_context: context.WorkContext |
| 206 | + ) -> None: |
| 207 | + """Test finalize handles failures where version is None.""" |
| 208 | + bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
| 209 | + bt.failed_packages.append( |
| 210 | + { |
| 211 | + "package": "failed-to-resolve", |
| 212 | + "version": None, |
| 213 | + "exception_type": "ResolutionError", |
| 214 | + "exception_message": "Could not resolve version", |
| 215 | + } |
| 216 | + ) |
| 217 | + |
| 218 | + bt.finalize(mock_context.work_dir) |
| 219 | + |
| 220 | + report_path = _find_failure_report(mock_context.work_dir) |
| 221 | + assert report_path is not None |
| 222 | + with open(report_path) as f: |
| 223 | + report = json.load(f) |
| 224 | + |
| 225 | + assert report["failures"][0]["version"] is None |
| 226 | + |
| 227 | + def test_finalize_report_multiple_failures( |
| 228 | + self, mock_context: context.WorkContext |
| 229 | + ) -> None: |
| 230 | + """Test finalize correctly reports multiple failures.""" |
| 231 | + bt = bootstrapper.Bootstrapper(ctx=mock_context, test_mode=True) |
| 232 | + bt.failed_packages.extend( |
| 233 | + [ |
| 234 | + { |
| 235 | + "package": "pkg-a", |
| 236 | + "version": "1.0.0", |
| 237 | + "exception_type": "BuildError", |
| 238 | + "exception_message": "Failed to compile", |
| 239 | + }, |
| 240 | + { |
| 241 | + "package": "pkg-b", |
| 242 | + "version": "2.0.0", |
| 243 | + "exception_type": "DownloadError", |
| 244 | + "exception_message": "Network timeout", |
| 245 | + }, |
| 246 | + ] |
| 247 | + ) |
| 248 | + |
| 249 | + bt.finalize(mock_context.work_dir) |
| 250 | + |
| 251 | + report_path = _find_failure_report(mock_context.work_dir) |
| 252 | + assert report_path is not None |
| 253 | + with open(report_path) as f: |
| 254 | + report = json.load(f) |
| 255 | + |
| 256 | + assert len(report["failures"]) == 2 |
| 257 | + packages = [f["package"] for f in report["failures"]] |
| 258 | + assert "pkg-a" in packages |
| 259 | + assert "pkg-b" in packages |
0 commit comments