Skip to content

Commit 96e7862

Browse files
committed
Extract a reprex from a failing expectation
1 parent 63b7549 commit 96e7862

File tree

4 files changed

+121
-0
lines changed

4 files changed

+121
-0
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export(expect_vector)
139139
export(expect_visible)
140140
export(expect_warning)
141141
export(expectation)
142+
export(extract_test)
142143
export(fail)
143144
export(find_test_scripts)
144145
export(get_reporter)

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 `extract_test()` function to extract a reprex from a failing expectation.
34
* Expectations now consistently return the value of the first argument, regardless of whether the expectation succeeds or fails. The primary exception are `expect_message()` and friends which will return the condition. This shouldn't affect existing tests, but will make failures clearer when you chain together multiple expectations (#2246).
45
* `set_state_inspector()` gains `tolerance` argument and ignores minor FP differences by default (@mcol, #2237).
56
* `expect_vector()` fails, instead of erroring, if `object` is not a vector (@plietar, #2224).

R/extract.R

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
#' Extract a reprex from an failed expectation
2+
#'
3+
#' `extract_test()` creates a minimal reprex for a failed expectation.
4+
#' It extracts all non-test code before the failed expectation as well as
5+
#' all code inside the test up to and including the failed expectation.
6+
#'
7+
#' @param location A string giving the location in the form
8+
#' `FILE:LINE[:COLUMN]`.
9+
#' @param path Path to write the reprex to. Defaults to `stdout()`.
10+
#' @export
11+
extract_test <- function(location, path = stdout()) {
12+
check_string(location)
13+
14+
pieces <- strsplit(location, ":")[[1]]
15+
if (!length(pieces) %in% c(2, 3)) {
16+
cli::cli_abort(
17+
"Expected {.arg location} to be of the form FILE:LINE[:COLUMN]"
18+
)
19+
}
20+
21+
test_path <- test_path(pieces[[1]])
22+
line <- as.integer(pieces[2])
23+
24+
lines <- extract_test_lines(test_path, line)
25+
base::writeLines(lines, con = path)
26+
}
27+
28+
extract_test_lines <- function(path, line, error_call = caller_env()) {
29+
check_string(path)
30+
if (!file.exists(path)) {
31+
cli::cli_abort(
32+
"{.arg path} ({.path path}) does not exist.",
33+
call = error_call
34+
)
35+
}
36+
check_number_whole(line, min = 1, call = error_call)
37+
38+
exprs <- parse(file = path, keep.source = TRUE)
39+
srcrefs <- attr(exprs, "srcref")
40+
41+
# Focus on srcrefs before the selected line
42+
keep <- start_line(srcrefs) <= line
43+
exprs <- exprs[keep]
44+
srcrefs <- srcrefs[keep]
45+
46+
# We first capture the prequel, all code outside of tests
47+
is_subtest <- map_lgl(exprs, is_subtest)
48+
if (any(!is_subtest)) {
49+
prequel <- c(
50+
comment_header("prequel"),
51+
map_chr(srcrefs[!is_subtest], as.character),
52+
""
53+
)
54+
} else {
55+
prequel <- NULL
56+
}
57+
58+
# Now we extract the contents of the test
59+
test_idx <- rev(which(is_subtest))[[1]]
60+
call <- exprs[[test_idx]]
61+
check_test_call(call, error_call = error_call)
62+
63+
test_contents <- attr(call[[3]], "srcref")[-1] # drop `{`
64+
keep <- start_line(test_contents) <= line
65+
test <- map_chr(test_contents[keep], as.character)
66+
67+
c(
68+
paste0("# Extracted from tests/testthat/", path, ":", line),
69+
prequel,
70+
comment_header("test"),
71+
test
72+
)
73+
}
74+
75+
# Helpers ---------------------------------------------------------------------
76+
77+
check_test_call <- function(expr, error_call = caller_env()) {
78+
if (!is_call(expr, n = 2)) {
79+
cli::cli_abort(
80+
"test call has unexpected number of arguments",
81+
internal = TRUE,
82+
call = error_call
83+
)
84+
}
85+
if (!is_call(expr[[3]], "{")) {
86+
cli::cli_abort(
87+
"test call has use {",
88+
internal = TRUE,
89+
call = error_call
90+
)
91+
}
92+
}
93+
94+
comment_header <- function(x) {
95+
paste0("# ", x, " ", strrep("-", 80 - nchar(x) - 3))
96+
}
97+
98+
start_line <- function(srcrefs) {
99+
map_int(srcrefs, \(x) x[[1]])
100+
}

man/extract_test.Rd

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)