This repository has been archived on 2026-11-08. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
platform-ci/tests/test_test_reports.py
forge-recoveryandClaude Sonnet 5.5 f2fbacf131
KofTwentyTwo/ci-policy: ci / fork-guard (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / go-nix-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / gradle-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-docker-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / java-maven-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / manifests-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / nix-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / python-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / rust-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / image-build-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / not-enrolled (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / content-validate-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / standards-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / node-matrix-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: ci / source-config-15 (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / verify (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / secrets (pull_request) Blocked by required conditions
KofTwentyTwo/ci-policy: Shared source and config CI / gate (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: ci / ci-result (pull_request) Waiting for needed jobs
KofTwentyTwo/ci-policy: native / fork-deny (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-0 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-1 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-2 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-macos-3 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-4 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-5 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-6 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / build-windows-7 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-8 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-9 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-10 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / dotnet-11 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-12 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-13 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-14 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-15 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-16 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-17 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-18 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / swift-19 (pull_request) Skipped
KofTwentyTwo/ci-policy: native / native-result (pull_request) Canceled after 5s
feat(ci): qualify shared security checks and available native cache
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-05 02:48:54 +00:00

154 lines
8.2 KiB
Python

"""JUnit report judgment: legacy Maven allow-empty behavior, strict XML handling, workspace containment."""
import tempfile
import unittest
from pathlib import Path
from typing import override
from platform_ci.test_reports import JUnitSummary, TestReportError, check_junit_reports
PASSING = '<testsuite name="a" tests="3" skipped="1" failures="0" errors="0"><testcase name="x"/><testcase name="y"><skipped/></testcase></testsuite>'
class JUnitReportTest(unittest.TestCase):
"""Reports are parsed as data; only failures, errors and malformed reports fail."""
@override
def setUp(self) -> None:
"""Create an empty workspace."""
directory = tempfile.TemporaryDirectory()
self.addCleanup(directory.cleanup)
self.workspace = Path(directory.name).resolve()
def _report(self, content: str | bytes, name: str = "target/surefire-reports/TEST-a.xml") -> Path:
path = self.workspace / name
path.parent.mkdir(parents=True, exist_ok=True)
if isinstance(content, str):
content = content.encode("utf-8")
path.write_bytes(content)
return Path(name)
def _check(self, *contents: str | bytes) -> JUnitSummary:
return check_junit_reports(self.workspace, [self._report(content, f"r/TEST-{index}.xml") for index, content in enumerate(contents)])
def test_passing_report_is_summarized(self) -> None:
"""Counts are totaled and skipped tests are not executed ones."""
summary = self._check(PASSING, '<testsuite tests="2" failures="0" errors="0"/>')
self.assertTrue(summary.passed)
self.assertEqual((summary.reports, summary.tests, summary.skipped, summary.executed, summary.failures, summary.errors), (2, 5, 1, 4, 0, 0))
def test_zero_reports_zero_tests_and_all_skipped_are_allowed(self) -> None:
"""The old Maven --allow-empty behavior: no new zero-report or all-skipped gate."""
self.assertTrue(check_junit_reports(self.workspace, []).passed)
self.assertTrue(self._check('<testsuite tests="0"/>').passed)
all_skipped = self._check('<testsuite tests="2" skipped="2"><testcase name="a"><skipped/></testcase></testsuite>')
self.assertTrue(all_skipped.passed)
self.assertEqual(all_skipped.executed, 0)
def test_failed_or_errored_counts_fail(self) -> None:
"""A nonzero failures or errors attribute fails, and every report is still totaled."""
for attributes, expected in (('failures="1"', (1, 0)), ('errors="2"', (0, 2))):
with self.subTest(attributes):
summary = self._check(f'<testsuite tests="3" {attributes}/>')
self.assertFalse(summary.passed)
self.assertEqual((summary.failures, summary.errors), expected)
def test_failing_or_erroring_testcases_fail_even_when_counts_claim_success(self) -> None:
"""Child elements are authoritative over a summary that says zero."""
for child, expected in (("failure", (1, 0)), ("error", (0, 1))):
with self.subTest(child):
summary = self._check(f'<testsuite tests="1" failures="0" errors="0"><testcase name="t"><{child} message="m"/></testcase></testsuite>')
self.assertFalse(summary.passed)
self.assertEqual((summary.failures, summary.errors), expected)
def test_testsuites_root_sums_its_suites(self) -> None:
"""An aggregate root counts each direct suite once."""
summary = self._check('<testsuites><testsuite tests="2" skipped="1"/><testsuite tests="3" failures="1"/></testsuites>')
self.assertEqual((summary.tests, summary.skipped, summary.failures), (5, 1, 1))
self.assertFalse(summary.passed)
def test_one_bad_report_fails_but_the_others_are_still_counted(self) -> None:
"""Problems name the workspace-relative report and never include report content."""
summary = self._check(PASSING, '<testsuite tests="1" failures="1"/>')
self.assertEqual(summary.tests, 4)
self.assertEqual(len(summary.problems), 1)
self.assertTrue(summary.problems[0].startswith("r/TEST-1.xml:"))
def test_malformed_reports_fail(self) -> None:
"""Broken XML, wrong roots, empty files, bad counts and skipped above tests are all problems."""
cases = {
"truncated": "<testsuite tests='1'>",
"empty": "",
"not xml": "tests passed",
"wrong root": '<report tests="1"/>',
"negative count": '<testsuite tests="-1"/>',
"word count": '<testsuite tests="many"/>',
"skipped above tests": '<testsuite tests="1" skipped="2"/>',
"invalid bytes": b"\xff\xfe\x00<testsuite",
}
for description, content in cases.items():
with self.subTest(description):
summary = self._check(content)
self.assertFalse(summary.passed)
self.assertEqual(len(summary.problems), 1)
def test_doctype_and_entities_are_rejected_without_expansion(self) -> None:
"""DTD and entity declarations fail, including an entity bomb, and nothing is expanded or fetched."""
bomb = '<!DOCTYPE testsuite [<!ENTITY a "aaaaaaaaaa"><!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;">]><testsuite tests="0" name="&b;"/>'
external = '<!DOCTYPE testsuite SYSTEM "http://127.0.0.1:9/evil.dtd"><testsuite tests="0"/>'
entity_only = '<!DOCTYPE testsuite [<!ENTITY x SYSTEM "file:///etc/passwd">]><testsuite tests="0"/>'
internal = '<!DOCTYPE testsuite [<!ENTITY x "value">]><testsuite tests="0" name="&x;"/>'
cases: dict[str, str | bytes] = {
"bomb": bomb,
"external dtd": external,
"external entity": entity_only,
"internal entity": internal,
"utf-16 bomb": bomb.encode("utf-16"),
"utf-16 external entity": entity_only.encode("utf-16"),
"utf-16 internal entity": internal.encode("utf-16"),
"utf-8 with declaration": b'<?xml version="1.0" encoding="UTF-8"?>' + external.encode("utf-8"),
}
for description, content in cases.items():
with self.subTest(description):
summary = self._check(content)
self.assertFalse(summary.passed)
self.assertIn("not allowed", summary.problems[0])
def test_utf16_report_without_declarations_is_parsed_once(self) -> None:
"""A BOM-marked UTF-16 report parses normally, and nested text and cases are counted once."""
summary = self._check(PASSING.encode("utf-16"))
self.assertTrue(summary.passed)
self.assertEqual((summary.tests, summary.skipped), (3, 1))
def test_report_outside_the_workspace_is_rejected_including_through_a_symlink(self) -> None:
"""An absolute path, parent traversal and a link to an outside file all raise."""
with tempfile.TemporaryDirectory() as outside_name:
outside = Path(outside_name) / "TEST-out.xml"
outside.write_text(PASSING, encoding="utf-8")
with self.assertRaises(TestReportError):
check_junit_reports(self.workspace, [outside])
link = self.workspace / "link.xml"
try:
link.symlink_to(outside)
except OSError:
self.skipTest("symbolic links are not available")
with self.assertRaises(TestReportError):
check_junit_reports(self.workspace, [Path("link.xml")])
with self.assertRaises(TestReportError):
check_junit_reports(self.workspace, [Path("../elsewhere.xml")])
def test_git_metadata_platform_checkout_missing_files_and_directories_are_rejected(self) -> None:
"""Only real report files in the checkout are read; a missing declared report is packaging's concern, not a pass here."""
git_report = self._report(PASSING, ".git/TEST-a.xml")
platform_report = self._report(PASSING, ".platform-ci/TEST-a.xml")
(self.workspace / "dir").mkdir()
for path in (git_report, platform_report, Path("missing.xml"), Path("dir")):
with self.subTest(str(path)), self.assertRaises(TestReportError):
check_junit_reports(self.workspace, [path])
def test_parser_runs_no_tools(self) -> None:
"""The module imports no process-starting facility, so tests can never be rerun from here."""
source = (Path(__file__).resolve().parent.parent / "platform_ci" / "test_reports.py").read_text(encoding="utf-8")
for forbidden in ("subprocess", "os.system", "socket", "urllib"):
self.assertNotIn(f"import {forbidden}", source)