Skip to content

Commit af3d6d2

Browse files
committed
Download snapshots from GitHub
Don't see any obvious way to test this, but I tried it out a bunch interactively. Snapshot test deliberately failing so I can double check that the message is useful. Fixes #1779
1 parent 87d61c5 commit af3d6d2

File tree

8 files changed

+189
-8
lines changed

8 files changed

+189
-8
lines changed

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Suggests:
4040
curl (>= 0.9.5),
4141
diffviewer (>= 0.1.0),
4242
digest (>= 0.6.33),
43+
gh,
4344
knitr,
4445
rmarkdown,
4546
rstudioapi,

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# testthat (development version)
22

3+
* New `snapshot_download_gh()` makes it easy to get snapshots off GitHub and into your local package (#1779).
34
* `expect_snapshot_file(name=)` must have a unique file path. If a snapshot file attempts to be saved with a duplicate `name`, an error will be thrown. (#1592)
45
* `test_dir()`, `test_file()`, `test_package()`, `test_check()`, `test_local()`, `source_file()` gain a `shuffle` argument uses `sample()` to randomly reorder the top-level expressions in each test file (#1942). This random reordering surfaces dependencies between tests and code outside of any test, as well as dependencies between tests. This helps you find and eliminate unintentional dependencies.
56
* `snapshot_accept(test)` now works when the test file name contains `.` (#1669).

R/snapshot-file.R

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,31 @@ snapshot_review_hint <- function(
180180

181181
path <- paste0("tests/testthat/_snaps/", test, "/", new_name(name))
182182

183+
if (check) {
184+
gh_hint <- snap_download_hint()
185+
on_gh <- !is.null(gh_hint)
186+
187+
if (on_gh) {
188+
bullets <- gh_hint
189+
} else {
190+
bullets <- c(
191+
if (ci) "* Download and unzip run artifact\n",
192+
if (!ci) "* Locate check directory\n",
193+
paste0("* Copy '", path, "' to local test directory\n")
194+
)
195+
}
196+
} else {
197+
bullets <- NULL
198+
}
199+
183200
paste0(
184-
if (check && ci) "* Download and unzip run artifact\n",
185-
if (check && !ci) "* Locate check directory\n",
186-
if (check) paste0("* Copy '", path, "' to local test directory\n"),
187-
if (check) "* ",
188-
cli::format_inline(
189-
"Run {.run testthat::snapshot_review('{test}/')} to review changes"
190-
)
201+
c(
202+
bullets,
203+
cli::format_inline(
204+
"* Run {.run testthat::snapshot_review('{test}/')} to review changes"
205+
)
206+
),
207+
collapse = ""
191208
)
192209
}
193210

R/snapshot-github.R

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#' Download snapshots from GitHub
2+
#'
3+
#' @description
4+
#' If your snapshots fail on GitHub, it can be a pain to figure out exactly
5+
#' why, or to incporate them into your local package. This function makes it
6+
#' easy.
7+
#'
8+
#' Note that you should not generally need to fill out this function yourself;
9+
#' instead copy and paste from the hint emitted on GitHub.
10+
#'
11+
#' @param repository Repository name, e.g. `"r-lib/testthat"`.
12+
#' @param job_id Job ID, e.g. `"47905180716"`. You can find this in the job url.
13+
#' @param dest_dir Directory to download to. Defaults to the current directory.
14+
#' @export
15+
snapshot_download_gh <- function(repository, job_id, dest_dir = ".") {
16+
check_installed("gh")
17+
18+
dest_snaps <- file.path(dest_dir, "tests", "testthat", "_snaps")
19+
if (!dir.exists(dest_snaps)) {
20+
cli::cli_abort("No snapshot directory found in {.file {dest_dir}}.")
21+
}
22+
23+
artifact_id <- gh_find_artifact(repository, job_id)
24+
25+
path <- withr::local_tempfile(pattern = "gh-snaps-")
26+
gh_download_artifact(repository, artifact_id, path)
27+
28+
inner_dir <- dir(path, full.names = TRUE)[1]
29+
src_snaps <- file.path(inner_dir, "tests", "testthat", "_snaps")
30+
dir_copy(src_snaps, dest_snaps)
31+
}
32+
33+
snap_download_hint <- function() {
34+
repository <- Sys.getenv("GITHUB_REPOSITORY")
35+
job_id <- Sys.getenv("GITHUB_JOB")
36+
37+
if (repository == "" || job_id == "") {
38+
return()
39+
}
40+
41+
sprintf(
42+
"* Call `gh_download_snaps(\"%s\", %s)` to download the snapshots from GitHub.\n",
43+
repository,
44+
job_id
45+
)
46+
}
47+
48+
49+
gh_find_artifact <- function(repository, job_id) {
50+
job_logs <- gh::gh(
51+
"GET /repos/{repository}/actions/jobs/{job_id}/logs",
52+
repository = repository,
53+
job_id = job_id,
54+
.send_headers = c("Accept" = "application/vnd.github.v3+json")
55+
)
56+
57+
log_lines <- strsplit(job_logs$message, "\r?\n")[[1]]
58+
matches <- re_match(log_lines, "Artifact download URL: (?<artifact_url>.*)")
59+
matches <- matches[!is.na(matches$artifact_url), ]
60+
61+
if (!nrow(matches)) {
62+
cli::cli_abort("Failed to find artifact")
63+
}
64+
65+
# Take last artifact URL; if the job has failed the previous artifact will
66+
# be the R CMD check logs
67+
artifact_url <- matches$artifact_url[nrow(matches)]
68+
basename(artifact_url)
69+
}
70+
71+
gh_download_artifact <- function(repository, artifact_id, path) {
72+
zip_path <- withr::local_tempfile(pattern = "gh-zip-")
73+
gh::gh(
74+
"/repos/{repository}/actions/artifacts/{artifact_id}/{archive_format}",
75+
repository = repository,
76+
artifact_id = artifact_id,
77+
archive_format = "zip",
78+
.destfile = zip_path
79+
)
80+
unzip(zip_path, exdir = path)
81+
invisible(path)
82+
}
83+
84+
# Directory helpers ------------------------------------------------------------
85+
86+
dir_create <- function(paths) {
87+
for (path in paths) {
88+
dir.create(path, recursive = TRUE, showWarnings = FALSE)
89+
}
90+
invisible(paths)
91+
}
92+
93+
dir_copy <- function(src_dir, dst_dir) {
94+
# First create directories
95+
dirs <- list.dirs(src_dir, recursive = TRUE, full.names = FALSE)
96+
dir_create(file.path(dst_dir, dirs))
97+
98+
# Then copy files
99+
files <- dir(src_dir, recursive = TRUE)
100+
src_files <- file.path(src_dir, files)
101+
dst_files <- file.path(dst_dir, files)
102+
same <- map_lgl(seq_along(files), \(i) {
103+
same_file(src_files[[i]], dst_files[[i]])
104+
})
105+
106+
n_new <- sum(!same)
107+
if (n_new == 0) {
108+
cli::cli_inform(c(i = "No new snapshots."))
109+
} else {
110+
cli::cli_inform(c(
111+
v = "Copying {n_new} new snapshots: {.file {files[!same]}}."
112+
))
113+
}
114+
115+
file.copy(src_files[!same], dst_files[!same], overwrite = TRUE)
116+
invisible()
117+
}
118+
119+
same_file <- function(x, y) {
120+
file.exists(x) && file.exists(y) && hash_file(x) == hash_file(y)
121+
}

R/snapshot.R

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,7 @@ snapshot_accept_hint <- function(variant, file, reset_output = TRUE) {
368368
}
369369

370370
paste0(
371+
snap_download_hint(),
371372
cli::format_inline(
372373
"* Run {.run testthat::snapshot_accept('{name}')} to accept the change."
373374
),

R/utils.R

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,28 @@ no_wrap <- function(x) {
6767
paste_c <- function(...) {
6868
paste0(c(...), collapse = "")
6969
}
70+
71+
# from rematch2
72+
re_match <- function(text, pattern, perl = TRUE, ...) {
73+
stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern))
74+
text <- as.character(text)
75+
match <- regexpr(pattern, text, perl = perl, ...)
76+
start <- as.vector(match)
77+
length <- attr(match, "match.length")
78+
end <- start + length - 1L
79+
matchstr <- substring(text, start, end)
80+
matchstr[start == -1] <- NA_character_
81+
res <- data.frame(stringsAsFactors = FALSE, .text = text, .match = matchstr)
82+
if (!is.null(attr(match, "capture.start"))) {
83+
gstart <- attr(match, "capture.start")
84+
glength <- attr(match, "capture.length")
85+
gend <- gstart + glength - 1L
86+
groupstr <- substring(text, gstart, gend)
87+
groupstr[gstart == -1] <- NA_character_
88+
dim(groupstr) <- dim(gstart)
89+
res <- cbind(groupstr, res, stringsAsFactors = FALSE)
90+
}
91+
names(res) <- c(attr(match, "capture.names"), ".text", ".match")
92+
class(res) <- c("tbl_df", "tbl", class(res))
93+
res
94+
}

tests/testthat/_snaps/snapshot-file.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
Code
2828
cat(snapshot_review_hint("lala", "foo.r", check = FALSE, ci = FALSE))
2929
Output
30-
Run `testthat::snapshot_review('lala/')` to review changes
30+
* Run `testthat::snapshot_review('lala/')` to review changes
3131

3232
---
3333

@@ -47,6 +47,13 @@
4747
* Copy 'tests/testthat/_snaps/lala/foo.new.r' to local test directory
4848
* Run `testthat::snapshot_review('lala/')` to review changes
4949

50+
---
51+
52+
Code
53+
cat(snapshot_review_hint("lala", "foo.r", check = TRUE, ci = TRUE))
54+
Output
55+
* Run `testthat::snapshot_review('lala/')` to review changes
56+
5057
# expect_snapshot_file validates its inputs
5158

5259
Code

tests/testthat/test-snapshot-file.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,14 @@ test_that("snapshot_hint output differs in R CMD check", {
180180
check = TRUE,
181181
ci = TRUE
182182
)))
183+
184+
withr::local_envvar(GITHUB_REPOSITORY = "r-lib/testthat", GITHUB_JOB = "123")
185+
expect_snapshot(cat(snapshot_review_hint(
186+
"lala",
187+
"foo.r",
188+
check = TRUE,
189+
ci = TRUE
190+
)))
183191
})
184192

185193
test_that("expect_snapshot_file validates its inputs", {

0 commit comments

Comments
 (0)