Skip to content

Commit d0f4d5b

Browse files
committed
Merged origin/main into llm-reporter
2 parents f98927d + 66ddd0c commit d0f4d5b

19 files changed

Lines changed: 247 additions & 76 deletions

NEWS.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,11 @@
33
* `test_dir()` will no longer run tests in parallel if only a single file is being tested (#2305).
44
* `default_parallel_reporter()` is no longer exported; use `default_reporter(parallel = TRUE)` instead (#2305).
55
* New `LlmReporter()` designed for LLMs to read. It's currently used automatically inside Claude code, cursor, and gemini cli, and you can set `AGENT=1` to use with any coding agent (#2287).
6+
* `local_mocked_s3_method()` and `local_mocked_s4_method()` can now mock methods that don't already exist, and can use `definition = NULL` to temporarily remove a method. `local_mocked_s4_method()` now also works when the generic is defined in another package (#2302).
7+
* `expect_snapshot()` now reports the original error class for base errors, rather than `rlang_error` (#2286).
8+
* `expect_success()` and `expect_failure()` are more clear about what the expectation actually did (#2297).
9+
* The hint to use `snapshot_download_gh()` is now only emitted when running in a job named "R-CMD-check" (#2300).
10+
* `expect_snapshot_file()` correctly reports file name if duplicated (@MichaelChirico, #2296).
611
* Fixed support for `shinytest2::AppDriver$expect_values()` screenshot snapshot failing on CI (#2293, #2288).
712

813
* testthat now emits OpenTelemetry traces for tests when tracing is enabled. Requires the otel and otelsdk packages (#2282).

R/expect-comparison.R

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,6 @@ failure_compare <- function(act, exp, operator) {
7474
actual_op
7575
)
7676
}
77-
7877
} else {
7978
msg_act <- sprintf(
8079
"Actual comparison: \"%s\" %s \"%s\"",

R/expect-constant.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
#' a <- 1:4
2020
#' show_failure(expect_true(length(a) == 3))
2121
#' show_failure(expect_equal(length(a), 3))
22-
#'
22+
#'
2323
#' x <- c(TRUE, TRUE, FALSE, TRUE)
2424
#' show_failure(expect_true(all(x)))
2525
#' show_failure(expect_all_true(x))

R/expect-self-test.R

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,36 @@ capture_success_failure <- function(expr) {
2626
)
2727
}
2828

29+
format_success_failure <- function(status, exp_n_success, exp_n_failure) {
30+
pluralise <- function(n, singular, plural) {
31+
paste(n, ngettext(n, singular, plural))
32+
}
33+
34+
tick <- cli::col_green(cli::symbol$tick)
35+
cross <- cli::col_red(cli::symbol$cross)
36+
37+
success_ok <- status$n_success == exp_n_success
38+
failure_ok <- status$n_failure == exp_n_failure
39+
40+
c(
41+
sprintf(
42+
"Expected %s and %s.",
43+
pluralise(exp_n_success, "success", "successes"),
44+
pluralise(exp_n_failure, "failure", "failures")
45+
),
46+
sprintf(
47+
"%s Observed %s.",
48+
if (success_ok) tick else cross,
49+
pluralise(status$n_success, "success", "successes")
50+
),
51+
sprintf(
52+
"%s Observed %s.",
53+
if (failure_ok) tick else cross,
54+
pluralise(status$n_failure, "failure", "failures")
55+
)
56+
)
57+
}
58+
2959
#' Test your custom expectations
3060
#'
3161
#' @description
@@ -44,17 +74,11 @@ capture_success_failure <- function(expr) {
4474
expect_success <- function(expr) {
4575
status <- capture_success_failure(expr)
4676

47-
expected <- "Expected exactly one success and no failures."
48-
if (status$n_success != 1) {
49-
actual <- sprintf("Actually succeeded %i times", status$n_success)
50-
fail(c(expected, actual))
51-
} else if (status$n_failure > 0) {
52-
actual <- sprintf("Actually failed %i times", status$n_failure)
53-
fail(c(expected, actual))
54-
} else {
77+
if (status$n_success == 1 && status$n_failure == 0) {
5578
pass()
79+
return(invisible())
5680
}
57-
81+
fail(format_success_failure(status, exp_n_success = 1, exp_n_failure = 0))
5882
invisible()
5983
}
6084

@@ -63,21 +87,17 @@ expect_success <- function(expr) {
6387
expect_failure <- function(expr, message = NULL, ...) {
6488
status <- capture_success_failure(expr)
6589

66-
expected <- "Expected exactly one failure and no successes."
67-
if (status$n_failure != 1) {
68-
actual <- sprintf("Actually failed %i times", status$n_failure)
69-
fail(c(expected, actual))
70-
} else if (status$n_success != 0) {
71-
actual <- sprintf("Actually succeeded %i times", status$n_success)
72-
fail(c(expected, actual))
73-
} else {
90+
if (status$n_failure == 1 && status$n_success == 0) {
7491
if (is.null(message)) {
7592
pass()
7693
} else {
7794
act <- labelled_value(status$last_failure$message, "failure message")
7895
expect_match_(act, message, ..., title = "message")
7996
}
97+
return(invisible())
8098
}
99+
100+
fail(format_success_failure(status, exp_n_success = 0, exp_n_failure = 1))
81101
invisible()
82102
}
83103

R/mock-oo.R

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
#' Mock S3 and S4 methods
22
#'
33
#' @description
4-
#' These functions allow you to temporarily override S3 and S4 methods that
5-
#' already exist. It works by using [registerS3method()]/[setMethod()] to
6-
#' temporarily replace the original definition.
4+
#' These functions temporarily override S3 or S4 methods. They can mock
5+
#' methods that don't already exist, or temporarily remove a method by setting
6+
#' `definition = NULL`.
77
#'
88
#' Learn more about mocking in `vignette("mocking")`.
99
#'
1010
#' @param generic A string giving the name of the generic.
1111
#' @param signature A character vector giving the signature of the method.
12-
#' @param definition A function providing the method definition.
12+
#' @param definition A function providing the method definition, or `NULL` to
13+
#' temporarily remove the method.
1314
#' @param frame Calling frame which determines the scope of the mock.
1415
#' Only needed when wrapping in another local helper.
1516
#' @export
@@ -29,16 +30,41 @@ local_mocked_s3_method <- function(
2930
) {
3031
check_string(generic)
3132
check_string(signature)
32-
check_function(definition)
33+
check_function(definition, allow_null = TRUE)
3334

3435
old <- utils::getS3method(generic, signature, optional = TRUE)
36+
37+
# Set the new method, or a pass-through stub if removing
38+
definition <- definition %||% function(...) NextMethod()
39+
registerS3method(generic, signature, definition, envir = frame)
40+
41+
# On cleanup, restore old method or remove the one we added
3542
if (is.null(old)) {
36-
cli::cli_abort(
37-
"Can't find existing S3 method {.code {generic}.{signature}()}."
43+
withr::defer(remove_s3_method(generic, signature, envir = frame), frame)
44+
} else {
45+
withr::defer(
46+
registerS3method(generic, signature, old, envir = frame),
47+
frame
3848
)
3949
}
40-
registerS3method(generic, signature, definition, envir = frame)
41-
withr::defer(registerS3method(generic, signature, old, envir = frame), frame)
50+
51+
invisible()
52+
}
53+
54+
remove_s3_method <- function(generic, class, envir) {
55+
# Extracted from registerS3method()
56+
group_generics <- c("Math", "Ops", "matrixOps", "Summary", "Complex")
57+
if (generic %in% group_generics) {
58+
s3_envir <- .BaseNamespaceEnv
59+
} else {
60+
genfun <- get(generic, envir = envir)
61+
s3_envir <- environment(genfun) %||% .BaseNamespaceEnv
62+
}
63+
64+
if (env_has(s3_envir, ".__S3MethodsTable__.")) {
65+
table <- env_get(s3_envir, ".__S3MethodsTable__.")
66+
env_unbind(table, paste0(generic, ".", class))
67+
}
4268
}
4369

4470
#' @rdname local_mocked_s3_method
@@ -51,20 +77,28 @@ local_mocked_s4_method <- function(
5177
) {
5278
check_string(generic)
5379
check_character(signature)
54-
check_function(definition)
80+
check_function(definition, allow_null = TRUE)
5581

56-
old <- methods::getMethod(generic, signature, optional = TRUE)
57-
if (is.null(old)) {
58-
name <- paste0(generic, "(", paste0(signature, collapse = ","), ")")
59-
cli::cli_abort(
60-
"Can't find existing S4 method {.code {name}}."
61-
)
82+
generic_def <- methods::getGeneric(generic)
83+
if (is.null(generic_def)) {
84+
cli::cli_abort("Can't find generic {.fn {generic}}.")
6285
}
63-
methods::setMethod(generic, signature, definition, where = topenv(frame))
64-
withr::defer(
65-
methods::setMethod(generic, signature, old, where = topenv(frame)),
66-
frame
67-
)
86+
87+
set_method <- function(generic, signature, def) {
88+
env <- topenv(frame)
89+
old <- methods::getMethod(generic, signature, optional = TRUE)
90+
if (is.null(def)) {
91+
methods::removeMethod(generic, signature, env)
92+
} else {
93+
suppressMessages(methods::setMethod(generic, signature, def, env))
94+
}
95+
old
96+
}
97+
98+
old <- set_method(generic_def, signature, definition)
99+
withr::defer(set_method(generic_def, signature, old), frame)
100+
101+
invisible()
68102
}
69103

70104

R/reporter-check.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ summary_line <- function(n_fail, n_warn, n_skip, n_pass) {
123123
snapshot_check_hint <- function() {
124124
intro <- "To review and process snapshots locally:"
125125

126-
if (on_gh()) {
126+
if (on_gh() && Sys.getenv("GITHUB_JOB") == "R-CMD-check") {
127127
repository <- Sys.getenv("GITHUB_REPOSITORY")
128128
run_id <- Sys.getenv("GITHUB_RUN_ID")
129129

R/snapshot-github.R

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
#' take the artifacts from.
88
#'
99
#' Note that you should not generally need to use this function manually;
10-
#' instead copy and paste from the hint emitted on GitHub.
10+
#' instead copy and paste from the hint emitted on GitHub. This hint is only
11+
#' emitted when running in a job named "R-CMD-check", since that's where the
12+
#' testthat artifact is typically uploaded.
1113
#'
1214
#' @param repository Repository owner/name, e.g. `"r-lib/testthat"`.
1315
#' @param run_id Run ID, e.g. `"47905180716"`. You can find this in the action url.
14-
#' @param dest_dir Directory to download to. Defaults to the current directory.
16+
#' @param dest_dir Package root directory. Defaults to the current directory.
1517
#' @export
1618
snapshot_download_gh <- function(repository, run_id, dest_dir = ".") {
1719
check_string(repository)

R/snapshot-reporter.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ SnapshotReporter <- R6::R6Class(
135135
save_path <- paste0(c(self$file, variant, name), collapse = "/")
136136
if (save_path %in% self$snap_file_saved) {
137137
cli::cli_abort(
138-
"Snapshot file names must be unique. {.arg name} has already been used.",
138+
"Snapshot file names must be unique. {.val {name}} has already been used.",
139139
call = trace_env
140140
)
141141
}

R/snapshot.R

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,22 @@ snapshot_replay.condition <- function(
185185
}
186186

187187
if (cnd_class) {
188-
type <- paste0(type, " <", class(x)[[1]], ">")
188+
type <- paste0(type, " <", error_class(x), ">")
189189
}
190190

191191
c(snap_header(state, type), snapshot_lines(msg, transform))
192192
}
193193

194+
error_class <- function(x) {
195+
# If error was entraced from base R error, use original error class
196+
# This is a little fragile because entrace() does not document this behaviour
197+
if (inherits(x, "rlang_error") && !is.null(x$error)) {
198+
x <- x$error
199+
}
200+
class(x)[[1]]
201+
}
202+
203+
194204
snapshot_lines <- function(x, transform = NULL) {
195205
x <- split_lines(x)
196206
if (!is.null(transform)) {

man/local_mocked_s3_method.Rd

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

0 commit comments

Comments
 (0)