55def run_gcov(src_files: list[Path], obj_dir: Path, repo_root: Path, output_dir: Path) ->
None:
58 @param[in] src_files Argument passed to `run_gcov()`.
59 @param[in] obj_dir Argument passed to `run_gcov()`.
60 @param[in] repo_root Argument passed to `run_gcov()`.
61 @param[in] output_dir Argument passed to `run_gcov()`.
64 cmd = [
"gcov",
"-o", str(obj_dir), str(src)]
65 proc = subprocess.run(cmd, cwd=str(repo_root), text=
True, capture_output=
True, check=
False)
66 if proc.returncode != 0:
68 f
"gcov failed for {src}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
70 for gcov_file
in repo_root.glob(
"*.gcov"):
71 gcov_file.replace(output_dir / gcov_file.name)
74def parse_gcov_file(gcov_path: Path, repo_root: Path) -> tuple[Path |
None, int, int]:
76 @brief Parse gcov file.
77 @param[in] gcov_path Argument passed to `parse_gcov_file()`.
78 @param[in] repo_root Argument passed to `parse_gcov_file()`.
79 @return Value returned by `parse_gcov_file()`.
85 for raw_line
in gcov_path.read_text(encoding=
"utf-8", errors=
"replace").splitlines():
86 if "Source:" in raw_line
and ":Source:" in raw_line:
87 source_path = raw_line.split(
"Source:", 1)[1].strip()
90 match = LINE_RE.match(raw_line)
93 count_token = match.group(1).strip()
94 if count_token ==
"-":
97 if count_token.startswith(
"#####")
or count_token.startswith(
"====="):
101 numeric =
"".join(ch
for ch
in count_token
if ch.isdigit())
110 return None, covered, total
112 source = Path(source_path)
113 if not source.is_absolute():
114 source = (repo_root / source).resolve()
116 source = source.resolve()
117 return source, covered, total
122 @brief Entry point for this script.
123 @return Value returned by `main()`.
126 repo_root = Path(__file__).resolve().parents[1]
127 src_dir = (repo_root / args.src_dir).resolve()
128 obj_dir = (repo_root / args.obj_dir).resolve()
129 output_dir = (repo_root / args.output_dir).resolve()
130 output_dir.mkdir(parents=
True, exist_ok=
True)
132 if not src_dir.is_dir():
133 raise SystemExit(f
"[coverage-c] src directory missing: {src_dir}")
134 if not obj_dir.is_dir():
135 raise SystemExit(f
"[coverage-c] obj directory missing: {obj_dir}")
137 for old
in output_dir.glob(
"*.gcov"):
140 src_files = sorted(src_dir.glob(
"*.c"))
142 raise SystemExit(f
"[coverage-c] no source files found in {src_dir}")
144 run_gcov(src_files, obj_dir, repo_root, output_dir)
146 by_source: dict[Path, tuple[int, int]] = {}
147 for gcov_path
in sorted(output_dir.glob(
"*.gcov")):
151 if source.parent != src_dir:
153 prev_cov, prev_total = by_source.get(source, (0, 0))
154 by_source[source] = (prev_cov + covered, prev_total + total)
156 print(
"[coverage-c] per-file line coverage")
157 print(
"[coverage-c] -----------------------------------------------")
162 for src
in src_files:
163 covered, executable = by_source.get(src, (0, 0))
168 percent = (100.0 * covered) / executable
170 total_exec += executable
171 rel = src.relative_to(repo_root)
172 print(f
"[coverage-c] {rel}: {covered}/{executable} ({percent:.2f}%)")
174 overall = 0.0
if total_exec == 0
else (100.0 * total_cov) / total_exec
175 print(
"[coverage-c] -----------------------------------------------")
176 print(f
"[coverage-c] weighted total: {total_cov}/{total_exec} ({overall:.2f}%)")
177 print(f
"[coverage-c] minimum required: {args.min_line:.2f}%")
179 summary_path = output_dir /
"summary.txt"
180 summary_path.write_text(
183 f
"weighted_total={overall:.4f}",
184 f
"covered_lines={total_cov}",
185 f
"executable_lines={total_exec}",
186 f
"minimum_required={args.min_line:.4f}",
194 print(
"[coverage-c] WARNING: missing or zero-coverage gcov data for:", file=sys.stderr)
196 print(f
"[coverage-c] - {src.relative_to(repo_root)}", file=sys.stderr)
198 if overall < args.min_line:
200 f
"[coverage-c] FAIL: coverage {overall:.2f}% is below required {args.min_line:.2f}%.",