Skip to content
Merged
3 changes: 2 additions & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Suggests:
RSQLite (>= 2.2.2),
shiny,
shinychat (>= 0.4.0),
testthat (>= 3.0.0),
testthat (>= 3.1.7),
tibble,
usethis
Config/Needs/website: brand.yml, tidyverse/tidytemplate
Expand Down Expand Up @@ -113,6 +113,7 @@ Collate:
'import-standalone-purrr.R'
'import-standalone-types-check.R'
'mcp.R'
'pkg-test-reporter.R'
'task_create_btw_md.R'
'task_create_readme.R'
'task_create_skill.R'
Expand Down
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# btw (development version)

* Package tests now support a compact reporter that shows per-file progress, timing, warning and failure details, and final counts. `btw pkg test` uses it by default, while `btw_tool_pkg_test()` defaults to concise output for non-streaming clients; both accept other testthat reporter names (#220).

# btw 1.5.0

## New features
Expand Down
2 changes: 1 addition & 1 deletion R/cli.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ install_btw_cli <- function(destdir = NULL, ...) {
"pkgload",
"callr",
"covr",
"testthat",
"testthat (>= 3.1.7)",
"rmarkdown",
"pkgsearch"
))
Expand Down
167 changes: 167 additions & 0 deletions R/pkg-test-reporter.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
# Compact testthat reporter used by btw pkg test and btw_tool_pkg_test().

btw_test_name <- function(file) {
sub("\\.[rR]$", "", sub("^test-", "", basename(file)))
}

btw_test_duration <- function(seconds) {
decimals <- if (seconds < 10) 2L else if (seconds < 100) 1L else 0L
if (round(seconds, decimals) >= 10 && decimals == 2L) {
decimals <- 1L
}
if (round(seconds, decimals) >= 100 && decimals == 1L) {
decimals <- 0L
}
sprintf(paste0("%.", decimals, "fs"), seconds)
}

btw_test_location <- function(result) {
ref <- result$srcref
if (!inherits(ref, "srcref")) {
return("")
}
filename <- attr(ref, "srcfile")$filename
if (is.null(filename)) {
return("")
}
sprintf("%s:%s:%s", filename, ref[[1]], ref[[2]])
}

btw_test_type <- function(result) {
sub("^expectation_", "", class(result)[[1]])
}

btw_compact_reporter <- function() {
rlang::check_installed("R6")

R6::R6Class("BtwCompactReporter", inherit = testthat::Reporter, public = list(
name = NULL,
file_id = NULL,
files = NULL,
color = NULL,
counts = NULL,
failures = NULL,
warnings = NULL,

initialize = function() {
super$initialize()
self$capabilities$parallel_support <- TRUE
self$capabilities$parallel_updates <- TRUE
self$files <- list()
self$color <- identical(self$out, stdout()) &&
sink.number() == 0L && cli::num_ansi_colors() > 1L
self$counts <- c(P = 0L, F = 0L, S = 0L, W = 0L)
self$failures <- list()
self$warnings <- list()
},
colorize = function(text, style) {
if (!self$color) {
return(text)
}
switch(
style,
muted = cli::col_grey(text),
pass = cli::col_green(text),
fail = cli::col_red(text),
warn = cli::col_yellow(text)
)
},
start_file = function(name) {
self$file_id <- name
self$name <- btw_test_name(name)
# In testthat's parallel mode, start_file() is repeated for every
# event. Keep each running file's timer and counts across events.
if (!is.null(self$files[[self$file_id]])) {
return(invisible(NULL))
}
self$files[[self$file_id]] <- list(
started = proc.time()[[3L]],
counts = c(P = 0L, F = 0L, S = 0L, W = 0L)
)
self$cat_line(self$colorize(paste0("@ ", self$name), "muted"))
if (identical(self$out, stdout())) flush(stdout())
},
add_result = function(context, test, result) {
type <- btw_test_type(result)
key <- if (type %in% c("failure", "error")) {
self$failures <- c(self$failures, list(result))
"F"
} else if (type == "skip") {
"S"
} else if (type == "warning") {
self$warnings <- c(self$warnings, list(result))
"W"
} else {
"P"
}
file <- self$files[[self$file_id]]
file$counts[[key]] <- file$counts[[key]] + 1L
self$files[[self$file_id]] <- file
self$counts[[key]] <- self$counts[[key]] + 1L
},
end_file = function() {
file <- self$files[[self$file_id]]
if (is.null(file)) {
return(invisible(NULL))
}
elapsed <- proc.time()[[3L]] - file$started
counts <- file$counts
status <- if (counts[["F"]] > 0L) {
"\u2717"
} else if (counts[["W"]] > 0L) {
"!"
} else {
"\u2713"
}
counts <- counts[counts > 0L]
if (!length(counts)) {
counts <- c(P = 0L)
}
summary <- paste(vapply(names(counts), function(key) {
style <- switch(key, P = "pass", F = "fail", S = "muted", W = "warn")
self$colorize(paste0(key, ":", counts[[key]]), style)
}, ""), collapse = " ")
styled_status <- self$colorize(
status, switch(status, "\u2713" = "pass", "\u2717" = "fail", "warn")
)
styled_time <- self$colorize(btw_test_duration(elapsed), "muted")
self$cat_line(sprintf(
"%s %s %s %s",
styled_status, self$name, styled_time, summary
))
if (identical(self$out, stdout())) flush(stdout())
self$files[[self$file_id]] <- NULL
},
end_reporter = function() {
if (length(self$warnings)) {
self$cat_line()
self$cat_line(self$colorize("======== WARNINGS ========", "warn"))
for (warning in self$warnings) {
self$cat_line(
self$colorize("WARN", "warn"), ": ",
btw_test_location(warning)
)
self$cat_line(format(warning))
self$cat_line()
}
}
if (length(self$failures)) {
self$cat_line()
self$cat_line(self$colorize("======== FAILURES ========", "fail"))
for (failure in self$failures) {
self$cat_line(
self$colorize(toupper(btw_test_type(failure)), "fail"),
": ", btw_test_location(failure)
)
self$cat_line(format(failure))
self$cat_line()
}
}
self$cat_line(sprintf(
"[ FAIL %d | WARN %d | SKIP %d | PASS %d ]",
self$counts[["F"]], self$counts[["W"]], self$counts[["S"]], self$counts[["P"]]
))
if (identical(self$out, stdout())) flush(stdout())
}
))$new()
}
68 changes: 50 additions & 18 deletions R/tool-pkg-devtools.R
Original file line number Diff line number Diff line change
Expand Up @@ -153,42 +153,69 @@ For iterative development, use the `btw_tool_pkg_test` if available or `devtools
#' Tool: Run package tests
#'
#' Run package tests using [devtools::test()]. Optionally filter tests by name
#' pattern.
#' pattern. The default `"minimal"` reporter returns failures and a final
#' summary without per-file progress, which suits non-streaming tool clients.
#' Use `"compact"` to include file starts, per-file results, and timings, or
#' pass a testthat reporter name.
#'
#' @param pkg Path to package directory. Defaults to '.'. Must be within
#' current working directory.
#' @param filter Optional regex to filter test files. Example: 'helper' matches
#' 'test-helper.R'.
#' @param reporter Either `"minimal"` (the default), `"compact"` (per-file
#' progress and timing), or a testthat reporter name passed to
#' [devtools::test()].
#' @inheritParams btw_tool_docs_package_news
#'
#' @returns The output from [devtools::test()].
#'
#' @seealso [btw_tools()]
#' @family pkg tools
#' @export
btw_tool_pkg_test <- function(pkg = ".", filter = NULL, `_intent`) {}
btw_tool_pkg_test <- function(pkg = ".", filter = NULL, reporter = "minimal", `_intent`) {}

btw_tool_pkg_test_impl <- function(pkg = ".", filter = NULL) {
btw_pkg_test_validate <- function(pkg, filter, reporter) {
check_string(pkg)
check_path_within_current_wd(pkg)
check_string(filter, allow_null = TRUE)
check_string(reporter)
}

filter_arg <- if (!is.null(filter)) {
check_string(filter)
sprintf(', filter = "%s"', filter)
} else {
""
}
btw_pkg_test_run <- function(pkg = ".", filter = NULL, reporter = "compact") {
btw_pkg_test_validate(pkg, filter, reporter)
withr::local_envvar(TESTTHAT_PROBLEMS = "false")

resolved_reporter <- switch(
reporter,
compact = if (utils::packageVersion("testthat") >= "3.1.7") {
btw_compact_reporter()
} else {
# Older testthat versions don't call the per-file reporter hooks.
"check"
},
minimal = if (utils::packageVersion("testthat") >= "3.3.2") "llm" else "check",
reporter
)
invisible(devtools::test(
pkg = pkg,
filter = filter,
stop_on_failure = FALSE,
export_all = TRUE,
reporter = resolved_reporter
))
}

rptr <- if (utils::packageVersion("testthat") >= "3.3.2") "llm" else "check"
btw_tool_pkg_test_impl <- function(pkg = ".", filter = NULL, reporter = "minimal") {
rlang::check_installed("testthat", version = "3.1.7")
btw_pkg_test_validate(pkg, filter, reporter)

# Use one runner for both the captured tool output and the streaming CLI.
code <- sprintf(
'devtools::test(pkg = "%s"%s, stop_on_failure = FALSE, export_all = TRUE, reporter = "%s")',
pkg,
filter_arg,
rptr
"btw:::btw_pkg_test_run(pkg = %s, filter = %s, reporter = %s)",
encodeString(pkg, quote = '"'),
if (is.null(filter)) "NULL" else encodeString(filter, quote = '"'),
encodeString(reporter, quote = '"')
)

withr::local_envvar(TESTTHAT_PROBLEMS = "false")
btw_tool_run_r_impl(code)
}

Expand All @@ -204,15 +231,16 @@ btw_tool_pkg_test_impl <- function(pkg = ".", filter = NULL) {
Runs `devtools::test()` which executes the test suite in tests/testthat/ and reports:
- Number of tests passed, failed, warned, and skipped
- Detailed failure messages with file locations
- Test execution time

The `compact` reporter also shows per-file progress and execution time.

The filter parameter accepts a regular expression matched against test file names after stripping the 'test-' prefix and '.R' extension. For example:
- filter = 'helper' runs test-helper.R
- filter = 'tool-.*' runs test-tool-docs.R, test-tool-files.R, etc.
- No filter runs all tests
- It is common to pair `test-{name}.R` with a source `{name}.R` file. To test this file, you can generally use filter = '{name}'.

Use `filter` when working on specific functionality to get faster feedback. The tool always runs all matching tests to completion regardless of failures.",
Use `filter` when working on specific functionality to get faster feedback. The tool always runs all matching tests to completion regardless of failures. `reporter = 'minimal'` (default) keeps output short for non-streaming clients, with failures and a final summary. Use `'compact'` for per-file progress and timing; the CLI defaults to compact. Other testthat reporter names are passed through.",
annotations = ellmer::tool_annotations(
title = "Testing package",
read_only_hint = FALSE,
Expand All @@ -227,6 +255,10 @@ Use `filter` when working on specific functionality to get faster feedback. The
filter = ellmer::type_string(
"Optional regex to filter test files. Example: 'helper' matches 'test-helper.R'.",
required = FALSE
),
reporter = ellmer::type_string(
"Reporter: 'minimal' (default, brief output), 'compact' (per-file progress and timing), or any testthat reporter name.",
required = FALSE
)
)
)
Expand Down
22 changes: 15 additions & 7 deletions exec/btw.R
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,13 @@ btw_pkg_check <- function(path) {
btw_output(btw:::btw_tool_pkg_check_impl(path))
}

btw_pkg_test <- function(path, filter) {
btw_output(
btw:::btw_tool_pkg_test_impl(
path,
if (has_value(filter)) filter else NULL
)
btw_pkg_test <- function(path, filter, reporter) {
# Run directly rather than via the capturing tool, so file starts are visible
# to tail -f while tests are still running.
btw:::btw_pkg_test_run(
path,
if (has_value(filter)) filter else NULL,
reporter
)
}

Expand Down Expand Up @@ -952,11 +953,18 @@ switch(
},

#| title: Run package tests
#| description: >
#| Run testthat tests. compact (default) streams file starts and results
#| with timing; minimal is a good choice for short, non-streaming output.
test = {
#| description: Regex to filter test files.
#| short: 'f'
filter <- ""
tryCatch(btw_pkg_test(path, filter), error = btw_error)
#| description: >
#| compact (default) shows per-file progress and timing. minimal shows
#| failures and a final summary. Other testthat reporter names work too.
reporter <- "compact"
tryCatch(btw_pkg_test(path, filter, reporter), error = btw_error)
},

#| title: Load package with pkgload
Expand Down
6 changes: 5 additions & 1 deletion inst/cli-skill/r-btw-cli/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,15 @@ Use `btw pkg` to run development tasks on an R package under active development.
```
btw pkg document [--path <dir>] Generate roxygen2 docs
btw pkg check [--path <dir>] Run R CMD check
btw pkg test [-f <filter>] [--path <dir>] Run testthat tests
btw pkg test [-f <filter>] [--reporter compact|minimal|<testthat-reporter>] [--path <dir>] Run testthat tests with live per-file progress by default
btw pkg load [--path <dir>] Load package with pkgload
btw pkg coverage [--file <f>] [--json] Compute test coverage
```

`btw pkg test` defaults to `--reporter compact` for live file progress and
per-file timings. Use `--reporter minimal` for a short failures-and-summary
report when you do not need to watch progress.

Use `btw pkg src` to inspect the **R namespace implementations** of installed
packages (or the dev package via `.`), e.g. to understand behavior the docs
don't cover. It returns exact source when available and deparsed functions
Expand Down
Loading
Loading