From 156feeeada52e9128781cb613d9c72e1a3995bb9 Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Thu, 15 Feb 2024 09:40:23 -0500 Subject: [PATCH 1/4] Add `expect_error_forwarded()`, `expect_warning_forwarded()`, and `expect_message_forwarded()` --- DESCRIPTION | 2 +- NAMESPACE | 3 + NEWS.md | 6 ++ R/expect-condition-forwarded.R | 76 +++++++++++++++++ R/expect-condition.R | 4 + .../test-expect-condition-forwarded.R | 81 +++++++++++++++++++ 6 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 R/expect-condition-forwarded.R create mode 100644 tests/testthat/test-expect-condition-forwarded.R diff --git a/DESCRIPTION b/DESCRIPTION index 0d38b02d9..5939b1c6e 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -55,4 +55,4 @@ Config/testthat/parallel: true Config/testthat/start-first: watcher, parallel* Encoding: UTF-8 Roxygen: list(markdown = TRUE, r6 = FALSE) -RoxygenNote: 7.2.3 +RoxygenNote: 7.3.1 diff --git a/NAMESPACE b/NAMESPACE index e13d30231..0cac60fb9 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -84,6 +84,7 @@ export(expect_equal) export(expect_equal_to_reference) export(expect_equivalent) export(expect_error) +export(expect_error_forwarded) export(expect_failure) export(expect_false) export(expect_gt) @@ -102,6 +103,7 @@ export(expect_lte) export(expect_mapequal) export(expect_match) export(expect_message) +export(expect_message_forwarded) export(expect_more_than) export(expect_named) export(expect_no_condition) @@ -130,6 +132,7 @@ export(expect_type) export(expect_vector) export(expect_visible) export(expect_warning) +export(expect_warning_forwarded) export(expectation) export(fail) export(find_test_scripts) diff --git a/NEWS.md b/NEWS.md index 6eb90a984..687d45e43 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,11 @@ # testthat (development version) +* Add new error, warning, and message capturing expectations for conditions from + other packages. `expect_error_forwarded()`, `expect_warning_forwarded()`, and + `expect_message_forwarded()` capture errors, warnings, and messages that come + from other code to match the message that is not in the control of the package + being tested (@billdenney, #1927). + # testthat 3.2.1 * Fix incorrect format string detected by latest R-devel. Fix thanks to diff --git a/R/expect-condition-forwarded.R b/R/expect-condition-forwarded.R new file mode 100644 index 000000000..153ac80d1 --- /dev/null +++ b/R/expect-condition-forwarded.R @@ -0,0 +1,76 @@ +#' Does code throw an error, warning, or message that is the same as another? +#' +#' @description +#' `expect_error_forwarded()`, `expect_warning_forwarded()`, and +#' `expect_message_forwarded()`, check that code throws an error, warning, +#' message, or condition with a message that matches an `expected` value from an +#' example. These functions are useful to test that a condition matches one +#' from another package that you do not control. +#' +#' @export +#' @family expectations +#' @inheritParams expect_that +#' @return The value of the first argument +expect_error_forwarded <- function(actual, expected, label = NULL) { + message <- error_message(expected) + expect_error({{ actual }}, regexp = message, fixed = TRUE, label = label) +} + +#' @export +#' @rdname expect_error_forwarded +expect_warning_forwarded <- function(actual, expected, label = NULL) { + message <- warning_message(expected) + expect_warning({{ actual }}, regexp = message, fixed = TRUE, label = label) +} + +#' @export +#' @rdname expect_error_forwarded +expect_message_forwarded <- function(actual, expected, label = NULL) { + message <- message_message(expected) + expect_message({{ actual }}, regexp = message, fixed = TRUE, label = label) +} + +error_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + error = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No error thrown") + } else { + out + } +} + +warning_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + warning = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No warning thrown") + } else { + out + } +} + +message_message <- function(code) { + out <- tryCatch( + { + code + NULL + }, + message = function(e) conditionMessage(e) + ) + if (is.null(out)) { + stop("No message thrown") + } else { + out + } +} diff --git a/R/expect-condition.R b/R/expect-condition.R index 7bf2395d4..e1cd25d88 100644 --- a/R/expect-condition.R +++ b/R/expect-condition.R @@ -6,6 +6,10 @@ #' or condition with a message that matches `regexp`, or a class that inherits #' from `class`. See below for more details. #' +#' If you need to test that an error, warning, or message is the same as one +#' given by another package, then see the [expect_error_forwarded()], +#' [expect_warning_forwarded()], and [expect_message_forwarded()] functions. +#' #' In the 3rd edition, these functions match (at most) a single condition. All #' additional and non-matching (if `regexp` or `class` are used) conditions #' will bubble up outside the expectation. If these additional conditions diff --git a/tests/testthat/test-expect-condition-forwarded.R b/tests/testthat/test-expect-condition-forwarded.R new file mode 100644 index 000000000..9b0d3a050 --- /dev/null +++ b/tests/testthat/test-expect-condition-forwarded.R @@ -0,0 +1,81 @@ +test_that("expect_error_forwarded general use", { + expect_success( + expect_error_forwarded( + stop(), + stop() + ) + ) + + # Matches + expect_success( + expect_error_forwarded( + stop("foo"), + stop("foo") + ) + ) + # Does not match + expect_error( + expect_error_forwarded( + stop("foo"), + stop("bar") + ) + ) + + # Matches + expect_success( + expect_warning_forwarded( + warning("foo"), + warning("foo") + ) + ) + # Does not match + expect_warning( + expect_error( + expect_warning_forwarded( + warning("foo"), + warning("bar") + ), + regexp = '`warning("foo")` did not throw the expected warning.', + fixed = TRUE + ), + regexp = "foo" + ) + + # Matches + expect_success( + expect_message_forwarded( + message("foo"), + message("foo") + ) + ) + # Does not match + expect_message( + expect_error( + expect_message_forwarded( + message("foo"), + message("bar") + ), + regexp = '`message("foo")` did not throw the expected message.', + fixed = TRUE + ), + regexp = "foo" + ) +}) + +test_that("expect_*_forwarded with no condition generated ", { + expect_error( + expect_error_forwarded(1, 1), + regexp = "No error thrown", + fixed = TRUE + ) + expect_error( + expect_warning_forwarded(1, 1), + regexp = "No warning thrown", + fixed = TRUE + ) + expect_error( + expect_message_forwarded(1, 1), + regexp = "No message thrown", + fixed = TRUE + ) +}) From d46466d15f2b4719e7705077c8688e95b1c4c387 Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Thu, 15 Feb 2024 09:42:54 -0500 Subject: [PATCH 2/4] Rebuild docs --- man/comparison-expectations.Rd | 1 + man/equality-expectations.Rd | 1 + man/expect_error.Rd | 5 ++++ man/expect_error_forwarded.Rd | 43 +++++++++++++++++++++++++++++++++ man/expect_length.Rd | 1 + man/expect_match.Rd | 1 + man/expect_named.Rd | 1 + man/expect_null.Rd | 1 + man/expect_output.Rd | 1 + man/expect_reference.Rd | 1 + man/expect_silent.Rd | 1 + man/inheritance-expectations.Rd | 1 + man/logical-expectations.Rd | 1 + 13 files changed, 59 insertions(+) create mode 100644 man/expect_error_forwarded.Rd diff --git a/man/comparison-expectations.Rd b/man/comparison-expectations.Rd index 5cbee6931..ca415bdf8 100644 --- a/man/comparison-expectations.Rd +++ b/man/comparison-expectations.Rd @@ -43,6 +43,7 @@ expect_gt(9, 10) Other expectations: \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/equality-expectations.Rd b/man/equality-expectations.Rd index e22afd695..aa8a9672a 100644 --- a/man/equality-expectations.Rd +++ b/man/equality-expectations.Rd @@ -86,6 +86,7 @@ expect_equal(sqrt(2) ^ 2, 2) Other expectations: \code{\link{comparison-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_error.Rd b/man/expect_error.Rd index 50851564b..8156c0d40 100644 --- a/man/expect_error.Rd +++ b/man/expect_error.Rd @@ -105,6 +105,10 @@ the captured condition. or condition with a message that matches \code{regexp}, or a class that inherits from \code{class}. See below for more details. +If you need to test that an error, warning, or message is the same as one +given by another package, then see the \code{\link[=expect_error_forwarded]{expect_error_forwarded()}}, +\code{\link[=expect_warning_forwarded]{expect_warning_forwarded()}}, and \code{\link[=expect_message_forwarded]{expect_message_forwarded()}} functions. + In the 3rd edition, these functions match (at most) a single condition. All additional and non-matching (if \code{regexp} or \code{class} are used) conditions will bubble up outside the expectation. If these additional conditions @@ -186,6 +190,7 @@ that code runs without errors/warnings/messages/conditions. Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_error_forwarded.Rd b/man/expect_error_forwarded.Rd new file mode 100644 index 000000000..b9e1f8bd9 --- /dev/null +++ b/man/expect_error_forwarded.Rd @@ -0,0 +1,43 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/expect-condition-forwarded.R +\name{expect_error_forwarded} +\alias{expect_error_forwarded} +\alias{expect_warning_forwarded} +\alias{expect_message_forwarded} +\title{Does code throw an error, warning, or message that is the same as another?} +\usage{ +expect_error_forwarded(actual, expected, label = NULL) + +expect_warning_forwarded(actual, expected, label = NULL) + +expect_message_forwarded(actual, expected, label = NULL) +} +\arguments{ +\item{label}{Used to customise failure messages. For expert use only.} +} +\value{ +The value of the first argument +} +\description{ +\code{expect_error_forwarded()}, \code{expect_warning_forwarded()}, and +\code{expect_message_forwarded()}, check that code throws an error, warning, +message, or condition with a message that matches an \code{expected} value from an +example. These functions are useful to test that a condition matches one +from another package that you do not control. +} +\seealso{ +Other expectations: +\code{\link{comparison-expectations}}, +\code{\link{equality-expectations}}, +\code{\link{expect_error}()}, +\code{\link{expect_length}()}, +\code{\link{expect_match}()}, +\code{\link{expect_named}()}, +\code{\link{expect_null}()}, +\code{\link{expect_output}()}, +\code{\link{expect_reference}()}, +\code{\link{expect_silent}()}, +\code{\link{inheritance-expectations}}, +\code{\link{logical-expectations}} +} +\concept{expectations} diff --git a/man/expect_length.Rd b/man/expect_length.Rd index 4e65e8069..2d1004ff1 100644 --- a/man/expect_length.Rd +++ b/man/expect_length.Rd @@ -32,6 +32,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, \code{\link{expect_null}()}, diff --git a/man/expect_match.Rd b/man/expect_match.Rd index 1272f53e3..151a71147 100644 --- a/man/expect_match.Rd +++ b/man/expect_match.Rd @@ -91,6 +91,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_named}()}, \code{\link{expect_null}()}, diff --git a/man/expect_named.Rd b/man/expect_named.Rd index bfa9ad9fc..a1e36e317 100644 --- a/man/expect_named.Rd +++ b/man/expect_named.Rd @@ -56,6 +56,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_null}()}, diff --git a/man/expect_null.Rd b/man/expect_null.Rd index 45b1adb12..9ad98e2d5 100644 --- a/man/expect_null.Rd +++ b/man/expect_null.Rd @@ -34,6 +34,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_output.Rd b/man/expect_output.Rd index c7d789f6e..259e9e50e 100644 --- a/man/expect_output.Rd +++ b/man/expect_output.Rd @@ -68,6 +68,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_reference.Rd b/man/expect_reference.Rd index e5a4b120e..da90fc844 100644 --- a/man/expect_reference.Rd +++ b/man/expect_reference.Rd @@ -44,6 +44,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/expect_silent.Rd b/man/expect_silent.Rd index 69688a2ec..460349aea 100644 --- a/man/expect_silent.Rd +++ b/man/expect_silent.Rd @@ -35,6 +35,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/inheritance-expectations.Rd b/man/inheritance-expectations.Rd index 1b2191620..6452dd8ff 100644 --- a/man/inheritance-expectations.Rd +++ b/man/inheritance-expectations.Rd @@ -68,6 +68,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, diff --git a/man/logical-expectations.Rd b/man/logical-expectations.Rd index da38c3bde..826255114 100644 --- a/man/logical-expectations.Rd +++ b/man/logical-expectations.Rd @@ -52,6 +52,7 @@ Other expectations: \code{\link{comparison-expectations}}, \code{\link{equality-expectations}}, \code{\link{expect_error}()}, +\code{\link{expect_error_forwarded}()}, \code{\link{expect_length}()}, \code{\link{expect_match}()}, \code{\link{expect_named}()}, From 9cbd218d70a7331d6d54ab4b03e079f2efeb792d Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Thu, 15 Feb 2024 10:31:30 -0500 Subject: [PATCH 3/4] Fix and update docs --- R/expect-condition-forwarded.R | 26 ++++++++++++++++++++------ man/expect_error_forwarded.Rd | 27 ++++++++++++++++++++++++--- 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/R/expect-condition-forwarded.R b/R/expect-condition-forwarded.R index 153ac80d1..e2ce9ccf9 100644 --- a/R/expect-condition-forwarded.R +++ b/R/expect-condition-forwarded.R @@ -10,24 +10,38 @@ #' @export #' @family expectations #' @inheritParams expect_that +#' @param expected A call that causes the expected error, warning, or message. +#' It is usually a call to another package than the one being tested. #' @return The value of the first argument -expect_error_forwarded <- function(actual, expected, label = NULL) { +#' @examples +#' myfun <- function(x) { +#' stopifnot(length(x) == 1) +#' } +#' +#' expect_error_forwarded( +#' myfun(x = 1:2), +#' { +#' x <- 1:2 +#' stopifnot(length(x) == 1) +#' } +#' ) +expect_error_forwarded <- function(object, expected, label = NULL) { message <- error_message(expected) - expect_error({{ actual }}, regexp = message, fixed = TRUE, label = label) + expect_error({{ object }}, regexp = message, fixed = TRUE, label = label) } #' @export #' @rdname expect_error_forwarded -expect_warning_forwarded <- function(actual, expected, label = NULL) { +expect_warning_forwarded <- function(object, expected, label = NULL) { message <- warning_message(expected) - expect_warning({{ actual }}, regexp = message, fixed = TRUE, label = label) + expect_warning({{ object }}, regexp = message, fixed = TRUE, label = label) } #' @export #' @rdname expect_error_forwarded -expect_message_forwarded <- function(actual, expected, label = NULL) { +expect_message_forwarded <- function(object, expected, label = NULL) { message <- message_message(expected) - expect_message({{ actual }}, regexp = message, fixed = TRUE, label = label) + expect_message({{ object }}, regexp = message, fixed = TRUE, label = label) } error_message <- function(code) { diff --git a/man/expect_error_forwarded.Rd b/man/expect_error_forwarded.Rd index b9e1f8bd9..93010f199 100644 --- a/man/expect_error_forwarded.Rd +++ b/man/expect_error_forwarded.Rd @@ -6,13 +6,21 @@ \alias{expect_message_forwarded} \title{Does code throw an error, warning, or message that is the same as another?} \usage{ -expect_error_forwarded(actual, expected, label = NULL) +expect_error_forwarded(object, expected, label = NULL) -expect_warning_forwarded(actual, expected, label = NULL) +expect_warning_forwarded(object, expected, label = NULL) -expect_message_forwarded(actual, expected, label = NULL) +expect_message_forwarded(object, expected, label = NULL) } \arguments{ +\item{object}{Object to test. + +Supports limited unquoting to make it easier to generate readable failures +within a function or for loop. See \link{quasi_label} for more details.} + +\item{expected}{A call that causes the expected error, warning, or message. +It is usually a call to another package than the one being tested.} + \item{label}{Used to customise failure messages. For expert use only.} } \value{ @@ -25,6 +33,19 @@ message, or condition with a message that matches an \code{expected} value from example. These functions are useful to test that a condition matches one from another package that you do not control. } +\examples{ +myfun <- function(x) { + stopifnot(length(x) == 1) +} + +expect_error_forwarded( + myfun(x = 1:2), + { + x <- 1:2 + stopifnot(length(x) == 1) + } +) +} \seealso{ Other expectations: \code{\link{comparison-expectations}}, From f731f7af8145795d230818d30dc208a8cf9f367d Mon Sep 17 00:00:00 2001 From: Bill Denney Date: Thu, 15 Feb 2024 10:38:06 -0500 Subject: [PATCH 4/4] Fix pkgdown --- _pkgdown.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/_pkgdown.yml b/_pkgdown.yml index 209eea3d6..77c7fcf81 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -38,6 +38,7 @@ reference: - expect_invisible - expect_output - expect_silent + - expect_error_forwarded - local_reproducible_output - title: Snapshot testing