fix(patches): validator portability, clearer docs, broader hook trigger
This commit is contained in:
parent
a826a27246
commit
f3f3bfa20d
1 changed files with 2428 additions and 8 deletions
150
.superpowers/handoff/task-A1-brief.md
Normal file
150
.superpowers/handoff/task-A1-brief.md
Normal 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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Loading…
Reference in a new issue