Skip to content

Commit d1d2740

Browse files
committed
Merge commit '597bb04a708ccc2f07d420823bf998fc7f037355'
2 parents 2270136 + 597bb04 commit d1d2740

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+569
-275
lines changed

.claude/settings.local.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
"Bash(find:*)",
66
"Bash(R:*)",
77
"Bash(rm:*)",
8-
"Bash(air format:*)"
8+
"Bash(air format:*)",
9+
"Edit(R/**)",
10+
"Edit(tests/**)",
11+
"Edit(vignettes/**)"
912
],
1013
"deny": []
1114
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
name: Claude Code Review
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize]
6+
# Optional: Only run on specific file changes
7+
# paths:
8+
# - "src/**/*.ts"
9+
# - "src/**/*.tsx"
10+
# - "src/**/*.js"
11+
# - "src/**/*.jsx"
12+
13+
jobs:
14+
claude-review:
15+
# Optional: Filter by PR author
16+
# if: |
17+
# github.event.pull_request.user.login == 'external-contributor' ||
18+
# github.event.pull_request.user.login == 'new-developer' ||
19+
# github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR'
20+
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
pull-requests: read
25+
issues: read
26+
id-token: write
27+
28+
steps:
29+
- name: Checkout repository
30+
uses: actions/checkout@v4
31+
with:
32+
fetch-depth: 1
33+
34+
- name: Run Claude Code Review
35+
id: claude-review
36+
uses: anthropics/claude-code-action@beta
37+
with:
38+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
39+
40+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
41+
# model: "claude-opus-4-1-20250805"
42+
43+
# Direct prompt for automated review (no @claude mention needed)
44+
direct_prompt: |
45+
Please review this pull request and provide feedback on:
46+
- Code quality and best practices
47+
- Potential bugs or issues
48+
- Performance considerations
49+
- Security concerns
50+
- Test coverage
51+
52+
Be constructive and helpful in your feedback.
53+
54+
# Optional: Use sticky comments to make Claude reuse the same comment on subsequent pushes to the same PR
55+
# use_sticky_comment: true
56+
57+
# Optional: Customize review based on file types
58+
# direct_prompt: |
59+
# Review this PR focusing on:
60+
# - For TypeScript files: Type safety and proper interface usage
61+
# - For API endpoints: Security, input validation, and error handling
62+
# - For React components: Performance, accessibility, and best practices
63+
# - For tests: Coverage, edge cases, and test quality
64+
65+
# Optional: Different prompts for different authors
66+
# direct_prompt: |
67+
# ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' &&
68+
# 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' ||
69+
# 'Please provide a thorough code review focusing on our coding standards and best practices.' }}
70+
71+
# Optional: Add specific tools for running tests or linting
72+
# allowed_tools: "Bash(npm run test),Bash(npm run lint),Bash(npm run typecheck)"
73+
74+
# Optional: Skip review for certain conditions
75+
# if: |
76+
# !contains(github.event.pull_request.title, '[skip-review]') &&
77+
# !contains(github.event.pull_request.title, '[WIP]')
78+

.github/workflows/claude.yml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
name: Claude Code
2+
3+
on:
4+
issue_comment:
5+
types: [created]
6+
pull_request_review_comment:
7+
types: [created]
8+
issues:
9+
types: [opened, assigned]
10+
pull_request_review:
11+
types: [submitted]
12+
13+
jobs:
14+
claude:
15+
if: |
16+
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
17+
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
18+
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
19+
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
20+
runs-on: ubuntu-latest
21+
permissions:
22+
contents: read
23+
pull-requests: read
24+
issues: read
25+
id-token: write
26+
actions: read # Required for Claude to read CI results on PRs
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v4
30+
with:
31+
fetch-depth: 1
32+
33+
- name: Install air
34+
uses: posit-dev/setup-air@v1
35+
36+
- uses: r-lib/actions/setup-r@v2
37+
with:
38+
use-public-rspm: true
39+
40+
- uses: r-lib/actions/setup-r-dependencies@v2
41+
with:
42+
extra-packages: any::rcmdcheck,roxygen2
43+
needs: check
44+
45+
- name: Run Claude Code
46+
id: claude
47+
uses: anthropics/claude-code-action@beta
48+
with:
49+
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
50+
51+
# This is an optional setting that allows Claude to read CI results on PRs
52+
additional_permissions: |
53+
actions: read
54+
55+
# Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
56+
# model: "claude-opus-4-1-20250805"
57+
58+
# Optional: Customize the trigger phrase (default: @claude)
59+
# trigger_phrase: "/claude"
60+
61+
# Optional: Trigger when specific user is assigned to an issue
62+
# assignee_trigger: "claude-bot"
63+
64+
# Optional: Allow Claude to run specific commands
65+
allowed_tools: "Bash(R:*);Bash(air format:*)"
66+
67+
# Optional: Add custom instructions for Claude to customize its behavior for your project
68+
# custom_instructions: |
69+
# Follow our coding standards
70+
# Ensure all new code has tests
71+
# Use TypeScript for new files
72+
73+
# Optional: Custom environment variables for Claude
74+
# claude_env: |
75+
# NODE_ENV: test

NEWS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# testthat (development version)
22

3+
* `expect_snapshot_file(name=)` must have a unique file path. If a snapshot file attempts to be saved with a duplicate `name`, an error will be thrown. (#1592)
4+
* `test_dir()`, `test_file()`, `test_package()`, `test_check()`, `test_local()`, `source_file()` gain a `shuffle` argument uses `sample()` to randomly reorder the top-level expressions in each test file (#1942). This random reordering surfaces dependencies between tests and code outside of any test, as well as dependencies between tests. This helps you find and eliminate unintentional dependencies.
5+
* `snapshot_accept(test)` now works when the test file name contains `.` (#1669).
36
* `local_mock()` and `with_mock()` have been deprecated because they are no longer permitted in R 4.5.
47
* `snapshot_review()` now passes `...` on to `shiny::runApp()` (#1928).
58
* `expect_named()` now gives more informative errors (#2091).
@@ -41,7 +44,7 @@
4144
* Fixed an issue preventing compilation from succeeding due to deprecation / removal of `std::uncaught_exception()` (@kevinushey, #2047).
4245
* New `skip_unless_r()` to skip running tests on unsuitable versions of R, e.g. `skip_unless_r(">= 4.1.0")` to skip tests that require, say, `...names` (@MichaelChirico, #2022)
4346
* `skip_on_os()` gains an option `"emscripten"` of the `os` argument to skip tests on Emscripten (@eitsupi, #2103).
44-
* New expectation, `expect_shape()`, for testing the shape (i.e., the `length()`, `nrow()`, `ncol()`, or `dim()`), all in one place (#1423, @michaelchirico).
47+
* New expectation, `expect_shape()`, for testing the shape (i.e., the `nrow()`, `ncol()`, or `dim()`), all in one place (#1423, @michaelchirico).
4548

4649
# testthat 3.2.3
4750

R/expect-length.R

Lines changed: 0 additions & 27 deletions
This file was deleted.

R/expect-shape.R

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,44 @@
1-
#' Do you expect an object with this shape?
1+
#' Do you expect an object with this length or shape?
22
#'
3-
#' This is a generalization of [expect_length()] to test the "shape" of
4-
#' more general objects like data.frames, matrices, and arrays.
3+
#' `expect_length()` inspects the [length()] of an object; `expect_shape()`
4+
#' inspects the "shape" (i.e. [nrow()], [ncol()], or [dim()]) of
5+
#' higher-dimensional objects like data.frames, matrices, and arrays.
56
#'
6-
#' @seealso [expect_length()] to specifically make assertions about the
7-
#' [length()] of a vector.
7+
#' @seealso [expect_vector()] to make assertions about the "size" of a vector.
88
#' @inheritParams expect_that
9-
#' @param ... Ignored.
10-
#' @param nrow,ncol Expected [nrow()]/[ncol()] of `object`.
11-
#' @param dim Expected [dim()] of `object`.
9+
#' @param n Expected length.
1210
#' @family expectations
1311
#' @export
1412
#' @examples
13+
#' expect_length(1, 1)
14+
#' expect_length(1:10, 10)
15+
#' show_failure(expect_length(1:10, 1))
16+
#'
1517
#' x <- matrix(1:9, nrow = 3)
1618
#' expect_shape(x, nrow = 3)
19+
#' show_failure(expect_shape(x, nrow = 4))
1720
#' expect_shape(x, ncol = 3)
21+
#' show_failure(expect_shape(x, ncol = 4))
1822
#' expect_shape(x, dim = c(3, 3))
23+
#' show_failure(expect_shape(x, dim = c(3, 4, 5)))
24+
expect_length <- function(object, n) {
25+
check_number_whole(n, min = 0)
26+
27+
act <- quasi_label(enquo(object))
28+
act$n <- length(act$val)
29+
30+
if (act$n != n) {
31+
msg <- sprintf("%s has length %i, not length %i.", act$lab, act$n, n)
32+
return(fail(msg))
33+
}
34+
pass(act$val)
35+
}
36+
37+
#' @param nrow,ncol Expected [nrow()]/[ncol()] of `object`.
38+
#' @param dim Expected [dim()] of `object`.
39+
#' @rdname expect_length
40+
#' @param ... Not used; used to force naming of other arguments.
41+
#' @export
1942
expect_shape = function(object, ..., nrow, ncol, dim) {
2043
check_dots_empty()
2144
check_exclusive(nrow, ncol, dim)

R/parallel.R

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ test_files_parallel <- function(
4141
stop_on_failure = FALSE,
4242
stop_on_warning = FALSE,
4343
wrap = TRUE, # unused, to match test_files signature
44-
load_package = c("none", "installed", "source")
44+
load_package = c("none", "installed", "source"),
45+
shuffle = FALSE
4546
) {
4647
# TODO: support timeouts. 20-30s for each file by default?
4748

@@ -59,7 +60,8 @@ test_files_parallel <- function(
5960
test_dir = test_dir,
6061
load_helpers = load_helpers,
6162
num_workers = num_workers,
62-
load_package = load_package
63+
load_package = load_package,
64+
shuffle = shuffle
6365
)
6466

6567
withr::with_dir(test_dir, {
@@ -210,7 +212,8 @@ queue_setup <- function(
210212
test_dir,
211213
num_workers,
212214
load_helpers,
213-
load_package
215+
load_package,
216+
shuffle = FALSE
214217
) {
215218
# TODO: observe `load_package`, but the "none" default is not
216219
# OK for the subprocess, because it'll not have the tested package
@@ -249,9 +252,11 @@ queue_setup <- function(
249252
})
250253
queue <- task_q$new(concurrency = num_workers, load_hook = load_hook)
251254

252-
fun <- transport_fun(function(path) asNamespace("testthat")$queue_task(path))
255+
fun <- transport_fun(function(path, shuffle) {
256+
asNamespace("testthat")$queue_task(path, shuffle)
257+
})
253258
for (path in test_paths) {
254-
queue$push(fun, list(path))
259+
queue$push(fun, list(path, shuffle))
255260
}
256261

257262
queue
@@ -282,19 +287,19 @@ queue_process_setup <- function(
282287
the$testing_env <- env
283288
}
284289

285-
queue_task <- function(path) {
290+
queue_task <- function(path, shuffle = FALSE) {
286291
withr::local_envvar("TESTTHAT_IS_PARALLEL" = "true")
287-
snapshotter <- SubprocessSnapshotReporter$new(
288-
snap_dir = "_snaps",
289-
fail_on_new = FALSE
290-
)
292+
snapshotter <- SubprocessSnapshotReporter$new(snap_dir = "_snaps")
291293
withr::local_options(testthat.snapshotter = snapshotter)
292294
reporters <- list(
293295
SubprocessReporter$new(),
294296
snapshotter
295297
)
296298
multi <- MultiReporter$new(reporters = reporters)
297-
with_reporter(multi, test_one_file(path, env = the$testing_env))
299+
with_reporter(
300+
multi,
301+
test_one_file(path, env = the$testing_env, shuffle = shuffle)
302+
)
298303
NULL
299304
}
300305

R/snapshot-file.R

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,8 +137,11 @@ expect_snapshot_file <- function(
137137
path,
138138
file_equal = compare,
139139
variant = variant,
140-
trace_env = caller_env()
141140
)
141+
if (inherits(equal, "expectation_failure")) {
142+
return(equal)
143+
}
144+
142145
hint <- snapshot_review_hint(snapshotter$file, name)
143146

144147
if (!equal) {
@@ -196,18 +199,19 @@ snapshot_file_equal <- function(
196199
snap_variant, # variant (optional)
197200
path, # path to new file
198201
file_equal = compare_file_binary,
199-
fail_on_new = FALSE,
202+
fail_on_new = NULL,
200203
trace_env = caller_env()
201204
) {
202205
if (!file.exists(path)) {
203-
cli::cli_abort("{.path {path}} not found.")
206+
cli::cli_abort("{.path {path}} not found.", call = trace_env)
204207
}
205208

206209
if (is.null(snap_variant)) {
207210
snap_test_dir <- file.path(snap_dir, snap_test)
208211
} else {
209212
snap_test_dir <- file.path(snap_dir, snap_variant, snap_test)
210213
}
214+
fail_on_new <- fail_on_new %||% on_ci()
211215

212216
cur_path <- file.path(snap_test_dir, snap_name)
213217
new_path <- file.path(snap_test_dir, new_name(snap_name))
@@ -232,12 +236,13 @@ snapshot_file_equal <- function(
232236
snap_name,
233237
"'"
234238
)
239+
240+
# We want to fail on CI since this suggests that the user has failed
241+
# to record the value locally
235242
if (fail_on_new) {
236243
return(fail(message, trace_env = trace_env))
237-
} else {
238-
testthat_warn(message)
239244
}
240-
245+
testthat_warn(message)
241246
TRUE
242247
}
243248
}

R/snapshot-manage.R

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ snapshot_meta <- function(files = NULL, path = "tests/testthat") {
196196
files <- files[!is_dir]
197197

198198
dirs <- substr(dirs, 1, nchar(dirs) - 1)
199-
files <- ifelse(tools::file_ext(files) == "", paste0(files, ".md"), files)
199+
# Match regardless of whether user include .md or not
200+
files <- c(files, paste0(files, ".md"))
200201

201202
out <- out[out$name %in% files | out$test %in% dirs, , drop = FALSE]
202203
}

0 commit comments

Comments
 (0)