-
Notifications
You must be signed in to change notification settings - Fork 149
feat(testing): add coverage #1397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -72,3 +72,8 @@ CLAUDE.MD | |
|
|
||
| /.mcp.json | ||
| *.speedscope.json | ||
|
|
||
| # Coverage | ||
| htmlcov/ | ||
| .coverage | ||
| .coverage.* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,110 @@ | ||
| #!/usr/bin/env python3 | ||
|
|
||
| from collections import defaultdict | ||
| import os | ||
| import subprocess | ||
| import sys | ||
|
|
||
| from coverage import CoverageData | ||
|
|
||
|
|
||
| def get_repo_root(): | ||
| result = subprocess.run( | ||
| ["git", "rev-parse", "--show-toplevel"], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
| return result.stdout.strip() | ||
|
|
||
|
|
||
| def blame_authors(filepath): | ||
| """Return {line_number: author} for every source line via git blame.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["git", "blame", "--line-porcelain", filepath], | ||
| capture_output=True, | ||
| text=True, | ||
| check=True, | ||
| ) | ||
| except subprocess.CalledProcessError: | ||
| return {} | ||
|
|
||
| authors = {} | ||
| current_line = None | ||
| for line in result.stdout.splitlines(): | ||
| # First line of each hunk: <sha> <orig_line> <final_line> [<num_lines>] | ||
| parts = line.split() | ||
| if ( | ||
| len(parts) >= 3 | ||
| and len(parts[0]) == 40 | ||
| and all(c in "0123456789abcdef" for c in parts[0]) | ||
| ): | ||
| current_line = int(parts[2]) | ||
| elif line.startswith("author "): | ||
| if current_line is not None: | ||
| authors[current_line] = line[len("author ") :] | ||
| return authors | ||
|
|
||
|
|
||
| def main(): | ||
| repo_root = get_repo_root() | ||
|
|
||
| cov_file = os.path.join(repo_root, ".coverage") | ||
| if not os.path.exists(cov_file): | ||
| print("Error: .coverage not found. Run bin/pytest-coverage first.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| data = CoverageData(basename=cov_file) | ||
| data.read() | ||
|
|
||
| stats = defaultdict(lambda: {"total": 0, "covered": 0}) | ||
|
|
||
| for abs_path in sorted(data.measured_files()): | ||
| # Convert to repo-relative path for git blame | ||
| try: | ||
| rel_path = os.path.relpath(abs_path, repo_root) | ||
| except ValueError: | ||
| continue | ||
| if rel_path.startswith(".."): | ||
| continue | ||
|
|
||
| authors = blame_authors(rel_path) | ||
| if not authors: | ||
| continue | ||
|
|
||
| covered_lines = set(data.lines(abs_path) or []) | ||
|
|
||
| for line_no, author in authors.items(): | ||
| stats[author]["total"] += 1 | ||
| if line_no in covered_lines: | ||
| stats[author]["covered"] += 1 | ||
|
|
||
| if not stats: | ||
| print("No coverage data with git blame information found.", file=sys.stderr) | ||
| sys.exit(1) | ||
|
|
||
| sorted_authors = sorted( | ||
| stats.items(), | ||
| key=lambda x: x[1]["covered"] / x[1]["total"] if x[1]["total"] else 0, | ||
| reverse=True, | ||
| ) | ||
|
|
||
| total_all = sum(s["total"] for _, s in sorted_authors) | ||
| covered_all = sum(s["covered"] for _, s in sorted_authors) | ||
|
|
||
| # Print table | ||
| hdr = f"{'Author':<30} {'Lines':>7} {'Covered':>9} {'Coverage':>10}" | ||
| sep = "\u2500" * len(hdr) | ||
| print(hdr) | ||
| print(sep) | ||
| for author, s in sorted_authors: | ||
| pct = 100.0 * s["covered"] / s["total"] if s["total"] else 0 | ||
| print(f"{author:<30} {s['total']:>7} {s['covered']:>9} {pct:>9.1f}%") | ||
| print(sep) | ||
| pct_all = 100.0 * covered_all / total_all if total_all else 0 | ||
| print(f"{'TOTAL':<30} {total_all:>7} {covered_all:>9} {pct_all:>9.1f}%") | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| #!/usr/bin/env bash | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| rm -f .coverage .coverage.* | ||
|
|
||
| export _DIMOS_COV=1 | ||
|
|
||
| uv run coverage run -m pytest "$@" -m 'not tool' | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. just to have a single place where this stuff is specified, should this be running ./bin/pytest-fast && ./bin/pytest-slow ? maybe not possible just mentioning preference
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But we don't want to run multiple command, right? And this is running |
||
| uv run coverage combine | ||
| uv run coverage html | ||
| uv run coverage report | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to have a single place where this stuff is specified, should this be running ./bin/pytest-coverage ?