|
| 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 | +} |
0 commit comments