From df4c998dc203b5a67416dfa6b29ae53f43f45ed5 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 2 Apr 2026 09:43:15 -0700 Subject: [PATCH 1/3] Add integration test for parentChain dep chain messaging Creates a dummy package tarball (dummypkgwithpryr) with Imports: pryr, places it in the Require file cache, then calls pkgDep() to verify that the "not on CRAN" message for pryr includes "(required by: dummypkgwithpryr)". Co-Authored-By: Claude Sonnet 4.6 --- .../test-16parentChain_integration_testthat.R | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/testthat/test-16parentChain_integration_testthat.R diff --git a/tests/testthat/test-16parentChain_integration_testthat.R b/tests/testthat/test-16parentChain_integration_testthat.R new file mode 100644 index 00000000..97aa975c --- /dev/null +++ b/tests/testthat/test-16parentChain_integration_testthat.R @@ -0,0 +1,81 @@ +test_that("parentChain shows in 'not on CRAN' message for deps of a local package", { + # Integration test for the parentChain feature (Issue: show why a package is needed). + # + # Strategy: + # - Build a minimal dummypkg_1.0.tar.gz with `Imports: pryr` in its DESCRIPTION. + # - Place the tarball in the Require package cache dir for the CRAN repos. + # identifyLocalFiles() scans that dir, so Require will find it there and read + # its DESCRIPTION without needing the package to be on CRAN or installed. + # - Call pkgDep("dummypkg"). The flow: + # 1. pkgDepCRAN sees dummypkg not on CRAN → Archive path + # 2. getArchiveDESCRIPTION → identifyLocalFiles finds dummypkg_1.0.tar.gz + # 3. Extracts DESCRIPTION → reads Imports: pryr + # 4. Recurses: pkgDepCRAN("pryr", parentChain = "dummypkg") + # 5. pryr is archived → message includes "(required by: dummypkg)" + # + # pryr was removed from CRAN and lives only in the CRAN archive. + + skip_if_offline2() + setupInitial <- setupTest() + + repos <- "https://cloud.r-project.org" + td <- Require:::tempdir2("test_parentChain_integration") + on.exit(unlink(td, recursive = TRUE), add = TRUE) + + # --- 1. Build dummypkg_1.0.tar.gz --- + pkgname <- "dummypkgwithpryr" + ver <- "1.0" + tarname <- paste0(pkgname, "_", ver, ".tar.gz") + + srcDir <- file.path(td, "src") + pkgDir <- file.path(srcDir, pkgname) + dir.create(pkgDir, recursive = TRUE) + writeLines(c( + paste0("Package: ", pkgname), + paste0("Version: ", ver), + "Title: Dummy package for testing parentChain messaging", + "Description: Imports pryr so that pkgDep will hit the archived-CRAN path for pryr.", + "Imports: pryr", + "License: GPL-3" + ), file.path(pkgDir, "DESCRIPTION")) + + tarfile <- file.path(td, tarname) + withr::with_dir(srcDir, + utils::tar(tarfile, files = pkgname, compression = "gzip", tar = "internal")) + + # --- 2. Place tarball in the Require cache for the repos --- + cacheDir <- Require:::cachePkgDirForRepo(repos, create = TRUE) + file.copy(tarfile, file.path(cacheDir, tarname)) + + # --- 3. Run pkgDep and capture messages --- + msgs <- character(0) + withCallingHandlers( + tryCatch( + pkgDep(pkgname, + repos = repos, + verbose = 1, + recursive = TRUE), # recursive=TRUE so pryr is processed and its "not on CRAN" msg is emitted + error = function(e) NULL # swallow downstream errors (e.g. network failures for pryr archive) + ), + message = function(m) { + msgs <<- c(msgs, conditionMessage(m)) + invokeRestart("muffleMessage") + } + ) + + # --- 4. Assert the dependency chain appears in the message --- + # Use word-boundary pattern so "dummypkgwithpryr" (which contains "pryr") is not matched. + not_on_cran_msgs <- msgs[grepl("not on CRAN", msgs, fixed = TRUE)] + pryr_not_on_cran <- not_on_cran_msgs[grepl("\\bpryr\\b", not_on_cran_msgs)] + + testthat::expect_true( + length(pryr_not_on_cran) > 0, + info = paste("Expected a 'pryr ... not on CRAN' message. Messages captured:\n", + paste(msgs, collapse = "\n")) + ) + testthat::expect_true( + any(grepl(paste0("required by: ", pkgname), pryr_not_on_cran, fixed = TRUE)), + info = paste("Expected '(required by:", pkgname, ")' in the pryr 'not on CRAN' message. Got:\n", + paste(pryr_not_on_cran, collapse = "\n")) + ) +}) From 00524dd21ebf548848033ed135e79fc7649c6dd4 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 2 Apr 2026 11:34:42 -0700 Subject: [PATCH 2/3] Add integration test for parentChain dep chain messaging; fix file:// repos - Integration test: builds a minimal file:// CRAN repo containing dummypkgwithpryr (Imports: pryr), calls pkgDep(), and asserts that the 'not on CRAN' message for pryr includes '(required by: dummypkgwithpryr)' - Bug fix in available.packagesCached: was prepending 'https://' to any repo not starting with 'http', which corrupted file:// URLs. Fixed to only prepend to bare domain names (no protocol). Co-Authored-By: Claude Sonnet 4.6 --- R/Require-helpers.R | 2 +- .../test-16parentChain_integration_testthat.R | 67 +++++++++++-------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/R/Require-helpers.R b/R/Require-helpers.R index 910c4232..3f96ca3e 100644 --- a/R/Require-helpers.R +++ b/R/Require-helpers.R @@ -451,7 +451,7 @@ available.packagesCached <- function(repos, purge, verbose = getOption("Require. types <- type } - missingHttp <- !startsWith(unlist(repos), "http") + missingHttp <- !grepl("^[a-z]+://", unlist(repos)) # only bare domain names, not file:// or other protocols if (any(missingHttp)) { repos[missingHttp] <- lapply(repos[missingHttp], function(r) { paste0("https://", r) diff --git a/tests/testthat/test-16parentChain_integration_testthat.R b/tests/testthat/test-16parentChain_integration_testthat.R index 97aa975c..5523fc3a 100644 --- a/tests/testthat/test-16parentChain_integration_testthat.R +++ b/tests/testthat/test-16parentChain_integration_testthat.R @@ -1,32 +1,27 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local package", { - # Integration test for the parentChain feature (Issue: show why a package is needed). + # Integration test for the parentChain feature. # - # Strategy: - # - Build a minimal dummypkg_1.0.tar.gz with `Imports: pryr` in its DESCRIPTION. - # - Place the tarball in the Require package cache dir for the CRAN repos. - # identifyLocalFiles() scans that dir, so Require will find it there and read - # its DESCRIPTION without needing the package to be on CRAN or installed. - # - Call pkgDep("dummypkg"). The flow: - # 1. pkgDepCRAN sees dummypkg not on CRAN → Archive path - # 2. getArchiveDESCRIPTION → identifyLocalFiles finds dummypkg_1.0.tar.gz - # 3. Extracts DESCRIPTION → reads Imports: pryr - # 4. Recurses: pkgDepCRAN("pryr", parentChain = "dummypkg") - # 5. pryr is archived → message includes "(required by: dummypkg)" + # Strategy: create a minimal file:// CRAN-like repo containing dummypkgwithpryr + # (which lists Imports: pryr). When pkgDep() queries available.packages() it finds + # dummypkgwithpryr there, reads Imports: pryr from the PACKAGES index, then recurses + # for pryr. pryr is archived on real CRAN, so pkgDepCRAN prints: + # "pryr (required by: dummypkgwithpryr) not on CRAN; checking CRAN archives ..." # - # pryr was removed from CRAN and lives only in the CRAN archive. + # Using a file:// repo avoids relying on cache-scanning (identifyLocalFiles), which + # has platform-specific behaviour, and keeps the test self-contained except for the + # final CRAN-archive lookup for pryr itself. skip_if_offline2() setupInitial <- setupTest() - repos <- "https://cloud.r-project.org" td <- Require:::tempdir2("test_parentChain_integration") on.exit(unlink(td, recursive = TRUE), add = TRUE) - # --- 1. Build dummypkg_1.0.tar.gz --- pkgname <- "dummypkgwithpryr" - ver <- "1.0" + ver <- "1.0" tarname <- paste0(pkgname, "_", ver, ".tar.gz") + # ── 1. Build dummypkgwithpryr_1.0.tar.gz ────────────────────────────────────── srcDir <- file.path(td, "src") pkgDir <- file.path(srcDir, pkgname) dir.create(pkgDir, recursive = TRUE) @@ -34,7 +29,7 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local packag paste0("Package: ", pkgname), paste0("Version: ", ver), "Title: Dummy package for testing parentChain messaging", - "Description: Imports pryr so that pkgDep will hit the archived-CRAN path for pryr.", + "Description: Imports pryr to exercise the archived-CRAN parentChain message.", "Imports: pryr", "License: GPL-3" ), file.path(pkgDir, "DESCRIPTION")) @@ -43,19 +38,33 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local packag withr::with_dir(srcDir, utils::tar(tarfile, files = pkgname, compression = "gzip", tar = "internal")) - # --- 2. Place tarball in the Require cache for the repos --- - cacheDir <- Require:::cachePkgDirForRepo(repos, create = TRUE) - file.copy(tarfile, file.path(cacheDir, tarname)) + # ── 2. Build a minimal file:// CRAN-like repo ───────────────────────────────── + # Layout: repoDir/src/contrib/PACKAGES (generated by tools::write_PACKAGES) + # repoDir/src/contrib/dummypkgwithpryr_1.0.tar.gz + repoDir <- file.path(td, "repo") + contribs <- file.path(repoDir, "src", "contrib") + dir.create(contribs, recursive = TRUE) + file.copy(tarfile, file.path(contribs, tarname)) + + # write_PACKAGES reads the tarball's DESCRIPTION to build the index + tools::write_PACKAGES(contribs, type = "source", verbose = FALSE) + + fileRepo <- paste0("file:///", normalizePath(repoDir, winslash = "/", mustWork = FALSE)) + + # ── 3. Run pkgDep with the file repo first, then CRAN for pryr ──────────────── + # repos order: file repo (finds dummypkgwithpryr) then CRAN (finds pryr is archived) + repos <- c(fileRepo, "https://cloud.r-project.org") - # --- 3. Run pkgDep and capture messages --- msgs <- character(0) withCallingHandlers( tryCatch( pkgDep(pkgname, - repos = repos, - verbose = 1, - recursive = TRUE), # recursive=TRUE so pryr is processed and its "not on CRAN" msg is emitted - error = function(e) NULL # swallow downstream errors (e.g. network failures for pryr archive) + repos = repos, + type = "source", # file repo only has src/contrib; binary lookup would find nothing + verbose = 1, + recursive = TRUE, + purge = TRUE), # purge so the file repo is queried fresh + error = function(e) NULL # swallow downstream errors after the message fires ), message = function(m) { msgs <<- c(msgs, conditionMessage(m)) @@ -63,9 +72,9 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local packag } ) - # --- 4. Assert the dependency chain appears in the message --- - # Use word-boundary pattern so "dummypkgwithpryr" (which contains "pryr") is not matched. + # ── 4. Assert the dependency chain appears in the "not on CRAN" message ──────── not_on_cran_msgs <- msgs[grepl("not on CRAN", msgs, fixed = TRUE)] + # Use word-boundary so "dummypkgwithpryr" (contains "pryr") is not matched pryr_not_on_cran <- not_on_cran_msgs[grepl("\\bpryr\\b", not_on_cran_msgs)] testthat::expect_true( @@ -75,7 +84,7 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local packag ) testthat::expect_true( any(grepl(paste0("required by: ", pkgname), pryr_not_on_cran, fixed = TRUE)), - info = paste("Expected '(required by:", pkgname, ")' in the pryr 'not on CRAN' message. Got:\n", - paste(pryr_not_on_cran, collapse = "\n")) + info = paste0("Expected '(required by: ", pkgname, ")' in the pryr 'not on CRAN' ", + "message. Got:\n", paste(pryr_not_on_cran, collapse = "\n")) ) }) From 99ad96520adabec9b81c9ec03750c964cb0a1406 Mon Sep 17 00:00:00 2001 From: Eliot McIntire Date: Thu, 2 Apr 2026 13:21:32 -0700 Subject: [PATCH 3/3] Switch integration test to local_mocked_bindings approach Avoid file:// CRAN repo (which triggers offline mode on R 4.2 Windows). Instead mock joinToAvailablePackages to inject VersionOnRepos + Imports for dummypkgwithpryr, letting the full recursive pkgDep code path run without any file or network access for the dummy package itself. Co-Authored-By: Claude Sonnet 4.6 --- .../test-16parentChain_integration_testthat.R | 85 ++++++++----------- 1 file changed, 34 insertions(+), 51 deletions(-) diff --git a/tests/testthat/test-16parentChain_integration_testthat.R b/tests/testthat/test-16parentChain_integration_testthat.R index 5523fc3a..f50c8bb8 100644 --- a/tests/testthat/test-16parentChain_integration_testthat.R +++ b/tests/testthat/test-16parentChain_integration_testthat.R @@ -1,70 +1,54 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local package", { # Integration test for the parentChain feature. # - # Strategy: create a minimal file:// CRAN-like repo containing dummypkgwithpryr - # (which lists Imports: pryr). When pkgDep() queries available.packages() it finds - # dummypkgwithpryr there, reads Imports: pryr from the PACKAGES index, then recurses - # for pryr. pryr is archived on real CRAN, so pkgDepCRAN prints: - # "pryr (required by: dummypkgwithpryr) not on CRAN; checking CRAN archives ..." + # We mock joinToAvailablePackages so it returns dummypkgwithpryr as a current-CRAN + # package (VersionOnRepos = "1.0", Imports = "pryr"). This avoids any file:// or + # network-reliability issues while still exercising the full recursive code path: + # getPkgDeps -> getDeps -> getDepsNonGH -> pkgDepCRAN -> (recurse for pryr) + # -> pkgDepCRAN("pryr", parentChain="dummypkgwithpryr") + # -> "pryr (required by: dummypkgwithpryr) not on CRAN; checking CRAN archives" # - # Using a file:// repo avoids relying on cache-scanning (identifyLocalFiles), which - # has platform-specific behaviour, and keeps the test self-contained except for the - # final CRAN-archive lookup for pryr itself. + # pryr is a real archived-CRAN package, so the final "not on CRAN" check is live. skip_if_offline2() setupInitial <- setupTest() - td <- Require:::tempdir2("test_parentChain_integration") - on.exit(unlink(td, recursive = TRUE), add = TRUE) - pkgname <- "dummypkgwithpryr" - ver <- "1.0" - tarname <- paste0(pkgname, "_", ver, ".tar.gz") - - # ── 1. Build dummypkgwithpryr_1.0.tar.gz ────────────────────────────────────── - srcDir <- file.path(td, "src") - pkgDir <- file.path(srcDir, pkgname) - dir.create(pkgDir, recursive = TRUE) - writeLines(c( - paste0("Package: ", pkgname), - paste0("Version: ", ver), - "Title: Dummy package for testing parentChain messaging", - "Description: Imports pryr to exercise the archived-CRAN parentChain message.", - "Imports: pryr", - "License: GPL-3" - ), file.path(pkgDir, "DESCRIPTION")) - - tarfile <- file.path(td, tarname) - withr::with_dir(srcDir, - utils::tar(tarfile, files = pkgname, compression = "gzip", tar = "internal")) - - # ── 2. Build a minimal file:// CRAN-like repo ───────────────────────────────── - # Layout: repoDir/src/contrib/PACKAGES (generated by tools::write_PACKAGES) - # repoDir/src/contrib/dummypkgwithpryr_1.0.tar.gz - repoDir <- file.path(td, "repo") - contribs <- file.path(repoDir, "src", "contrib") - dir.create(contribs, recursive = TRUE) - file.copy(tarfile, file.path(contribs, tarname)) + repos <- "https://cloud.r-project.org" - # write_PACKAGES reads the tarball's DESCRIPTION to build the index - tools::write_PACKAGES(contribs, type = "source", verbose = FALSE) - - fileRepo <- paste0("file:///", normalizePath(repoDir, winslash = "/", mustWork = FALSE)) - - # ── 3. Run pkgDep with the file repo first, then CRAN for pryr ──────────────── - # repos order: file repo (finds dummypkgwithpryr) then CRAN (finds pryr is archived) - repos <- c(fileRepo, "https://cloud.r-project.org") + # Mock joinToAvailablePackages: for dummypkgwithpryr, inject VersionOnRepos + Imports + # so pkgDepCRAN treats it as a current-CRAN package whose DESCRIPTION we already have. + # For all other packages (e.g. pryr), call the real function. + real_join <- Require:::joinToAvailablePackages + testthat::local_mocked_bindings( + joinToAvailablePackages = function(pkgDT, repos, type, which, verbose) { + out <- real_join(pkgDT, repos, type, which, verbose) + isDummy <- out$Package %in% pkgname + if (any(isDummy)) { + data.table::set(out, which(isDummy), "VersionOnRepos", "1.0") + data.table::set(out, which(isDummy), "Repository", + "https://cloud.r-project.org") + # Inject Imports so assignPkgDTtoSaveNames discovers pryr as a dep + for (col in which) { + if (is.null(out[[col]])) + data.table::set(out, NULL, col, NA_character_) + } + data.table::set(out, which(isDummy), "Imports", "pryr") + } + out + }, + .package = "Require" + ) msgs <- character(0) withCallingHandlers( tryCatch( pkgDep(pkgname, repos = repos, - type = "source", # file repo only has src/contrib; binary lookup would find nothing verbose = 1, recursive = TRUE, - purge = TRUE), # purge so the file repo is queried fresh - error = function(e) NULL # swallow downstream errors after the message fires + purge = TRUE), + error = function(e) NULL ), message = function(m) { msgs <<- c(msgs, conditionMessage(m)) @@ -72,9 +56,8 @@ test_that("parentChain shows in 'not on CRAN' message for deps of a local packag } ) - # ── 4. Assert the dependency chain appears in the "not on CRAN" message ──────── + # Word-boundary grep so "dummypkgwithpryr" (which contains "pryr") is not matched not_on_cran_msgs <- msgs[grepl("not on CRAN", msgs, fixed = TRUE)] - # Use word-boundary so "dummypkgwithpryr" (contains "pryr") is not matched pryr_not_on_cran <- not_on_cran_msgs[grepl("\\bpryr\\b", not_on_cran_msgs)] testthat::expect_true(