Skip to content

Commit f52ff38

Browse files
authored
Merge branch 'main' into argument-checking
2 parents c61430f + e5cd770 commit f52ff38

File tree

12 files changed

+140
-71
lines changed

12 files changed

+140
-71
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{
22
"permissions": {
33
"allow": [
4-
"Bash(find:*)"
4+
"Bash(find:*)",
5+
"Bash(R:*)"
56
],
67
"deny": []
7-
}
8+
},
9+
"$schema": "https://json.schemastore.org/claude-code-settings.json"
810
}

CLAUDE.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ testthat is R's most popular unit testing framework, used by thousands of CRAN p
88

99
## Key Development Commands
1010

11-
### Testing
12-
- `devtools::test()` or `Ctrl/Cmd+Shift+T` in RStudio - Run all tests
13-
- `devtools::test_file("tests/testthat/test-filename.R")` - Run tests in a specific file
14-
- `testthat::test_local()` - Run tests for local source package
15-
- `testthat::test_package("testthat")` - Run tests for installed package
16-
- `R CMD check` - Full package check including tests
11+
General advice:
12+
* When running R from the console, always run it with `--quiet --vanilla`
13+
* Always run `air format .` after generating code
14+
15+
### Development tools
1716

18-
### Building and Installation
19-
- `devtools::load_all()` or `Ctrl/Cmd+Shift+L` - Load package for development
17+
- `devtools::test()` - Run all tests
18+
- `devtools::test_file("tests/testthat/test-filename.R")` - Run tests in a specific file
19+
- `devtools::load_all()` - Load package for development
2020
- `devtools::document()` - Generate documentation
2121
- `devtools::check()` - Run R CMD check
2222
- `devtools::install()` - Install package locally

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# testthat (development version)
22

33
* `expect_*()` functions consistently and rigorously check their inputs (#1754).
4+
* `JunitReporter()` no longer fails with `"no applicable method for xml_add_child"` for warnings outside of tests (#1913). Additionally, warnings now save their backtraces.
5+
* `JunitReporter()` strips ANSI escapes in more placese (#1852, #2032).
6+
* `try_again()` is now publicised. The first argument is now the number of retries, not tries (#2050).
47
* `vignette("custom-expectations)` has been overhauled to make it much clearer how to create high-quality expectations (#2113, #2132, #2072).
58
* `expect_snapshot()` and friends will now fail when creating a new snapshot on CI. This is usually a signal that you've forgotten to run it locally before committing (#1461).
69
* `expect_snapshot_value()` can now handle expressions that generate `-` (#1678) or zero length atomic vectors (#2042).

R/reporter-junit.R

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@ JunitReporter <- R6::R6Class(
108108
time <- self$elapsed_time()
109109
self$suite_time <- self$suite_time + time
110110

111+
# If no context was started (e.g., warnings outside tests), create a default one
112+
if (is.null(self$suite)) {
113+
self$start_context(context %||% "(unknown)")
114+
}
115+
111116
# XML node for test case
112117
name <- test %||% "(unnamed)"
113118
testcase <- xml2::xml_add_child(
@@ -120,7 +125,7 @@ JunitReporter <- R6::R6Class(
120125

121126
first_line <- function(x) {
122127
loc <- expectation_location(x, " (", ")")
123-
paste0(strsplit(x$message, split = "\n")[[1]][1], loc)
128+
paste0(strsplit(cli::ansi_strip(x$message), split = "\n")[[1]][1], loc)
124129
}
125130

126131
# add an extra XML child node if not a success
@@ -147,6 +152,9 @@ JunitReporter <- R6::R6Class(
147152
} else if (expectation_skip(result)) {
148153
xml2::xml_add_child(testcase, "skipped", message = first_line(result))
149154
self$skipped <- self$skipped + 1
155+
} else if (expectation_warning(result)) {
156+
warning_node <- xml2::xml_add_child(testcase, "system-out")
157+
xml2::xml_text(warning_node) <- cli::ansi_strip(format(result))
150158
}
151159
},
152160

R/try-again.R

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,38 @@
1-
#' Try evaluating an expressing multiple times until it succeeds.
1+
#' Try evaluating an expressing multiple times until it succeeds
22
#'
3-
#' @param times Maximum number of attempts.
4-
#' @param code Code to evaluate
5-
#' @keywords internal
3+
#' If you have a flaky test, you can use `try_again()` to run it a few times
4+
#' until it succeeds. In most cases, you are better fixing the underlying
5+
#' cause of the flakeyness, but sometimes that's not possible.
6+
#'
7+
#' @param times Number of times to retry.
8+
#' @param code Code to evaluate.
69
#' @export
710
#' @examples
8-
#' third_try <- local({
9-
#' i <- 3
10-
#' function() {
11-
#' i <<- i - 1
12-
#' if (i > 0) fail(paste0("i is ", i))
13-
#' }
14-
#' })
15-
#' try_again(3, third_try())
11+
#' usually_return_1 <- function(i) {
12+
#' if (runif(1) < 0.1) 0 else 1
13+
#' }
14+
#'
15+
#' \dontrun{
16+
#' # 10% chance of failure:
17+
#' expect_equal(usually_return_1(), 1)
18+
#'
19+
#' # 1% chance of failure:
20+
#' try_again(3, expect_equal(usually_return_1(), 1))
21+
#' }
1622
try_again <- function(times, code) {
17-
while (times > 0) {
18-
e <- tryCatch(
19-
withCallingHandlers(
20-
{
21-
code
22-
NULL
23-
},
24-
warning = function(e) {
25-
if (
26-
identical(e$message, "restarting interrupted promise evaluation")
27-
) {
28-
tryInvokeRestart("muffleWarning")
29-
}
30-
}
31-
),
32-
expectation_failure = function(e) {
33-
e
34-
},
35-
error = function(e) {
36-
e
37-
}
38-
)
23+
check_number_whole(times, min = 1)
3924

40-
if (is.null(e)) {
41-
return(invisible(TRUE))
42-
}
25+
code <- enquo(code)
4326

44-
times <- times - 1L
27+
i <- 1
28+
while (i <= times) {
29+
tryCatch(
30+
return(eval(get_expr(code), get_env(code))),
31+
expectation_failure = function(cnd) NULL
32+
)
33+
cli::cli_inform(c(i = "Expectation failed; trying again ({i})..."))
34+
i <- i + 1
4535
}
4636

47-
exp_signal(e)
37+
eval(get_expr(code), get_env(code))
4838
}

_pkgdown.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ reference:
4141
- expect_invisible
4242
- expect_output
4343
- expect_silent
44+
45+
- subtitle: Helpers
46+
contents:
47+
- try_again
4448
- local_reproducible_output
4549

4650
- title: Snapshot testing

man/try_again.Rd

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

tests/testthat/_snaps/reporter-junit.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,12 @@
4545
</testcase>
4646
</testsuite>
4747
<testsuite name="Warnings" timestamp="1999:12:31 23:59:59" hostname="nodename" tests="2" skipped="1" failures="0" errors="0" time="0">
48-
<testcase time="0" classname="Warnings" name="warnings_get_backtraces"/>
48+
<testcase time="0" classname="Warnings" name="warnings_get_backtraces">
49+
<system-out>def
50+
Backtrace:
51+
x
52+
1. \-f()</system-out>
53+
</testcase>
4954
<testcase time="0" classname="Warnings" name="warnings_get_backtraces">
5055
<skipped message="Reason: empty test ('reporters/tests.R:44:1')"/>
5156
</testcase>

tests/testthat/_snaps/try-again.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# tries multiple times
2+
3+
Code
4+
result <- try_again(3, third_try())
5+
Message
6+
i Expectation failed; trying again (1)...
7+
i Expectation failed; trying again (2)...
8+
9+
---
10+
11+
Code
12+
try_again(1, third_try())
13+
Message
14+
i Expectation failed; trying again (1)...
15+
Condition
16+
Error:
17+
! `i` (`actual`) is not equal to 0 (`expected`).
18+
19+
`actual`: 1.0
20+
`expected`: 0.0
21+

tests/testthat/test-reporter-junit.R

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,37 @@ test_that("permit Java-style class names", {
77
class <- "package_name_or_domain.ClassName"
88
expect_equal(classnameOK(class), class)
99
})
10+
11+
test_that("ANSI escapes are stripped from all user text in XML", {
12+
skip_if_not_installed("xml2")
13+
14+
tmp <- withr::local_tempfile(fileext = ".xml")
15+
reporter <- JunitReporterMock$new(file = tmp)
16+
reporter$start_reporter()
17+
18+
text_with_ansi <- "\033[33mFirst line\033[0m\nSecond line"
19+
reporter$start_context("c")
20+
reporter$start_test("c", "t")
21+
reporter$add_result("c", "t", new_expectation("error", text_with_ansi))
22+
reporter$add_result("c", "t", new_expectation("failure", text_with_ansi))
23+
reporter$add_result("c", "t", new_expectation("skip", text_with_ansi))
24+
reporter$end_test()
25+
reporter$end_context()
26+
reporter$end_reporter()
27+
28+
expect_no_error(xml2::read_xml(tmp))
29+
})
30+
31+
test_that("warnings outside context don't cause xml_add_child errors", {
32+
skip_if_not_installed("xml2")
33+
34+
tmp <- withr::local_tempfile(fileext = ".xml")
35+
reporter <- JunitReporterMock$new(file = tmp)
36+
reporter$start_reporter()
37+
38+
# This would previously fail with "no applicable method for 'xml_add_child'"
39+
expect_no_error({
40+
reporter$add_result(NULL, "test", new_expectation("warning", "test"))
41+
})
42+
reporter$end_reporter()
43+
})

0 commit comments

Comments
 (0)