1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
|
#' Source a file, directory of files, or various important subsets
#'
#' These are used by [test_dir()] and friends
#'
#' @param path Path to files.
#' @param pattern Regular expression used to filter files.
#' @param env Environment in which to evaluate code.
#' @param desc A character vector used to filter tests. This is used to
#' (recursively) filter the content of the file, so that only the non-test
#' code up to and including the matching test is run.
#' @param chdir Change working directory to `dirname(path)`?
#' @param wrap Automatically wrap all code within [test_that()]? This ensures
#' that all expectations are reported, even if outside a test block.
#' @param shuffle If `TRUE`, randomly reorder the top-level expressions
#' in the file.
#' @export
#' @keywords internal
source_file <- function(
path,
env = test_env(),
chdir = TRUE,
desc = NULL,
wrap = TRUE,
shuffle = FALSE,
error_call = caller_env()
) {
check_string(path, call = error_call)
if (!file.exists(path)) {
cli::cli_abort("{.arg path} does not exist.", call = error_call)
}
if (!is.environment(env)) {
stop_input_type(env, "an environment", call = error_call)
}
check_character(desc, allow_null = TRUE)
lines <- brio::read_lines(path)
srcfile <- srcfilecopy(
path,
lines,
file.info(path)[1, "mtime"],
isFile = TRUE
)
## We need to parse from a connection, because parse() has a bug,
## and converts the input to the native encoding, if the text arg is used
con <- textConnection(lines, encoding = "UTF-8")
withr::defer(try(close(con), silent = TRUE))
exprs <- parse(con, n = -1, srcfile = srcfile, encoding = "UTF-8")
if (shuffle) {
exprs <- sample(exprs)
}
exprs <- filter_desc(exprs, desc, error_call = error_call)
n <- length(exprs)
if (n == 0L) {
return(invisible())
}
if (chdir) {
old_dir <- setwd(dirname(path))
withr::defer(setwd(old_dir))
}
withr::local_options(testthat_topenv = env, testthat_path = path)
if (wrap) {
invisible(test_code(
code = exprs,
env = env,
reporter = get_reporter() %||% StopReporter$new()
))
} else {
withCallingHandlers(
invisible(eval(exprs, env)),
error = function(err) {
cli::cli_abort(
"Failed to evaluate {.path {path}}.",
parent = err,
call = error_call
)
}
)
}
}
filter_desc <- function(exprs, descs, error_call = caller_env()) {
if (length(descs) == 0) {
return(exprs)
}
desc <- descs[[1]]
subtest_idx <- which(unname(map_lgl(exprs, is_subtest)))
matching_idx <- keep(subtest_idx, \(idx) exprs[[idx]][[2]] == desc)
if (length(matching_idx) == 0) {
cli::cli_abort(
"Failed to find test with description {.str {desc}}.",
call = error_call
)
} else if (length(matching_idx) > 1) {
cli::cli_abort(
"Found multiple tests with description {.str {desc}}.",
call = error_call
)
}
# Want all code up to and including the matching test, except for subtests
keep_idx <- setdiff(seq2(1, matching_idx), setdiff(subtest_idx, matching_idx))
# Recursively inspect the components of the subtest
exprs[[matching_idx]][[3]] <- filter_desc(
exprs[[matching_idx]][[3]],
descs[-1],
error_call = error_call
)
exprs[keep_idx]
}
is_subtest <- function(expr) {
is_call(expr, c("test_that", "describe", "it"), n = 2) &&
is_string(expr[[2]]) &&
is_call(expr[[3]], "{")
}
#' @rdname source_file
#' @export
source_dir <- function(
path,
pattern = "\\.[rR]$",
env = test_env(),
chdir = TRUE,
wrap = TRUE,
shuffle = FALSE
) {
files <- sort(dir(path, pattern, full.names = TRUE))
error_call <- current_env()
lapply(files, function(path) {
source_file(
path,
env = env,
chdir = chdir,
wrap = wrap,
shuffle = shuffle,
error_call = error_call
)
})
}
#' @rdname source_file
#' @export
source_test_helpers <- function(path = "tests/testthat", env = test_env()) {
source_dir(path, "^helper.*\\.[rR]$", env = env, wrap = FALSE)
}
#' @rdname source_file
#' @export
source_test_setup <- function(path = "tests/testthat", env = test_env()) {
source_dir(path, "^setup.*\\.[rR]$", env = env, wrap = FALSE)
}
#' @rdname source_file
#' @export
source_test_teardown <- function(path = "tests/testthat", env = test_env()) {
source_dir(path, "^teardown.*\\.[rR]$", env = env, wrap = FALSE)
}
|