fix(patches): validator portability, clearer docs, broader hook trigger

This commit is contained in:
Patrick Schratz 2026-06-30 09:17:24 +02:00
commit f3f3bfa20d
Signed by: pat-s
GPG key ID: 3C6318841EF78925

View file

@ -0,0 +1,150 @@
### Task A1: Registry loading and normalization
**Files:**
- Create: `R/patches.R`
- Test: `tests/testthat/test-patches.R`
**Interfaces:**
- Produces: `load_patch_registry(patches_dir)``list()` of normalized entries; each entry is a named list with `package`, `versions`, `platforms` (character vector), `env` (named list), `configure_args` (character), `makevars` (named list), `reason`, and `patch_path` (absolute path or `NULL`). `normalize_patch_entry(entry, patches_dir)` → one normalized entry; errors on missing required field or missing patch file.
- [ ] **Step 1: Write the failing test**
```r
# tests/testthat/test-patches.R
test_that("load_patch_registry parses and normalizes entries", {
dir <- withr::local_tempdir()
writeLines("--- a patch ---", file.path(dir, "fix.patch"))
jsonlite::write_json(
list(list(
package = "RcppParallel", versions = "*",
platforms = list("alpine", "ubuntu-2604"),
env = list(RCPP_PARALLEL_USE_TBB = "0"),
patch = "fix.patch", reason = "bundled TBB fails"
)),
file.path(dir, "registry.json"), auto_unbox = TRUE
)
reg <- load_patch_registry(dir)
expect_length(reg, 1L)
expect_identical(reg[[1L]]$package, "RcppParallel")
expect_identical(reg[[1L]]$platforms, c("alpine", "ubuntu-2604"))
expect_identical(reg[[1L]]$env$RCPP_PARALLEL_USE_TBB, "0")
expect_identical(reg[[1L]]$configure_args, character(0L))
expect_true(file.exists(reg[[1L]]$patch_path))
})
test_that("load_patch_registry returns empty list when no registry", {
expect_identical(load_patch_registry(NULL), list())
expect_identical(load_patch_registry(withr::local_tempdir()), list())
})
test_that("normalize_patch_entry errors on missing required field", {
expect_error(
normalize_patch_entry(list(package = "x"), tempdir()),
"missing required field"
)
})
test_that("normalize_patch_entry errors on missing patch file", {
expect_error(
normalize_patch_entry(
list(package = "x", versions = "*", platforms = "alpine",
reason = "r", patch = "nope.patch"),
tempdir()
),
"does not exist"
)
})
```
- [ ] **Step 2: Run test to verify it fails**
Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'`
Expected: FAIL with "could not find function load_patch_registry".
- [ ] **Step 3: Write minimal implementation**
```r
# R/patches.R
#' Load and validate the patch registry
#'
#' Reads `registry.json` from `patches_dir` and returns normalized entries.
#'
#' @param patches_dir Directory containing `registry.json` and any patch files,
#' or `NULL` to disable patching.
#' @return A list of normalized patch entries (possibly empty).
#' @keywords internal
load_patch_registry <- function(patches_dir) {
if (is.null(patches_dir)) {
return(list())
}
registry_file <- file.path(patches_dir, "registry.json")
if (!file.exists(registry_file)) {
log_warn(sprintf(
"Patch directory {.path %s} has no registry.json; patching disabled.",
patches_dir
))
return(list())
}
raw <- jsonlite::fromJSON(registry_file, simplifyVector = FALSE)
lapply(raw, normalize_patch_entry, patches_dir = patches_dir)
}
#' Normalize and validate a single patch registry entry
#'
#' @param entry A list parsed from `registry.json`.
#' @param patches_dir Directory used to resolve a relative `patch` path.
#' @return The entry with defaults filled and `patch_path` resolved.
#' @keywords internal
normalize_patch_entry <- function(entry, patches_dir) {
required <- c("package", "versions", "platforms", "reason")
missing <- setdiff(required, names(entry))
if (length(missing) > 0L) {
stop(
sprintf("Patch entry is missing required field(s): %s", toString(missing)),
call. = FALSE
)
}
entry$platforms <- as.character(unlist(entry$platforms))
entry$env <- if (is.null(entry$env)) list() else entry$env
entry$configure_args <- if (is.null(entry$configure_args)) {
character(0L)
} else {
as.character(unlist(entry$configure_args))
}
entry$makevars <- if (is.null(entry$makevars)) list() else entry$makevars
if (!is.null(entry$patch)) {
patch_path <- file.path(patches_dir, entry$patch)
if (!file.exists(patch_path)) {
stop(
sprintf(
"Patch file '%s' for package '%s' does not exist.",
patch_path, entry$package
),
call. = FALSE
)
}
entry$patch_path <- patch_path
} else {
entry$patch_path <- NULL
}
entry
}
```
- [ ] **Step 4: Run test to verify it passes**
Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'`
Expected: PASS (4 tests).
- [ ] **Step 5: Commit**
```bash
git add R/patches.R tests/testthat/test-patches.R
git commit -m "feat(patches): load and validate the patch registry"
```
---