Skip to content

Commit 502f880

Browse files
committed
Merge commit 'fa29578e09f9eeafc6b9822364e29b07c8bd7a8c'
2 parents fc0fac2 + fa29578 commit 502f880

28 files changed

+344
-172
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
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).
4+
* Power `expect_mapequal()` with `waldo::compare(list_as_map = TRUE)` (#1521).
35
* 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.)
46
* `expect_snapshot()` no longer skips on CRAN, as that skips the rest of the test. Instead it just returns, neither succeeding nor failing (#1585).
57
* Interrupting a test now prints the test name. This makes it easier to tell where a very slow test might be hanging (#1464)

β€ŽR/expect-setequal.Rβ€Ž

Lines changed: 4 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
#' (i.e. `y` is a subset of `x`).
77
#' * `expect_in(x, y)` tests every element of `x` is in `y`
88
#' (i.e. `x` is a subset of `y`).
9-
#' * `expect_mapequal(x, y)` tests that `x` and `y` have the same names, and
10-
#' that `x[names(y)]` equals `y`.
9+
#' * `expect_mapequal(x, y)` treats lists as if they are mappings between names
10+
#' and values. Concretely, this drops `NULL`s in both objects and sorts
11+
#' named components.
1112
#'
1213
#' Note that `expect_setequal()` ignores names, and you will be warned if both
1314
#' `object` and `expected` have them.
@@ -77,37 +78,7 @@ expect_mapequal <- function(object, expected) {
7778
act <- quasi_label(enquo(object))
7879
exp <- quasi_label(enquo(expected))
7980

80-
check_vector(object)
81-
check_map_names(object)
82-
check_vector(expected)
83-
check_map_names(expected)
84-
85-
# Length-0 vectors are OK whether named or unnamed.
86-
if (length(act$val) == 0 && length(exp$val) == 0) {
87-
testthat_warn("`object` and `expected` are empty lists")
88-
return(pass(act$val))
89-
}
90-
91-
act_nms <- names(act$val)
92-
exp_nms <- names(exp$val)
93-
if (setequal(act_nms, exp_nms)) {
94-
act <- labelled_value(act$val[exp_nms], act$lab)
95-
return(expect_waldo_equal_("equal", act, exp))
96-
}
97-
98-
act_miss <- setdiff(exp_nms, act_nms)
99-
if (length(act_miss) > 0) {
100-
vals <- paste0(encodeString(act_miss, quote = '"'), ", ")
101-
return(fail(paste0("Names absent from `object`: ", vals)))
102-
}
103-
104-
exp_miss <- setdiff(act_nms, exp_nms)
105-
if (length(exp_miss) > 0) {
106-
vals <- paste0(encodeString(exp_miss, quote = '"'), ", ")
107-
return(fail(paste0("Names absent from `expected`: ", vals)))
108-
}
109-
110-
pass(act$val)
81+
expect_waldo_equal_("equal", act, exp, list_as_map = TRUE)
11182
}
11283

11384
#' @export

β€Ž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.

0 commit comments

Comments
Β (0)