- CI warm: ccache dep, continue-on-error, 60min timeout, YAMS_DISABLE_FAISS - Faiss: YAMS_HAS_FAISS guard, Apple Clang disable, OpenMP detection - Metadata: retry constraint failures, deduplicate batch UPSERT entries - Docker: wget for Zig, gfortran, YAMS_DISABLE_FAISS=1 - CI config: free-disk-space v1.3.1, libomp-18-dev, experimental trigger - Weekly benchmarks: faiss disable, ccache, diagnostics capture - Simeon: bump to 7763a97, EntropyRouter, concept_count - CorpusStats: extraction readiness fields - Remove committed artifacts (.wrangler, .artifacts, dispatcher_external) - Fix .vscode/settings.json hardcoded path Signed-off-by: trvon <git@trevon.dev>
306 lines
9.5 KiB
Python
306 lines
9.5 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from collections import defaultdict
|
|
from pathlib import Path
|
|
|
|
|
|
FILE_RE = re.compile(r"^File '([^']+)'$")
|
|
LINES_RE = re.compile(r"^Lines executed:([0-9]+(?:\.[0-9]+)?|nan)% of ([0-9]+)$")
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser(
|
|
description="Refresh daemon gcov summaries from gcov run JSON and/or live gcov captures"
|
|
)
|
|
parser.add_argument(
|
|
"--build-dir",
|
|
default="build/coverage",
|
|
help="Coverage build directory (default: build/coverage)",
|
|
)
|
|
parser.add_argument(
|
|
"--workspace-root",
|
|
default=".",
|
|
help="Workspace root used for path normalization (default: .)",
|
|
)
|
|
parser.add_argument(
|
|
"--run-json",
|
|
action="append",
|
|
default=[],
|
|
help="Existing gcov run JSON input (repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--capture-artifact",
|
|
action="append",
|
|
default=[],
|
|
help="Artifact path (.o or .gcno) to run gcov against (repeatable)",
|
|
)
|
|
parser.add_argument(
|
|
"--keep-captured-history",
|
|
action="store_true",
|
|
help="Keep existing run-json entries for recaptured artifacts instead of replacing them",
|
|
)
|
|
parser.add_argument(
|
|
"--capture-out",
|
|
help="Optional path to write captured gcov entries as JSON",
|
|
)
|
|
parser.add_argument(
|
|
"--write-merged-run-json",
|
|
help="Optional path to write merged gcov run JSON",
|
|
)
|
|
parser.add_argument(
|
|
"--summary-json",
|
|
help="Raw summary output path (default: <build-dir>/daemon_gcov_summary.json)",
|
|
)
|
|
parser.add_argument(
|
|
"--normalized-json",
|
|
help="Normalized summary output path (default: <build-dir>/daemon_gcov_summary_normalized.json)",
|
|
)
|
|
parser.add_argument(
|
|
"--dedup-json",
|
|
help="Deduped summary output path (default: <build-dir>/daemon_gcov_summary_dedup.json)",
|
|
)
|
|
return parser.parse_args()
|
|
|
|
|
|
def load_run_entries(path: Path) -> list[dict]:
|
|
with path.open("r", encoding="utf-8") as handle:
|
|
data = json.load(handle)
|
|
if not isinstance(data, list):
|
|
raise ValueError(f"Expected list in {path}")
|
|
return data
|
|
|
|
|
|
def normalize_artifact_path(path_text: str, workspace_root: Path) -> str:
|
|
artifact_path = Path(path_text)
|
|
if not artifact_path.is_absolute():
|
|
artifact_path = workspace_root / artifact_path
|
|
return str(artifact_path.resolve(strict=False))
|
|
|
|
|
|
def capture_gcov_entries(
|
|
artifacts: list[str], workspace_root: Path, build_dir: Path
|
|
) -> list[dict]:
|
|
entries = []
|
|
for artifact in artifacts:
|
|
artifact_path = Path(normalize_artifact_path(artifact, workspace_root))
|
|
proc = subprocess.run(
|
|
["gcov", "-b", "-c", str(artifact_path)],
|
|
cwd=build_dir,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
entries.append(
|
|
{
|
|
"file": str(artifact_path),
|
|
"code": proc.returncode,
|
|
"stdout": proc.stdout,
|
|
"stderr": proc.stderr,
|
|
}
|
|
)
|
|
return entries
|
|
|
|
|
|
def is_daemon_source(path_text: str) -> bool:
|
|
normalized = path_text.replace("\\", "/")
|
|
if not normalized.endswith((".cpp", ".cc", ".cxx")):
|
|
return False
|
|
return (
|
|
"/daemon/" in normalized
|
|
or normalized.startswith("daemon/")
|
|
or "src/daemon/" in normalized
|
|
)
|
|
|
|
|
|
def normalize_source_path(source: str, workspace_root: Path, build_dir: Path) -> str:
|
|
normalized = source.replace("\\", "/")
|
|
if os.path.isabs(normalized):
|
|
return str(Path(normalized).resolve())
|
|
|
|
while normalized.startswith("./"):
|
|
normalized = normalized[2:]
|
|
while normalized.startswith("../"):
|
|
normalized = normalized[3:]
|
|
|
|
if normalized.startswith(("src/", "include/", "tests/")):
|
|
return str((workspace_root / normalized).resolve(strict=False))
|
|
|
|
src_idx = normalized.find("src/")
|
|
if src_idx != -1:
|
|
return str((workspace_root / normalized[src_idx:]).resolve(strict=False))
|
|
|
|
include_idx = normalized.find("include/")
|
|
if include_idx != -1:
|
|
return str((workspace_root / normalized[include_idx:]).resolve(strict=False))
|
|
|
|
return str((build_dir / normalized).resolve(strict=False))
|
|
|
|
|
|
def parse_run_entries(
|
|
entries: list[dict], workspace_root: Path, build_dir: Path
|
|
) -> tuple[list[dict], list[dict]]:
|
|
raw_records = []
|
|
normalized_records = []
|
|
|
|
for entry in entries:
|
|
text_parts = []
|
|
stdout = entry.get("stdout")
|
|
stderr = entry.get("stderr")
|
|
if stdout:
|
|
text_parts.append(stdout)
|
|
if stderr:
|
|
text_parts.append(stderr)
|
|
text = "\n".join(text_parts)
|
|
current_source = None
|
|
|
|
for line in text.splitlines():
|
|
file_match = FILE_RE.match(line.strip())
|
|
if file_match:
|
|
current_source = file_match.group(1)
|
|
continue
|
|
|
|
lines_match = LINES_RE.match(line.strip())
|
|
if not lines_match or current_source is None:
|
|
continue
|
|
if not is_daemon_source(current_source):
|
|
current_source = None
|
|
continue
|
|
|
|
pct_text, lines_text = lines_match.groups()
|
|
pct = 0.0 if pct_text == "nan" else round(float(pct_text), 2)
|
|
lines = int(lines_text)
|
|
|
|
raw_record = {"file": current_source, "pct": pct, "lines": lines}
|
|
raw_records.append(raw_record)
|
|
normalized_records.append(
|
|
{
|
|
"file": normalize_source_path(
|
|
current_source, workspace_root, build_dir
|
|
),
|
|
"pct": pct,
|
|
"lines": lines,
|
|
}
|
|
)
|
|
current_source = None
|
|
|
|
return raw_records, normalized_records
|
|
|
|
|
|
def dedup_records(records: list[dict]) -> list[dict]:
|
|
groups: dict[str, list[dict]] = defaultdict(list)
|
|
for record in records:
|
|
groups[record["file"]].append(record)
|
|
|
|
deduped = []
|
|
for file_path, variants in groups.items():
|
|
best = max(variants, key=lambda item: (item["pct"], item["lines"]))
|
|
deduped.append(
|
|
{
|
|
"file": file_path,
|
|
"pct": best["pct"],
|
|
"lines": best["lines"],
|
|
"variants": len(variants),
|
|
}
|
|
)
|
|
deduped.sort(key=lambda item: item["file"])
|
|
return deduped
|
|
|
|
|
|
def write_json(path: Path, data: object) -> None:
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
with path.open("w", encoding="utf-8") as handle:
|
|
json.dump(data, handle, indent=2, sort_keys=False)
|
|
handle.write("\n")
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
workspace_root = Path(args.workspace_root).resolve()
|
|
build_dir = (workspace_root / args.build_dir).resolve()
|
|
|
|
run_json_paths = args.run_json or [str(build_dir / "daemon_gcov_run.json")]
|
|
run_entries = []
|
|
for run_json in run_json_paths:
|
|
run_entries.extend(load_run_entries((workspace_root / run_json).resolve()))
|
|
|
|
captured_artifacts = {
|
|
normalize_artifact_path(path_text, workspace_root)
|
|
for path_text in args.capture_artifact
|
|
}
|
|
replaced_run_entries = 0
|
|
if captured_artifacts and not args.keep_captured_history:
|
|
filtered_run_entries = []
|
|
for entry in run_entries:
|
|
entry_file = entry.get("file")
|
|
if (
|
|
isinstance(entry_file, str)
|
|
and normalize_artifact_path(entry_file, workspace_root)
|
|
in captured_artifacts
|
|
):
|
|
replaced_run_entries += 1
|
|
continue
|
|
filtered_run_entries.append(entry)
|
|
run_entries = filtered_run_entries
|
|
|
|
captured_entries = capture_gcov_entries(
|
|
args.capture_artifact, workspace_root, build_dir
|
|
)
|
|
merged_entries = [*run_entries, *captured_entries]
|
|
|
|
raw_records, normalized_records = parse_run_entries(
|
|
merged_entries, workspace_root, build_dir
|
|
)
|
|
deduped_records = dedup_records(normalized_records)
|
|
|
|
summary_json = (
|
|
Path(args.summary_json)
|
|
if args.summary_json
|
|
else build_dir / "daemon_gcov_summary.json"
|
|
)
|
|
normalized_json = (
|
|
Path(args.normalized_json)
|
|
if args.normalized_json
|
|
else build_dir / "daemon_gcov_summary_normalized.json"
|
|
)
|
|
dedup_json = (
|
|
Path(args.dedup_json)
|
|
if args.dedup_json
|
|
else build_dir / "daemon_gcov_summary_dedup.json"
|
|
)
|
|
|
|
write_json(summary_json.resolve(), raw_records)
|
|
write_json(normalized_json.resolve(), normalized_records)
|
|
write_json(dedup_json.resolve(), deduped_records)
|
|
|
|
if args.capture_out:
|
|
write_json(Path(args.capture_out).resolve(), captured_entries)
|
|
if args.write_merged_run_json:
|
|
write_json(Path(args.write_merged_run_json).resolve(), merged_entries)
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"run_entries": len(run_entries),
|
|
"replaced_run_entries": replaced_run_entries,
|
|
"captured_entries": len(captured_entries),
|
|
"raw_records": len(raw_records),
|
|
"normalized_records": len(normalized_records),
|
|
"deduped_records": len(deduped_records),
|
|
"summary_json": str(summary_json.resolve()),
|
|
"normalized_json": str(normalized_json.resolve()),
|
|
"dedup_json": str(dedup_json.resolve()),
|
|
},
|
|
indent=2,
|
|
sort_keys=True,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|