All checks were successful
ci/crow/manual/weekly-patch-proposals Pipeline was successful
ci/crow/cron/process-updates/15 Pipeline was successful
ci/crow/cron/process-updates/11 Pipeline was successful
ci/crow/cron/process-updates/16 Pipeline was successful
ci/crow/cron/process-updates/18 Pipeline was successful
## Problem A live `propose-patches.R --open-issue` run aborted with: ``` Patch registry validation FAILED: - ambiguous duplicate entries: hmmTMB|ubuntu-2604|*, imt|ubuntu-2604|*, refundBayes|ubuntu-2604|* Error: Candidate registry failed validation; not writing or proposing. ``` Root cause: a package whose failing builds match **more than one** auto-proposable signature (e.g. some logs hit `tbb-stddef-removed`, others `rcppparallel-bundled-tbb`) landed in two separate signature groups, so the proposer emitted two candidate entries with the same `package|platforms|versions` key. The validator's ambiguous-duplicate check then failed the whole candidate set, aborting the run — so even the unambiguous candidates (e.g. `vacalibration`) never got proposed. ## Fix Add a pure, tested `dedupe_candidates()` that: - keeps a package's candidate only when it maps to a **single** signature (collapsing an accidental repeat under the same signature), and - routes a package that maps to **multiple** signatures (conflicting fix tiers -- makevars vs source patch) to **human triage** instead of guessing between them. The proposer prints the skipped ambiguous packages and proceeds with the clean candidates, so one ambiguous package no longer blocks the rest. This matches the issue #115 guardrail: don't auto-pick when the fix is ambiguous. ## Verification - New unit tests in `test-proposal-tracking-lib.R` cover the single-signature, multi-signature (conflict), duplicate-under-one-signature, and empty cases. - Reproduced the original failure end-to-end with a stubbed DB (`hmmTMB` ambiguous + `vacalibration` clean): `hmmTMB` is now listed under "Ambiguous -> human triage", `vacalibration` is proposed, and the candidate registry validates (`Patch registry OK`). - Full suite: 78 tests pass; all pre-commit hooks pass. Reviewed-on: #119
137 lines
4.5 KiB
R
137 lines
4.5 KiB
R
source(file.path("..", "proposal-tracking-lib.R"))
|
|
source(file.path("..", "failing-builds-classify.R"))
|
|
|
|
mk_failures <- function() {
|
|
data.frame(
|
|
name = c("StanHeaders", "rstan", "RcppParallel", "somepkg"),
|
|
platform = c("alpine-321", "alpine-321", "alpine-320", "redhat-9"),
|
|
arch = c("amd64", "arm64", "amd64", "amd64"),
|
|
error_text = c(
|
|
"fatal error: tbb/tbb_stddef.h: No such file or directory",
|
|
"In file: tbb/tbb_stddef.h: No such file or directory",
|
|
"Error: USE_TBB=Linux is not supported; bundled TBB on musl",
|
|
"some unmatched failure"
|
|
),
|
|
stringsAsFactors = FALSE
|
|
)
|
|
}
|
|
|
|
test_that("merge_ledger appends new proposals and preserves existing history", {
|
|
existing <- list(list(
|
|
package = "fs",
|
|
signature = "system-libuv-link-leak",
|
|
status = "merged"
|
|
))
|
|
new <- list(
|
|
list(
|
|
package = "fs",
|
|
signature = "system-libuv-link-leak",
|
|
status = "proposed"
|
|
),
|
|
list(
|
|
package = "StanHeaders",
|
|
signature = "tbb-stddef-removed",
|
|
status = "proposed"
|
|
)
|
|
)
|
|
merged <- merge_ledger(existing, new)
|
|
expect_length(merged, 2L) # fs is deduped, StanHeaders added
|
|
fs <- Filter(function(r) r$package == "fs", merged)[[1L]]
|
|
expect_identical(fs$status, "merged") # existing status preserved, not clobbered
|
|
})
|
|
|
|
test_that("merge_ledger handles an empty/NULL starting ledger", {
|
|
new <- list(list(package = "x", signature = "s"))
|
|
expect_length(merge_ledger(NULL, new), 1L)
|
|
expect_length(merge_ledger(list(), new), 1L)
|
|
})
|
|
|
|
test_that("dedupe_candidates keeps single-signature pkgs, routes conflicts to triage", {
|
|
candidates <- list(
|
|
list(package = "StanHeaders", signature = "tbb-stddef-removed"),
|
|
list(package = "hmmTMB", signature = "tbb-stddef-removed"),
|
|
list(package = "hmmTMB", signature = "rcppparallel-bundled-tbb") # conflict
|
|
)
|
|
out <- dedupe_candidates(candidates)
|
|
kept <- vapply(out$keep, function(c) c$package, character(1L))
|
|
expect_identical(sort(kept), "StanHeaders") # hmmTMB dropped as ambiguous
|
|
expect_true("hmmTMB" %in% names(out$ambiguous))
|
|
expect_setequal(
|
|
out$ambiguous$hmmTMB,
|
|
c("tbb-stddef-removed", "rcppparallel-bundled-tbb")
|
|
)
|
|
})
|
|
|
|
test_that("dedupe_candidates collapses a package repeated under one signature", {
|
|
candidates <- list(
|
|
list(package = "rstan", signature = "tbb-stddef-removed"),
|
|
list(package = "rstan", signature = "tbb-stddef-removed")
|
|
)
|
|
out <- dedupe_candidates(candidates)
|
|
expect_length(out$keep, 1L)
|
|
expect_length(out$ambiguous, 0L)
|
|
})
|
|
|
|
test_that("dedupe_candidates handles the empty list", {
|
|
out <- dedupe_candidates(list())
|
|
expect_length(out$keep, 0L)
|
|
expect_length(out$ambiguous, 0L)
|
|
})
|
|
|
|
test_that("signature_hit_rate splits addressed vs open per signature", {
|
|
report <- build_triage_report(mk_failures(), registered_pkgs = "RcppParallel")
|
|
hit <- signature_hit_rate(report, registered_pkgs = "RcppParallel")
|
|
tbb <- Filter(function(h) h$signature == "tbb-stddef-removed", hit)[[1L]]
|
|
expect_identical(tbb$packages, 2L) # StanHeaders + rstan
|
|
expect_identical(tbb$addressed, 0L)
|
|
expect_identical(tbb$open, 2L)
|
|
expect_true(tbb$auto_proposable)
|
|
|
|
rcpp <- Filter(function(h) h$signature == "rcppparallel-bundled-tbb", hit)[[
|
|
1L
|
|
]]
|
|
expect_identical(rcpp$addressed, 1L) # already registered
|
|
expect_identical(rcpp$open, 0L)
|
|
|
|
# Unclassified failures never appear as a signature.
|
|
expect_false(
|
|
"unclassified" %in% vapply(hit, function(h) h$signature, character(1L))
|
|
)
|
|
})
|
|
|
|
test_that("proposed_vs_merged marks a package merged once it is registered", {
|
|
ledger <- list(
|
|
list(
|
|
package = "StanHeaders",
|
|
signature = "tbb-stddef-removed",
|
|
status = "proposed"
|
|
),
|
|
list(
|
|
package = "rstan",
|
|
signature = "tbb-stddef-removed",
|
|
status = "proposed"
|
|
)
|
|
)
|
|
pvm <- proposed_vs_merged(ledger, registered_pkgs = "StanHeaders")
|
|
expect_identical(pvm$total, 2L)
|
|
expect_identical(pvm$merged, 1L)
|
|
stan <- Filter(function(r) r$package == "StanHeaders", pvm$records)[[1L]]
|
|
expect_identical(stan$status, "merged")
|
|
})
|
|
|
|
test_that("retirement_candidates flags entries whose package no longer fails", {
|
|
entries <- list(
|
|
list(package = "RcppParallel"),
|
|
list(package = "oldpkg")
|
|
)
|
|
# RcppParallel still fails; oldpkg does not -> only oldpkg is retirable.
|
|
out <- retirement_candidates(
|
|
entries,
|
|
failing_pkgs = c("RcppParallel", "StanHeaders")
|
|
)
|
|
expect_identical(out, "oldpkg")
|
|
expect_length(
|
|
retirement_candidates(entries, failing_pkgs = c("RcppParallel", "oldpkg")),
|
|
0L
|
|
)
|
|
})
|