build-cran-binaries/.superpowers/handoff/task-A1-brief.md

4.7 KiB

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

# 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/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
git add R/patches.R tests/testthat/test-patches.R
git commit -m "feat(patches): load and validate the patch registry"