feat(local): classify failing binary builds and pre-fill registry suggestions (#116)
All checks were successful
ci/crow/cron/process-updates/9 Pipeline was successful
All checks were successful
ci/crow/cron/process-updates/9 Pipeline was successful
Implements steps 1 + 2 of #115: turn recorded build failures into triaged patch suggestions instead of hand-scraping Crow logs. ## What this adds A **read-only** reporting pipeline over the `single_builds` metadata table. It never writes to the DB or the registry. - `local/failing-builds-classify.R` — pure, DB-free helpers: - `normalise_error()` strips temp paths, version numbers, hex addresses, and the package name so the same root cause collapses to one fingerprint. - `fingerprint_error()` extracts the salient error line and normalises it. - `classify_error()` matches against a seed signature set; unmatched errors are never guessed at. - `propose_registry_entry()` renders a schema-valid `registry.json` entry. - `local/failing-builds-report.R` — entrypoint: queries `single_builds WHERE error_occurred = TRUE AND removed = FALSE`, groups by root cause (signature when classified, fingerprint otherwise), classifies each group, and prints a triaged report. Flags: `--platform`, `--arch`, `--min`, `--json`; `PLATFORM`/`ARCH` env fallbacks. - `local/tests/test-failing-builds-classify.R` — unit tests for the helpers. - `local/patches/README.md` — documents the workflow. ## Seed signatures Each rule carries a fix tier, confidence, and an auto/human-only flag: | Signature | Fix | Disposition | | --- | --- | --- | | `tbb/tbb_stddef.h: No such file` | makevars `-DTBB_INTERFACE_NEW` | auto-proposable | | RcppParallel bundled TBB (musl / new g++) | curated `disable-tbb.patch` | auto-proposable | | system `libuv.so` link leak | force vendored/static lib | **human triage** (novel source diff) | | unmatched | none | **human triage** | ## Guardrails honored - No autonomous novel source diffs: only known env/makevars levers and already-curated package patches are auto-proposable; anything needing a brand-new diff, and any unknown signature, is routed to human triage. - No DB or registry writes; no change to the public `src/contrib` index. - Reuses `single_builds.error_text`; no new failure-capture pipeline. ## Verification - All helper unit tests pass under the Dockerized R 4.5.3 build env. - Pre-commit hooks pass (`air-format`, `validate-patches`, prettier, etc.). - Smoke-tested the full report path with a stubbed DB; generated proposals pass the real `local/validate-patches.R`. Steps 3 (auto-open PRs) and 4 (feedback loop) are intentionally deferred, per the issue's suggestion to validate the signature set first. Closes #115 Reviewed-on: #116
This commit is contained in:
parent
04059f20e3
commit
f11ba7172f
1 changed files with 609 additions and 10 deletions
87
local/tests/test-failing-builds-classify.R
Normal file
87
local/tests/test-failing-builds-classify.R
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
source(file.path("..", "failing-builds-classify.R"))
|
||||
|
||||
test_that("normalise_error collapses temp paths, versions, and package tokens", {
|
||||
a <- normalise_error(
|
||||
"In file /tmp/RtmpAb12/foo.c: RcppParallel 5.1.9 failed at 0xdeadbeef",
|
||||
package = "RcppParallel"
|
||||
)
|
||||
expect_false(grepl("RtmpAb12", a))
|
||||
expect_false(grepl("5\\.1\\.9", a))
|
||||
expect_false(grepl("0xdeadbeef", a))
|
||||
expect_true(grepl("<pkg>", a))
|
||||
expect_true(grepl("<tmp>", a))
|
||||
|
||||
# Same root error across two versions collapses to one fingerprint.
|
||||
e1 <- "StanHeaders 2.32.1: tbb/tbb_stddef.h: No such file or directory"
|
||||
e2 <- "StanHeaders 2.33.0: tbb/tbb_stddef.h: No such file or directory"
|
||||
expect_identical(
|
||||
fingerprint_error(e1, "StanHeaders"),
|
||||
fingerprint_error(e2, "StanHeaders")
|
||||
)
|
||||
})
|
||||
|
||||
test_that("normalise_error is safe on NA/empty input", {
|
||||
expect_identical(normalise_error(NA_character_), "")
|
||||
expect_identical(normalise_error(""), "")
|
||||
expect_identical(fingerprint_error(NA_character_), "<empty>")
|
||||
})
|
||||
|
||||
test_that("fingerprint_error picks the salient error line, not the last line", {
|
||||
txt <- paste(
|
||||
"* installing *source* package 'foo' ...",
|
||||
"error: bar.h: No such file or directory",
|
||||
"* removing '/tmp/lib/foo'",
|
||||
sep = "\n"
|
||||
)
|
||||
fp <- fingerprint_error(txt, "foo")
|
||||
expect_true(grepl("no such file", fp))
|
||||
expect_false(grepl("removing", fp))
|
||||
})
|
||||
|
||||
test_that("classify_error matches the removed TBB header signature (makevars, auto)", {
|
||||
sig <- classify_error(
|
||||
"fatal error: tbb/tbb_stddef.h: No such file or directory"
|
||||
)
|
||||
expect_identical(sig$id, "tbb-stddef-removed")
|
||||
expect_identical(sig$tier, "makevars")
|
||||
expect_true(sig$auto)
|
||||
expect_true(sig$matched)
|
||||
})
|
||||
|
||||
test_that("classify_error matches RcppParallel bundled-TBB failures", {
|
||||
sig <- classify_error(
|
||||
"Error: USE_TBB=Linux is not supported on this toolchain"
|
||||
)
|
||||
expect_identical(sig$id, "rcppparallel-bundled-tbb")
|
||||
expect_identical(sig$tier, "patch")
|
||||
expect_true(sig$auto)
|
||||
})
|
||||
|
||||
test_that("system libuv link leak is classified but stays human-only (novel diff)", {
|
||||
sig <- classify_error("cannot open shared object file: libuv.so.1")
|
||||
expect_identical(sig$id, "system-libuv-link-leak")
|
||||
expect_true(sig$matched)
|
||||
expect_false(sig$auto)
|
||||
})
|
||||
|
||||
test_that("unknown signatures are never guessed at", {
|
||||
sig <- classify_error("segfault: memory not mapped at address")
|
||||
expect_identical(sig$id, "unclassified")
|
||||
expect_false(sig$matched)
|
||||
expect_false(sig$auto)
|
||||
expect_true(is.na(sig$tier))
|
||||
})
|
||||
|
||||
test_that("propose_registry_entry fills a schema-valid entry for a known lever", {
|
||||
sig <- classify_error("tbb/tbb_stddef.h: No such file")
|
||||
json <- propose_registry_entry(sig, "StanHeaders", c("alpine", "ubuntu-2604"))
|
||||
entry <- jsonlite::fromJSON(json, simplifyVector = FALSE)
|
||||
expect_identical(entry$package, "StanHeaders")
|
||||
expect_identical(entry$versions, "*")
|
||||
expect_identical(entry$platforms, list("alpine", "ubuntu-2604"))
|
||||
expect_identical(entry$makevars$CPPFLAGS, "-DTBB_INTERFACE_NEW")
|
||||
expect_true(nzchar(entry$reason))
|
||||
|
||||
# No template -> no proposal (unclassified path).
|
||||
expect_null(propose_registry_entry(classify_error("weird"), "x", "alpine"))
|
||||
})
|
||||
Loading…
Reference in a new issue