Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
66b166a
New expectation function, expect_shape()
MichaelChirico Oct 4, 2021
d4853ff
revert spurious NAMESPACE edits
MichaelChirico Oct 4, 2021
69c410a
really this time
MichaelChirico Oct 4, 2021
e89fff9
finish documentation
MichaelChirico Oct 4, 2021
cdb15a2
refactor for clarity, add tests
MichaelChirico Oct 13, 2021
268aee8
Merge branch 'main' into expect-shape
MichaelChirico Jul 24, 2025
89c0c82
restyle NEWS
MichaelChirico Jul 24, 2025
b74737f
move to own file
MichaelChirico Jul 24, 2025
769ccc1
tighter wording: integer-->numeric
MichaelChirico Jul 24, 2025
9aede6f
completeness comment
MichaelChirico Jul 24, 2025
dd23081
new file in pkgdown ref
MichaelChirico Jul 24, 2025
eb0dc5a
Merge branch 'main' into expect-shape
MichaelChirico Jul 24, 2025
1a020dc
reorder news after merge
MichaelChirico Jul 24, 2025
ffde38b
Rearrange to desired signature, adjust tests
MichaelChirico Jul 24, 2025
0661094
tweak NEWS
MichaelChirico Jul 24, 2025
ac0acbc
copy-paste roxygenize...
MichaelChirico Jul 24, 2025
150425b
expect_snapshot() over expect_error()
MichaelChirico Jul 24, 2025
53b0ee8
expect_failure() -> expect_snapshot_failure()
MichaelChirico Jul 24, 2025
a72708b
manual roxygenize continues
MichaelChirico Jul 24, 2025
da7fdf2
manual \description too
MichaelChirico Jul 24, 2025
d873766
rlang::check_exclusive
MichaelChirico Jul 24, 2025
3710870
Check length(dim) to get better errors
MichaelChirico Jul 24, 2025
170543e
new edge tests, improve structure
MichaelChirico Jul 24, 2025
a88340d
more 0-dimension edge case checks
MichaelChirico Jul 24, 2025
e35122a
S3 dispatch check
MichaelChirico Jul 24, 2025
ff4d7c5
Robustness: dim() can return NA
MichaelChirico Jul 24, 2025
4bc32fc
Use fail() + early return, which exposed faulty logic
MichaelChirico Jul 24, 2025
1bdf1cd
update snapshots
MichaelChirico Jul 24, 2025
3ed14c4
remove unneeded early return
MichaelChirico Jul 24, 2025
58654fb
Don't get generic 'object' label by passing to expect_length(); corre…
MichaelChirico Jul 24, 2025
ff4c894
Re-document
hadley Jul 25, 2025
2ca461b
Style tweaks; validate more inputs
hadley Jul 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export(expect_s3_class)
export(expect_s4_class)
export(expect_s7_class)
export(expect_setequal)
export(expect_shape)
export(expect_silent)
export(expect_snapshot)
export(expect_snapshot_error)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* `it()` now finds the correct evaluation environment in more cases (@averissimo, #2085).
* Fixed an issue preventing compilation from succeeding due to deprecation / removal of `std::uncaught_exception()` (@kevinushey, #2047).
* 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)
* New expectation, `expect_shape()`, for testing the shape (i.e., the `length()`, `nrow()` and/or `ncol()`, or `dim()`, all in one place (#1423, @michaelchirico).

# testthat 3.2.3

Expand Down
3 changes: 2 additions & 1 deletion R/expect-length.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#' Does code return a vector with the specified length?
#'
#' @seealso [expect_vector()] to make assertions about the "size" of a vector
#' @seealso [expect_vector()] to make assertions about the "size" of a vector,
#' [expect_shape()] for more general assertions about object "shape".
#' @inheritParams expect_that
#' @param n Expected length.
#' @family expectations
Expand Down
76 changes: 76 additions & 0 deletions R/expect-shape.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#' Does code return an object with the specified shape?
#'
#' By "shape", we mean an object's [dim()], or, for one-dimensional objects,
#' it's [length()]. Thus this is an extension of [expect_length()] to more
#' general objects like [data.frame()], [matrix()], and [array()].
#' To wit, first, the object's `dim()` is checked. If non-`NULL`, it is compared
#' to `shape` (or one/both of `nrow`, `ncol`, if they are supplied, in which
#' case they take precedence). If `dim(object)` is `NULL`, `length(object)`
#' is compared to `shape`.
#'
#' @seealso [expect_length()] to specifically make assertions about the
#' [length()] of a vector.
#' @inheritParams expect_that
#' @param shape Expected shape, a numeric vector.
#' @param nrow Expected number of rows, numeric.
#' @param ncol Expected number of columns, numeric.
#' @family expectations
#' @export
#' @examples
expect_shape = function(object, shape, nrow, ncol) {
stopifnot(
missing(shape) || is.numeric(shape),
missing(nrow) || is.numeric(nrow),
missing(ncol) || is.numeric(ncol)
)

dim_object <- dim(object)
if (is.null(dim_object)) {
if (missing(shape)) {
stop("`shape` must be provided for one-dimensional inputs")
}
return(expect_length(object, shape))
}

act <- quasi_label(enquo(object), arg = "object")

if (missing(nrow) && missing(ncol)) {
# testing dim
if (missing(shape)) {
stop("`shape` must be provided if `nrow` and `ncol` are not")
}
act$shape <- dim_object

expect(
isTRUE(all.equal(act$shape, shape)),
sprintf("%s has shape (%s), not (%s).", act$lab, toString(act$shape), toString(shape))
)
} else if (missing(nrow) && !missing(ncol)) {
# testing only ncol
act$ncol <- dim_object[2L]

expect(
act$ncol == ncol,
sprintf("%s has %i columns, not %i.", act$lab, act$ncol, ncol)
)
} else if (!missing(nrow) && missing(ncol)) {
# testing only nrow
act$nrow <- dim_object[1L]

expect(
act$nrow == nrow,
sprintf("%s has %i rows, not %i.", act$lab, act$nrow, nrow)
)
} else { # !missing(nrow) && !missing(ncol)
# testing both nrow & ncol (useful, e.g., for testing dim(.)[1:2] for arrays
act$nrow <- dim_object[1L]
act$ncol <- dim_object[2L]

expect(
act$nrow == nrow && act$ncol == ncol,
sprintf("%s has %i rows and %i columns, not %i rows and %i columns", act$lab, act$nrow, act$ncol, nrow, ncol)
)
}

return(act$val)
}
1 change: 1 addition & 0 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ reference:
- subtitle: Vectors
contents:
- expect_length
- expect_shape
- expect_gt
- expect_match
- expect_named
Expand Down
46 changes: 46 additions & 0 deletions man/expect_shape.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 51 additions & 0 deletions tests/testthat/test-expect-shape.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
test_that("shape computed correctly", {
# equivalent to expect_length
expect_success(expect_shape(1, 1))
expect_failure(expect_shape(1, 2), "has length 1, not length 2.")
expect_success(expect_shape(1:10, 10))
expect_success(expect_shape(letters[1:5], 5))

# testing dim()
expect_success(expect_shape(matrix(nrow = 5, ncol = 4), c(5L, 4L)))
expect_failure(expect_shape(matrix(nrow = 6, ncol = 3), c(6L, 2L)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few of these should use expect_snapshot_failure() so we can see the failure messages.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have a good sense of when to use one vs. the other; at least one snapshot_failure() for each of the arguments makes sense to me superficially. I can add more plain expect_failure() if you see fit.

expect_failure(expect_shape(matrix(nrow = 6, ncol = 3), c(7L, 3L)))
expect_success(expect_shape(data.frame(1:10, 11:20), c(10, 2)))
expect_success(expect_shape(array(dim = 1:3), 1:3))

# testing nrow=
expect_success(expect_shape(matrix(nrow = 5, ncol = 4), nrow = 5L))
expect_failure(expect_shape(matrix(nrow = 5, ncol = 5), nrow = 6L))
expect_success(expect_shape(data.frame(1:10, 11:20), nrow = 10L))

# testing ncol=
expect_success(expect_shape(matrix(nrow = 5, ncol = 4), ncol = 4L))
expect_failure(expect_shape(matrix(nrow = 5, ncol = 5), ncol = 7L))
expect_success(expect_shape(data.frame(1:10, 11:20), ncol = 2L))

# testing nrow= and ncol=
expect_success(expect_shape(matrix(nrow = 5, ncol = 4), nrow = 5L, ncol = 4L))
expect_failure(expect_shape(matrix(nrow = 5, ncol = 5), nrow = 6L, ncol = 5L))
expect_success(expect_shape(data.frame(1:10, 11:20), nrow = 10L, ncol = 2L))
expect_success(expect_shape(array(dim = 5:7), nrow = 5L, ncol = 6L))

# precedence of manual nrow/ncol over shape
expect_success(expect_shape(matrix(nrow = 7, ncol = 10), 1:2, nrow = 7L))
expect_success(expect_shape(matrix(nrow = 7, ncol = 10), 1:2, ncol = 10L))
})

test_that("uses S4 dim method", {
A <- setClass("ExpectShapeA", slots = c(x = "numeric", y = "numeric"))
setMethod("dim", "ExpectShapeA", function(x) 8:10)
expect_success(expect_shape(A(x = 1:9, y = 3), 8:10))
})

test_that("returns input", {
x <- list(1:10, letters)
out <- expect_shape(x, 2)
expect_identical(out, x)
})

test_that("at least one argument is required", {
expect_error(expect_shape(1:10), "`shape` must be provided for one-dimensional inputs", fixed = TRUE)
expect_error(expect_shape(cbind(1:2)), "`shape` must be provided if `nrow` and `ncol` are not")
})
Loading