Skip to content

Commit 77a3afb

Browse files
committed
ci: API-surface coverage script (honest coverage for device-code headers)
1 parent a388e22 commit 77a3afb

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
#!/usr/bin/env python3
2+
"""API-surface coverage: % of public documented entry points exercised by test/cuda/.
3+
4+
Honest metric for a CUDA header library (device-code line coverage does not
5+
exist: nvcc emits no instrumentation for __device__ code, and instrumenting
6+
the Python drivers would measure the harness, not the library). Instead we
7+
enumerate the public surface — every doc-commented function in the library
8+
headers — and check which names are referenced by at least one GPU test TU.
9+
10+
Usage: python .github/scripts/surface_coverage.py [--json OUT] [--list-gaps]
11+
Exit 0 always (reporting tool); the number lands in a shields.io endpoint
12+
badge JSON when --json is given.
13+
"""
14+
import argparse, json, pathlib, re, sys
15+
16+
ROOT = pathlib.Path(__file__).resolve().parents[2]
17+
HEADERS = [*ROOT.glob("glass*.cuh"), *(ROOT / "src").rglob("*.cuh")]
18+
TESTS = list((ROOT / "test" / "cuda").glob("*.cu"))
19+
20+
# A public entry point = a `/** ... */` doc comment whose following declaration
21+
# names a function. Captures free functions and namespaced ones alike.
22+
_FN = re.compile(
23+
r"/\*\*.*?\*/\s*(?:template\s*<[^;{]*?>\s*)?"
24+
r"(?:__device__|__host__|inline|constexpr|static|\s)*[\w:<>,*&\s]+?"
25+
r"\b([a-z_][a-zA-Z0-9_]*)\s*\(", re.S)
26+
_SKIP = {"if", "for", "while", "switch", "return", "sizeof", "defined"}
27+
28+
29+
def main():
30+
ap = argparse.ArgumentParser()
31+
ap.add_argument("--json")
32+
ap.add_argument("--list-gaps", action="store_true")
33+
args = ap.parse_args()
34+
35+
surface = {} # name -> first header
36+
for h in HEADERS:
37+
for m in _FN.finditer(h.read_text(errors="ignore")):
38+
n = m.group(1)
39+
if n not in _SKIP and not n.endswith("_impl") and not n.startswith("_"):
40+
surface.setdefault(n, h.relative_to(ROOT))
41+
test_text = "\n".join(t.read_text(errors="ignore") for t in TESTS)
42+
covered = {n for n in surface if re.search(rf"\b{re.escape(n)}\s*[(<]", test_text)}
43+
gaps = sorted(set(surface) - covered)
44+
pct = 100.0 * len(covered) / max(1, len(surface))
45+
print(f"surface: {len(surface)} public entry points, "
46+
f"{len(covered)} exercised by test/cuda -> {pct:.1f}%")
47+
if args.list_gaps and gaps:
48+
print("uncovered:")
49+
for n in gaps:
50+
print(f" {n:32s} ({surface[n]})")
51+
if args.json:
52+
color = "brightgreen" if pct >= 90 else "green" if pct >= 80 else "yellow"
53+
pathlib.Path(args.json).write_text(json.dumps({
54+
"schemaVersion": 1, "label": "API surface tested",
55+
"message": f"{len(covered)}/{len(surface)} ({pct:.0f}%)",
56+
"color": color}))
57+
print(f"wrote {args.json}")
58+
return 0
59+
60+
61+
if __name__ == "__main__":
62+
sys.exit(main())

0 commit comments

Comments
 (0)