From 9c07c3c2a4c83ce6da867e22e2d720f18f59ad6f Mon Sep 17 00:00:00 2001 From: zdz2101 Date: Thu, 14 May 2026 11:06:47 -0700 Subject: [PATCH 1/3] test: add gh-mocked coverage for snapshot helpers Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/utils-github.R | 63 +++++---- R/utils-workflows.R | 33 ++--- tests/testthat/test-snapshot-gh-mocks.R | 167 ++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 52 deletions(-) create mode 100644 tests/testthat/test-snapshot-gh-mocks.R diff --git a/R/utils-github.R b/R/utils-github.R index f7d5fa6..feebfe9 100644 --- a/R/utils-github.R +++ b/R/utils-github.R @@ -55,6 +55,12 @@ resolve_package <- function(org, repo, ref = NULL, date = NULL) { ) } +#' Execute a gh api call and return stdout/stderr lines +#' @keywords internal +gh_api <- function(args) { + system2("gh", args, stdout = TRUE, stderr = TRUE) +} + #' Resolve a ref by date — find the latest release/tag on or before the given date #' @@ -67,31 +73,27 @@ resolve_ref_by_date <- function(org, repo, date = NULL) { if (is.null(date)) { # Get latest release tag - result <- system2( - "gh", c("api", paste0("repos/", full_repo, "/releases/latest"), "--jq", ".tag_name"), - stdout = TRUE, stderr = TRUE + result <- gh_api( + c("api", paste0("repos/", full_repo, "/releases/latest"), "--jq", ".tag_name") ) if (length(result) > 0 && !grepl("Not Found", result[1])) { return(trimws(result[1])) } # No releases — fall back to default branch - result <- system2( - "gh", c("api", paste0("repos/", full_repo), "--jq", ".default_branch"), - stdout = TRUE, stderr = TRUE + result <- gh_api( + c("api", paste0("repos/", full_repo), "--jq", ".default_branch") ) return(trimws(result[1])) } # Get all release tag names and dates separately - tags <- system2( - "gh", c("api", paste0("repos/", full_repo, "/releases"), "--paginate", - "--jq", ".[].tag_name"), - stdout = TRUE, stderr = TRUE + tags <- gh_api( + c("api", paste0("repos/", full_repo, "/releases"), "--paginate", + "--jq", ".[].tag_name") ) - dates <- system2( - "gh", c("api", paste0("repos/", full_repo, "/releases"), "--paginate", - "--jq", ".[].published_at"), - stdout = TRUE, stderr = TRUE + dates <- gh_api( + c("api", paste0("repos/", full_repo, "/releases"), "--paginate", + "--jq", ".[].published_at") ) if (length(tags) == 0 || all(grepl("Not Found", tags))) { @@ -127,10 +129,9 @@ resolve_tag_by_date <- function(org, repo, date) { full_repo <- paste0(org, "/", repo) # Get tags with their commit dates - result <- system2( - "gh", c("api", paste0("repos/", full_repo, "/tags"), "--paginate", - "--jq", ".[].name"), - stdout = TRUE, stderr = TRUE + result <- gh_api( + c("api", paste0("repos/", full_repo, "/tags"), "--paginate", + "--jq", ".[].name") ) if (length(result) == 0) { @@ -139,16 +140,14 @@ resolve_tag_by_date <- function(org, repo, date) { # For each tag, get the commit date tag_dates <- lapply(result, function(tag) { - commit_info <- system2( - "gh", c("api", paste0("repos/", full_repo, "/git/ref/tags/", tag), - "--jq", ".object.sha"), - stdout = TRUE, stderr = TRUE + commit_info <- gh_api( + c("api", paste0("repos/", full_repo, "/git/ref/tags/", tag), + "--jq", ".object.sha") ) sha <- trimws(commit_info[1]) - commit_date_str <- system2( - "gh", c("api", paste0("repos/", full_repo, "/commits/", sha), - "--jq", ".commit.committer.date"), - stdout = TRUE, stderr = TRUE + commit_date_str <- gh_api( + c("api", paste0("repos/", full_repo, "/commits/", sha), + "--jq", ".commit.committer.date") ) list(tag = tag, date = as.Date(substr(trimws(commit_date_str[1]), 1, 10))) }) @@ -172,9 +171,8 @@ resolve_tag_by_date <- function(org, repo, date) { #' @return Character. The full commit SHA. gh_get_sha <- function(org, repo, ref) { full_repo <- paste0(org, "/", repo) - result <- system2( - "gh", c("api", paste0("repos/", full_repo, "/commits/", ref), "--jq", ".sha"), - stdout = TRUE, stderr = TRUE + result <- gh_api( + c("api", paste0("repos/", full_repo, "/commits/", ref), "--jq", ".sha") ) if (length(result) == 0 || grepl("Not Found", result[1])) { stop("Could not resolve ref '", ref, "' for ", full_repo) @@ -190,10 +188,9 @@ gh_get_sha <- function(org, repo, ref) { #' @return Character. The package version string. gh_get_version <- function(org, repo, sha) { full_repo <- paste0(org, "/", repo) - result <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/DESCRIPTION?ref=", sha), - "--jq", ".content"), - stdout = TRUE, stderr = TRUE + result <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/DESCRIPTION?ref=", sha), + "--jq", ".content") ) if (length(result) == 0 || grepl("Not Found", result[1])) { diff --git a/R/utils-workflows.R b/R/utils-workflows.R index 6ca322f..3ad4385 100644 --- a/R/utils-workflows.R +++ b/R/utils-workflows.R @@ -32,9 +32,8 @@ pull_workflows <- function(resolved, path) { #' List contents of a GitHub directory, returning name/type/path for each entry #' @keywords internal gh_list_contents <- function(full_repo, dir_path, sha) { - json_str <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha)), - stdout = TRUE, stderr = TRUE + json_str <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha)) ) if (length(json_str) == 0 || any(grepl("Not Found", json_str))) { @@ -48,20 +47,17 @@ gh_list_contents <- function(full_repo, dir_path, sha) { # problematic with escaping, parse the raw JSON with a simple approach # We'll re-call gh with separate jq queries for each field - names <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), - "--jq", ".[].name"), - stdout = TRUE, stderr = TRUE + names <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), + "--jq", ".[].name") ) - types <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), - "--jq", ".[].type"), - stdout = TRUE, stderr = TRUE + types <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), + "--jq", ".[].type") ) - paths <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), - "--jq", ".[].path"), - stdout = TRUE, stderr = TRUE + paths <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), + "--jq", ".[].path") ) if (length(names) == 0) return(NULL) @@ -93,10 +89,9 @@ pull_workflow_dir <- function(full_repo, sha, api_path, local_dir) { #' Pull a single workflow file from GitHub #' @keywords internal pull_workflow_file <- function(full_repo, sha, api_path, local_path) { - file_content <- system2( - "gh", c("api", paste0("repos/", full_repo, "/contents/", api_path, "?ref=", sha), - "--jq", ".content"), - stdout = TRUE, stderr = TRUE + file_content <- gh_api( + c("api", paste0("repos/", full_repo, "/contents/", api_path, "?ref=", sha), + "--jq", ".content") ) if (length(file_content) > 0 && !any(grepl("Not Found", file_content))) { diff --git a/tests/testthat/test-snapshot-gh-mocks.R b/tests/testthat/test-snapshot-gh-mocks.R new file mode 100644 index 0000000..66506bd --- /dev/null +++ b/tests/testthat/test-snapshot-gh-mocks.R @@ -0,0 +1,167 @@ +test_that("resolve_package resolves explicit refs via mocked gh api", { + local_mocked_bindings( + gh_api = function(args) { + cmd <- paste(args, collapse = " ") + if (grepl("/commits/v1.2.3", cmd, fixed = TRUE) && grepl(".sha", cmd, fixed = TRUE)) { + return("abc123") + } + if (grepl("contents/DESCRIPTION?ref=abc123", cmd, fixed = TRUE) && grepl(".content", cmd, fixed = TRUE)) { + desc <- "Package: gsm.core\nVersion: 1.9.0\n" + return(base64enc::base64encode(charToRaw(desc))) + } + stop("Unexpected gh_api args: ", cmd) + }, + .package = "workr" + ) + + out <- workr:::resolve_package("Gilead-BioStats", "gsm.core", ref = "v1.2.3") + expect_equal(out$org, "Gilead-BioStats") + expect_equal(out$repo, "gsm.core") + expect_equal(out$ref, "v1.2.3") + expect_equal(out$sha, "abc123") + expect_equal(out$version, "1.9.0") +}) + +test_that("resolve_package uses date-based fallback when ref is not provided", { + local_mocked_bindings( + gh_api = function(args) { + cmd <- paste(args, collapse = " ") + if (grepl("/releases --paginate --jq .\\[\\]\\.tag_name", cmd)) { + return(c("v1.0.0", "v1.1.0")) + } + if (grepl("/releases --paginate --jq .\\[\\]\\.published_at", cmd)) { + return(c("2024-01-15T00:00:00Z", "2024-06-15T00:00:00Z")) + } + if (grepl("/commits/v1.0.0", cmd, fixed = TRUE) && grepl(".sha", cmd, fixed = TRUE)) { + return("sha-v1-0-0") + } + if (grepl("contents/DESCRIPTION?ref=sha-v1-0-0", cmd, fixed = TRUE) && grepl(".content", cmd, fixed = TRUE)) { + desc <- "Package: gsm.core\nVersion: 1.0.0\n" + return(base64enc::base64encode(charToRaw(desc))) + } + stop("Unexpected gh_api args: ", cmd) + }, + .package = "workr" + ) + + out <- workr:::resolve_package( + org = "Gilead-BioStats", + repo = "gsm.core", + ref = NULL, + date = as.Date("2024-05-01") + ) + + expect_equal(out$ref, "v1.0.0") + expect_equal(out$sha, "sha-v1-0-0") + expect_equal(out$version, "1.0.0") +}) + +test_that("resolve_package errors on malformed gh commit lookup response", { + local_mocked_bindings( + gh_api = function(args) { + cmd <- paste(args, collapse = " ") + if (grepl("/commits/dev", cmd, fixed = TRUE) && grepl(".sha", cmd, fixed = TRUE)) { + return("Not Found") + } + stop("Unexpected gh_api args: ", cmd) + }, + .package = "workr" + ) + + expect_error( + workr:::resolve_package("Gilead-BioStats", "gsm.core", ref = "dev"), + "Could not resolve ref" + ) +}) + +test_that("gh_list_contents parses directory entries from mocked gh api", { + local_mocked_bindings( + gh_api = function(args) { + cmd <- paste(args, collapse = " ") + if (!("--jq" %in% args)) { + return("[]") + } + if (grepl(".[].name", cmd, fixed = TRUE)) { + return(c("0_other", "foo.yaml")) + } + if (grepl(".[].type", cmd, fixed = TRUE)) { + return(c("dir", "file")) + } + if (grepl(".[].path", cmd, fixed = TRUE)) { + return(c("inst/workflow/0_other", "inst/workflow/foo.yaml")) + } + stop("Unexpected gh_api args: ", cmd) + }, + .package = "workr" + ) + + entries <- workr:::gh_list_contents("Gilead-BioStats/gsm.core", "inst/workflow", "abc123") + expect_length(entries, 2) + expect_equal(entries[[1]]$name, "0_other") + expect_equal(entries[[1]]$type, "dir") + expect_equal(entries[[2]]$name, "foo.yaml") + expect_equal(entries[[2]]$type, "file") +}) + +test_that("pull_workflows skips packages with no workflow directory", { + tmp <- tempfile("workr-pull-workflows-") + dir.create(tmp, recursive = TRUE) + on.exit(unlink(tmp, recursive = TRUE), add = TRUE) + + local_mocked_bindings( + gh_list_contents = function(full_repo, dir_path, sha) { + NULL + }, + .package = "workr" + ) + + resolved <- list(list(org = "Gilead-BioStats", repo = "gsm.core", sha = "abc123")) + expect_message( + workr:::pull_workflows(resolved, tmp), + "No inst/workflow found" + ) + expect_true(dir.exists(file.path(tmp, "workflows"))) +}) + +test_that("pull_workflows dispatches file and directory entries", { + tmp <- tempfile("workr-pull-workflows-") + dir.create(tmp, recursive = TRUE) + on.exit(unlink(tmp, recursive = TRUE), add = TRUE) + + dir_calls <- character() + file_calls <- character() + + local_mocked_bindings( + gh_list_contents = function(full_repo, dir_path, sha) { + list( + list(type = "dir", name = "0_other", path = "inst/workflow/0_other"), + list(type = "file", name = "root.yaml", path = "inst/workflow/root.yaml") + ) + }, + pull_workflow_dir = function(full_repo, sha, api_path, local_dir) { + dir_calls <<- c(dir_calls, paste(full_repo, sha, api_path, local_dir, sep = "|")) + invisible(NULL) + }, + pull_workflow_file = function(full_repo, sha, api_path, local_path) { + file_calls <<- c(file_calls, paste(full_repo, sha, api_path, local_path, sep = "|")) + invisible(NULL) + }, + .package = "workr" + ) + + resolved <- list(list(org = "Gilead-BioStats", repo = "gsm.core", sha = "abc123")) + workr:::pull_workflows(resolved, tmp) + + expect_length(dir_calls, 1) + expect_length(file_calls, 1) + dir_parts <- strsplit(dir_calls[[1]], "|", fixed = TRUE)[[1]] + file_parts <- strsplit(file_calls[[1]], "|", fixed = TRUE)[[1]] + expect_equal(dir_parts[1], "Gilead-BioStats/gsm.core") + expect_equal(dir_parts[2], "abc123") + expect_equal(dir_parts[3], "inst/workflow/0_other") + expect_equal(basename(dir_parts[4]), "0_other") + expect_equal(file_parts[1], "Gilead-BioStats/gsm.core") + expect_equal(file_parts[2], "abc123") + expect_equal(file_parts[3], "inst/workflow/root.yaml") + expect_equal(basename(file_parts[4]), "root.yaml") +}) From dd498e0edcac1a9d9a036f883f2c106f65686434 Mon Sep 17 00:00:00 2001 From: zdz2101 Date: Fri, 15 May 2026 08:34:55 -0700 Subject: [PATCH 2/3] Add snapshot orchestration and gh fallback tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/utils-workflows.R | 8 +- tests/testthat/test-snapshot-gh-mocks.R | 105 ++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 7 deletions(-) diff --git a/R/utils-workflows.R b/R/utils-workflows.R index 3ad4385..a7a3753 100644 --- a/R/utils-workflows.R +++ b/R/utils-workflows.R @@ -40,13 +40,7 @@ gh_list_contents <- function(full_repo, dir_path, sha) { return(NULL) } - # Minimal JSON parsing — extract name, type, path from each object - json_text <- paste0(json_str, collapse = "\n") - - # Use R's built-in JSON-ish parsing via the gh CLI --jq, but since that's - - # problematic with escaping, parse the raw JSON with a simple approach - # We'll re-call gh with separate jq queries for each field + # Re-call gh with separate jq queries for each field. names <- gh_api( c("api", paste0("repos/", full_repo, "/contents/", dir_path, "?ref=", sha), "--jq", ".[].name") diff --git a/tests/testthat/test-snapshot-gh-mocks.R b/tests/testthat/test-snapshot-gh-mocks.R index 66506bd..7a72383 100644 --- a/tests/testthat/test-snapshot-gh-mocks.R +++ b/tests/testthat/test-snapshot-gh-mocks.R @@ -74,6 +74,25 @@ test_that("resolve_package errors on malformed gh commit lookup response", { ) }) +test_that("resolve_ref_by_date falls back to default branch when latest release is missing", { + local_mocked_bindings( + gh_api = function(args) { + cmd <- paste(args, collapse = " ") + if (grepl("/releases/latest", cmd, fixed = TRUE) && grepl(".tag_name", cmd, fixed = TRUE)) { + return("Not Found") + } + if (grepl("repos/Gilead-BioStats/gsm.core --jq .default_branch", cmd, fixed = TRUE)) { + return("main") + } + stop("Unexpected gh_api args: ", cmd) + }, + .package = "workr" + ) + + out <- workr:::resolve_ref_by_date("Gilead-BioStats", "gsm.core", date = NULL) + expect_equal(out, "main") +}) + test_that("gh_list_contents parses directory entries from mocked gh api", { local_mocked_bindings( gh_api = function(args) { @@ -103,6 +122,49 @@ test_that("gh_list_contents parses directory entries from mocked gh api", { expect_equal(entries[[2]]$type, "file") }) +test_that("pkgManifest writes manifest outputs with mocked GitHub resolution", { + tmp <- tempfile("workr-pkg-manifest-") + dir.create(tmp, recursive = TRUE) + on.exit(unlink(tmp, recursive = TRUE), add = TRUE) + + resolve_calls <- list() + + local_mocked_bindings( + resolve_package = function(org, repo, ref = NULL, date = NULL) { + resolve_calls <<- append(resolve_calls, list(list(org = org, repo = repo, ref = ref, date = date))) + list( + org = org, + repo = repo, + version = "1.0.0", + repository = paste0("https://github.com/", org, "/", repo), + url = paste0("https://github.com/", org, "/", repo, "/archive/sha-", repo, ".tar.gz"), + sha = paste0("sha-", repo), + ref = ref + ) + }, + pull_workflows = function(resolved, path) { + dir.create(file.path(path, "workflows"), recursive = TRUE, showWarnings = FALSE) + writeLines("name: mocked-workflow", file.path(path, "workflows", "root.yaml")) + invisible(NULL) + }, + .package = "workr" + ) + + out <- workr::pkgManifest( + path = tmp, + packageList = c("Gilead-BioStats/gsm.core@v1.2.3", "Gilead-BioStats/gsm.mapping"), + branch = "dev" + ) + + expect_true(file.exists(file.path(tmp, "manifest.csv"))) + expect_true(file.exists(file.path(tmp, "rproject.toml"))) + expect_true(file.exists(file.path(tmp, "workflows", "root.yaml"))) + expect_equal(nrow(out), 2) + expect_equal(out$package, c("gsm.core", "gsm.mapping")) + expect_equal(resolve_calls[[1]]$ref, "v1.2.3") + expect_equal(resolve_calls[[2]]$ref, "dev") +}) + test_that("pull_workflows skips packages with no workflow directory", { tmp <- tempfile("workr-pull-workflows-") dir.create(tmp, recursive = TRUE) @@ -165,3 +227,46 @@ test_that("pull_workflows dispatches file and directory entries", { expect_equal(file_parts[3], "inst/workflow/root.yaml") expect_equal(basename(file_parts[4]), "root.yaml") }) + +test_that("pull_workflow_file skips write when content lookup is Not Found", { + tmp <- tempfile("workr-pull-workflow-file-") + on.exit(unlink(tmp, force = TRUE), add = TRUE) + + local_mocked_bindings( + gh_api = function(args) { + "Not Found" + }, + .package = "workr" + ) + + workr:::pull_workflow_file( + full_repo = "Gilead-BioStats/gsm.core", + sha = "abc123", + api_path = "inst/workflow/root.yaml", + local_path = tmp + ) + + expect_false(file.exists(tmp)) +}) + +test_that("pull_workflow_file writes decoded file when content is available", { + tmp <- tempfile("workr-pull-workflow-file-") + on.exit(unlink(tmp, force = TRUE), add = TRUE) + + local_mocked_bindings( + gh_api = function(args) { + base64enc::base64encode(charToRaw("name: workflow\n")) + }, + .package = "workr" + ) + + workr:::pull_workflow_file( + full_repo = "Gilead-BioStats/gsm.core", + sha = "abc123", + api_path = "inst/workflow/root.yaml", + local_path = tmp + ) + + expect_true(file.exists(tmp)) + expect_identical(readLines(tmp)[1], "name: workflow") +}) From da94717d433ab511b32ee6842d967b1e1f81912c Mon Sep 17 00:00:00 2001 From: zdz2101 Date: Fri, 15 May 2026 08:46:34 -0700 Subject: [PATCH 3/3] Mark gh_api roxygen block noRd Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- R/utils-github.R | 1 + 1 file changed, 1 insertion(+) diff --git a/R/utils-github.R b/R/utils-github.R index feebfe9..ff80792 100644 --- a/R/utils-github.R +++ b/R/utils-github.R @@ -57,6 +57,7 @@ resolve_package <- function(org, repo, ref = NULL, date = NULL) { #' Execute a gh api call and return stdout/stderr lines #' @keywords internal +#' @noRd gh_api <- function(args) { system2("gh", args, stdout = TRUE, stderr = TRUE) }