|
3 | 3 |
|
4 | 4 | Usage: dep_table.py BASELINE.zip AFTER.zip |
5 | 5 |
|
6 | | -Each archive is a GitHub Actions run-log zip. The `deps` CI job prints two |
7 | | -marker-delimited boostdep reports into its log: |
| 6 | +Each archive is a GitHub Actions run-log zip. The `deps` job of the |
| 7 | +dependency-report workflow prints two marker-delimited boostdep reports into |
| 8 | +its log: |
8 | 9 |
|
9 | 10 | ===DEP-BRIEF-START=== boostdep --brief graph ===DEP-BRIEF-END=== |
10 | 11 | ===DEP-PRIMARY-START=== boostdep graph (primary) ===DEP-PRIMARY-END=== |
11 | 12 |
|
12 | 13 | From those it derives two metrics and prints their delta vs the baseline: |
13 | | - - the set of transitive Boost modules graph depends on (from --brief), and |
| 14 | + - the set of transitive Boost modules graph depends on (from --brief: |
| 15 | + Primary + Secondary sections = the full transitive closure), and |
14 | 16 | - the header-inclusion weight of each direct dependency (from the primary |
15 | | - report: how many distinct boost/graph/* headers pull that dependency in). |
| 17 | + report: how many distinct boost/graph files pull that dependency in). |
16 | 18 | Weights are the live signal, they drop toward zero as coupling is removed; |
17 | 19 | the module count is the coarse target that only moves when a dep hits zero. |
18 | 20 | """ |
|
22 | 24 |
|
23 | 25 | BRIEF = ("===DEP-BRIEF-START===", "===DEP-BRIEF-END===") |
24 | 26 | PRIMARY = ("===DEP-PRIMARY-START===", "===DEP-PRIMARY-END===") |
| 27 | +# Module names as boostdep prints them, e.g. "numeric~conversion". |
| 28 | +MODULE = re.compile(r"[A-Za-z0-9_.~-]+") |
| 29 | +TIMESTAMP = re.compile(r"^\d{4}-\d\d-\d\dT[\d:.]+Z\s?") # GitHub log line prefix |
| 30 | +ANSI = re.compile(r"\x1b\[[0-9;]*m") |
25 | 31 |
|
26 | 32 |
|
27 | | -def read_logs(zip_path): |
28 | | - """Concatenate the top-level per-job .txt logs from a run-log archive.""" |
29 | | - out = [] |
| 33 | +def read_clean_lines(zip_path): |
| 34 | + """Top-level per-job logs, with GitHub timestamp and ANSI prefixes stripped.""" |
| 35 | + lines = [] |
30 | 36 | with zipfile.ZipFile(zip_path) as z: |
31 | 37 | for entry in z.namelist(): |
32 | 38 | if "/" in entry or not entry.endswith(".txt"): |
33 | 39 | continue |
34 | | - out.append(z.read(entry).decode("utf-8", "replace")) |
35 | | - return "\n".join(out) |
36 | | - |
37 | | - |
38 | | -def block(text, markers): |
39 | | - m = re.search(re.escape(markers[0]) + r"(.*?)" + re.escape(markers[1]), text, re.S) |
40 | | - return m.group(1) if m else "" |
| 40 | + for ln in z.read(entry).decode("utf-8", "replace").splitlines(): |
| 41 | + lines.append(TIMESTAMP.sub("", ANSI.sub("", ln))) |
| 42 | + return lines |
| 43 | + |
| 44 | + |
| 45 | +def block(lines, markers): |
| 46 | + """Lines strictly between the marker lines, matched exactly. |
| 47 | +
|
| 48 | + Exact matching is what skips the echoed `echo "<marker>"` command lines |
| 49 | + GitHub prepends to a run step, so we capture boostdep's real stdout. |
| 50 | + """ |
| 51 | + start, end = markers |
| 52 | + out, capturing = [], False |
| 53 | + for ln in lines: |
| 54 | + s = ln.strip() |
| 55 | + if s == start: |
| 56 | + capturing = True |
| 57 | + continue |
| 58 | + if s == end and capturing: |
| 59 | + break |
| 60 | + if capturing: |
| 61 | + out.append(ln) |
| 62 | + return out |
41 | 63 |
|
42 | 64 |
|
43 | | -def parse_brief(text): |
| 65 | +def parse_brief(lines): |
44 | 66 | """--brief output -> set of module names (one bare name per line).""" |
45 | 67 | mods = set() |
46 | | - for line in text.splitlines(): |
47 | | - s = line.strip() |
| 68 | + for ln in lines: |
| 69 | + s = ln.strip() |
48 | 70 | if not s or s.startswith("#") or s.lower().startswith("brief dependency"): |
49 | 71 | continue |
50 | | - if re.fullmatch(r"[A-Za-z0-9_.-]+", s): |
| 72 | + if MODULE.fullmatch(s): |
51 | 73 | mods.add(s) |
52 | 74 | return mods |
53 | 75 |
|
54 | 76 |
|
55 | | -def parse_weights(text): |
56 | | - """Primary report -> {dependency: number of distinct boost/graph headers that pull it in}.""" |
| 77 | +def parse_weights(lines): |
| 78 | + """Primary report -> {dependency: number of distinct graph files that pull it in}.""" |
57 | 79 | weights, cur = {}, None |
58 | | - for line in text.splitlines(): |
59 | | - head = re.match(r"^([A-Za-z0-9_.-]+):\s*$", line) # "module:" at column 0 |
| 80 | + for ln in lines: |
| 81 | + head = re.match(r"^([A-Za-z0-9_.~-]+):\s*$", ln) # "module:" at column 0 |
60 | 82 | if head: |
61 | 83 | cur = head.group(1) |
62 | 84 | weights.setdefault(cur, set()) |
63 | 85 | continue |
64 | | - frm = re.match(r"^\s+from\s+(.+?)\s*$", line) |
| 86 | + frm = re.match(r"^\s+from\s+(.+?)\s*$", ln) |
65 | 87 | if frm and cur is not None: |
66 | 88 | weights[cur].add(frm.group(1)) |
67 | 89 | return {k: len(v) for k, v in weights.items()} |
68 | 90 |
|
69 | 91 |
|
70 | 92 | def main(base_zip, pr_zip): |
71 | | - base, pr = read_logs(base_zip), read_logs(pr_zip) |
72 | | - base_mods = parse_brief(block(base, BRIEF)) |
73 | | - pr_mods = parse_brief(block(pr, BRIEF)) |
74 | | - base_w = parse_weights(block(base, PRIMARY)) |
75 | | - pr_w = parse_weights(block(pr, PRIMARY)) |
76 | | - |
77 | | - # Header-inclusion weights: the live signal. Show only rows that changed, |
78 | | - # most-reduced first. |
79 | | - print("**Header-inclusion weights** (graph headers pulling each direct dependency in):") |
| 93 | + bl, pl = read_clean_lines(base_zip), read_clean_lines(pr_zip) |
| 94 | + base_mods = parse_brief(block(bl, BRIEF)) |
| 95 | + pr_mods = parse_brief(block(pl, BRIEF)) |
| 96 | + base_w = parse_weights(block(bl, PRIMARY)) |
| 97 | + pr_w = parse_weights(block(pl, PRIMARY)) |
| 98 | + |
| 99 | + # Header-inclusion weights: the live signal. Only rows that changed, most-reduced first. |
| 100 | + print("**Header-inclusion weights** (graph files pulling each direct dependency in):") |
80 | 101 | print() |
81 | 102 | changed = [] |
82 | 103 | for dep in base_w.keys() | pr_w.keys(): |
|
0 commit comments