Skip to content

Commit aec76b6

Browse files
authored
Merge branch 'main' into slow-reporter
2 parents e238b00 + 86480e2 commit aec76b6

File tree

15 files changed

+183
-27
lines changed

15 files changed

+183
-27
lines changed

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
* New `SlowReporter` makes it easier to find the slowest tests in your package. The easiest way to run it is with `devtools::test(reporter = "slow")` (#1466).
4+
* On CRAN, `test_that()` now automatically skips if a package is not installed (#1585). Practically, this means that you no longer need to check that suggested packages are installed. (We don't do this in the tidyverse because we think it has limited payoff, but other styles advise differently.)
5+
* `expect_snapshot()` no longer skips on CRAN, as that skips the rest of the test. Instead it just returns, neither succeeding nor failing (#1585).
6+
* Interrupting a test now prints the test name. This makes it easier to tell where a very slow test might be hanging (#1464)
47
* Parallel testthat now does not ignore test files with syntax errors (#1360).
58
* `expect_lt()`, `expect_gt()`, and friends have a refined display that is more likely to display the correct number of digits and shows you the actual values compared.
69
* `describe()`, `it()`, and `test_that()` now have a shared stack of descriptions so that if you nest any inside of each other, any resulting failures will show you the full path.

R/parallel-taskq.R

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
PROCESS_DONE <- 200L
1313
PROCESS_STARTED <- 201L
1414
PROCESS_MSG <- 301L
15+
PROCESS_OUTPUT <- 302L
1516
PROCESS_EXITED <- 500L
1617
PROCESS_CRASHED <- 501L
1718
PROCESS_CLOSED <- 502L
@@ -50,7 +51,9 @@ task_q <- R6::R6Class(
5051
state = "waiting",
5152
fun = I(list(fun)),
5253
args = I(list(args)),
53-
worker = I(list(NULL))
54+
worker = I(list(NULL)),
55+
path = args[[1]],
56+
startup = I(list(NULL))
5457
)
5558
private$schedule()
5659
invisible(id)
@@ -62,15 +65,49 @@ task_q <- R6::R6Class(
6265
if (x == Inf) -1 else as.integer(as.double(x, "secs") * 1000)
6366
}
6467
repeat {
68+
pr <- vector(mode = "list", nrow(private$tasks))
6569
topoll <- which(private$tasks$state == "running")
66-
conns <- lapply(
70+
pr[topoll] <- processx::poll(
6771
private$tasks$worker[topoll],
68-
function(x) x$get_poll_connection()
72+
as_ms(timeout)
6973
)
70-
pr <- processx::poll(conns, as_ms(timeout))
71-
ready <- topoll[pr == "ready"]
72-
results <- lapply(ready, function(i) {
73-
msg <- private$tasks$worker[[i]]$read()
74+
results <- lapply(seq_along(pr), function(i) {
75+
# nothing from this worker?
76+
if (is.null(pr[[i]]) || all(pr[[i]] != "ready")) {
77+
return()
78+
}
79+
80+
# there is a testthat message?
81+
worker <- private$tasks$worker[[i]]
82+
msg <- if (pr[[i]][["process"]] == "ready") {
83+
worker$read()
84+
}
85+
86+
# there is an output message?
87+
has_output <- pr[[i]][["output"]] == "ready" ||
88+
pr[[i]][["error"]] == "ready"
89+
outmsg <- NULL
90+
if (has_output) {
91+
lns <- c(worker$read_output_lines(), worker$read_error_lines())
92+
inc <- paste0(worker$read_output(), worker$read_error())
93+
if (nchar(inc)) {
94+
lns <- c(lns, strsplit(inc, "\n", fixed = TRUE)[[1]])
95+
}
96+
# startup message?
97+
if (is.na(private$tasks$path[i])) {
98+
private$tasks$startup[[i]] <- c(private$tasks$startup[[i]], lns)
99+
} else {
100+
outmsg <- structure(
101+
list(
102+
code = PROCESS_OUTPUT,
103+
message = lns,
104+
path = private$tasks$path[i]
105+
),
106+
class = "testthat_message"
107+
)
108+
}
109+
}
110+
74111
## TODO: why can this be NULL?
75112
if (is.null(msg) || msg$code == PROCESS_MSG) {
76113
private$tasks$state[[i]] <- "running"
@@ -100,9 +137,10 @@ task_q <- R6::R6Class(
100137
class = c("testthat_process_error", "testthat_error")
101138
)
102139
}
103-
msg
140+
compact(list(msg, outmsg))
104141
})
105-
results <- results[!map_lgl(results, is.null)]
142+
# single list for all workers
143+
results <- compact(unlist(results, recursive = FALSE))
106144

107145
private$schedule()
108146
if (is.finite(timeout)) {
@@ -132,9 +170,11 @@ task_q <- R6::R6Class(
132170
state = "running",
133171
fun = nl,
134172
args = nl,
135-
worker = nl
173+
worker = nl,
174+
path = NA_character_,
175+
startup = nl
136176
)
137-
rsopts <- callr::r_session_options(...)
177+
rsopts <- callr::r_session_options(stdout = "|", stderr = "|", ...)
138178
for (i in seq_len(concurrency)) {
139179
rs <- callr::r_session$new(rsopts, wait = FALSE)
140180
private$tasks$worker[[i]] <- rs
@@ -176,7 +216,10 @@ task_q <- R6::R6Class(
176216
file <- private$tasks$args[[task_no]][[1]]
177217
if (is.null(fun)) {
178218
msg$error$stdout <- msg$stdout
179-
msg$error$stderr <- msg$stderr
219+
msg$error$stderr <- paste(
220+
c(private$tasks$startup[[task_no]], msg$stderr),
221+
collapse = "\n"
222+
)
180223
abort(
181224
paste0(
182225
"testthat subprocess failed to start, stderr:\n",

R/parallel.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,12 @@ parallel_event_loop_smooth <- function(queue, reporters, test_dir) {
143143

144144
updated <- FALSE
145145
for (x in msgs) {
146+
if (x$code == PROCESS_OUTPUT) {
147+
lns <- paste0("> ", x$path, ": ", x$message)
148+
cat("\n", file = stdout())
149+
base::writeLines(lns, stdout())
150+
next
151+
}
146152
if (x$code != PROCESS_MSG) {
147153
next
148154
}
@@ -178,6 +184,11 @@ parallel_event_loop_chunky <- function(queue, reporters, test_dir) {
178184
while (!queue$is_idle()) {
179185
msgs <- queue$poll(Inf)
180186
for (x in msgs) {
187+
if (x$code == PROCESS_OUTPUT) {
188+
lns <- paste0("> ", x$path, ": ", x$message)
189+
base::writeLines(lns, stdout())
190+
next
191+
}
181192
if (x$code != PROCESS_MSG) {
182193
next
183194
}

R/reporter-stop.R

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,53 @@ StopReporter <- R6::R6Class(
1414
"StopReporter",
1515
inherit = Reporter,
1616
public = list(
17-
failures = NULL,
17+
# All expectations that need to be reported (error, failure, warning, skip)
18+
issues = NULL,
19+
# Expectations that should cause the test to fail (error, failure)
1820
n_fail = 0L,
21+
# Successful expectations
22+
n_success = 0L,
1923
stop_reporter = TRUE,
2024
praise = TRUE,
2125

2226
initialize = function(stop_reporter = TRUE, praise = TRUE) {
2327
super$initialize()
24-
self$failures <- Stack$new()
28+
self$issues <- Stack$new()
2529
self$praise <- praise
2630
self$stop_reporter <- stop_reporter
2731
},
2832

2933
start_test = function(context, test) {
30-
self$failures <- Stack$new()
34+
self$issues <- Stack$new()
3135
},
3236

3337
add_result = function(context, test, result) {
3438
if (expectation_success(result)) {
39+
self$n_success <- self$n_success + 1
3540
return()
3641
}
3742

3843
if (expectation_broken(result)) {
3944
self$n_fail <- self$n_fail + 1
4045
}
41-
42-
self$failures$push(result)
46+
self$issues$push(result)
4347
},
4448

4549
end_test = function(context, test) {
4650
self$local_user_output()
4751

48-
failures <- self$failures$as_list()
49-
if (length(failures) == 0 && self$praise) {
50-
self$cat_line(colourise("Test passed", "success"), " ", praise_emoji())
51-
return()
52-
}
53-
54-
messages <- map_chr(failures, issue_summary, rule = TRUE)
55-
if (length(messages) > 0) {
52+
if (self$issues$size() == 0) {
53+
if (self$praise && self$n_success > 0) {
54+
emoji <- praise_emoji()
55+
self$cat_line(colourise("Test passed", "success"), " ", emoji)
56+
}
57+
} else {
58+
issues <- self$issues$as_list()
59+
messages <- map_chr(issues, issue_summary, rule = TRUE)
5660
self$cat_line(messages, "\n")
5761
}
5862
},
63+
5964
stop_if_needed = function() {
6065
if (self$stop_reporter && self$n_fail > 0) {
6166
abort("Test failed", call = NULL)

R/snapshot.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ expect_snapshot_helper <- function(
313313
trace_env = caller_env()
314314
) {
315315
if (!cran && on_cran()) {
316-
skip("On CRAN")
316+
return(invisible())
317317
}
318318

319319
snapshotter <- get_snapshotter()

R/test-that.R

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
149149
register_expectation(e, debug_end)
150150
invokeRestart("end_test")
151151
}
152+
handle_interrupt <- function(e) {
153+
if (!is.null(test)) {
154+
cat("\n")
155+
cli::cli_inform(c("!" = "Interrupting test: {test}"))
156+
}
157+
}
152158

153159
test_env <- new.env(parent = env)
154160
old <- options(rlang_trace_top_env = test_env)[[1]]
@@ -167,10 +173,16 @@ test_code <- function(code, env, reporter = NULL, skip_on_empty = TRUE) {
167173
}
168174
},
169175
expectation = handle_expectation,
176+
packageNotFoundError = function(e) {
177+
if (on_cran()) {
178+
skip(paste0("{", e$package, "} is not installed."))
179+
}
180+
},
170181
skip = handle_skip,
171182
warning = handle_warning,
172183
message = handle_message,
173-
error = handle_error
184+
error = handle_error,
185+
interrupt = handle_interrupt
174186
),
175187
# some errors may need handling here, e.g., stack overflow
176188
error = handle_fatal
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
test_that("stdout/stderr in parallel code", {
2+
skip_on_covr()
3+
withr::local_envvar(TESTTHAT_PARALLEL = "TRUE")
4+
out <- capture.output(suppressMessages(testthat::test_local(
5+
test_path("test-parallel", "stdout"),
6+
reporter = "summary"
7+
)))
8+
expect_true("> test-stdout-2.R: This is a message!" %in% out)
9+
expect_true(any(grepl("test-stdout-3.R: [1] 1 2 3", out, fixed = TRUE)))
10+
11+
out2 <- capture.output(suppressMessages(testthat::test_local(
12+
test_path("test-parallel", "stdout"),
13+
reporter = "progress"
14+
)))
15+
expect_true("> test-stdout-2.R: This is a message!" %in% out2)
16+
expect_true(any(grepl("test-stdout-3.R: [1] 1 2 3", out2, fixed = TRUE)))
17+
})
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
Package: setup
2+
Title: What the Package Does (One Line, Title Case)
3+
Version: 0.0.0.9000
4+
Authors@R:
5+
person(given = "First",
6+
family = "Last",
7+
role = c("aut", "cre"),
8+
email = "[email protected]",
9+
comment = c(ORCID = "YOUR-ORCID-ID"))
10+
Description: What the package does (one paragraph).
11+
License: `use_mit_license()`, `use_gpl3_license()` or friends to
12+
pick a license
13+
Encoding: UTF-8
14+
LazyData: true
15+
Roxygen: list(markdown = TRUE)
16+
RoxygenNote: 7.1.1
17+
Suggests:
18+
testthat
19+
Config/testthat/parallel: true
20+
Config/testthat/edition: 3
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Generated by roxygen2: do not edit by hand
2+
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
library(testthat)
2+
library(ok)
3+
4+
test_check("ok")

0 commit comments

Comments
 (0)