aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/coverage
blob: c421a7cf39c72d30a126990e612d4e80b8f6b555 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#!/usr/bin/env python3
"""How much of slosh the test suite actually runs.

    make coverage

Builds a second binary with gcc's `--coverage`, points the suite at it with
$SLOSH_BIN, and reads the counters back with gcov. Nothing is installed and
nothing else changes: the normal build is still zig, and this one lives
entirely in build/cov.

It is a number to look at, not a number to chase. A line that no test executes
is worth knowing about; a line executed by a test that asserts nothing is
worth nothing, and no coverage tool can tell the two apart.
"""

import os
import re
import subprocess
import sys

HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(HERE)
COV = os.path.join(ROOT, "build", "cov")


def gcov_one(src):
    """gcov prints a summary per file; the counters are in build/cov."""
    out = subprocess.run(
        ["gcov", "-o", COV, "-n", os.path.join("src", src)],
        capture_output=True,
        text=True,
        cwd=ROOT,
    ).stdout
    # File 'src/app.c'
    # Lines executed:62.34% of 1234
    pct = lines = None
    seen = False
    for line in out.split("\n"):
        if line.startswith("File '") and line.endswith("src/%s'" % src):
            seen = True
        elif seen and line.startswith("Lines executed:"):
            m = re.match(r"Lines executed:([\d.]+)% of (\d+)", line)
            if m:
                pct, lines = float(m.group(1)), int(m.group(2))
            break
    return pct, lines


def main():
    if not os.path.isdir(COV):
        print("no coverage build: run `make coverage`", file=sys.stderr)
        return 1

    rows = []
    for src in sorted(os.listdir(os.path.join(ROOT, "src"))):
        if not src.endswith(".c"):
            continue
        pct, lines = gcov_one(src)
        if pct is None:
            continue
        rows.append((src, pct, lines))

    if not rows:
        print(
            "no counters: did the suite run against build/cov/slosh?", file=sys.stderr
        )
        return 1

    total = sum(l for _, _, l in rows)
    hit = sum(round(p * l / 100) for _, p, l in rows)

    print(f"{'file':<16} {'lines':>7} {'covered':>8}")
    print("-" * 34)
    for src, pct, lines in sorted(rows, key=lambda r: -r[2]):
        bar = "#" * int(pct / 5)
        print(f"{src:<16} {lines:>7} {pct:>7.1f}%  {bar}")
    print("-" * 34)
    print(f"{'total':<16} {total:>7} {100.0 * hit / total:>7.1f}%")

    # One number here is a lie for a mechanical reason, and an unexplained low
    # number is worse than none: it invites someone to "fix" tested code.
    print()
    print("server.c reads low because the session server is a daemonised")
    print("child that ends with _exit(), which skips the flush gcov relies")
    print("on. Its counters are lost, not its tests -- test_session.py and")
    print("the live suite drive that path.")
    return 0


sys.exit(main())