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

Data Structures

class  PathAwareIgnore
 

Functions

str normalize_path (str|Path path)
 Resolve a filesystem path to the canonical string used as a coverage-map key.
 
argparse.Namespace parse_args ()
 Parse coverage thresholds, targets, output location, and pytest arguments for this gate.
 
list[str] build_trace_ignoredirs ()
 Build trace ignoredirs.
 
dict[str, dict[int, int]] collect_counts (trace.CoverageResults results)
 Transform Python trace results into a per-file, per-line execution-count mapping.
 
tuple[int, int, float] compute_file_coverage (Path target, dict[str, dict[int, int]] counts_by_file)
 Compute file coverage.
 
int main ()
 Entry point for this script.
 

Variables

 REPO_ROOT = Path(__file__).resolve().parents[2]
 
list DEFAULT_TARGETS
 

Detailed Description

Run pytest under stdlib trace and enforce a line-coverage threshold.

Function Documentation

◆ normalize_path()

str python_coverage_gate.normalize_path ( str | Path  path)

Resolve a filesystem path to the canonical string used as a coverage-map key.

Parameters
[in]pathFilesystem path argument passed to normalize_path().
Returns
Value returned by normalize_path().

Definition at line 41 of file python_coverage_gate.py.

41def normalize_path(path: str | Path) -> str:
42 """!
43 @brief Resolve a filesystem path to the canonical string used as a coverage-map key.
44 @param[in] path Filesystem path argument passed to `normalize_path()`.
45 @return Value returned by `normalize_path()`.
46 """
47 return str(Path(path).resolve())
48
49
Here is the caller graph for this function:

◆ parse_args()

argparse.Namespace python_coverage_gate.parse_args ( )

Parse coverage thresholds, targets, output location, and pytest arguments for this gate.

Returns
Value returned by parse_args().

Definition at line 50 of file python_coverage_gate.py.

50def parse_args() -> argparse.Namespace:
51 """!
52 @brief Parse coverage thresholds, targets, output location, and pytest arguments for this gate.
53 @return Value returned by `parse_args()`.
54 """
55 parser = argparse.ArgumentParser(
56 description=__doc__,
57 formatter_class=argparse.RawDescriptionHelpFormatter,
58 epilog=(
59 "Examples:\n"
60 " python3 tests/tooling/python_coverage_gate.py\n"
61 " python3 tests/tooling/python_coverage_gate.py --target picurv_cli/core.py --target generators/grid.gen\n"
62 " python3 tests/tooling/python_coverage_gate.py --pytest-args -- -q tests/test_cli_smoke.py\n"
63 ),
64 )
65 parser.add_argument(
66 "--min-line",
67 type=float,
68 default=70.0,
69 help="Minimum required weighted line coverage percent (default: 70.0).",
70 )
71 parser.add_argument(
72 "--target",
73 action="append",
74 default=[],
75 help=(
76 "Repository-relative file to include in coverage computation "
77 "(repeatable). Defaults to core runtime scripts."
78 ),
79 )
80 parser.add_argument(
81 "--output-dir",
82 default="coverage/python",
83 help="Repository-relative output directory for coverage artifacts (default: coverage/python).",
84 )
85 parser.add_argument(
86 "--pytest-args",
87 nargs=argparse.REMAINDER,
88 default=["-q"],
89 help="Arguments passed to pytest (prefix with --pytest-args -- ...).",
90 )
91 return parser.parse_args()
92
93
Here is the caller graph for this function:

◆ build_trace_ignoredirs()

list[str] python_coverage_gate.build_trace_ignoredirs ( )

Build trace ignoredirs.

Returns
Value returned by build_trace_ignoredirs().

Definition at line 94 of file python_coverage_gate.py.

94def build_trace_ignoredirs() -> list[str]:
95 """!
96 @brief Build trace ignoredirs.
97 @return Value returned by `build_trace_ignoredirs()`.
98 """
99 ignoredirs = {
100 normalize_path(sys.prefix),
101 normalize_path(sys.exec_prefix),
102 normalize_path(Path(sys.prefix) / "lib"),
103 }
104 for raw in list(sys.path):
105 if not raw:
106 continue
107 path = Path(raw)
108 if not path.exists():
109 continue
110 resolved = normalize_path(path)
111 if "/site-packages" in resolved or "/dist-packages" in resolved:
112 ignoredirs.add(resolved)
113 return sorted(ignoredirs)
114
115
Head of a generic C-style linked list.
Definition variables.h:445
Here is the call graph for this function:
Here is the caller graph for this function:

◆ collect_counts()

dict[str, dict[int, int]] python_coverage_gate.collect_counts ( trace.CoverageResults  results)

Transform Python trace results into a per-file, per-line execution-count mapping.

Parameters
[in]resultsArgument passed to collect_counts().
Returns
Value returned by collect_counts().

Definition at line 116 of file python_coverage_gate.py.

116def collect_counts(results: trace.CoverageResults) -> dict[str, dict[int, int]]:
117 """!
118 @brief Transform Python trace results into a per-file, per-line execution-count mapping.
119 @param[in] results Argument passed to `collect_counts()`.
120 @return Value returned by `collect_counts()`.
121 """
122 counts_by_file: dict[str, dict[int, int]] = {}
123 for (filename, lineno), count in results.counts.items():
124 file_key = normalize_path(filename)
125 counts_by_file.setdefault(file_key, {})[lineno] = count
126 return counts_by_file
127
128
Here is the call graph for this function:
Here is the caller graph for this function:

◆ compute_file_coverage()

tuple[int, int, float] python_coverage_gate.compute_file_coverage ( Path  target,
dict[str, dict[int, int]]  counts_by_file 
)

Compute file coverage.

Parameters
[in]targetArgument passed to compute_file_coverage().
[in]counts_by_fileArgument passed to compute_file_coverage().
Returns
Value returned by compute_file_coverage().

Definition at line 129 of file python_coverage_gate.py.

129def compute_file_coverage(target: Path, counts_by_file: dict[str, dict[int, int]]) -> tuple[int, int, float]:
130 """!
131 @brief Compute file coverage.
132 @param[in] target Argument passed to `compute_file_coverage()`.
133 @param[in] counts_by_file Argument passed to `compute_file_coverage()`.
134 @return Value returned by `compute_file_coverage()`.
135 """
136 finder = getattr(trace, "find_executable_linenos", None)
137 if finder is None:
138 finder = trace._find_executable_linenos # type: ignore[attr-defined]
139 executable = finder(str(target))
140 executable_lines = set(executable.keys())
141 total = len(executable_lines)
142 if total == 0:
143 return 0, 0, 100.0
144
145 observed = counts_by_file.get(normalize_path(target), {})
146 covered = sum(1 for line in executable_lines if observed.get(line, 0) > 0)
147 percent = (100.0 * covered) / total
148 return covered, total, percent
149
150
Here is the call graph for this function:
Here is the caller graph for this function:

◆ main()

int python_coverage_gate.main ( )

Entry point for this script.

Returns
Value returned by main().

Definition at line 151 of file python_coverage_gate.py.

151def main() -> int:
152 """!
153 @brief Entry point for this script.
154 @return Value returned by `main()`.
155 """
156 args = parse_args()
157 output_dir = (REPO_ROOT / args.output_dir).resolve()
158 output_dir.mkdir(parents=True, exist_ok=True)
159
160 targets_raw = args.target if args.target else DEFAULT_TARGETS
161 targets = [Path(REPO_ROOT / rel).resolve() for rel in targets_raw]
162 for target in targets:
163 if not target.exists():
164 raise SystemExit(f"[coverage-python] target not found: {target}")
165
166 pytest_args = list(args.pytest_args)
167 if pytest_args and pytest_args[0] == "--":
168 pytest_args = pytest_args[1:]
169 if not pytest_args:
170 pytest_args = ["-q"]
171
172 ignoredirs = build_trace_ignoredirs()
173
174 import pytest
175
176 tracer = trace.Trace(count=True, trace=False, ignoredirs=ignoredirs)
177 # Python 3.8's trace._Ignore caches decisions by file basename, so seeing an
178 # ignored dependency named core.py can incorrectly hide this repository's
179 # picurv_cli/core.py for the rest of a full-suite run.
180 tracer.ignore = PathAwareIgnore(ignoredirs)
181 exit_code = tracer.runfunc(pytest.main, pytest_args)
182 results = tracer.results()
183
184 counts_by_file = collect_counts(results)
185
186 print("[coverage-python] per-file line coverage")
187 print("[coverage-python] -----------------------------------------------")
188
189 total_cov = 0
190 total_exec = 0
191 for target in targets:
192 covered, executable, percent = compute_file_coverage(target, counts_by_file)
193 total_cov += covered
194 total_exec += executable
195 rel = target.relative_to(REPO_ROOT)
196 print(f"[coverage-python] {rel}: {covered}/{executable} ({percent:.2f}%)")
197
198 overall = 100.0 if total_exec == 0 else (100.0 * total_cov) / total_exec
199 print("[coverage-python] -----------------------------------------------")
200 print(f"[coverage-python] weighted total: {total_cov}/{total_exec} ({overall:.2f}%)")
201 print(f"[coverage-python] minimum required: {args.min_line:.2f}%")
202
203 summary_path = output_dir / "summary.txt"
204 summary_path.write_text(
205 "\n".join(
206 [
207 f"weighted_total={overall:.4f}",
208 f"covered_lines={total_cov}",
209 f"executable_lines={total_exec}",
210 f"minimum_required={args.min_line:.4f}",
211 ]
212 )
213 + "\n",
214 encoding="utf-8",
215 )
216
217 if int(exit_code) != 0:
218 print(f"[coverage-python] pytest failed with exit code {exit_code}.", file=sys.stderr)
219 return int(exit_code)
220 if overall < args.min_line:
221 print(
222 f"[coverage-python] FAIL: coverage {overall:.2f}% is below required {args.min_line:.2f}%.",
223 file=sys.stderr,
224 )
225 return 2
226 return 0
227
228
int main(int argc, char **argv)
Entry point for the postprocessor executable.
Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ REPO_ROOT

python_coverage_gate.REPO_ROOT = Path(__file__).resolve().parents[2]

Definition at line 13 of file python_coverage_gate.py.

◆ DEFAULT_TARGETS

list python_coverage_gate.DEFAULT_TARGETS
Initial value:
1= [
2 "picurv_cli/core.py",
3 "picurv_cli/cli.py",
4]

Definition at line 14 of file python_coverage_gate.py.