Skip to content

Commit 78a4785

Browse files
hadleyjennybc
andauthored
Optionally report on lack of coverage (#2654)
* Optionally report on lack of coverage And use that display for LLMs and in non-interactive contexts. It may have been lazy to not deprecate `show_report`, but I just can't imagine anyone is actually using it. Let me know if you think I'm wrong. Fixes #2632 * Move the requirement for DT to the interactive report case * Fix so that claude can run this test file * Avoid covr-ception * Mention removal of `show_report`, in case someone's confused --------- Co-authored-by: Jenny Bryan <jenny.f.bryan@gmail.com>
1 parent d3a352b commit 78a4785

6 files changed

Lines changed: 184 additions & 24 deletions

File tree

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
* `load_all()` now errors if called recursively, i.e. if you accidentally include a `load_all()` call in one of your R source files (#2617).
1313
* `release()` is deprecated in favour of `usethis::use_release_issue()`.
1414
* `show_news()` now looks for NEWS files in the same locations as `utils::news()`: `inst/NEWS.Rd`, `NEWS.md`, `NEWS`, and `inst/NEWS` (@arcresu, #2499).
15+
* `test_coverage()` and `test_coverage_active_file()` gain a new `report` argument that can be set to `"html"` (the default, for an interactive browser report), `"zero"` (prints uncovered lines to the console, used for LLMs and non-interactive contexts), or `"silent"`. The `show_report` argument has been removed (#2632).
1516
* `test_file()` and `test_coverage_file()` are now defunct. These were deprecated in devtools 2.4.0 (2021-04-07) in favour of `test_active_file()` and `test_coverage_active_file()`. Removing `test_file()` eliminates the conflict with `testthat::test_file()`.
1617

1718
# devtools 2.4.6

R/test.R

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,16 @@ load_package_for_testing <- function(pkg) {
8686
}
8787
}
8888

89-
#' @param show_report Show the test coverage report.
89+
#' @param report How to display the coverage report.
90+
#' * `"html"` opens an interactive report in the browser.
91+
#' * `"zero"` prints uncovered lines to the console.
92+
#' * `"silent"` returns the coverage object without display.
93+
#'
94+
#' Defaults to `"html"` if interactive; otherwise to `"zero"`.
9095
#' @export
9196
#' @rdname test
92-
test_coverage <- function(pkg = ".", show_report = interactive(), ...) {
93-
rlang::check_installed(c("covr", "DT"))
97+
test_coverage <- function(pkg = ".", report = NULL, ...) {
98+
rlang::check_installed("covr")
9499

95100
save_all()
96101
pkg <- as.package(pkg)
@@ -101,23 +106,19 @@ test_coverage <- function(pkg = ".", show_report = interactive(), ...) {
101106
withr::local_envvar(r_env_vars())
102107
coverage <- covr::package_coverage(pkg$path, ...)
103108

104-
if (isTRUE(show_report)) {
105-
covr::report(coverage)
106-
}
107-
108-
invisible(coverage)
109+
show_report(coverage, report = report, path = pkg$path)
109110
}
110111

111112
#' @rdname test
112113
#' @export
113114
test_coverage_active_file <- function(
114115
file = find_active_file(),
115116
filter = TRUE,
116-
show_report = interactive(),
117+
report = NULL,
117118
export_all = TRUE,
118119
...
119120
) {
120-
rlang::check_installed(c("covr", "DT"))
121+
rlang::check_installed("covr")
121122
check_dots_used(action = getOption("devtools.ellipsis_action", rlang::warn))
122123

123124
test_file <- find_test_file(file)
@@ -159,17 +160,7 @@ test_coverage_active_file <- function(
159160
attr(coverage, "relative") <- TRUE
160161
attr(coverage, "package") <- pkg
161162

162-
if (isTRUE(show_report)) {
163-
covered <- unique(covr::display_name(coverage))
164-
165-
if (length(covered) == 1) {
166-
covr::file_report(coverage)
167-
} else {
168-
covr::report(coverage)
169-
}
170-
}
171-
172-
invisible(coverage)
163+
show_report(coverage, report = report, path = pkg$path)
173164
}
174165

175166

@@ -188,6 +179,64 @@ uses_testthat <- function(pkg = ".") {
188179
any(dir_exists(paths))
189180
}
190181

182+
report_default <- function(report, call = rlang::caller_env()) {
183+
if (is.null(report)) {
184+
if (is_llm() || !rlang::is_interactive()) "zero" else "html"
185+
} else {
186+
rlang::arg_match(report, c("silent", "zero", "html"), error_call = call)
187+
}
188+
}
189+
190+
show_report <- function(coverage, report, path, call = rlang::caller_env()) {
191+
report <- report_default(report, call = call)
192+
193+
if (report == "html") {
194+
rlang::check_installed("DT")
195+
covered <- unique(covr::display_name(coverage))
196+
197+
if (length(covered) == 1) {
198+
covr::file_report(coverage)
199+
} else {
200+
covr::report(coverage)
201+
}
202+
} else if (report == "zero") {
203+
zero <- covr::zero_coverage(coverage)
204+
if (nrow(zero) == 0) {
205+
cli::cli_inform(c(v = "All lines covered!"))
206+
} else {
207+
for (file in unique(zero$filename)) {
208+
file_zero <- zero[zero$filename == file, ]
209+
lines_by_fun <- split(file_zero$line, file_zero$functions)
210+
211+
rel_path <- path_rel(file, path)
212+
cli::cli_inform("Uncovered lines in {.file {rel_path}}:")
213+
for (fun in names(lines_by_fun)) {
214+
lines <- paste0(collapse_lines(lines_by_fun[[fun]]), collapse = ", ")
215+
cli::cli_inform(c("*" = "{.fn {fun}}: {lines}"))
216+
}
217+
}
218+
}
219+
}
220+
invisible(coverage)
221+
}
222+
223+
collapse_lines <- function(x) {
224+
x <- sort(unique(x))
225+
breaks <- c(0, which(diff(x) != 1), length(x))
226+
227+
ranges <- character(length(breaks) - 1)
228+
for (i in seq_along(ranges)) {
229+
start <- x[breaks[i] + 1]
230+
end <- x[breaks[i + 1]]
231+
if (start == end) {
232+
ranges[i] <- as.character(start)
233+
} else {
234+
ranges[i] <- paste0(start, "-", end)
235+
}
236+
}
237+
ranges
238+
}
239+
191240

192241
#' Defunct functions
193242
#'

R/utils.R

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,14 @@ is_rstudio_running <- function() {
4949
!is_testing() && rstudioapi::isAvailable()
5050
}
5151

52+
# Copied from testthat:::is_llm()
53+
is_llm <- function() {
54+
nzchar(Sys.getenv("AGENT")) ||
55+
nzchar(Sys.getenv("CLAUDECODE")) ||
56+
nzchar(Sys.getenv("GEMINI_CLI")) ||
57+
nzchar(Sys.getenv("CURSOR_AGENT"))
58+
}
59+
5260
# Suppress cli wrapping
5361
no_wrap <- function(x) {
5462
x <- gsub("{", "{{", x, fixed = TRUE)

man/test.Rd

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

tests/testthat/_snaps/test.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# test_coverage_active_file() computes coverage
2+
3+
Code
4+
test_coverage_active_file(file.path(pkg, "R", "math.R"), report = "zero")
5+
Message
6+
Uncovered lines in 'R/math.R':
7+
* `compute()`: 4-5
8+
* `multiply()`: 2
9+
10+
# test_coverage_active_file() reports full coverage
11+
12+
Code
13+
test_coverage_active_file(file.path(pkg, "R", "math.R"), report = "zero")
14+
Message
15+
v All lines covered!
16+
17+
# report_default() does its job
18+
19+
Code
20+
report_default("bad")
21+
Condition
22+
Error:
23+
! `report` must be one of "silent", "zero", or "html", not "bad".
24+

tests/testthat/test-test.R

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,74 @@ test_that("stop_on_failure defaults to FALSE", {
5959
"Test failures"
6060
)
6161
})
62+
63+
test_that("test_coverage_active_file() computes coverage", {
64+
skip_on_covr()
65+
pkg <- local_package_create()
66+
writeLines(
67+
c(
68+
"add <- function(x, y) x + y",
69+
"multiply <- function(x, y) x * y",
70+
"compute <- function(x) {",
71+
" x + 1",
72+
" x + 2",
73+
"}"
74+
),
75+
file.path(pkg, "R", "math.R")
76+
)
77+
dir_create(file.path(pkg, "tests", "testthat"))
78+
writeLines(
79+
c(
80+
"test_that('add works', {",
81+
" expect_equal(add(1, 2), 3)",
82+
"})"
83+
),
84+
file.path(pkg, "tests", "testthat", "test-math.R")
85+
)
86+
87+
expect_snapshot(test_coverage_active_file(
88+
file.path(pkg, "R", "math.R"),
89+
report = "zero"
90+
))
91+
})
92+
93+
test_that("test_coverage_active_file() reports full coverage", {
94+
skip_on_covr()
95+
pkg <- local_package_create()
96+
writeLines(
97+
"add <- function(x, y) x + y",
98+
file.path(pkg, "R", "math.R")
99+
)
100+
dir_create(file.path(pkg, "tests", "testthat"))
101+
writeLines(
102+
c(
103+
"test_that('add works', {",
104+
" expect_equal(add(1, 2), 3)",
105+
"})"
106+
),
107+
file.path(pkg, "tests", "testthat", "test-math.R")
108+
)
109+
110+
expect_snapshot(test_coverage_active_file(
111+
file.path(pkg, "R", "math.R"),
112+
report = "zero"
113+
))
114+
})
115+
116+
test_that("report_default() does its job", {
117+
withr::local_options(rlang_interactive = FALSE)
118+
expect_equal(report_default(NULL), "zero")
119+
120+
withr::local_options(rlang_interactive = TRUE)
121+
if (!is_llm()) {
122+
expect_equal(report_default(NULL), "html")
123+
}
124+
125+
withr::local_envvar(AGENT = 1)
126+
expect_equal(report_default(NULL), "zero")
127+
128+
expect_equal(report_default("silent"), "silent")
129+
expect_equal(report_default("zero"), "zero")
130+
expect_equal(report_default("html"), "html")
131+
expect_snapshot(report_default("bad"), error = TRUE)
132+
})

0 commit comments

Comments
 (0)