import io
import os
import subprocess
import sys
import tempfile
import unittest
from contextlib import redirect_stdout
from pathlib import Path
SCRIPTS = Path(__file__).resolve().parent
sys.path.insert(0, str(SCRIPTS))
GATE = SCRIPTS / "coverage_gate.py"
REQUIRE_BOTH = ("--require", "mac", "--require", "win")
MAC = """TN:
SF:/Users/someone/proc/src/a.rs
DA:1,1
DA:2,4
DA:3,0
DA:5,0
DA:6,0
DA:7,0
end_of_record
"""
WIN = """TN:
SF:C:\\a\\proc\\proc\\src\\a.rs
DA:1,0
DA:2,0
DA:3,2
DA:5,0
DA:6,0
DA:7,0
end_of_record
SF:C:\\Users\\runner\\.cargo\\registry\\src\\index.crates.io-1949cf8c6b5b557f\\serde-1.0.0\\src\\a.rs
DA:1,0
DA:99,0
end_of_record
"""
class GateTest(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.root = Path(self.tmp.name)
(self.root / "src").mkdir()
(self.root / "src" / "a.rs").write_text("// fixture\n")
(self.root / "src" / "sub").mkdir()
(self.root / "src" / "sub" / "a.rs").write_text("// fixture\n")
self.mac = self.write("mac.info", MAC)
self.win = self.write("win.info", WIN)
def tearDown(self):
self.tmp.cleanup()
def write(self, name, text):
p = self.root / name
p.write_text(text)
return str(p)
def run_gate(self, *args):
return subprocess.run(
[sys.executable, str(GATE), "--repo-root", str(self.root), *args],
capture_output=True,
text=True,
)
def test_merge_counts_line_hit_in_either_file(self):
r = self.run_gate(self.mac, self.win)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertNotIn("src/a.rs:3", r.stdout)
self.assertIn("src/a.rs:5", r.stdout)
self.assertIn("src/a.rs:7", r.stdout)
def test_each_lcov_gets_its_own_total_line_and_it_decides_nothing(self):
r = self.run_gate(self.mac, self.win)
self.assertEqual(r.returncode, 0, r.stderr)
self.assertRegex(r.stdout, r"mac\.info\s+2/6\s+33\.3%")
self.assertRegex(r.stdout, r"win\.info\s+1/6\s+16\.6%")
self.assertRegex(r.stdout, r"TOTAL\s+3/6\s+50\.0%")
r = self.run_gate(self.mac, self.win, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 1)
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 0, r.stdout)
self.assertRegex(r.stdout, r"mac\.info\s+2/3\s+66\.6%")
def test_one_missed_line_never_reads_as_a_full_hundred(self):
rows = "".join(f"DA:{n},{0 if n == 4000 else 1}\n" for n in range(1, 4001))
big = self.write("big.info", f"SF:/x/proc/src/a.rs\n{rows}end_of_record\n")
r = self.run_gate(big, self.mac)
self.assertRegex(r.stdout, r"big\.info\s+3999/4000\s+99\.9%")
def test_single_file_leaves_line_three_uncovered(self):
r = self.run_gate(self.mac)
self.assertIn("src/a.rs:3", r.stdout)
def test_registry_dependency_is_ignored(self):
r = self.run_gate(self.win)
self.assertNotIn(":99", r.stdout)
self.assertIn("src/a.rs", r.stdout)
self.assertIn("1/6", r.stdout)
def test_longest_repo_path_wins(self):
sub = self.write(
"sub.info", "SF:/x/proc/src/sub/a.rs\nDA:1,1\nend_of_record\n"
)
r = self.run_gate(sub)
self.assertIn("src/sub/a.rs", r.stdout)
self.assertIn("1/1", r.stdout)
def test_unknown_file_is_ignored(self):
other = self.write("o.info", "SF:/x/proc/src/nope.rs\nDA:1,0\nend_of_record\n")
r = self.run_gate(other, "--enforce", "--require", "o.info")
self.assertEqual(r.returncode, 0, r.stdout)
def test_exclusions_remove_lines(self):
ex = self.write("ex.txt", "# header\nsrc/a.rs:5-6 not reachable\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex)
self.assertNotIn("src/a.rs:5", r.stdout)
self.assertNotIn("src/a.rs:6", r.stdout)
self.assertIn("src/a.rs:7", r.stdout)
def test_enforce_fails_while_line_seven_uncovered(self):
r = self.run_gate(self.mac, self.win, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 1)
self.assertIn("src/a.rs:7", r.stdout)
def test_enforce_passes_when_everything_covered_or_excluded(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 0, r.stdout)
def test_report_only_exits_zero(self):
r = self.run_gate(self.mac)
self.assertEqual(r.returncode, 0)
def test_custom_cargo_home_registry_is_ignored(self):
dep = self.write(
"dep.info",
"SF:/out/cargo-home/registry/src/index.crates.io-1949cf8c6b5b557f/"
"serde-1.0.228/src/a.rs\nDA:1,0\nDA:2,0\nend_of_record\n"
"SF:/out/cargo-home/git/checkouts/foo-1234/abcdef/src/a.rs\n"
"DA:3,0\nend_of_record\n"
"SF:/rustc/abc123/library/core/src/a.rs\nDA:4,0\nend_of_record\n"
"SF:/work/src/a.rs\nDA:1,1\nend_of_record\n",
)
r = self.run_gate(dep, "--enforce", "--require", "dep.info")
self.assertEqual(r.returncode, 0, r.stdout)
self.assertIn("1/1", r.stdout)
def test_exclusion_without_reason_is_rejected(self):
ex = self.write("ex.txt", "src/a.rs:5-6\n")
r = self.run_gate(self.mac, "--exclusions", ex)
self.assertNotEqual(r.returncode, 0)
self.assertIn("reason", r.stderr)
def test_exclusion_of_unknown_file_or_reversed_range_is_rejected(self):
ex = self.write("ex.txt", "src/nope.rs:1-2 gone\n")
r = self.run_gate(self.mac, "--exclusions", ex)
self.assertNotEqual(r.returncode, 0)
ex = self.write("ex2.txt", "src/a.rs:6-5 backwards\n")
r = self.run_gate(self.mac, "--exclusions", ex)
self.assertNotEqual(r.returncode, 0)
def test_stale_exclusion_warns_and_fails_under_enforce(self):
ex = self.write("ex.txt", "src/a.rs:5-7 real\nsrc/a.rs:1 stale\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex)
self.assertEqual(r.returncode, 0, r.stdout)
self.assertIn("stale exclusion", r.stdout)
self.assertIn("src/a.rs:1", r.stdout)
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 1)
def test_stale_exclusion_for_file_without_data(self):
ex = self.write("ex.txt", "src/sub/a.rs:1-2 no data for this file\n")
r = self.run_gate(self.mac, "--exclusions", ex)
self.assertIn("stale exclusion", r.stdout)
def test_report_counts_excluded_lines_per_file(self):
ex = self.write("ex.txt", "src/a.rs:5-6 two lines\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex)
self.assertIn("excluded: src/a.rs 2", r.stdout)
def test_enforce_fails_when_a_required_os_file_is_missing(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(
self.mac, "--exclusions", ex, "--enforce", "--require", "mac", "--require", "win"
)
self.assertEqual(r.returncode, 1, r.stdout)
self.assertIn("win", r.stdout)
self.assertIn("missing lcov", r.stdout)
def test_enforce_fails_on_an_unexpected_extra_lcov_file(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
extra = self.write("other.info", MAC)
r = self.run_gate(
self.mac, self.win, extra, "--exclusions", ex, "--enforce",
"--require", "mac", "--require", "win",
)
self.assertEqual(r.returncode, 1, r.stdout)
self.assertIn("unexpected lcov", r.stdout)
def test_enforce_fails_when_one_name_matches_two_files(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
twin = self.write("mac2.info", MAC)
r = self.run_gate(
self.mac, twin, self.win, "--exclusions", ex, "--enforce",
"--require", "mac", "--require", "win",
)
self.assertEqual(r.returncode, 1, r.stdout)
self.assertIn("matches 2 lcov files", r.stdout)
def test_enforce_passes_with_exactly_the_required_files(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(
self.mac, self.win, "--exclusions", ex, "--enforce",
"--require", "mac", "--require", "win",
)
self.assertEqual(r.returncode, 0, r.stdout)
def test_enforce_without_any_require_is_an_error(self):
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce")
self.assertNotEqual(r.returncode, 0, r.stdout)
self.assertIn("--enforce needs at least one --require", r.stdout + r.stderr)
def test_require_is_only_checked_under_enforce(self):
r = self.run_gate(self.mac, "--require", "mac", "--require", "win")
self.assertEqual(r.returncode, 0, r.stdout)
def test_enforce_fails_for_a_source_file_with_code_and_no_data(self):
(self.root / "src" / "b.rs").write_text("// lib\npub fn f() {}\n")
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 1, r.stdout)
self.assertIn("no coverage data: src/b.rs", r.stdout)
def test_file_without_data_is_not_failed_in_report_mode(self):
(self.root / "src" / "b.rs").write_text("pub fn f() {}\n")
r = self.run_gate(self.mac, self.win)
self.assertEqual(r.returncode, 0, r.stdout)
def test_file_with_no_functions_needs_no_data(self):
(self.root / "src" / "m.rs").write_text("// fn old() {}\npub mod a;\n")
ex = self.write("ex.txt", "src/a.rs:5-7 fixture\n")
r = self.run_gate(self.mac, self.win, "--exclusions", ex, "--enforce", *REQUIRE_BOTH)
self.assertEqual(r.returncode, 0, r.stdout)
if __name__ == "__main__":
unittest.main()