31 @return Value returned by `parse_args()`.
33 parser = argparse.ArgumentParser(
35 formatter_class=argparse.RawDescriptionHelpFormatter,
38 " python3 scripts/python_coverage_gate.py\n"
39 " python3 scripts/python_coverage_gate.py --target scripts/picurv --target scripts/grid.gen\n"
40 " python3 scripts/python_coverage_gate.py --pytest-args -- -q tests/test_cli_smoke.py\n"
47 help=
"Minimum required weighted line coverage percent (default: 70.0).",
54 "Repository-relative file to include in coverage computation "
55 "(repeatable). Defaults to core runtime scripts."
60 default=
"coverage/python",
61 help=
"Repository-relative output directory for coverage artifacts (default: coverage/python).",
65 nargs=argparse.REMAINDER,
67 help=
"Arguments passed to pytest (prefix with --pytest-args -- ...).",
69 return parser.parse_args()
94def collect_counts(results: trace.CoverageResults) -> dict[str, dict[int, int]]:
96 @brief Collect counts.
97 @param[in] results Argument passed to `collect_counts()`.
98 @return Value returned by `collect_counts()`.
100 counts_by_file: dict[str, dict[int, int]] = {}
101 for (filename, lineno), count
in results.counts.items():
103 counts_by_file.setdefault(file_key, {})[lineno] = count
104 return counts_by_file
109 @brief Compute file coverage.
110 @param[in] target Argument passed to `compute_file_coverage()`.
111 @param[in] counts_by_file Argument passed to `compute_file_coverage()`.
112 @return Value returned by `compute_file_coverage()`.
114 finder = getattr(trace,
"find_executable_linenos",
None)
116 finder = trace._find_executable_linenos
117 executable = finder(str(target))
118 executable_lines = set(executable.keys())
119 total = len(executable_lines)
124 covered = sum(1
for line
in executable_lines
if observed.get(line, 0) > 0)
125 percent = (100.0 * covered) / total
126 return covered, total, percent
131 @brief Entry point for this script.
132 @return Value returned by `main()`.
135 output_dir = (REPO_ROOT / args.output_dir).resolve()
136 output_dir.mkdir(parents=
True, exist_ok=
True)
138 targets_raw = args.target
if args.target
else DEFAULT_TARGETS
139 targets = [Path(REPO_ROOT / rel).resolve()
for rel
in targets_raw]
140 for target
in targets:
141 if not target.exists():
142 raise SystemExit(f
"[coverage-python] target not found: {target}")
144 pytest_args =
list(args.pytest_args)
145 if pytest_args
and pytest_args[0] ==
"--":
146 pytest_args = pytest_args[1:]
154 tracer = trace.Trace(count=
True, trace=
False, ignoredirs=ignoredirs)
155 exit_code = tracer.runfunc(pytest.main, pytest_args)
156 results = tracer.results()
160 print(
"[coverage-python] per-file line coverage")
161 print(
"[coverage-python] -----------------------------------------------")
165 for target
in targets:
168 total_exec += executable
169 rel = target.relative_to(REPO_ROOT)
170 print(f
"[coverage-python] {rel}: {covered}/{executable} ({percent:.2f}%)")
172 overall = 100.0
if total_exec == 0
else (100.0 * total_cov) / total_exec
173 print(
"[coverage-python] -----------------------------------------------")
174 print(f
"[coverage-python] weighted total: {total_cov}/{total_exec} ({overall:.2f}%)")
175 print(f
"[coverage-python] minimum required: {args.min_line:.2f}%")
177 summary_path = output_dir /
"summary.txt"
178 summary_path.write_text(
181 f
"weighted_total={overall:.4f}",
182 f
"covered_lines={total_cov}",
183 f
"executable_lines={total_exec}",
184 f
"minimum_required={args.min_line:.4f}",
191 if int(exit_code) != 0:
192 print(f
"[coverage-python] pytest failed with exit code {exit_code}.", file=sys.stderr)
193 return int(exit_code)
194 if overall < args.min_line:
196 f
"[coverage-python] FAIL: coverage {overall:.2f}% is below required {args.min_line:.2f}%.",