# Author: cirakg & claude ai # Summary: Tests for tools/ciq_test_report.py, the judge of Connect IQ unit-test logs. """Tests for tools/ciq_test_report.py, the judge of Connect IQ test logs.""" from __future__ import annotations import xml.etree.ElementTree as ET from pathlib import Path import ciq_test_report as rep from hypothesis import given from hypothesis import strategies as st DATA = Path(__file__).parent / "data" PASSING = """\ ------------------------------------------------------------------------------ Executing test smokeVersionIsSemver... DEBUG (14:16): version 0.1.0 PASS ------------------------------------------------------------------------------ Executing test otherTest... PASS ============================================================================== RESULTS Test: Status: smokeVersionIsSemver PASS otherTest PASS Ran 2 tests PASSED (passed=2, failed=0, errors=0) """ def test_real_sdk_log_passes() -> None: report = rep.parse((DATA / "runnoevil_pass.log").read_text(encoding="utf-8")) assert report.results == {"smokeVersionIsSemver": "PASS"} assert (report.ran, report.passed, report.failed, report.errors) == (1, 1, 0, 0) assert report.problems(min_tests=1) == (rep.EXIT_PASS, []) def test_passing_log_passes() -> None: report = rep.parse(PASSING) assert report.results == {"smokeVersionIsSemver": "PASS", "otherTest": "PASS"} assert report.output["smokeVersionIsSemver"] == ["DEBUG (14:16): version 0.1.0", "PASS"] assert report.problems(min_tests=1) == (rep.EXIT_PASS, []) def test_documented_older_summary_format_passes() -> None: text = PASSING.replace("PASSED (passed=2, failed=0, errors=0)", "PASSED (failures=0, errors=0)") report = rep.parse(text) assert report.passed is None assert report.problems(min_tests=1)[0] == rep.EXIT_PASS def test_status_words_are_case_insensitive() -> None: text = PASSING.replace("PASS\n", "Pass\n").replace("PASSED (", "Passed (") assert rep.parse(text).problems(min_tests=1)[0] == rep.EXIT_PASS def test_failed_test_fails() -> None: text = PASSING.replace("otherTest PASS", "otherTest FAIL").replace( "PASSED (passed=2, failed=0", "FAILED (passed=1, failed=1" ) code, reasons = rep.parse(text).problems(min_tests=1) assert code == rep.EXIT_FAIL assert "otherTest: FAIL" in reasons def test_error_row_fails_even_if_summary_claims_pass() -> None: text = PASSING.replace("otherTest PASS", "otherTest ERROR") code, reasons = rep.parse(text).problems(min_tests=1) assert code == rep.EXIT_FAIL assert "otherTest: ERROR" in reasons assert any("passed=2 but 1 rows" in r for r in reasons) def test_log_without_results_is_incomplete() -> None: truncated = PASSING.partition("RESULTS")[0] assert rep.parse(truncated).problems(min_tests=1)[0] == rep.EXIT_INCOMPLETE def test_too_few_tests_fails() -> None: code, reasons = rep.parse(PASSING).problems(min_tests=3) assert code == rep.EXIT_FAIL assert any("expected at least 3" in r for r in reasons) def test_ran_count_must_match_rows() -> None: code, reasons = rep.parse(PASSING.replace("Ran 2 tests", "Ran 3 tests")).problems(min_tests=1) assert code == rep.EXIT_FAIL assert any("'Ran 3'" in r for r in reasons) def test_junit_output_is_valid(tmp_path: Path) -> None: log = tmp_path / "fr70.log" log.write_text(PASSING.replace("otherTest PASS", "otherTest FAIL")) junit = tmp_path / "out" / "fr70.xml" assert rep.main([str(log), "--device", "fr70", "--junit", str(junit)]) == rep.EXIT_FAIL suite = ET.parse(junit).getroot() assert suite.get("tests") == "2" assert suite.get("failures") == "1" assert [c.get("name") for c in suite.iter("testcase")] == ["smokeVersionIsSemver", "otherTest"] def test_missing_log_is_incomplete(tmp_path: Path) -> None: assert rep.main([str(tmp_path / "nope.log"), "--device", "x"]) == rep.EXIT_INCOMPLETE @given(st.text()) def test_parser_never_crashes(text: str) -> None: code, _ = rep.parse(text).problems(min_tests=1) assert code in (rep.EXIT_PASS, rep.EXIT_FAIL, rep.EXIT_INCOMPLETE)