|
| 1 | +#' State inspected |
| 2 | +#' |
| 3 | +#' @description |
| 4 | +#' One of the most pernicious challenges to debug is when a test runs fine |
| 5 | +#' in your test suite, but fails when you run it interactively (or similarly, |
| 6 | +#' it fails randomly when running your tests in parallel). One of the most |
| 7 | +#' common causes of this problem is accidentally changing global state in a |
| 8 | +#' previous test (e.g. changing an option, an environment variable, or the |
| 9 | +#' working directory). This is hard to debug, because it's very hard to figure |
| 10 | +#' out which test made the change. |
| 11 | +#' |
| 12 | +#' Luckily testthat provides a tool to figure out if tests are changing global |
| 13 | +#' state. You can register a state inspector with `set_state_inspector()` and |
| 14 | +#' testthat will run it before and after each test, store the results, then |
| 15 | +#' report if there are any differences. For example, if you wanted to see if |
| 16 | +#' any of your tests were changing options or environment variables, you could |
| 17 | +#' put this code in `tests/testthat/helper-state.R`: |
| 18 | +#' |
| 19 | +#' ```R |
| 20 | +#' set_state_inspector(function() { |
| 21 | +#' list( |
| 22 | +#' options = options(), |
| 23 | +#' envvars = Sys.getenv() |
| 24 | +#' ) |
| 25 | +#' }) |
| 26 | +#' ``` |
| 27 | +#' |
| 28 | +#' (You might discover other packages outside your control are changing |
| 29 | +#' the global state, in which case you might want to modify this function |
| 30 | +#' to ignore those values.) |
| 31 | +#' |
| 32 | +#' Other problems that can be troublesome to resolve are CRAN check notes that |
| 33 | +#' report things like connections being left open. You can easily debug |
| 34 | +#' that problem with: |
| 35 | +#' |
| 36 | +#' ```R |
| 37 | +#' set_state_inspector(function() { |
| 38 | +#' getAllConnections() |
| 39 | +#' } |
| 40 | +#' ``` |
| 41 | +#' |
| 42 | +#' @export |
| 43 | +#' @param callback Either a zero-argument function that returns an object |
| 44 | +#' capturing global state that you're interested in, or `NULL`. |
| 45 | +set_state_inspector <- function(callback) { |
| 46 | + |
| 47 | + if (!is.null(callback) && !(is.function(callback) && length(formals(callback)) == 0)) { |
| 48 | + cli::cli_abort("{.arg callback} must be a zero-arg function, or NULL") |
| 49 | + } |
| 50 | + |
| 51 | + the$state_inspector <- callback |
| 52 | + invisible() |
| 53 | +} |
| 54 | + |
| 55 | +testthat_state_condition <- function(before, after, call) { |
| 56 | + |
| 57 | + diffs <- waldo_compare(before, after, x_arg = "before", y_arg = "after") |
| 58 | + |
| 59 | + if (length(diffs) == 0) { |
| 60 | + return(NULL) |
| 61 | + } |
| 62 | + |
| 63 | + srcref <- attr(call, "srcref") |
| 64 | + warning_cnd( |
| 65 | + message = c("Global state has changed:", set_names(format(diffs), "")), |
| 66 | + srcref = srcref |
| 67 | + ) |
| 68 | +} |
| 69 | + |
| 70 | +inspect_state <- function() { |
| 71 | + if (is.null(the$state_inspector)) { |
| 72 | + NULL |
| 73 | + } else { |
| 74 | + the$state_inspector() |
| 75 | + } |
| 76 | +} |
0 commit comments