52def run_gcov(src_files: list[Path], obj_dir: Path, repo_root: Path, output_dir: Path) ->
None:
55 cmd = [
"gcov",
"-o", str(obj_dir), str(src)]
56 proc = subprocess.run(cmd, cwd=str(repo_root), text=
True, capture_output=
True, check=
False)
57 if proc.returncode != 0:
59 f
"gcov failed for {src}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
61 for gcov_file
in repo_root.glob(
"*.gcov"):
62 gcov_file.replace(output_dir / gcov_file.name)
65def parse_gcov_file(gcov_path: Path, repo_root: Path) -> tuple[Path |
None, int, int]:
66 """Parse gcov file."""
71 for raw_line
in gcov_path.read_text(encoding=
"utf-8", errors=
"replace").splitlines():
72 if "Source:" in raw_line
and ":Source:" in raw_line:
73 source_path = raw_line.split(
"Source:", 1)[1].strip()
76 match = LINE_RE.match(raw_line)
79 count_token = match.group(1).strip()
80 if count_token ==
"-":
83 if count_token.startswith(
"#####")
or count_token.startswith(
"====="):
87 numeric =
"".join(ch
for ch
in count_token
if ch.isdigit())
96 return None, covered, total
98 source = Path(source_path)
99 if not source.is_absolute():
100 source = (repo_root / source).resolve()
102 source = source.resolve()
103 return source, covered, total
107 """Entry point for this script."""
109 repo_root = Path(__file__).resolve().parents[1]
110 src_dir = (repo_root / args.src_dir).resolve()
111 obj_dir = (repo_root / args.obj_dir).resolve()
112 output_dir = (repo_root / args.output_dir).resolve()
113 output_dir.mkdir(parents=
True, exist_ok=
True)
115 if not src_dir.is_dir():
116 raise SystemExit(f
"[coverage-c] src directory missing: {src_dir}")
117 if not obj_dir.is_dir():
118 raise SystemExit(f
"[coverage-c] obj directory missing: {obj_dir}")
120 for old
in output_dir.glob(
"*.gcov"):
123 src_files = sorted(src_dir.glob(
"*.c"))
125 raise SystemExit(f
"[coverage-c] no source files found in {src_dir}")
127 run_gcov(src_files, obj_dir, repo_root, output_dir)
129 by_source: dict[Path, tuple[int, int]] = {}
130 for gcov_path
in sorted(output_dir.glob(
"*.gcov")):
134 if source.parent != src_dir:
136 prev_cov, prev_total = by_source.get(source, (0, 0))
137 by_source[source] = (prev_cov + covered, prev_total + total)
139 print(
"[coverage-c] per-file line coverage")
140 print(
"[coverage-c] -----------------------------------------------")
145 for src
in src_files:
146 covered, executable = by_source.get(src, (0, 0))
151 percent = (100.0 * covered) / executable
153 total_exec += executable
154 rel = src.relative_to(repo_root)
155 print(f
"[coverage-c] {rel}: {covered}/{executable} ({percent:.2f}%)")
157 overall = 0.0
if total_exec == 0
else (100.0 * total_cov) / total_exec
158 print(
"[coverage-c] -----------------------------------------------")
159 print(f
"[coverage-c] weighted total: {total_cov}/{total_exec} ({overall:.2f}%)")
160 print(f
"[coverage-c] minimum required: {args.min_line:.2f}%")
162 summary_path = output_dir /
"summary.txt"
163 summary_path.write_text(
166 f
"weighted_total={overall:.4f}",
167 f
"covered_lines={total_cov}",
168 f
"executable_lines={total_exec}",
169 f
"minimum_required={args.min_line:.4f}",
177 print(
"[coverage-c] WARNING: missing or zero-coverage gcov data for:", file=sys.stderr)
179 print(f
"[coverage-c] - {src.relative_to(repo_root)}", file=sys.stderr)
181 if overall < args.min_line:
183 f
"[coverage-c] FAIL: coverage {overall:.2f}% is below required {args.min_line:.2f}%.",