PICurv 0.1.0
A Parallel Particle-In-Cell Solver for Curvilinear LES
Loading...
Searching...
No Matches
Functions | Variables
c_coverage_gate Namespace Reference

Functions

argparse.Namespace parse_args ()
 
None run_gcov (list[Path] src_files, Path obj_dir, Path repo_root, Path output_dir)
 
tuple[Path|None, int, int] parse_gcov_file (Path gcov_path, Path repo_root)
 
int main ()
 

Variables

 LINE_RE = re.compile(r"^\s*([^:]+):\s*([0-9]+):(.*)$")
 

Detailed Description

Generate gcov line-coverage summary for src/*.c and enforce a threshold.

Function Documentation

◆ parse_args()

argparse.Namespace c_coverage_gate.parse_args ( )
Parse args.

Definition at line 16 of file c_coverage_gate.py.

16def parse_args() -> argparse.Namespace:
17 """Parse args."""
18 parser = argparse.ArgumentParser(
19 description=__doc__,
20 formatter_class=argparse.RawDescriptionHelpFormatter,
21 epilog=(
22 "Examples:\n"
23 " python3 scripts/c_coverage_gate.py\n"
24 " python3 scripts/c_coverage_gate.py --min-line 60\n"
25 " python3 scripts/c_coverage_gate.py --src-dir src --obj-dir obj --output-dir coverage/c\n"
26 ),
27 )
28 parser.add_argument(
29 "--src-dir",
30 default="src",
31 help="Repository-relative C source directory scanned for *.c files (default: src).",
32 )
33 parser.add_argument(
34 "--obj-dir",
35 default="obj",
36 help="Repository-relative directory containing coverage objects (*.gcda/*.gcno) (default: obj).",
37 )
38 parser.add_argument(
39 "--output-dir",
40 default="coverage/c",
41 help="Repository-relative output directory for gcov files and summary.txt (default: coverage/c).",
42 )
43 parser.add_argument(
44 "--min-line",
45 type=float,
46 default=55.0,
47 help="Minimum required weighted line coverage percent (default: 55.0).",
48 )
49 return parser.parse_args()
50
51
Here is the caller graph for this function:

◆ run_gcov()

None c_coverage_gate.run_gcov ( list[Path]  src_files,
Path  obj_dir,
Path  repo_root,
Path  output_dir 
)
Run gcov.

Definition at line 52 of file c_coverage_gate.py.

52def run_gcov(src_files: list[Path], obj_dir: Path, repo_root: Path, output_dir: Path) -> None:
53 """Run gcov."""
54 for src in src_files:
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:
58 raise RuntimeError(
59 f"gcov failed for {src}\nstdout:\n{proc.stdout}\nstderr:\n{proc.stderr}"
60 )
61 for gcov_file in repo_root.glob("*.gcov"):
62 gcov_file.replace(output_dir / gcov_file.name)
63
64
Here is the caller graph for this function:

◆ parse_gcov_file()

tuple[Path | None, int, int] c_coverage_gate.parse_gcov_file ( Path  gcov_path,
Path  repo_root 
)
Parse gcov file.

Definition at line 65 of file c_coverage_gate.py.

65def parse_gcov_file(gcov_path: Path, repo_root: Path) -> tuple[Path | None, int, int]:
66 """Parse gcov file."""
67 source_path = None
68 covered = 0
69 total = 0
70
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()
74 continue
75
76 match = LINE_RE.match(raw_line)
77 if not match:
78 continue
79 count_token = match.group(1).strip()
80 if count_token == "-":
81 continue
82
83 if count_token.startswith("#####") or count_token.startswith("====="):
84 total += 1
85 continue
86
87 numeric = "".join(ch for ch in count_token if ch.isdigit())
88 if not numeric:
89 continue
90
91 total += 1
92 if int(numeric) > 0:
93 covered += 1
94
95 if not source_path:
96 return None, covered, total
97
98 source = Path(source_path)
99 if not source.is_absolute():
100 source = (repo_root / source).resolve()
101 else:
102 source = source.resolve()
103 return source, covered, total
104
105
Here is the caller graph for this function:

◆ main()

int c_coverage_gate.main ( )
Entry point for this script.

Definition at line 106 of file c_coverage_gate.py.

106def main() -> int:
107 """Entry point for this script."""
108 args = parse_args()
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)
114
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}")
119
120 for old in output_dir.glob("*.gcov"):
121 old.unlink()
122
123 src_files = sorted(src_dir.glob("*.c"))
124 if not src_files:
125 raise SystemExit(f"[coverage-c] no source files found in {src_dir}")
126
127 run_gcov(src_files, obj_dir, repo_root, output_dir)
128
129 by_source: dict[Path, tuple[int, int]] = {}
130 for gcov_path in sorted(output_dir.glob("*.gcov")):
131 source, covered, total = parse_gcov_file(gcov_path, repo_root)
132 if source is None:
133 continue
134 if source.parent != src_dir:
135 continue
136 prev_cov, prev_total = by_source.get(source, (0, 0))
137 by_source[source] = (prev_cov + covered, prev_total + total)
138
139 print("[coverage-c] per-file line coverage")
140 print("[coverage-c] -----------------------------------------------")
141
142 total_cov = 0
143 total_exec = 0
144 missing = []
145 for src in src_files:
146 covered, executable = by_source.get(src, (0, 0))
147 if executable == 0:
148 missing.append(src)
149 percent = 0.0
150 else:
151 percent = (100.0 * covered) / executable
152 total_cov += covered
153 total_exec += executable
154 rel = src.relative_to(repo_root)
155 print(f"[coverage-c] {rel}: {covered}/{executable} ({percent:.2f}%)")
156
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}%")
161
162 summary_path = output_dir / "summary.txt"
163 summary_path.write_text(
164 "\n".join(
165 [
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}",
170 ]
171 )
172 + "\n",
173 encoding="utf-8",
174 )
175
176 if missing:
177 print("[coverage-c] WARNING: missing or zero-coverage gcov data for:", file=sys.stderr)
178 for src in missing:
179 print(f"[coverage-c] - {src.relative_to(repo_root)}", file=sys.stderr)
180
181 if overall < args.min_line:
182 print(
183 f"[coverage-c] FAIL: coverage {overall:.2f}% is below required {args.min_line:.2f}%.",
184 file=sys.stderr,
185 )
186 return 2
187
188 return 0
189
190
int main(int argc, char **argv)
Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ LINE_RE

c_coverage_gate.LINE_RE = re.compile(r"^\s*([^:]+):\s*([0-9]+):(.*)$")

Definition at line 13 of file c_coverage_gate.py.