Title: | Lightweight Precondition, Postcondition, and Sanity Checks |
---|---|
Description: | Implements fast, safe, and customizable assertions routines, which can be used in place of `base::stopifnot()`. |
Authors: | Taras Zakharko [aut, cre] |
Maintainer: | Taras Zakharko <[email protected]> |
License: | MIT + file LICENSE |
Version: | 0.1.0 |
Built: | 2024-11-07 04:26:51 UTC |
Source: | https://github.com/tzakharko/precondition |
diagnose_assertion_failure()
displays customized failure message and
diagnosis in assertions such as precondition()
. This can be used to
implement assertion helpers. This function does nothing if invoked outside
an assertion (see details). The function forwarded_arg_label()
looks up a
forwarded argument and formats it as a string (used in custom diagnostic
messages).
diagnose_assertion_failure(message, ..., .details) forwarded_arg_label(arg)
diagnose_assertion_failure(message, ..., .details) forwarded_arg_label(arg)
message |
diagnostic message to show (see |
... |
expressions to diagnose (forwarded to |
.details |
an optional data frame with diagnosis data |
arg |
a forwarded function argument |
If invoked as part of an assertion (e.g. precondition()
),
diagnose_assertion_failure()
provides a custom failure message and
diagnosis. If invoked in any other context, the function does nothing. This
can be used to implement custom assertions helpers that behave like regular
binary predicates (functions) under normal circumstances and generate a
customized assertion failure report when used as part of an assertion
(see examples).
The first argument to diagnose_assertion_failure()
is a character vector
with a custom failure message. This vector will be formatted as error
bullets viarlang::format_error_bullets()
. Any subsequent argument will be
forwarded to diagnose_assertion_failure()
for diagnosis. For custom
diagnosis, the user can supply their own data frame with diagnosis details
via optional argument .details
. The format of this data frame must be
identical to one returned by diagnose_assertion_failure()
.
The function forwarded_arg_label()
looks up a forwarded expression and
formats it as a single string suitable for inclusion in diagnostic
messages.
diagnose_assertion_failure()
always returns FALSE
.
# returns TRUE if x is a positive, integer, FALSE otherwise # if invoked as part of an assertion displays a custom failure diagnosis is_positive_int <- function(x) { is.integer(x) && length(x) == 1L && (x > 0) || { diagnose_assertion_failure( sprintf("`%s` must be a positive integer", forwarded_arg_label(x)), {{x}} ) } } # for all intends and purposes this is just a regular R function that returns # TRUE or FALSE is_positive_int(5L) is_positive_int(-5L) # guard to avoid throwing errors if(FALSE) { # ... but it will provide custom diagnosis if invoked inside an assertion precondition(is_positive_int(-5L)) }
# returns TRUE if x is a positive, integer, FALSE otherwise # if invoked as part of an assertion displays a custom failure diagnosis is_positive_int <- function(x) { is.integer(x) && length(x) == 1L && (x > 0) || { diagnose_assertion_failure( sprintf("`%s` must be a positive integer", forwarded_arg_label(x)), {{x}} ) } } # for all intends and purposes this is just a regular R function that returns # TRUE or FALSE is_positive_int(5L) is_positive_int(-5L) # guard to avoid throwing errors if(FALSE) { # ... but it will provide custom diagnosis if invoked inside an assertion precondition(is_positive_int(-5L)) }
Assertions in the precondition
package support debug markers to provide
user-friendly assertion failure diagnosis. The low-level diagnostic
machinery is implemented by diagnose_expressions()
. Advanced users can
make use of this function in their own code or when implementing custom
assertion helpers (see diagnose_assertion_failure()
).
Use single curly braces {x}
to mark expressions of interest and make them
appear as separate entries in the diagnostic output. Use double curly braces
{{x}}
to perform checks on behalf of a parent function and display
diagnostics in the context of the parent.
diagnose_expressions(..., .env)
diagnose_expressions(..., .env)
... |
expressions to diagnose |
.env |
(advanced) the environment where the diagnosis should be performed |
diagnose_expressions()
supports two kinds of debug markers. Both rely on
wrapping expressions in one or more curly braces {}
.
wrapping an expression in curly braces (e.g. {x} > 0
) means that the this
expression is of particular interest and should be diagnosed separately.
The braces will be removed from the diagnostic output and the wrapped
expression will be added as a separate entry in the diagnostic table
(note: diagnose_expressions({x} > 0)
is equivalent to debug_expressions (x > 0, x)
).
wrapping a function argument in two curly braces (e.g. 'arg > 0) means that the argument is being been forwarded from a parent function. This concept of forwarding is borrowed from tidyverse's rlang::embrace-operator. A forwarded argument will be replaced by the original caller expression in the diagnostic output.
diagnose_expressions()
returns a data frame with one row per diagnosed
expression(either supplied as an argument or marked via {}
) and three
columns. The column expr
is a list of diagnosed expressions, with debug
markers processed and substituted. The column eval_result
is a list of
evaluated results for each diagnosed expressions. The column is_error
is a
logical vector where value of TRUE
indicates that an error occurred when
evaluating the respective expression. In this case the corresponding value
of eval_result
will capture the error condition.
Note that expressions or their parts might be evaluated more then once during diagnosis. Side effects in diagnosed expressions can lead to unexpected behavior.
a data frame with diagnostic information
x <- 10 diagnose_expressions({x} > 0, {x} > 15) helper <- function(arg) { cat(sprintf("`arg` is forwarded `%s`\n", forwarded_arg_label(arg))) diagnose_expressions({{{arg}}} > 0) } fun <- function(x) { helper(x) } fun(10)
x <- 10 diagnose_expressions({x} > 0, {x} > 15) helper <- function(arg) { cat(sprintf("`arg` is forwarded `%s`\n", forwarded_arg_label(arg))) diagnose_expressions({{{arg}}} > 0) } fun <- function(x) { helper(x) } fun(10)
fatal_error()
is equivalent to the base function base::stop()
,
except it is intended to signal critical errors where recovery
is impossible or unfeasible.
Fatal errors are signaled via rlang::abort()
with the class
precondition/fatal_error
. The option fatal_error_action
controls
the behavior of the fatal errors.
option(fatal_error_action = "inform")
will display a
warning if a fatal error has been prevented from bubbling up to the #
user(either via 'tryCatch() or some other error handling mechanism). This
is the default setting and will draw user's attention to a fatal error
occurring.
option(fatal_error_action = "none")
will make fatal errors
behave like regular R error conditions. Use this if your code contains custom
logic for handling fatal errors.
option(fatal_error_action = 'terminate')
will immediately
the program execution without saving the workspace or running finalizers
when a fatal error occurs.
fatal_error(bullets, ...)
fatal_error(bullets, ...)
bullets |
a character vector containing the error message,
can be formatted in the style of |
... |
reserved for future use |
fatal_error()
is used in sanity_check()
to report critical assertion failures.
The assertions described here are similar in functionality to the base R
function base::stopifnot()
, but focusing on better diagnostics, safer
behavior, and customizability.
precondition()
fails with diagnosis if its arguments do not evaluate as
TRUE
. Use this assertion function to check function arguments or data
inputs against code invariants.
postcondition()
is as above, but the assertion is performed when the
calling function successfully returns. Use this assertion to check that
the function has produced a well-formed result (via base::returnValue ()) or behavior.
sanity_check()
is as above, but the program execution will immediately
terminate via fatal_error()
, bypassing R's error-checking mechanisms.
Use this predicate to validate critical internal assumptions your code
relies upon. Failing a sanity check means that your program contains an
unrecoverable logical error and cannot reasonably continue execution.
To facilitate debugging, the assertions used with these functions can be
enhanced with debug markers. This enables informative error messages and
makes it easier to understand why the assertion has failed. First,
assertions can include custom informative messages, supplied via literal
string arguments to the assertion function. Second, key parts of the
assertion expression can be wrapped in curly braces(e.g. {x} > 0
). If the
assertion fails, the values marked in such way will be diagnosed and
displayed as a separate entry in the error message. See the examples on how
to use these features and the details section how to implement even more
custom functionality.
Under certain circumstances these predicates might evaluate the assertion expression multiple times. Beware of combining them with side effects.
precondition(...) postcondition(...) sanity_check(...)
precondition(...) postcondition(...) sanity_check(...)
... |
one or more expressions to check (with optional assertion messages) |
A precondition is an assertion that specifies a set of conditions that must
be true in order for the execution to proceed in a meaningful way. This is
usually conditioned on the user input or environment in some way. A
postcondition is an assertion that must be true if a function has executed
in a meaningful way. Pre- and postconditions explicitly state the contract
of a function and make it easier to debug correct function usage. Note:
postcondition(check)
is similar to on.exit(stopifnot(check))
, except
that the postcondition will not be checked if an error occurred during
function execution.
A sanity check is an assertion that specifies a set of conditions that the program implicitly assumes to be true. A sanity check failure means that the core logic of the program is broken and error recovery is either impossible or not meaningful. Sanity checks are used to test the internal logic of your code and will result in an immediate program termination if failed (via fatal_error).
The arguments to these assertion functions are either expressions that should
evaluate to TRUE
or literal string constants containing informative
messages (e.g. sanity_check("x is not NULL", !is.null(x))
). Should the
assertion fail, the provided message will be displayed. Note that this
message must be a string literal, you cannot compute it or use a variable.
The following will not work correctly: 'sanity_check(paste0("x is
not", "NULL"), !is.null(x)).
Assertion expression support debug-markers. See diagnose_assertion_failure()
on how to implement custom assertion helpers.
TRUE
on assertion success, raises an error of class
precondition/assertion_error
on assertion failure
# These examples are guarded to avoid throwing errors if (FALSE) { # function contract is accepting a positive value and returning up to 20 fun <- function(x) { precondition("`x` should be positive", {x} > 0) postcondition(returnValue() <= 20) out <- x*2 sanity_check("twice `x` is larger than `x`", {out} > {x}) out } fun(5) fun(0) fun(10) }
# These examples are guarded to avoid throwing errors if (FALSE) { # function contract is accepting a positive value and returning up to 20 fun <- function(x) { precondition("`x` should be positive", {x} > 0) postcondition(returnValue() <= 20) out <- x*2 sanity_check("twice `x` is larger than `x`", {out} > {x}) out } fun(5) fun(0) fun(10) }