#!/usr/bin/env python3 """Покрытие изменённых строк тестами. Складывает `go test -coverprofile` и `git diff -U0 `: показывает, какие изменённые исполняемые строки не покрыты ни одним тестом. Общий процент по пакету бесполезен для ревью — важно, покрыт ли именно новый код. Использование: scripts/diff-coverage.py """ import re import subprocess import sys from collections import defaultdict HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@") BLOCK = re.compile(r"^(.+):(\d+)\.\d+,(\d+)\.\d+ (\d+) (\d+)$") def module_path() -> str: with open("go.mod", encoding="utf-8") as f: for line in f: if line.startswith("module "): return line.split(None, 1)[1].strip() return "" def coverage_blocks(profile: str, module: str): """file -> [(start, end, count)] по репо-относительным путям.""" blocks = defaultdict(list) with open(profile, encoding="utf-8") as f: for line in f: m = BLOCK.match(line.strip()) if not m: continue path, start, end, _stmts, count = m.groups() if module and path.startswith(module + "/"): path = path[len(module) + 1:] blocks[path].append((int(start), int(end), int(count))) return blocks def changed_lines(base: str): """file -> {номера добавленных/изменённых строк} для нетестовых .go.""" out = subprocess.run( ["git", "diff", "-U0", base, "--", "*.go"], capture_output=True, text=True, check=True, ).stdout changed = defaultdict(set) current = None for line in out.splitlines(): if line.startswith("+++ b/"): path = line[6:] current = None if path.endswith("_test.go") else path elif line.startswith("@@") and current: m = HUNK.match(line) if m: start = int(m.group(1)) count = int(m.group(2) or 1) changed[current].update(range(start, start + count)) return changed def main() -> int: if len(sys.argv) != 3: print(__doc__, file=sys.stderr) return 2 profile, base = sys.argv[1], sys.argv[2] blocks = coverage_blocks(profile, module_path()) changed = changed_lines(base) total = uncovered = 0 report = [] for path in sorted(changed): gaps = [] for line in sorted(changed[path]): covering = [b for b in blocks.get(path, []) if b[0] <= line <= b[1]] if not covering: continue # не исполняемая строка (объявление, комментарий, скобка) total += 1 if all(b[2] == 0 for b in covering): uncovered += 1 gaps.append(line) if gaps: report.append((path, gaps)) if total == 0: print("изменённых исполняемых строк нет (или профиль не содержит этих пакетов)") return 0 print(f"изменённых исполняемых строк: {total}, не покрыто: {uncovered}" f" ({100 * (total - uncovered) // total}% покрытия диффа)") for path, gaps in report: print(f" {path}: {compact(gaps)}") return 0 def compact(lines): """[3,4,5,9] -> '3-5,9'.""" out, start, prev = [], lines[0], lines[0] for line in lines[1:] + [None]: if line == prev + 1: prev = line continue out.append(str(start) if start == prev else f"{start}-{prev}") start = prev = line return ",".join(out) if __name__ == "__main__": sys.exit(main())