Skip to content

Commit b2b4830

Browse files
authored
Reporters can now take the output of run_all_with_progress (#84)
* Reports can operate on top of the two test outputs * Add headless API
1 parent fd3b9ca commit b2b4830

File tree

4 files changed

+97
-38
lines changed

4 files changed

+97
-38
lines changed

src/contraqctor/qc/base.py

Lines changed: 31 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,7 @@ def run_all(self) -> t.Generator[Result, None, None]:
792792
Yields:
793793
Result: Result objects produced by all test methods.
794794
"""
795+
795796
for test in self.get_tests():
796797
yield from self.run_test(test)
797798

@@ -1256,14 +1257,39 @@ def _update_suite_progress(
12561257
summary_line = f"[cyan]{suite_name:<{suite_name_width}} | {status_bar} | {stats.get_status_summary()}"
12571258
progress.update(suite_task, description=summary_line)
12581259

1260+
def run_all(self) -> t.Dict[t.Optional[str], t.List[Result]]:
1261+
"""Run all tests in all suites without progress display.
1262+
1263+
Executes all tests and collects results without visual progress reporting.
1264+
1265+
Returns:
1266+
Dict[Optional[str], List[Result]]: Results grouped by test group name.
1267+
"""
1268+
collected_tests = self._collect_tests()
1269+
collected_results: t.List[_TaggedResult] = []
1270+
1271+
for group, tests_in_group in _TaggedTest.group_by_group(collected_tests):
1272+
for suite, tests_in_suite in _TaggedTest.group_by_suite(tests_in_group):
1273+
results: t.List[Result] = []
1274+
for test in tests_in_suite:
1275+
results.extend(suite.run_test(test.test))
1276+
for result in results:
1277+
collected_results.append(
1278+
_TaggedResult(suite=suite, group=group, result=result, test=result.test_reference)
1279+
)
1280+
1281+
self._results = collected_results
1282+
1283+
out: t.Dict[t.Optional[str], t.List[Result]] = {}
1284+
for group, grouped_results in _TaggedResult.group_by_group(collected_results):
1285+
out[group] = [tagged_result.result for tagged_result in grouped_results]
1286+
return out
1287+
12591288
def run_all_with_progress(
12601289
self,
12611290
*,
12621291
reporter: t.Optional["Reporter"] = None,
1263-
render_context: bool = True,
1264-
render_description: bool = True,
1265-
render_traceback: bool = True,
1266-
render_message: bool = True,
1292+
**reporter_kwargs: t.Any,
12671293
) -> t.Dict[t.Optional[str], t.List[Result]]:
12681294
"""Run all tests in all suites with a rich progress display.
12691295
@@ -1272,10 +1298,6 @@ def run_all_with_progress(
12721298
12731299
Args:
12741300
reporter: Optional reporter to use for output. If None, uses ConsoleReporter.
1275-
render_context: Whether to render test context in result output.
1276-
render_description: Whether to render test descriptions in result output.
1277-
render_traceback: Whether to render tracebacks for errors in result output.
1278-
render_message: Whether to render test result messages in result output.
12791301
12801302
Returns:
12811303
Dict[Optional[str], List[Result]]: Results grouped by test group name.
@@ -1386,14 +1408,9 @@ def run_all_with_progress(
13861408

13871409
self._results = collected_results
13881410
if self._results:
1389-
total_stats = ResultsStatistics.from_results([tr.result for tr in self._results])
13901411
reporter.report_results(
13911412
self._results,
1392-
total_stats,
1393-
render_description=render_description,
1394-
render_traceback=render_traceback,
1395-
render_message=render_message,
1396-
render_context=render_context,
1413+
**reporter_kwargs,
13971414
)
13981415

13991416
out: t.Dict[t.Optional[str], t.List[Result]] = {}

src/contraqctor/qc/reporters.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@ class Reporter(abc.ABC):
3737
@abc.abstractmethod
3838
def report_results(
3939
self,
40-
results: t.List[_TaggedResult],
41-
statistics: ResultsStatistics,
40+
results: dict[str | None, list[Result]] | list[_TaggedResult],
4241
*,
4342
render_context: bool = True,
4443
render_description: bool = True,
@@ -82,8 +81,7 @@ def __init__(
8281

8382
def report_results(
8483
self,
85-
results: t.List[_TaggedResult],
86-
statistics: ResultsStatistics,
84+
results: dict[str | None, list[Result]] | list[_TaggedResult],
8785
*,
8886
render_context: bool = True,
8987
render_description: bool = True,
@@ -108,6 +106,7 @@ def report_results(
108106
if not results:
109107
return
110108

109+
results = _normalize_results(results)
111110
# Setup serializer and serialize ALL results if needed
112111
# (not just the ones being displayed)
113112
serializer = None
@@ -238,7 +237,7 @@ class HtmlReporter(Reporter):
238237

239238
def __init__(
240239
self,
241-
output_path: t.Union[str, Path],
240+
output_path: t.Union[str, Path] = "report.html",
242241
template_dir: t.Optional[t.Union[str, Path]] = None,
243242
default_group_name: str = "Ungrouped",
244243
serializer: t.Optional[ContextExportableObjSerializer] = None,
@@ -258,14 +257,13 @@ def __init__(
258257

259258
def report_results(
260259
self,
261-
results: t.List[_TaggedResult],
262-
statistics: ResultsStatistics,
260+
results: dict[str | None, list[Result]] | list[_TaggedResult],
263261
*,
264262
render_context: bool = True,
265263
render_description: bool = True,
266264
render_traceback: bool = True,
267265
render_message: bool = True,
268-
serialize_context_exportable_obj: bool = False,
266+
serialize_context_exportable_obj: bool = True,
269267
**kwargs,
270268
) -> None:
271269
"""Generate HTML report of test results.
@@ -282,6 +280,8 @@ def report_results(
282280
"""
283281
template = self.env.get_template("report.html")
284282

283+
results = _normalize_results(results)
284+
285285
grouped_results = []
286286
for group, test_results in _TaggedResult.group_by_group(results):
287287
group_name = group or self.default_group_name
@@ -327,7 +327,7 @@ def report_results(
327327

328328
html_content = template.render(
329329
groups=grouped_results,
330-
statistics=statistics,
330+
statistics=ResultsStatistics.from_results([tr.result for tr in results]),
331331
status_color=STATUS_COLOR,
332332
render_context=render_context,
333333
render_description=render_description,
@@ -339,3 +339,28 @@ def report_results(
339339
)
340340

341341
self.output_path.write_text(html_content, encoding="utf-8")
342+
343+
344+
def _normalize_results(
345+
results: dict[str | None, list[Result]] | list[_TaggedResult],
346+
) -> list[_TaggedResult]:
347+
"""Normalize results to a list of _TaggedResult instances.
348+
349+
Args:
350+
results: Either a dict mapping group names to lists of Results,
351+
or a list of _TaggedResult instances.
352+
353+
Returns:
354+
A list of _TaggedResult instances.
355+
"""
356+
if isinstance(results, dict):
357+
normalized_results = []
358+
for group, test_results in results.items():
359+
for result in test_results:
360+
assert result.suite_reference is not None, "Result must have suite_reference set"
361+
normalized_results.append(
362+
_TaggedResult(suite=result.suite_reference, result=result, group=group, test=result.test_reference)
363+
)
364+
return normalized_results
365+
else:
366+
return results

tests/test_qc/test_runner.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,30 @@ def test_run_all_with_progress(self, mock_progress):
123123
assert stats[Status.ERROR] == 2
124124
assert stats[Status.SKIPPED] == 2
125125
assert stats[Status.WARNING] == 2
126+
127+
def test_run_all(self):
128+
"""Test running all tests without progress reporting."""
129+
130+
runner = Runner()
131+
suite1 = MockSuite()
132+
suite2 = MockSuite()
133+
134+
runner.add_suite(suite1) # Default group (None)
135+
runner.add_suite(suite2, group="TestGroup")
136+
137+
grouped_results = runner.run_all()
138+
139+
assert None in grouped_results
140+
assert "TestGroup" in grouped_results
141+
142+
assert len(grouped_results[None]) == 6 # All tests in MockSuite
143+
assert len(grouped_results["TestGroup"]) == 6 # All tests in MockSuite
144+
145+
all_results = grouped_results[None] + grouped_results["TestGroup"]
146+
stats = ResultsStatistics.from_results(all_results)
147+
148+
assert stats[Status.PASSED] == 4
149+
assert stats[Status.FAILED] == 2
150+
assert stats[Status.ERROR] == 2
151+
assert stats[Status.SKIPPED] == 2
152+
assert stats[Status.WARNING] == 2

tests/test_qc/test_serializers.py

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77

88
from contraqctor.qc._context_extensions import ContextExportableObj
9-
from contraqctor.qc.base import ResultsStatistics, Suite, _TaggedResult
9+
from contraqctor.qc.base import Suite, _TaggedResult
1010
from contraqctor.qc.reporters import ConsoleReporter, HtmlReporter
1111
from contraqctor.qc.serializers import (
1212
ContextExportableObjSerializer,
@@ -293,13 +293,9 @@ def test_fail_with_image(self):
293293
result = next(iter(results_iter))
294294
tagged_results.append(_TaggedResult(suite=suite, group="Test", result=result, test=test_method))
295295

296-
stats = ResultsStatistics.from_results([tr.result for tr in tagged_results])
297-
298296
with tempfile.TemporaryDirectory() as tmpdir:
299297
reporter = ConsoleReporter()
300-
reporter.report_results(
301-
tagged_results, stats, serialize_context_exportable_obj=True, asset_output_dir=tmpdir
302-
)
298+
reporter.report_results(tagged_results, serialize_context_exportable_obj=True, asset_output_dir=tmpdir)
303299

304300
files = list(Path(tmpdir).glob("*.png"))
305301
assert len(files) == 2
@@ -324,16 +320,14 @@ def test_fail_with_image(self):
324320
result = next(iter(results_iter))
325321
tagged_results.append(_TaggedResult(suite=suite, group="Test", result=result, test=test_method))
326322

327-
stats = ResultsStatistics.from_results([tr.result for tr in tagged_results])
328-
329323
default_dir = Path("./report/assets")
330324
if default_dir.exists():
331325
import shutil
332326

333327
shutil.rmtree(default_dir)
334328

335329
reporter = ConsoleReporter()
336-
reporter.report_results(tagged_results, stats, serialize_context_exportable_obj=True)
330+
reporter.report_results(tagged_results, serialize_context_exportable_obj=True)
337331

338332
assert default_dir.exists()
339333
files = list(default_dir.glob("*.png"))
@@ -367,12 +361,10 @@ def test_with_image(self):
367361
result = next(iter(results_iter))
368362
tagged_results.append(_TaggedResult(suite=suite, group="Test", result=result, test=test_method))
369363

370-
stats = ResultsStatistics.from_results([tr.result for tr in tagged_results])
371-
372364
with tempfile.TemporaryDirectory() as tmpdir:
373365
output_path = Path(tmpdir) / "test_report.html"
374366
reporter = HtmlReporter(output_path)
375-
reporter.report_results(tagged_results, stats, serialize_context_exportable_obj=True)
367+
reporter.report_results(tagged_results, serialize_context_exportable_obj=True)
376368

377369
assert output_path.exists()
378370
content = output_path.read_text()
@@ -398,12 +390,10 @@ def test_with_image(self):
398390
result = next(iter(results_iter))
399391
tagged_results.append(_TaggedResult(suite=suite, group="Test", result=result, test=test_method))
400392

401-
stats = ResultsStatistics.from_results([tr.result for tr in tagged_results])
402-
403393
with tempfile.TemporaryDirectory() as tmpdir:
404394
output_path = Path(tmpdir) / "test_report.html"
405395
reporter = HtmlReporter(output_path)
406-
reporter.report_results(tagged_results, stats, serialize_context_exportable_obj=False)
396+
reporter.report_results(tagged_results, serialize_context_exportable_obj=False)
407397

408398
assert output_path.exists()
409399
content = output_path.read_text()

0 commit comments

Comments
 (0)