Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 106 additions & 44 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def diagnostic_paths_for_commit() -> tuple[Path, Path, str]:
DIAGNOSTIC_DIR.mkdir(parents=True, exist_ok=True)
commit_id = current_commit_id()
logd_path = DIAGNOSTIC_DIR / f"build-{commit_id}.logd"
metadata_path = DIAGNOSTIC_DIR / f"build-{commit_id}-metadata.json"
metadata_path = DIAGNOSTIC_DIR / f"build-{commit_id}.json"
return logd_path, metadata_path, commit_id


Expand Down Expand Up @@ -292,18 +292,29 @@ def build_module(
if module.name == "engine":

build_type = "Release" if release else "Debug"
cfg_result = subprocess.run(
["cmake", "-S", ".", "-B", "build",
f"-DCMAKE_BUILD_TYPE={build_type}"],
cwd=str(module.dir),
capture_output=True,
text=True,
timeout=120,
env=env,
)
try:
cfg_result = subprocess.run(
["cmake", "-S", ".", "-B", "build",
f"-DCMAKE_BUILD_TYPE={build_type}"],
cwd=str(module.dir),
capture_output=True,
text=True,
timeout=120,
env=env,
)
except subprocess.TimeoutExpired:
return False, time.time() - start, "CMake configure TIMEOUT (120s)"
except FileNotFoundError as e:
return False, 0, f"Command not found: {e}"
if cfg_result.returncode != 0:
output_lines = []
if cfg_result.stdout:
output_lines.append(cfg_result.stdout.strip())
if cfg_result.stderr:
output_lines.append(cfg_result.stderr.strip())
output = "\n".join(output_lines)
return False, time.time() - start, (
f"CMake configure failed:\n{cfg_result.stderr}")
f"CMake configure failed:\n{output}")
if verbose:
print(f" {color('cmake configured', Colors.GRAY)}")
cmd = ["cmake", "--build", "build"]
Expand Down Expand Up @@ -427,20 +438,83 @@ def collect_system_info() -> str:
return "\n".join(lines)


def build_diagnostic_report(
results: list[tuple[str, bool, float, str, Optional[str]]],
commit_id: str,
logd_relpaths: Optional[list[str]] = None,
password: Optional[str] = None,
logd_error: Optional[str] = None,
chunked: bool = False,
) -> dict:
diagnostic_logd: Optional[str | list[str]]
if not logd_relpaths:
diagnostic_logd = None
elif len(logd_relpaths) == 1:
diagnostic_logd = logd_relpaths[0]
else:
diagnostic_logd = logd_relpaths

decrypt_target = logd_relpaths[0] if logd_relpaths and len(logd_relpaths) == 1 else None
if logd_relpaths and len(logd_relpaths) > 1:
decrypt_target = str((DIAGNOSTIC_DIR / f"build-{commit_id}.logd").relative_to(ROOT))

report = {
"generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"commit": commit_id,
"diagnostic_logd": diagnostic_logd,
"diagnostic_logd_error": logd_error,
"chunked": chunked,
"chunk_size_bytes": DIAGNOSTIC_CHUNK_SIZE if chunked else None,
"password": password,
"decrypt_command": (
f"encryptly unpack {decrypt_target} <outdir> --password {password}"
if decrypt_target and password else None
),
"total_modules": len(results),
"passed": sum(1 for _, s, _, _, _ in results if s),
"failed": sum(1 for _, s, _, _, _ in results if not s),
"modules": [
{
"name": name,
"status": "PASS" if success else "FAIL",
"elapsed_seconds": round(elapsed, 3),
"artifact": binary,
"output": output,
}
for name, success, elapsed, output, binary in results
],
"pr_note": (
(f"Include the encrypted diagnostic logd artifact(s): {', '.join(logd_relpaths)}. " if logd_relpaths else "Encrypted diagnostic logd artifact was not created; include this JSON report showing why. ")
+ "The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. "
+ "Maintainers may ask you to remove these diagnostic artifacts before merging."
),
}
return report


def write_diagnostic_report(metadata_path: Path, report: dict) -> None:
metadata_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
print(f" {color('✓', Colors.GREEN)} {metadata_path.relative_to(ROOT)} created")


def generate_logd(
results: list[tuple[str, bool, float, str, Optional[str]]],
verbose: bool = False,
) -> bool:
logd_path, metadata_path, commit_id = diagnostic_paths_for_commit()
display_logd = logd_path.relative_to(ROOT)
print(f"\n {color('▸', Colors.CYAN)} Finalizing {color(str(display_logd), Colors.BOLD)}...")
print(f"\n {color('▸', Colors.CYAN)} Finalizing diagnostics for {color(str(display_logd), Colors.BOLD)}...")

# Always write the JSON report first. The encrypted .logd is useful, but the
# report is required even when the build failed before compilation started or
# when encryptly itself is unavailable.
write_diagnostic_report(metadata_path, build_diagnostic_report(results, commit_id))

encryptly_bin = get_encryptly_bin()
if encryptly_bin is None:
print(
f" {color('✗', Colors.RED)} encryptly binary not found "
f"({encryptly_platform_help()}); cannot create {display_logd}"
)
error = f"encryptly binary not found ({encryptly_platform_help()}); cannot create {display_logd}"
print(f" {color('✗', Colors.RED)} {error}")
write_diagnostic_report(metadata_path, build_diagnostic_report(results, commit_id, logd_error=error))
return False

# Workspace must live under $HOME because encryptly refuses paths outside home.
Expand Down Expand Up @@ -503,45 +577,33 @@ def generate_logd(
timeout=300,
)
if sr.returncode != 0:
error = sr.stderr.strip() or sr.stdout.strip() or "encryptly pack failed"
print(
f" {color('✗', Colors.RED)} {logd_path.relative_to(ROOT)} creation failed: "
f"{sr.stderr.strip() or sr.stdout.strip()}"
f"{error}"
)
if logd_path.exists():
logd_path.unlink()
write_diagnostic_report(
metadata_path,
build_diagnostic_report(results, commit_id, logd_error=error),
)
return False

safe_pw = sr.stdout.strip()
logd_files = split_diagnostic_logd(logd_path)
logd_relpaths = [str(path.relative_to(ROOT)) for path in logd_files]
diagnostic_logd = logd_relpaths[0] if len(logd_relpaths) == 1 else logd_relpaths
decrypt_target = logd_relpaths[0] if len(logd_relpaths) == 1 else str(logd_path.relative_to(ROOT))
metadata = {
"generated_at": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"commit": commit_id,
"diagnostic_logd": diagnostic_logd,
"chunked": len(logd_files) > 1,
"chunk_size_bytes": DIAGNOSTIC_CHUNK_SIZE if len(logd_files) > 1 else None,
"password": safe_pw,
"decrypt_command": f"encryptly unpack {decrypt_target} <outdir> --password {safe_pw}",
"total_modules": len(results),
"passed": sum(1 for _, s, _, _, _ in results if s),
"failed": sum(1 for _, s, _, _, _ in results if not s),
"modules": [
{
"name": name,
"status": "PASS" if success else "FAIL",
"elapsed_seconds": round(elapsed, 3),
"artifact": binary,
}
for name, success, elapsed, _, binary in results
],
"pr_note": (
f"Include this metadata and {', '.join(logd_relpaths)} in your PR. "
"Maintainers may ask you to remove these diagnostic artifacts before merging."
write_diagnostic_report(
metadata_path,
build_diagnostic_report(
results,
commit_id,
logd_relpaths=logd_relpaths,
password=safe_pw,
chunked=len(logd_files) > 1,
),
}
metadata_path.write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
)

for path in logd_files:
size_kb = path.stat().st_size / 1024.0
Expand All @@ -554,7 +616,6 @@ def generate_logd(
f" {color('✓', Colors.GREEN)} split oversized diagnostic log into "
f"{len(logd_files)} chunks of at most {DIAGNOSTIC_CHUNK_SIZE // (1024 * 1024)} MiB"
)
print(f" {color('✓', Colors.GREEN)} {metadata_path.relative_to(ROOT)} created")
if safe_pw:
print()
print(f" {color('Password', Colors.BOLD)} - this is required to decrypt the diagnostic log,")
Expand Down Expand Up @@ -687,6 +748,7 @@ def main():
if DIAGNOSTIC_DIR.exists():
diagnostic_artifacts.extend(DIAGNOSTIC_DIR.glob("build-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].logd"))
diagnostic_artifacts.extend(DIAGNOSTIC_DIR.glob("build-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-part*.logd"))
diagnostic_artifacts.extend(DIAGNOSTIC_DIR.glob("build-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f].json"))
diagnostic_artifacts.extend(DIAGNOSTIC_DIR.glob("build-[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]-metadata.json"))
for artifact in diagnostic_artifacts:
if artifact.exists():
Expand Down
12 changes: 0 additions & 12 deletions diagnostic/build-00000000-metadata.json

This file was deleted.

23 changes: 23 additions & 0 deletions diagnostic/build-00000000.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"generated_at": "2026-06-16T15:23:47.496569+00:00",
"commit": "00000000",
"diagnostic_logd": "diagnostic/build-00000000.logd",
"diagnostic_logd_error": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "4c7df15ab09fbb066197",
"decrypt_command": "encryptly unpack diagnostic/build-00000000.logd <outdir> --password 4c7df15ab09fbb066197",
"total_modules": 1,
"passed": 0,
"failed": 1,
"modules": [
{
"name": "frailbox",
"status": "FAIL",
"elapsed_seconds": 0,
"artifact": null,
"output": "Command not found: [Errno 2] No such file or directory: 'make'"
}
],
"pr_note": "Include this JSON diagnostic report and diagnostic/build-00000000.logd in your PR. Maintainers may ask you to remove these diagnostic artifacts before merging."
}
38 changes: 38 additions & 0 deletions tools/tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Log Parser Fixtures & Validation

These are **independent** fixtures for `tools/log_aggregator.py`. Unlike the
legacy suite (which generated test data from the same parser logic and could
false-pass), every line here is **hand-written** from real-world log examples
and was never produced by the parser.

## Layout

```
tools/tests/fixtures/
json_sample.log 3 structured JSON log lines (level/severity/lvl variants)
text_sample.log 3 plain-text lines (ISO, standard, syslog timestamps)
nginx_sample.log 2 nginx access-log lines (200 and 500 status)
malformed.log 4 unsupported/partial lines that must NOT crash parsing
tools/tests/test_log_parser_fixtures.py validation script
```

## What it checks

For each supported format the script asserts the extracted:

- **timestamp** — preserved (JSON) or parsed to a unix int (text/nginx)
- **level** — correct mapping (`error`/`warn`/`info`/`debug`)
- **service / format** — correct source field or `nginx`
- **key fields** — `message`, `request`, `status`, `remote_addr`, etc.

It also proves malformed lines (broken JSON, no-timestamp text, garbage) are
handled **without raising** — parsers return `None` or a best-effort result.

## Run it

```bash
python3 tools/tests/test_log_parser_fixtures.py
```

Exit `0` = all checks passed. This script is the required validation for the
log-parser fixtures bounty; it runs in isolation and has no other repo deps.
3 changes: 3 additions & 0 deletions tools/tests/fixtures/json_sample.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"timestamp":"2024-03-12T08:15:30Z","level":"error","service":"payments","message":"charge failed for txn 9f3a","order_id":"9f3a"}
{"time":"2024-03-12T09:00:00","severity":"warn","logger":"cache","msg":"redis eviction triggered for key session:42"}
{"@timestamp":"2024-03-12T09:05:11","lvl":"info","app":"api","event":"request handled","path":"/health"}
4 changes: 4 additions & 0 deletions tools/tests/fixtures/malformed.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[1, 2, 3]
this line has no timestamp and no level indicator
{"this is": "not valid json
PARTIAL%%%@@@broken@@@%%%PARTIAL
2 changes: 2 additions & 0 deletions tools/tests/fixtures/nginx_sample.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
127.0.0.1 - - [12/Mar/2024:08:30:00 +0000] "GET /api/health HTTP/1.1" 200 512 "http://localhost/" "curl/8.0"
10.0.0.9 - - [12/Mar/2024:08:31:22 +0000] "POST /api/charge HTTP/1.1" 500 0 "-" "Mozilla/5.0"
3 changes: 3 additions & 0 deletions tools/tests/fixtures/text_sample.log
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2024-03-12T08:20:00 [auth] ERROR failed login attempt from 10.0.0.5
2024-03-12 08:21:15 worker WARNING disk usage at 92 percent
Mar 12 08:25:33 scheduler DEBUG job tick executed
Loading