добавлено окружение разработки: контейнер и гейт ревью
- сервис поднимается через `task up`/`task restart`, данные вынесены в ./data и переживают пересборку — поток с телефона не прерывается - `task gate` гоняет build/vet/lint/test/race/покрытие диффа/миграции, плюс два шага под healthlog: данные о здоровье в индексе и синхронность образцов конфига - `task review:context` собирает вход для архитектурного прохода ревью
This commit is contained in:
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Покрытие изменённых строк тестами.
|
||||
|
||||
Складывает `go test -coverprofile` и `git diff -U0 <base>`: показывает, какие
|
||||
изменённые исполняемые строки не покрыты ни одним тестом. Общий процент по
|
||||
пакету бесполезен для ревью — важно, покрыт ли именно новый код.
|
||||
|
||||
Использование: scripts/diff-coverage.py <coverprofile> <base-rev>
|
||||
"""
|
||||
|
||||
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())
|
||||
Reference in New Issue
Block a user