|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Generate a folder-level coverage summary from Cobertura XML.""" |
| 3 | + |
| 4 | +import sys |
| 5 | +import xml.etree.ElementTree as ET |
| 6 | +from collections import defaultdict |
| 7 | + |
| 8 | + |
| 9 | +def make_bar(pct, width=20): |
| 10 | + filled = round(pct / 100 * width) |
| 11 | + return "\u2593" * filled + "\u2591" * (width - filled) |
| 12 | + |
| 13 | + |
| 14 | +def main(): |
| 15 | + xml_path = sys.argv[1] if len(sys.argv) > 1 else "coverage.xml" |
| 16 | + output_path = sys.argv[2] if len(sys.argv) > 2 else "coverage-summary.md" |
| 17 | + depth = int(sys.argv[3]) if len(sys.argv) > 3 else 2 |
| 18 | + |
| 19 | + tree = ET.parse(xml_path) |
| 20 | + root = tree.getroot() |
| 21 | + |
| 22 | + # Aggregate by package at specified depth |
| 23 | + groups = defaultdict(lambda: {"hits": 0, "lines": 0}) |
| 24 | + |
| 25 | + for pkg in root.findall(".//package"): |
| 26 | + name = pkg.get("name", "") |
| 27 | + parts = name.split(".") |
| 28 | + key = ".".join(parts[:depth]) if len(parts) >= depth else name |
| 29 | + |
| 30 | + for line in pkg.findall(".//line"): |
| 31 | + groups[key]["lines"] += 1 |
| 32 | + if int(line.get("hits", "0")) > 0: |
| 33 | + groups[key]["hits"] += 1 |
| 34 | + |
| 35 | + total_lines = sum(g["lines"] for g in groups.values()) |
| 36 | + total_hits = sum(g["hits"] for g in groups.values()) |
| 37 | + total_pct = (total_hits / total_lines * 100) if total_lines else 0 |
| 38 | + |
| 39 | + # Sort by coverage ascending (worst first) |
| 40 | + sorted_groups = sorted( |
| 41 | + groups.items(), |
| 42 | + key=lambda x: (x[1]["hits"] / x[1]["lines"] * 100) if x[1]["lines"] else 0, |
| 43 | + ) |
| 44 | + |
| 45 | + lines = [] |
| 46 | + lines.append(f"## Coverage Report \u2014 {total_pct:.0f}%") |
| 47 | + lines.append("") |
| 48 | + lines.append( |
| 49 | + f"`{make_bar(total_pct, 30)}` **{total_pct:.1f}%** ({total_hits} / {total_lines} lines)" |
| 50 | + ) |
| 51 | + lines.append("") |
| 52 | + lines.append("| Package | Coverage | Progress | Lines |") |
| 53 | + lines.append("|:--------|:--------:|:---------|------:|") |
| 54 | + |
| 55 | + for key, g in sorted_groups: |
| 56 | + pct = (g["hits"] / g["lines"] * 100) if g["lines"] else 0 |
| 57 | + lines.append( |
| 58 | + f"| `{key}` | {pct:.1f}% | `{make_bar(pct)}` | {g['hits']} / {g['lines']} |" |
| 59 | + ) |
| 60 | + |
| 61 | + lines.append( |
| 62 | + f"| **Total** | **{total_pct:.1f}%** | | **{total_hits} / {total_lines}** |" |
| 63 | + ) |
| 64 | + lines.append("") |
| 65 | + |
| 66 | + with open(output_path, "w") as f: |
| 67 | + f.write("\n".join(lines)) |
| 68 | + |
| 69 | + print(f"Coverage summary written to {output_path}") |
| 70 | + |
| 71 | + |
| 72 | +if __name__ == "__main__": |
| 73 | + main() |
0 commit comments