From 70c90c3448178955877a4d0a8b5eb0e350180dd7 Mon Sep 17 00:00:00 2001 From: Daniel Sjoberg Date: Fri, 12 Apr 2024 07:24:21 -0700 Subject: [PATCH] Add {cli} error messaging for `assert_one_to_one()` (#427) * Add {cli} error messaging for `assert_one_to_one()` * doc update * Update assertions.R * adding example * adding example * doc update * added snapshot * Update test-get.R * #424 assert_one_to_one: add examples and test * #424 assert_one_to_one: fix test --------- Co-authored-by: Ben Straub Co-authored-by: Stefan Bundfuss --- R/assertions.R | 80 +++++++++++++++++++++++------ man/assert_one_to_one.Rd | 60 +++++++++++++++++++++- tests/testthat/_snaps/assertions.md | 17 ++++-- tests/testthat/test-assertions.R | 62 +++++++++++++++------- tests/testthat/test-get.R | 3 ++ 5 files changed, 183 insertions(+), 39 deletions(-) diff --git a/R/assertions.R b/R/assertions.R index 6c4e0efa..e7c24a83 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -1605,6 +1605,12 @@ assert_list_element <- function(list, #' #' @param vars2 Second list of variables #' +#' @param message string passed to `cli::cli_abort(message)`. When `NULL`, default messaging +#' is used (see examples for default messages). `"dataset_name"` can be used in messaging. +#' +#' @param dataset_name string indicating the label/symbol of the object being checked. +#' Default is `rlang::caller_arg(dataset)`. +#' @inheritParams assert_logical_scalar #' #' @return #' An error if the condition is not meet. The input otherwise. @@ -1613,7 +1619,40 @@ assert_list_element <- function(list, #' @family assertion #' @export #' -assert_one_to_one <- function(dataset, vars1, vars2) { +#' @examples +#' library(dplyr) +#' library(rlang) +#' +#' df <- tribble( +#' ~SPECIES, ~SPECIESN, +#' "DOG", 1L, +#' "CAT", 2L, +#' "DOG", 1L +#' ) +#' +#' assert_one_to_one(df, vars1 = exprs(SPECIES), vars2 = exprs(SPECIESN)) +#' +#' df_many <- tribble( +#' ~SPECIES, ~SPECIESN, +#' "DOG", 1L, +#' "CAT", 2L, +#' "DOG", 3L +#' ) +#' +#' try( +#' assert_one_to_one(df_many, vars1 = exprs(SPECIES), vars2 = exprs(SPECIESN)) +#' ) +#' +#' try( +#' assert_one_to_one(df_many, vars1 = exprs(SPECIESN), vars2 = exprs(SPECIES)) +#' ) +assert_one_to_one <- function(dataset, + vars1, + vars2, + dataset_name = rlang::caller_arg(dataset), + message = NULL, + class = "assert_one_to_one", + call = parent.frame()) { assert_vars(vars1) assert_vars(vars2) assert_data_frame(dataset, required_vars = expr_c(vars1, vars2)) @@ -1623,34 +1662,45 @@ assert_one_to_one <- function(dataset, vars1, vars2) { group_by(!!!vars1) %>% filter(n() > 1) %>% arrange(!!!vars1) + if (nrow(one_to_many) > 0) { admiraldev_environment$one_to_many <- one_to_many - abort( - paste0( - "For some values of ", - vars2chr(vars1), - " there is more than one value of ", - vars2chr(vars2), - ".\nCall `get_one_to_many_dataset()` to get all one to many values." + + message <- message %||% + c("For some values of {.val {vars2chr(vars1)}} there is more than one + value of {.val {vars2chr(vars2)}}", + "i" = "Call {.fun get_one_to_many_dataset} to get all one-to-many values." ) + + cli::cli_abort( + message = message, + call = call, + class = c(class, "assert-admiraldev") ) } + many_to_one <- uniques %>% group_by(!!!vars2) %>% filter(n() > 1) %>% arrange(!!!vars2) + if (nrow(many_to_one) > 0) { admiraldev_environment$many_to_one <- many_to_one - abort( - paste0( - "There is more than one value of ", - vars2chr(vars1), - " for some values of ", - vars2chr(vars2), - ".\nCall `get_many_to_one_dataset()` to get all many to one values." + + message <- message %||% + c("There is more than one value of {.val {vars2chr(vars1)}} for some + values of {.val {vars2chr(vars2)}}", + "i" = "Call {.fun get_many_to_one_dataset} to get all many-to-one values." ) + + cli::cli_abort( + message = message, + call = call, + class = c(class, "assert-admiraldev") ) } + + invisible(dataset) } #' Is a Variable in a Dataset a Date or Datetime Variable? diff --git a/man/assert_one_to_one.Rd b/man/assert_one_to_one.Rd index ec0017cf..2129feef 100644 --- a/man/assert_one_to_one.Rd +++ b/man/assert_one_to_one.Rd @@ -4,7 +4,15 @@ \alias{assert_one_to_one} \title{Is There a One to One Mapping between Variables?} \usage{ -assert_one_to_one(dataset, vars1, vars2) +assert_one_to_one( + dataset, + vars1, + vars2, + dataset_name = rlang::caller_arg(dataset), + message = NULL, + class = "assert_one_to_one", + call = parent.frame() +) } \arguments{ \item{dataset}{Dataset to be checked @@ -14,6 +22,28 @@ The variables specified for \code{vars1} and \code{vars2} are expected.} \item{vars1}{First list of variables} \item{vars2}{Second list of variables} + +\item{dataset_name}{string indicating the label/symbol of the object being checked. +Default is \code{rlang::caller_arg(dataset)}.} + +\item{message}{string passed to \code{cli::cli_abort(message)}. When \code{NULL}, default messaging +is used (see examples for default messages). \code{"dataset_name"} can be used in messaging.} + +\item{class}{Subclass of the condition.} + +\item{call}{The execution environment of a currently running +function, e.g. \code{call = caller_env()}. The corresponding function +call is retrieved and mentioned in error messages as the source +of the error. + +You only need to supply \code{call} when throwing a condition from a +helper function which wouldn't be relevant to mention in the +message. + +Can also be \code{NULL} or a \link[rlang:topic-defuse]{defused function call} to +respectively not display any call or hard-code a code to display. + +For more information about error calls, see \ifelse{html}{\link[rlang:topic-error-call]{Including function calls in error messages}}{\link[rlang:topic-error-call]{Including function calls in error messages}}.} } \value{ An error if the condition is not meet. The input otherwise. @@ -21,6 +51,34 @@ An error if the condition is not meet. The input otherwise. \description{ Checks if there is a one to one mapping between two lists of variables. } +\examples{ +library(dplyr) +library(rlang) + +df <- tribble( + ~SPECIES, ~SPECIESN, + "DOG", 1L, + "CAT", 2L, + "DOG", 1L +) + +assert_one_to_one(df, vars1 = exprs(SPECIES), vars2 = exprs(SPECIESN)) + +df_many <- tribble( + ~SPECIES, ~SPECIESN, + "DOG", 1L, + "CAT", 2L, + "DOG", 3L +) + +try( + assert_one_to_one(df_many, vars1 = exprs(SPECIES), vars2 = exprs(SPECIESN)) +) + +try( + assert_one_to_one(df_many, vars1 = exprs(SPECIESN), vars2 = exprs(SPECIES)) +) +} \seealso{ Checks for valid input and returns warning or errors messages: \code{\link{assert_atomic_vector}()}, diff --git a/tests/testthat/_snaps/assertions.md b/tests/testthat/_snaps/assertions.md index a2809dbb..f9150a7e 100644 --- a/tests/testthat/_snaps/assertions.md +++ b/tests/testthat/_snaps/assertions.md @@ -261,7 +261,16 @@ ! List element "val" must be `>=0` in argument `input`: i But, `input[[2]]$val = -1`, and `input[[3]]$val = -2` -# assert_date_var Test 86: error if variable is not a date or datetime variable +# assert_one_to_one Test 84: error if there is a one to many mapping + + Code + assert_one_to_one(pharmaversesdtm::dm, exprs(DOMAIN), exprs(USUBJID)) + Condition + Error: + ! For some values of "DOMAIN" there is more than one value of "USUBJID" + i Call `get_one_to_many_dataset()` to get all one-to-many values. + +# assert_date_var Test 87: error if variable is not a date or datetime variable Code example_fun(dataset = my_data, var = USUBJID) @@ -269,7 +278,7 @@ Error in `example_fun()`: ! Column "USUBJID" in dataset `dataset` must be a date or datetime, but is a character vector. -# assert_date_vector Test 90: error if `arg` is NULL and optional is FALSE +# assert_date_vector Test 91: error if `arg` is NULL and optional is FALSE Code example_fun(NULL) @@ -277,7 +286,7 @@ Error in `example_fun()`: ! Argument `arg` must be a date or datetime, but is NULL. -# assert_atomic_vector Test 91: error if input is not atomic vector +# assert_atomic_vector Test 92: error if input is not atomic vector Code assert_atomic_vector(x) @@ -285,7 +294,7 @@ Error: ! Argument `x` must be an atomic vector, but is a list. -# assert_same_type Test 93: error if different type +# assert_same_type Test 94: error if different type Code assert_same_type(true_value, false_value, missing_value) diff --git a/tests/testthat/test-assertions.R b/tests/testthat/test-assertions.R index 87633c45..bed1fdc7 100644 --- a/tests/testthat/test-assertions.R +++ b/tests/testthat/test-assertions.R @@ -1268,6 +1268,13 @@ test_that("assert_list_element Test 83: error if the elements do not fulfill the ## Test 84: error if there is a one to many mapping ---- test_that("assert_one_to_one Test 84: error if there is a one to many mapping", { expect_error( + assert_one_to_one(pharmaversesdtm::dm, exprs(DOMAIN), exprs(USUBJID)), + class = "assert_one_to_one" + ) + admiraldev_environment$one_to_many <- NULL + + expect_snapshot( + error = TRUE, assert_one_to_one(pharmaversesdtm::dm, exprs(DOMAIN), exprs(USUBJID)) ) admiraldev_environment$one_to_many <- NULL @@ -1276,14 +1283,31 @@ test_that("assert_one_to_one Test 84: error if there is a one to many mapping", ## Test 85: error if there is a many to one mapping ---- test_that("assert_one_to_one Test 85: error if there is a many to one mapping", { expect_error( - assert_one_to_one(pharmaversesdtm::dm, exprs(USUBJID), exprs(DOMAIN)) + assert_one_to_one(pharmaversesdtm::dm, exprs(USUBJID), exprs(DOMAIN)), + class = "assert_one_to_one" ) admiraldev_environment$many_to_one <- NULL }) +## Test 86: dataset is returned invisible if one-to-one ---- +test_that("assert_one_to_one Test 86: dataset is returned invisible if one-to-one", { + df <- tibble::tribble( + ~SPECIES, ~SPECIESN, + "DOG", 1L, + "CAT", 2L, + "DOG", 1L + ) + + df_out <- expect_invisible( + assert_one_to_one(df, vars1 = exprs(SPECIES), vars2 = exprs(SPECIESN)) + ) + + expect_equal(df_out, expected = df) +}) + # assert_date_var ---- -## Test 86: error if variable is not a date or datetime variable ---- -test_that("assert_date_var Test 86: error if variable is not a date or datetime variable", { +## Test 87: error if variable is not a date or datetime variable ---- +test_that("assert_date_var Test 87: error if variable is not a date or datetime variable", { example_fun <- function(dataset, var) { var <- assert_symbol(enexpr(var)) assert_date_var(dataset = dataset, var = !!var) @@ -1311,18 +1335,18 @@ test_that("assert_date_var Test 86: error if variable is not a date or datetime }) # assert_date_vector ---- -## Test 87: returns error if input vector is not a date formatted ---- -test_that("assert_date_vector Test 87: returns error if input vector is not a date formatted", { +## Test 88: returns error if input vector is not a date formatted ---- +test_that("assert_date_vector Test 88: returns error if input vector is not a date formatted", { expect_error(assert_date_vector("2018-08-23")) }) -## Test 88: returns invisible if input is date formatted ---- -test_that("assert_date_vector Test 88: returns invisible if input is date formatted", { +## Test 89: returns invisible if input is date formatted ---- +test_that("assert_date_vector Test 89: returns invisible if input is date formatted", { expect_invisible(assert_date_vector(as.Date("2022-10-25"))) }) -## Test 89: no error if `arg` is NULL and optional is TRUE ---- -test_that("assert_date_vector Test 89: no error if `arg` is NULL and optional is TRUE", { +## Test 90: no error if `arg` is NULL and optional is TRUE ---- +test_that("assert_date_vector Test 90: no error if `arg` is NULL and optional is TRUE", { example_fun <- function(arg) { assert_date_vector(arg, optional = TRUE) } @@ -1332,8 +1356,8 @@ test_that("assert_date_vector Test 89: no error if `arg` is NULL and optional is ) }) -## Test 90: error if `arg` is NULL and optional is FALSE ---- -test_that("assert_date_vector Test 90: error if `arg` is NULL and optional is FALSE", { +## Test 91: error if `arg` is NULL and optional is FALSE ---- +test_that("assert_date_vector Test 91: error if `arg` is NULL and optional is FALSE", { example_fun <- function(arg) { assert_date_vector(arg, optional = FALSE) } @@ -1350,8 +1374,8 @@ test_that("assert_date_vector Test 90: error if `arg` is NULL and optional is FA # assert_atomic_vector ---- -## Test 91: error if input is not atomic vector ---- -test_that("assert_atomic_vector Test 91: error if input is not atomic vector", { +## Test 92: error if input is not atomic vector ---- +test_that("assert_atomic_vector Test 92: error if input is not atomic vector", { x <- list("a", "a", "b", "c", "d", "d", 1, 1, 4) expect_error(assert_atomic_vector(x), class = "assert_atomic_vector") expect_snapshot( @@ -1361,15 +1385,15 @@ test_that("assert_atomic_vector Test 91: error if input is not atomic vector", { }) # assert_same_type ---- -## Test 92: no error if same type ---- -test_that("assert_same_type Test 92: no error if same type", { +## Test 93: no error if same type ---- +test_that("assert_same_type Test 93: no error if same type", { true_value <- "Y" false_value <- "N" expect_invisible(assert_same_type(true_value, false_value)) }) -## Test 93: error if different type ---- -test_that("assert_same_type Test 93: error if different type", { +## Test 94: error if different type ---- +test_that("assert_same_type Test 94: error if different type", { true_value <- "Y" false_value <- "N" missing_value <- 0 @@ -1385,8 +1409,8 @@ test_that("assert_same_type Test 93: error if different type", { ) }) -## Test 94: works as intended ---- -test_that("assert_same_type Test 94: works as intended", { +## Test 95: works as intended ---- +test_that("assert_same_type Test 95: works as intended", { expect_equal( valid_time_units(), c("years", "months", "days", "hours", "minutes", "seconds") diff --git a/tests/testthat/test-get.R b/tests/testthat/test-get.R index 1988fdcc..91b5bbfb 100644 --- a/tests/testthat/test-get.R +++ b/tests/testthat/test-get.R @@ -72,6 +72,9 @@ test_that("get_source_vars Test 6: no source vars returns NULL", { # get_dataset ---- ## Test 7: get_dataset works ---- test_that("get_dataset Test 7: get_dataset works", { + admiraldev_environment$many_to_one <- NULL + admiraldev_environment$one_to_many <- NULL + expect_equal(NULL, get_dataset("one_to_many")) })