Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# testthat (development version)

* `expect_matches()` failures should be a little easier to read (#2135).
* New `local_on_cran(TRUE)` allows you to simulate how your tests will run on CRAN (#2112).
* `expect_no_*()` now executes the entire code block, rather than stopping at the first message or warning (#1991).
* `expect_no_failures()` and `expect_no_successes()` are now deprecated as `expect_success()` now test for no failures and `expect_failure()` tests for no successes (#)
Expand Down
44 changes: 20 additions & 24 deletions R/expect-match.R
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
#' Does a string match a regular expression?
#'
#' @details
#' `expect_match()` is a wrapper around [grepl()]. See its documentation for
#' more detail about the individual arguments. `expect_no_match()` provides
#' the complementary case, checking that a string *does not* match a regular
#' expression.
#' `expect_match()` checks if a character vector matches a regular expression,
#' powered by [grepl()].
#'
#' `expect_no_match()` provides the complementary case, checking that a
#' character vector *does not* match a regular expression.
#'
#' @inheritParams expect_that
#' @inheritParams base::grepl
Expand All @@ -21,12 +22,11 @@
#' expect_match("Testing is fun", "f.n")
#' expect_no_match("Testing is fun", "horrible")
#'
#' \dontrun{
#' expect_match("Testing is fun", "horrible")
#' show_failure(expect_match("Testing is fun", "horrible"))
#' show_failure(expect_match("Testing is fun", "horrible", fixed = TRUE))
#'
#' # Zero-length inputs always fail
#' expect_match(character(), ".")
#' }
#' show_failure(expect_match(character(), "."))
expect_match <- function(
object,
regexp,
Expand All @@ -37,11 +37,14 @@ expect_match <- function(
info = NULL,
label = NULL
) {
# Capture here to avoid environment-related messiness
act <- quasi_label(enquo(object), label, arg = "object")
stopifnot(is.character(regexp), length(regexp) == 1)

stopifnot(is.character(act$val))
check_character(object)
check_string(regexp)
check_bool(perl)
check_bool(fixed)
check_bool(all)

Comment on lines +42 to +47
Copy link
Member

Choose a reason for hiding this comment

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

  • label is likely checked above by quasi_label(), do we want to check info ? It's still used in the empty case below.
  • Do you want to take this as an opportunity to add a grepl_args = list() (or = NULL ) argument and soft-deprecate ... (with a lifecycle warning)?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't care that much about info since we're phasing it out in new expectations.

I don't want to do any bigger refactorings because if I did I'd really want to stop it from being vectorising and instead make it work with a single string; but that's going to break a bunch of tests.

Copy link
Member

Choose a reason for hiding this comment

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

Could we use the all argument for enforcing a scalar? all = NA or all = "scalar" would mean to expect a scalar, with a lifecycle warning to add all = TRUE if a non-scalar is provided without setting all ?

Copy link
Member Author

Choose a reason for hiding this comment

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

I don't think it's important enough to do all that work.

if (length(object) == 0) {
return(fail(sprintf("%s is empty.", act$lab), info = info))
}
Expand Down Expand Up @@ -75,11 +78,7 @@ expect_no_match <- function(
# Capture here to avoid environment-related messiness
act <- quasi_label(enquo(object), label, arg = "object")
stopifnot(is.character(regexp), length(regexp) == 1)

stopifnot(is.character(act$val))
if (length(object) == 0) {
return(fail(sprintf("%s is empty.", act$lab), info = info))
}

expect_match_(
act = act,
Expand Down Expand Up @@ -114,20 +113,17 @@ expect_match_ <- function(
return(pass(act$val))
}

escape <- if (fixed) identity else escape_regex

text <- encodeString(act$val)
if (length(act$val) == 1) {
values <- paste0("Actual value: \"", escape(encodeString(act$val)), "\"")
values <- paste0('Text: "', text, '"')
} else {
values <- paste0(
"Actual values:\n",
paste0("* ", escape(encodeString(act$val)), collapse = "\n")
)
values <- paste0("Text:\n", paste0("* ", text, collapse = "\n"))
}

msg <- sprintf(
if (negate) "%s does match %s.\n%s" else "%s does not match %s.\n%s",
escape(act$lab),
if (negate) "%s matches %s %s.\n%s" else "%s does not match %s %s.\n%s",
act$lab,
if (fixed) "string" else "regexp",
encodeString(regexp, quote = '"'),
values
)
Expand Down
25 changes: 0 additions & 25 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,6 @@
#' @export
magrittr::`%>%`

escape_regex <- function(x) {
chars <- c(
"*",
".",
"?",
"^",
"+",
"$",
"|",
"(",
")",
"[",
"]",
"{",
"}",
"\\"
)
gsub(
paste0("([\\", paste0(collapse = "\\", chars), "])"),
"\\\\\\1",
x,
perl = TRUE
)
}

can_entrace <- function(cnd) {
!inherits(cnd, "Throwable")
}
Expand Down
7 changes: 3 additions & 4 deletions man/expect_match.Rd

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

57 changes: 57 additions & 0 deletions tests/testthat/_snaps/expect-match.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# generates useful failure messages

`zero` is empty.

---

`one` does not match regexp "asdf".
Text: "bcde"

---

`many` does not match regexp "asdf".
Text:
* a
* b
* c
* d
* e

# checks its inputs

Code
expect_match(1)
Condition
Error in `expect_match()`:
! `object` must be a character vector, not the number 1.
Code
expect_match("x", 1)
Condition
Error in `expect_match()`:
! `regexp` must be a single string, not the number 1.
Code
expect_match("x", "x", fixed = 1)
Condition
Error in `expect_match()`:
! `fixed` must be `TRUE` or `FALSE`, not the number 1.
Code
expect_match("x", "x", perl = 1)
Condition
Error in `expect_match()`:
! `perl` must be `TRUE` or `FALSE`, not the number 1.
Code
expect_match("x", "x", all = 1)
Condition
Error in `expect_match()`:
! `all` must be `TRUE` or `FALSE`, not the number 1.

# expect_no_match works

`x` matches string "e*".
Text: "te*st"

---

`x` matches regexp "TEST".
Text: "test"

4 changes: 2 additions & 2 deletions tests/testthat/_snaps/expect-output.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# expect = string checks for match

`g\(\)` does not match "x".
Actual value: "!"
`g()` does not match regexp "x".
Text: "!"

---

Expand Down
4 changes: 2 additions & 2 deletions tests/testthat/_snaps/expect-self-test.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# expect_failure() can optionally match message

Failure message does not match "banana".
Actual value: "apple"
Failure message does not match regexp "banana".
Text: "apple"

# errors in expect_success bubble up

Expand Down
65 changes: 30 additions & 35 deletions tests/testthat/test-expect-match.R
Original file line number Diff line number Diff line change
@@ -1,47 +1,42 @@
test_that("extra arguments to matches passed onto grepl", {
expect_success(expect_match("te*st", "e*", fixed = TRUE))
expect_success(expect_match("test", "TEST", ignore.case = TRUE))
})
test_that("generates useful failure messages", {
zero <- character(0)
expect_snapshot_failure(expect_match(zero, 'asdf'))

test_that("special regex characters are escaped in output", {
error <- tryCatch(
expect_match("f() test", "f() test"),
expectation = function(e) e$message
)
expect_equal(
error,
"\"f\\(\\) test\" does not match \"f() test\".\nActual value: \"f\\(\\) test\""
)
})
one <- "bcde"
expect_snapshot_failure(expect_match(one, 'asdf'))

test_that("correct reporting of expected label", {
expect_failure(expect_match("[a]", "[b]"), escape_regex("[a]"), fixed = TRUE)
expect_failure(expect_match("[a]", "[b]", fixed = TRUE), "[a]", fixed = TRUE)
many <- letters[1:5]
expect_snapshot_failure(expect_match(many, 'asdf'))
})

test_that("errors if obj is empty str", {
x <- character(0)
err <- expect_error(
expect_match(x, 'asdf'),
class = "expectation_failure"
)
expect_match(err$message, 'is empty')
test_that("checks its inputs", {
expect_snapshot(error = TRUE, {
expect_match(1)
expect_match("x", 1)
expect_match("x", "x", fixed = 1)
expect_match("x", "x", perl = 1)
expect_match("x", "x", all = 1)
})
})

test_that("prints multiple unmatched values", {
err <- expect_error(
expect_match(letters[1:10], 'asdf'),
class = "expectation_failure"
)
expect_match(err$message, "does not match")
test_that("extra arguments passed onto grepl", {
expect_failure(expect_match("\\s", "\\s"))
expect_success(expect_match("\\s", "\\s", fixed = TRUE))

expect_failure(expect_match("test", "TEST"))
expect_success(expect_match("test", "TEST", ignore.case = TRUE))
})

test_that("expect_no_match works", {
expect_success(expect_no_match("[a]", "[b]"))
expect_success(expect_no_match("[a]", "[b]", fixed = TRUE))
expect_failure(
expect_no_match("te*st", "e*", fixed = TRUE),
escape_regex("te*st")
)
expect_failure(expect_no_match("test", "TEST", ignore.case = TRUE), "test")

x <- "te*st"
expect_snapshot_failure(expect_no_match(x, "e*", fixed = TRUE))
x <- "test"
expect_snapshot_failure(expect_no_match(x, "TEST", ignore.case = TRUE))
})

test_that("empty string is never a match", {
expect_success(expect_no_match(character(), "x"))
})
Loading