#!/usr/bin/env python3 """Deterministic subprocess checks for the TRX executed-floor guard.""" import os from pathlib import Path import subprocess import tempfile import unittest SCRIPT = Path(__file__).with_name("assert-not-all-skipped.sh") NS = "http://microsoft.com/schemas/VisualStudio/TeamTest/2010" def trx(executed, outcomes, summary_skipped=0, namespace=NS, extra=""): declaration = f' xmlns="{namespace}"' if namespace else "" results = "".join(f'' for outcome in outcomes) return (f'{results}{extra}' f'') class SkipGuardTests(unittest.TestCase): def setUp(self): self.directory = tempfile.TemporaryDirectory(prefix="trx-skip-fixtures-") self.addCleanup(self.directory.cleanup) self.root = Path(self.directory.name) def write(self, name, content): path = self.root / name path.write_text(content, encoding="utf-8") return path def run_guard(self, *paths, minimum=None): env = os.environ.copy() env.pop("MIN_EXECUTED", None) if minimum is not None: env["MIN_EXECUTED"] = str(minimum) return subprocess.run(["bash", str(SCRIPT), *map(str, paths)], env=env, text=True, capture_output=True, check=False) def assert_report(self, result, status, executed, skipped, files=1): self.assertEqual(status, result.returncode, result.stdout + result.stderr) self.assertIn(f"files={files} executed={executed} skipped={skipped} ", result.stdout) def test_mixed_results_ignore_zero_summary_skip_counter(self): # A summary-only skip count misses the three actual NotExecuted records. path = self.write("mixed.trx", trx(827, ["Passed"] * 827 + ["NotExecuted"] * 3)) result = self.run_guard(path) self.assert_report(result, 0, 827, 3) self.assertIn("mixed.trx: executed=827 skipped=3", result.stdout) def test_all_skipped_fails_default_floor_but_report_only_passes(self): path = self.write("all-skipped.trx", trx(0, ["NotExecuted"] * 188)) self.assert_report(self.run_guard(path), 1, 0, 188) self.assert_report(self.run_guard(path, minimum=0), 0, 0, 188) def test_zero_tests_fails_default_floor_but_report_only_passes(self): path = self.write("zero.trx", trx(0, [])) self.assert_report(self.run_guard(path), 1, 0, 0) self.assert_report(self.run_guard(path, minimum=0), 0, 0, 0) def test_agreeing_summary_and_outcomes_are_not_double_counted(self): path = self.write("agreeing.trx", trx(1, ["Passed", "NotExecuted"], 1)) self.assert_report(self.run_guard(path), 0, 1, 1) def test_outcomes_are_authoritative_when_summary_overstates_skips(self): path = self.write("overstated.trx", trx(1, ["Passed", "NotExecuted"], 9)) self.assert_report(self.run_guard(path), 0, 1, 1) def test_namespace_path_and_exact_outcome_exclude_decoys(self): extra = ('' '' '<UnitTestResult outcome="NotExecuted"/>') content = trx(2, ["Passed", "Failed", "NotExecuted", "notexecuted"], extra=extra) # Prefixes on Results/UnitTestResult must work just like the default namespace. content = content.replace('', '').replace('', '') content = content.replace('') self.assertEqual(1, self.run_guard(path, minimum=0).returncode) def test_malformed_xml_keeps_summary_fallback_and_gate_status(self): path = self.write("malformed.trx", '') self.assert_report(self.run_guard(path, minimum=2), 0, 2, 3) self.assert_report(self.run_guard(path, minimum=3), 1, 2, 3) if __name__ == "__main__": unittest.main(verbosity=2)