### Task A3: Cache key and version resolution **Files:** - Modify: `R/patches.R` - Test: `tests/testthat/test-patches.R` **Interfaces:** - Consumes: normalized entries. - Produces: `patch_cache_key(entry, version, platform, arch, r_minor)` → string `"_____"`, where `hash12` covers `env`/`configure_args`/`makevars`/patch bytes; `resolve_patch_version(entry)` → latest CRAN version satisfying `entry$versions`, or `NA_character_`; `describe_patch(entry)` → short human label. - [ ] **Step 1: Write the failing test** ```r test_that("patch_cache_key is stable and sensitive to env/patch changes", { e1 <- list(package = "P", env = list(A = "1"), configure_args = character(0L), makevars = list(), patch_path = NULL) e2 <- e1; e2$env <- list(A = "2") k1 <- patch_cache_key(e1, "1.0", "alpine-324", "amd64", "4.5") expect_identical(k1, patch_cache_key(e1, "1.0", "alpine-324", "amd64", "4.5")) expect_false(identical( k1, patch_cache_key(e2, "1.0", "alpine-324", "amd64", "4.5") )) expect_match(k1, "^P_1.0_alpine-324_amd64_4.5_[0-9a-f]{12}$") }) test_that("resolve_patch_version returns latest for wildcard, NA when unmet", { local_mocked_bindings( cran_package = function(pkg) list(Version = "5.1.12"), .package = "pkgsearch" ) expect_identical( resolve_patch_version(list(package = "RcppParallel", versions = "*")), "5.1.12" ) expect_identical( resolve_patch_version(list(package = "RcppParallel", versions = ">=9.0")), NA_character_ ) }) test_that("describe_patch summarizes the active overrides", { expect_match( describe_patch(list(env = list(RCPP_PARALLEL_USE_TBB = "0"), configure_args = character(0L), makevars = list(), patch_path = NULL)), "env: RCPP_PARALLEL_USE_TBB=0" ) expect_match( describe_patch(list(env = list(), configure_args = character(0L), makevars = list(), patch_path = "/x/fix.patch")), "source patch" ) }) ``` - [ ] **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 patch_cache_key". - [ ] **Step 3: Write minimal implementation (append to `R/patches.R`)** ```r #' Compute the cache key for a patched binary #' @keywords internal patch_cache_key <- function(entry, version, platform, arch, r_minor) { payload <- list( env = entry$env, configure_args = entry$configure_args, makevars = entry$makevars, patch = if (!is.null(entry$patch_path)) { readBin(entry$patch_path, "raw", file.size(entry$patch_path)) } else { raw(0L) } ) tmp <- tempfile() on.exit(unlink(tmp), add = TRUE) saveRDS(payload, tmp) hash <- substr(unname(tools::md5sum(tmp)), 1L, 12L) sprintf( "%s_%s_%s_%s_%s_%s", entry$package, version, platform, arch, r_minor, hash ) } #' Resolve the CRAN version to build for a patch entry #' #' Returns the latest CRAN version satisfying the entry's `versions` constraint, #' or `NA_character_` when CRAN's latest does not satisfy it or lookup fails. #' @keywords internal resolve_patch_version <- function(entry) { latest <- tryCatch( pkgsearch::cran_package(entry$package)$Version, error = function(e) NA_character_ ) if (is.na(latest)) { return(NA_character_) } if (identical(entry$versions, "*") || version_satisfies(latest, entry$versions)) { return(latest) } NA_character_ } #' Short human label describing a patch entry's overrides #' @keywords internal describe_patch <- function(entry) { bits <- character(0L) if (length(entry$env) > 0L) { bits <- c(bits, sprintf( "env: %s", paste( names(entry$env), unlist(entry$env), sep = "=", collapse = "," ) )) } if (length(entry$configure_args) > 0L) { bits <- c(bits, sprintf("configure: %s", toString(entry$configure_args))) } if (length(entry$makevars) > 0L) { bits <- c(bits, "makevars") } if (!is.null(entry$patch_path)) { bits <- c(bits, "source patch") } if (length(bits) == 0L) "no-op" else paste(bits, collapse = "; ") } ``` - [ ] **Step 4: Run test to verify it passes** Run: `Rscript -e 'devtools::load_all("."); testthat::test_file("tests/testthat/test-patches.R")'` Expected: PASS. (Note: `local_mocked_bindings` requires testthat >= 3.1.7; bincraft uses 3e.) - [ ] **Step 5: Commit** ```bash git add R/patches.R tests/testthat/test-patches.R git commit -m "feat(patches): cache key, version resolution, and patch description" ``` ---