-
Notifications
You must be signed in to change notification settings - Fork 340
Improve self-testing capabilities #2119
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
7698c6c
87e6a9f
f21f8bc
217cb1c
066ab59
3e7628d
056f7bc
f79d7cc
464251f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,30 +1,43 @@ | ||
| capture_failure <- new_capture("expectation_failure") | ||
| capture_success <- function(expr) { | ||
| capture_success_failure <- function(expr) { | ||
| cnd <- NULL | ||
|
|
||
| withCallingHandlers( | ||
| expr, | ||
| expectation_failure = function(cnd) { | ||
| invokeRestart("continue_test") | ||
| }, | ||
| expectation_success = function(cnd) { | ||
| cnd <<- cnd | ||
| } | ||
| n_success <- 0 | ||
| n_failure <- 0 | ||
|
|
||
| last_failure <- NULL | ||
|
|
||
| withRestarts( | ||
| withCallingHandlers( | ||
| expr, | ||
| expectation_failure = function(cnd) { | ||
| last_failure <<- cnd | ||
| n_failure <<- n_failure + 1 | ||
| # Finish the test without bubbling up | ||
| invokeRestart("failed") | ||
| }, | ||
| expectation_success = function(cnd) { | ||
| n_success <<- n_success + 1 | ||
| # Don't bubble up to any other handlers | ||
| invokeRestart("continue_test") | ||
| } | ||
| ), | ||
| failed = function() {} | ||
| ) | ||
| cnd | ||
| } | ||
|
|
||
| new_capture("expectation_success") | ||
| list( | ||
| n_success = n_success, | ||
| n_failure = n_failure, | ||
| last_failure = last_failure | ||
| ) | ||
| } | ||
|
|
||
| #' Tools for testing expectations | ||
| #' | ||
| #' @description | ||
| #' * `expect_success()` and `expect_failure()` check that there's at least | ||
| #' one success or failure respectively. | ||
| #' * `expect_snapshot_failure()` records the failure message so that you can | ||
| #' manually check that it is informative. | ||
| #' * `expect_no_success()` and `expect_no_failure()` check that are no | ||
| #' successes or failures. | ||
| #' `expect_success()` checks that there's exactly one success and no failures; | ||
| #' `expect_failure()` checks that there's exactly one failure and no successes. | ||
| #' `expect_snapshot_failure()` records the failure message so that you can | ||
| #' manually check that it is informative. | ||
| #' | ||
| #' Use `show_failure()` in examples to print the failure message without | ||
| #' throwing an error. | ||
|
|
@@ -34,42 +47,50 @@ new_capture("expectation_success") | |
| #' @param ... Other arguments passed on to [expect_match()]. | ||
| #' @export | ||
| expect_success <- function(expr) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems likely to be a breaking change. I guess you have a system for revdep checking before release, but it might be worth trying out on a sample of 50/100 packages now to see how big the impact might be. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking at https://github.com/search?q=org%3Acran+expect_success+path%3A%2F%5Etests%5C%2Ftestthat%5C%2F%2F&type=code suggests that there will be a few packages that this breaks, but most of the uses of |
||
| exp <- capture_success(expr) | ||
| status <- capture_success_failure(expr) | ||
|
|
||
| if (is.null(exp)) { | ||
| if (status$n_success == 0) { | ||
| fail("Expectation did not succeed") | ||
| } else { | ||
| succeed() | ||
| } else if (status$n_success > 1) { | ||
| fail(sprintf( | ||
| "Expectation succeeded %i times, instead of once", | ||
| status$n_success | ||
| )) | ||
| } | ||
| invisible(NULL) | ||
| } | ||
|
|
||
| #' @export | ||
| #' @rdname expect_success | ||
| expect_no_success <- function(expr) { | ||
| exp <- capture_success(expr) | ||
|
|
||
| if (!is.null(exp)) { | ||
| fail("Expectation succeeded") | ||
| } else { | ||
| succeed() | ||
| if (status$n_failure > 0) { | ||
| fail(sprintf( | ||
| "Expectation failed %i times, instead of zero", | ||
| status$n_failure | ||
| )) | ||
| } | ||
| invisible(NULL) | ||
|
|
||
| pass(NULL) | ||
| } | ||
|
|
||
| #' @export | ||
| #' @rdname expect_success | ||
| expect_failure <- function(expr, message = NULL, ...) { | ||
| exp <- capture_failure(expr) | ||
| status <- capture_success_failure(expr) | ||
|
|
||
| if (is.null(exp)) { | ||
| if (status$n_failure == 0) { | ||
| fail("Expectation did not fail") | ||
| } else if (!is.null(message)) { | ||
| expect_match(exp$message, message, ...) | ||
| } else { | ||
| succeed() | ||
| } else if (status$n_failure > 1) { | ||
| # This should be impossible, but including for completeness | ||
| fail("Expectation failed more than once") | ||
|
Comment on lines
+78
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh yeah then you wouldn't need this branch |
||
| } | ||
| invisible(NULL) | ||
|
|
||
| if (status$n_success != 0) { | ||
| fail(sprintf( | ||
| "Expectation succeeded %i times, instead of never", | ||
| status$n_success | ||
| )) | ||
| } | ||
|
|
||
| if (!is.null(message)) { | ||
| return(expect_match(status$last_failure$message, message, ...)) | ||
| } | ||
| pass(NULL) | ||
| } | ||
|
|
||
| #' @export | ||
|
|
@@ -78,12 +99,36 @@ expect_snapshot_failure <- function(expr) { | |
| expect_snapshot_error(expr, "expectation_failure") | ||
| } | ||
|
|
||
| #' Test for absence of success or failure | ||
| #' | ||
| #' @description | ||
| #' `r lifecycle::badge("deprecated")` | ||
| #' | ||
| #' These functions are deprecated because [expect_success()] and | ||
| #' [expect_failure()] now test for exactly one success or no failures, and | ||
| #' exactly one failure and no successes. | ||
| #' | ||
| #' @keywords internal | ||
| #' @export | ||
| #' @rdname expect_success | ||
| expect_no_success <- function(expr) { | ||
| lifecycle::deprecate_warn("3.3.0", "expect_no_success()", "expect_failure()") | ||
| status <- capture_success_failure(expr) | ||
|
|
||
| if (status$n_success > 0) { | ||
| fail("Expectation succeeded") | ||
| } else { | ||
| succeed() | ||
| } | ||
| invisible(NULL) | ||
| } | ||
|
|
||
| #' @export | ||
| #' @rdname expect_no_success | ||
| expect_no_failure <- function(expr) { | ||
| exp <- capture_failure(expr) | ||
| lifecycle::deprecate_warn("3.3.0", "expect_no_failure()", "expect_success()") | ||
| status <- capture_success_failure(expr) | ||
|
|
||
| if (!is.null(exp)) { | ||
| if (status$n_failure > 0) { | ||
| fail("Expectation failed") | ||
| } else { | ||
| succeed() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,5 +44,5 @@ try_again <- function(times, code) { | |
| times <- times - 1L | ||
| } | ||
|
|
||
| stop(e) | ||
| exp_signal(e) | ||
| } | ||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # errors in expect_success bubble up | ||
|
|
||
| Code | ||
| expect_success(abort("error")) | ||
| Condition | ||
| Error: | ||
| ! error | ||
|
|
||
| # expect_no are deprecated | ||
|
|
||
| Code | ||
| expect_no_failure(succeed()) | ||
| Condition | ||
| Warning: | ||
| `expect_no_failure()` was deprecated in testthat 3.3.0. | ||
| i Please use `expect_success()` instead. | ||
| Code | ||
| expect_no_success(fail()) | ||
| Condition | ||
| Warning: | ||
| `expect_no_success()` was deprecated in testthat 3.3.0. | ||
| i Please use `expect_failure()` instead. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,12 +12,9 @@ test_that("uses specified width", { | |
|
|
||
| test_that("creates file on first run", { | ||
| file <- withr::local_tempfile() | ||
| expect_success( | ||
| expect_warning( | ||
| expect_known_output(cat("ok!\n"), file), | ||
| "Creating reference" | ||
| ) | ||
| ) | ||
| expect_known_output(cat("ok!\n"), file) |> | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the 4.1.0 requirement is so satisfying 🧘♂️ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The simple joys of being able to use R features that were released 5 years ago 😆 |
||
| expect_success() |> | ||
| expect_warning("Creating reference") | ||
|
|
||
| expect_true(file.exists(file)) | ||
| }) | ||
|
|
@@ -76,12 +73,9 @@ test_that("correctly matches to a file", { | |
| }) | ||
|
|
||
| test_that("first run is successful", { | ||
| expect_success( | ||
| expect_warning( | ||
| expect_known_value(2, "two.rds"), | ||
| "Creating reference" | ||
| ) | ||
| ) | ||
| expect_known_value(2, "two.rds") |> | ||
| expect_success() |> | ||
| expect_warning("Creating reference") | ||
| unlink("two.rds") | ||
| }) | ||
|
|
||
|
|
@@ -130,12 +124,9 @@ test_that("version 3 is possible", { | |
| # expect_known_hash ------------------------------------------------------- | ||
|
|
||
| test_that("empty hash succeeds with warning", { | ||
| expect_success( | ||
| expect_warning( | ||
| expect_known_hash(1:10), | ||
| "No recorded hash" | ||
| ) | ||
| ) | ||
| expect_known_hash(1:10) |> | ||
| expect_success() |> | ||
| expect_warning("No recorded hash") | ||
| }) | ||
|
|
||
| test_that("only succeeds if hash is correct", { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IIUC
n_failureis either 0 or 1, because we're unwinding back tofailedat the first failure?In this case I think it would be a little clearer to remove
n_failureand renamelast_failure(whose "last" prefix imply there might be more than one assigned here) to justfailure. Then you test for!is.null(failure)instead ofn_failure > 0.Also fine as is but then I'd add a comment saying we expect at most 1 failure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is a good suggestion but I'm going to leave it until after #2129 because there I am encountering a weird situation where
fail()is not causing an expectation to exit and I think that this might help me debug it.