Skip to content

Commit fa29578

Browse files
authored
Slow test reporter (#2166)
Initial prototype by claude, but lots of iteration by me. Fixes #1466
1 parent f303e68 commit fa29578

24 files changed

+321
-1
lines changed

CLAUDE.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@ General advice:
1818
- `devtools::test_file("tests/testthat/test-filename.R")` - Run tests in a specific file
1919
- DO NOT USE `devtools::test_active_file()`
2020
- `devtools::load_all()` - Load package for development
21-
- `devtools::document()` - Generate documentation
2221
- `devtools::check()` - Run R CMD check
2322
- `devtools::install()` - Install package locally
2423

24+
### Documentation
25+
26+
- Always run `devtools::document()` after changing any roxygen2 docs.
27+
2528
## Core Architecture
2629

2730
### Main Components

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export(ProgressReporter)
4545
export(RStudioReporter)
4646
export(Reporter)
4747
export(SilentReporter)
48+
export(SlowReporter)
4849
export(StopReporter)
4950
export(SummaryReporter)
5051
export(TapReporter)

NEWS.md

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

3+
* 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).
34
* Power `expect_mapequal()` with `waldo::compare(list_as_map = TRUE)` (#1521).
45
* 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.)
56
* `expect_snapshot()` no longer skips on CRAN, as that skips the rest of the test. Instead it just returns, neither succeeding nor failing (#1585).

R/reporter-slow.R

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
#' Test reporter: show timings for slow tests
2+
#'
3+
#' @description
4+
#' `SlowReporter` is designed to identify slow tests. It reports the
5+
#' execution time for each test and can optionally filter out tests that
6+
#' run faster than a specified threshold (default: 1 second). This reporter
7+
#' is useful for performance optimization and identifying tests that may
8+
#' benefit from optimization or parallelization.
9+
#'
10+
#' @export
11+
#' @family reporters
12+
SlowReporter <- R6::R6Class(
13+
"SlowReporter",
14+
inherit = Reporter,
15+
public = list(
16+
min_time = 0.5,
17+
test_timings = NULL,
18+
current_test_start = NULL,
19+
current_file = NULL,
20+
21+
initialize = function(min_time = 0.5, ...) {
22+
super$initialize(...)
23+
self$min_time <- min_time
24+
self$test_timings <- list()
25+
},
26+
27+
start_reporter = function(context) {
28+
self$cat_line(
29+
cli::style_bold("Slow tests"),
30+
" (showing tests >= ",
31+
self$min_time,
32+
"s)"
33+
)
34+
self$cat_line()
35+
},
36+
37+
start_file = function(file) {
38+
self$current_file <- file
39+
},
40+
41+
start_test = function(context, test) {
42+
self$current_test_start <- proc.time()[[3]]
43+
},
44+
45+
end_test = function(context, test) {
46+
if (is.null(self$current_test_start)) {
47+
return()
48+
}
49+
50+
time_taken <- proc.time()[[3]] - self$current_test_start
51+
52+
# Store timing information
53+
timing <- list(
54+
file = self$current_file,
55+
test = test,
56+
time = time_taken
57+
)
58+
self$test_timings[[length(self$test_timings) + 1]] <- timing
59+
60+
if (time_taken >= self$min_time) {
61+
self$show_timing(timing)
62+
}
63+
64+
self$current_test_start <- NULL
65+
},
66+
67+
end_reporter = function() {
68+
if (length(self$test_timings) == 0) {
69+
return()
70+
}
71+
72+
all_times <- map_dbl(self$test_timings, \(x) x$time)
73+
is_slow <- all_times >= self$min_time
74+
75+
self$cat_line()
76+
self$rule(cli::style_bold("Summary"), line = 2)
77+
self$cat_line("All tests: ", sprintf("%.2fs", sum(all_times)))
78+
self$cat_line("Slow tests: ", sprintf("%.2fs", sum(all_times[is_slow])))
79+
80+
if (sum(is_slow) <= 10) {
81+
return()
82+
}
83+
84+
# Sort by time descending for summary
85+
slowest <- self$test_timings[order(all_times, decreasing = TRUE)]
86+
87+
self$cat_line()
88+
self$rule(cli::style_bold("Slowest tests:"), line = 1)
89+
90+
# Show top 10 slowest tests
91+
for (i in 1:10) {
92+
self$show_timing(slowest[[i]])
93+
}
94+
95+
if (length(slowest) > 10) {
96+
self$cat_line("... and ", length(slowest) - 10, " more slow tests")
97+
}
98+
99+
self$cat_line()
100+
},
101+
show_timing = function(timing) {
102+
time <- sprintf("%.2fs", timing$time)
103+
self$cat_line("[", time, "] ", time, " ", timing$file, ": ", timing$test)
104+
}
105+
)
106+
)

R/reporter-timing.R

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#' Test reporter: show timings for slow tests
2+
#'
3+
#' @description
4+
#' `SlowReporter` is designed to identify slow tests. It reports the
5+
#' execution time for each test, ignoring tests faster than a specified
6+
#' threshold (default: 0.5s).
7+
#'
8+
#' The easiest way to run it over your package is with
9+
#' `devtools::test(reporter = "slow")`.
10+
#'
11+
#' @export
12+
#' @family reporters
13+
SlowReporter <- R6::R6Class(
14+
"SlowReporter",
15+
inherit = Reporter,
16+
public = list(
17+
min_time = NA_real_,
18+
test_timings = NULL,
19+
current_test_start = NULL,
20+
current_file = NULL,
21+
22+
initialize = function(min_time = 0.5, ...) {
23+
check_number_decimal(min_time, min = 0)
24+
25+
super$initialize(...)
26+
self$min_time <- min_time
27+
self$test_timings <- list()
28+
},
29+
30+
start_file = function(file) {
31+
self$current_file <- file
32+
},
33+
34+
start_test = function(context, test) {
35+
self$current_test_start <- proc.time()[[3]]
36+
},
37+
38+
end_test = function(context, test) {
39+
if (is.null(self$current_test_start)) {
40+
return()
41+
}
42+
43+
time_taken <- proc.time()[[3]] - self$current_test_start
44+
45+
# Store timing information
46+
timing <- list(
47+
file = self$current_file,
48+
test = test,
49+
time = time_taken
50+
)
51+
self$test_timings[[length(self$test_timings) + 1]] <- timing
52+
53+
if (time_taken >= self$min_time) {
54+
self$show_timing(timing)
55+
}
56+
57+
self$current_test_start <- NULL
58+
},
59+
60+
end_reporter = function() {
61+
if (length(self$test_timings) == 0) {
62+
return()
63+
}
64+
65+
all_times <- map_dbl(self$test_timings, \(x) x$time)
66+
is_slow <- all_times >= self$min_time
67+
68+
self$cat_line()
69+
self$rule(cli::style_bold("Summary"), line = 2)
70+
self$cat_line("All tests: ", sprintf("%.2fs", sum(all_times)))
71+
self$cat_line("Slow tests: ", sprintf("%.2fs", sum(all_times[is_slow])))
72+
73+
if (sum(is_slow) <= 10) {
74+
return()
75+
}
76+
77+
# Sort by time descending for summary
78+
slowest <- self$test_timings[order(all_times, decreasing = TRUE)]
79+
80+
self$cat_line()
81+
self$rule(cli::style_bold("Slowest tests:"), line = 1)
82+
83+
# Show top 10 slowest tests
84+
for (i in 1:10) {
85+
self$show_timing(slowest[[i]])
86+
}
87+
88+
if (length(slowest) > 10) {
89+
self$cat_line("... and ", length(slowest) - 10, " more slow tests")
90+
}
91+
92+
self$cat_line()
93+
},
94+
show_timing = function(timing) {
95+
time <- sprintf("%.2fs", timing$time)
96+
self$cat_line("[", time, "] ", timing$file, ": ", timing$test)
97+
}
98+
)
99+
)

man/CheckReporter.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/DebugReporter.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/FailReporter.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/JunitReporter.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/ListReporter.Rd

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)