test_check_coverage.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. #!/usr/bin/env python3
  2. """Fixture tests for scripts/check_coverage.py (plan todo 1).
  3. Every case builds an lcov tracefile in a fresh temporary directory, runs the
  4. real checker over it, and asserts the exit code and the message the contract
  5. requires. No network, no framework: `python3 scripts/test_check_coverage.py`.
  6. """
  7. from __future__ import annotations
  8. import contextlib
  9. import importlib.util
  10. import io
  11. import shutil
  12. import sys
  13. import tempfile
  14. from pathlib import Path
  15. REPO = Path(__file__).resolve().parent.parent
  16. CHECKER_PATH = REPO / "scripts" / "check_coverage.py"
  17. # Independent copy of the canonical regex: if the checker's constant drifts
  18. # from the one CI uses, this literal stops matching and the test fails.
  19. CANONICAL_EXCLUDE = r"(^|/)tests/|(^|/)tests\.rs$|(^|/)tests_[^/]*\.rs$|test_support\.rs$|test_guard\.rs$"
  20. def _load_checker():
  21. spec = importlib.util.spec_from_file_location("check_coverage", CHECKER_PATH)
  22. if spec is None or spec.loader is None:
  23. raise RuntimeError(f"cannot load {CHECKER_PATH}")
  24. module = importlib.util.module_from_spec(spec)
  25. spec.loader.exec_module(module)
  26. return module
  27. CM = _load_checker()
  28. def _module_path(name: str) -> str:
  29. spec = CM.MODULE_SPECS[name]
  30. return spec if not spec.endswith("/") else spec + "mod.rs"
  31. def _module_records(names, hits: int, lines: int = 4) -> list:
  32. return [
  33. (_module_path(name), [(line_no, hits) for line_no in range(1, lines + 1)])
  34. for name in names
  35. ]
  36. def _lcov_text(records: list) -> str:
  37. out: list[str] = []
  38. for sf, das in records:
  39. out.append(f"SF:{sf}")
  40. for line_no, hits in das:
  41. out.append(f"DA:{line_no},{hits}")
  42. out.append("end_of_record")
  43. return "\n".join(out) + "\n"
  44. def _write_lcov(directory: Path, records: list) -> Path:
  45. lcov_path = directory / "lcov.info"
  46. lcov_path.write_text(_lcov_text(records), encoding="utf-8")
  47. return lcov_path
  48. def _run(records: list, *args: str) -> tuple[int, str]:
  49. directory = Path(tempfile.mkdtemp(prefix="check-coverage-"))
  50. try:
  51. lcov_path = _write_lcov(directory, records)
  52. stdout = io.StringIO()
  53. with contextlib.redirect_stdout(stdout):
  54. code = CM.main([str(lcov_path), *args])
  55. return code, stdout.getvalue()
  56. finally:
  57. shutil.rmtree(directory, ignore_errors=True)
  58. def test_full_coverage_passes():
  59. records = _module_records(CM.MODULE_ORDER, hits=1)
  60. code, out = _run(
  61. records,
  62. "--global-floor",
  63. "100.0",
  64. "--security-floor",
  65. "100.0",
  66. )
  67. assert code == 0, out
  68. assert "RESULT: PASS" in out, out
  69. def test_zero_coverage_fails_below_global_floor():
  70. records = _module_records(CM.MODULE_ORDER, hits=0)
  71. code, out = _run(records, "--global-floor", "1.0", "--security-floor", "0.0")
  72. assert code == 1, out
  73. assert "global below floor" in out, out
  74. def test_missing_security_group_fails_closed():
  75. records = _module_records(("ui", "config", "tls"), hits=1)
  76. code, out = _run(records, "--global-floor", "0.0", "--security-floor", "0.0")
  77. assert code == 1, out
  78. assert "security" in out, out
  79. assert "fail closed" in out, out
  80. def test_tests_e2e_records_are_excluded():
  81. records = _module_records(CM.MODULE_ORDER, hits=1)
  82. records.append(("tests/e2e/flow.rs", [(1, 0), (2, 0)]))
  83. code, out = _run(
  84. records,
  85. "--global-floor",
  86. "100.0",
  87. "--security-floor",
  88. "100.0",
  89. )
  90. assert code == 0, out
  91. assert "RESULT: PASS" in out, out
  92. def test_module_test_files_are_excluded():
  93. records = _module_records(CM.MODULE_ORDER, hits=1)
  94. records.append(("src/protocol/tests_extra.rs", [(1, 0), (2, 0)]))
  95. records.append(("src/protocol/tests.rs", [(1, 0)]))
  96. records.append(("src/test_support.rs", [(1, 0)]))
  97. records.append(("src/test_guard.rs", [(1, 0)]))
  98. code, out = _run(
  99. records,
  100. "--global-floor",
  101. "100.0",
  102. "--security-floor",
  103. "100.0",
  104. )
  105. assert code == 0, out
  106. assert "RESULT: PASS" in out, out
  107. def test_one_module_below_its_floor_fails():
  108. records: list = []
  109. for name in CM.MODULE_ORDER:
  110. hits = 0 if name == "crypto" else 1
  111. records.extend(_module_records((name,), hits=hits))
  112. code, out = _run(
  113. records,
  114. "--module-floor",
  115. "crypto=100.0",
  116. "--global-floor",
  117. "0.0",
  118. "--security-floor",
  119. "0.0",
  120. )
  121. assert code == 1, out
  122. assert "module 'crypto' below floor" in out, out
  123. def test_canonical_exclude_regex_matches_ci_contract():
  124. assert CM.EXCLUDE_PATTERN == CANONICAL_EXCLUDE
  125. assert CM.EXCLUDE_RE.pattern == CANONICAL_EXCLUDE
  126. for path in (
  127. "tests/e2e/flow.rs",
  128. "tests/integration.rs",
  129. "src/protocol/tests.rs",
  130. "src/protocol/tests_extra.rs",
  131. "src/test_support.rs",
  132. "src/test_guard.rs",
  133. ):
  134. assert CM.EXCLUDE_RE.search(path), path
  135. for path in ("src/protocol/cimd.rs", "src/routes.rs", "src/crypto/hash.rs"):
  136. assert not CM.EXCLUDE_RE.search(path), path
  137. def test_aggregation_merges_duplicates_and_totals_modules():
  138. records = [
  139. ("src/crypto/a.rs", [(1, 1), (2, 0)]),
  140. ("src/crypto/a.rs", [(1, 3), (3, 1)]),
  141. ("src/crypto/b.rs", [(1, 0)]),
  142. ("src/jose/j.rs", [(1, 1)]),
  143. ("src/other.rs", [(1, 0)]),
  144. ("tests/e2e/x.rs", [(1, 0)]),
  145. ]
  146. directory = Path(tempfile.mkdtemp(prefix="check-coverage-agg-"))
  147. try:
  148. lcov_path = _write_lcov(directory, records)
  149. totals = CM.aggregate(
  150. CM.parse_lcov(lcov_path.read_text(encoding="utf-8")), lcov_path
  151. )
  152. finally:
  153. shutil.rmtree(directory, ignore_errors=True)
  154. assert totals["src/crypto/a.rs"] == {1: 3, 2: 0, 3: 1}
  155. assert totals["src/crypto/b.rs"] == {1: 0}
  156. assert "tests/e2e/x.rs" not in totals
  157. counts = CM.module_counts(totals)
  158. assert counts["crypto"] == {"files": 2, "found": 4, "hit": 2}
  159. assert counts["jose"] == {"files": 1, "found": 1, "hit": 1}
  160. global_found = sum(len(lines) for lines in totals.values())
  161. global_hit = sum(CM.line_counts(lines)[1] for lines in totals.values())
  162. assert (global_hit, global_found) == (3, 6)
  163. assert CM.percent(global_hit, global_found) == 50.0
  164. def main() -> int:
  165. tests = [
  166. value
  167. for name, value in sorted(globals().items())
  168. if name.startswith("test_") and callable(value)
  169. ]
  170. failed = 0
  171. for test in tests:
  172. try:
  173. test()
  174. except AssertionError as exc:
  175. failed += 1
  176. print(f"FAIL {test.__name__}: {exc}")
  177. else:
  178. print(f"PASS {test.__name__}")
  179. print(f"{len(tests) - failed}/{len(tests)} passed")
  180. return 1 if failed else 0
  181. if __name__ == "__main__":
  182. sys.exit(main())