testit/0000755000176200001440000000000014035577302011574 5ustar liggesuserstestit/NAMESPACE0000644000176200001440000000022114035567147013014 0ustar liggesusers# Generated by roxygen2: do not edit by hand export("%==%") export(assert) export(has_error) export(has_warning) export(test_pkg) import(utils) testit/man/0000755000176200001440000000000013357673757012370 5ustar liggesuserstestit/man/has_message.Rd0000644000176200001440000000127014035567147015123 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/testit.R \name{has_warning} \alias{has_warning} \alias{has_error} \title{Check if an R expression produces warnings or errors} \usage{ has_warning(expr) has_error(expr, silent = !interactive()) } \arguments{ \item{expr}{an R expression} \item{silent}{logical: should the report of error messages be suppressed?} } \value{ A logical value. } \description{ The two functions \code{has_warning()} and \code{has_error()} check if an expression produces warnings and errors, respectively. } \examples{ has_warning(1 + 1) has_warning(1:2 + 1:3) has_error(2 - 3) has_error(1 + "a") has_error(stop("err"), silent = TRUE) } testit/man/assert.Rd0000644000176200001440000000757114035567147014157 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/testit.R \name{assert} \alias{assert} \alias{\%==\%} \title{Assertions with an optional message} \usage{ assert(fact, ...) x \%==\% y } \arguments{ \item{fact}{a message for the assertions when any of them fails; treated the same way as expressions in \code{...} if it is not a character string, which means you do not have to provide a message to this function} \item{...}{an R expression; see Details} \item{x, y}{two R objects to be compared} } \value{ For \code{assert()}, invisible \code{NULL} if all expressions returned \code{TRUE}, otherwise an error is signaled and the user-provided message is emitted. For \code{\%==\%}, \code{TRUE} or \code{FALSE}. } \description{ The function \code{assert()} was inspired by \code{\link{stopifnot}()}. It emits a message in case of errors, which can be a helpful hint for diagnosing the errors (\code{stopifnot()} only prints the possibly truncated source code of the expressions). The infix operator \code{\%==\%} is simply an alias of the \code{\link{identical}()} function to make it slightly easier and intuitive to write test conditions. \code{x \%==\% y} is the same as \code{identical(x, y)}. When it is used inside \code{assert()}, a message will be printed if the returned value is not \code{TRUE}, to show the values of the LHS (\code{x}) and RHS (\code{y}) via \code{\link{str}()}, which can be helpful for you to check why the assertion failed. } \details{ For the \code{...} argument, it should be a single R expression wrapped in \code{{}}. This expression may contain multiple sub-expressions. A sub-expression is treated as a test condition if it is wrapped in \code{()} (meaning its value will be checked to see if it is a logical vector containing any \code{FALSE} values) , otherwise it is evaluated in the normal way and its value will not be checked. If the value of the last sub-expression is logical, it will also be treated as a test condition. } \note{ The internal implementation of \code{assert()} is different with the \code{stopifnot()} function in R \pkg{base}: (1) the custom message \code{fact} is emitted if an error occurs; (2) \code{assert()} requires the logical values to be non-empty (\code{logical(0)} will trigger an error); (3) if \code{...} contains a compound expression in \code{{}} that returns \code{FALSE} (e.g., \code{if (TRUE) {1+1; FALSE}}), the first and the last but one line of the source code from \code{\link{deparse}()} are printed in the error message, otherwise the first line is printed; (4) the arguments in \code{...} are evaluated sequentially, and \code{assert()} will signal an error upon the first failed assertion, and will ignore the rest of assertions. } \examples{ ## The first way to write assertions ------------------- assert("T is bad for TRUE, and so is F for FALSE", { T = FALSE F = TRUE (T != TRUE) # note the parentheses (F != FALSE) }) assert("A Poisson random number is non-negative", { x = rpois(1, 10) (x >= 0) (x > -1) # () is optional because it's the last expression }) ## The second way to write assertions -------------------- assert("one equals one", 1 == 1) assert("seq and : produce equal sequences", seq(1L, 10L) == 1L:10L) assert("seq and : produce identical sequences", identical(seq(1L, 10L), 1L:10L)) # multiple tests T = FALSE F = TRUE assert("T is bad for TRUE, and so is F for FALSE", T != TRUE, F != FALSE) # a mixture of tests assert("Let's pray all of them will pass", 1 == 1, 1 != 2, letters[4] == "d", rev(rev(letters)) == letters) # logical(0) cannot pass assert(), although stopifnot() does not care try(assert("logical(0) cannot pass", 1 == integer(0))) stopifnot(1 == integer(0)) # it's OK! # a compound expression try(assert("this if statement returns TRUE", if (TRUE) { x = 1 x == 2 })) # no message assert(!FALSE, TRUE, is.na(NA)) } testit/man/test_pkg.Rd0000644000176200001440000000363014035567147014466 0ustar liggesusers% Generated by roxygen2: do not edit by hand % Please edit documentation in R/testit.R \name{test_pkg} \alias{test_pkg} \title{Run the tests of a package in its namespace} \usage{ test_pkg(package, dir = c("testit", "tests/testit")) } \arguments{ \item{package}{the package name} \item{dir}{the directory of the test files; by default, it is the directory \file{testit/} or \file{tests/testit/} under the current working directory} } \value{ \code{NULL}. All test files are executed, unless an error occurs. } \description{ The main purpose of this function is to expose the namespace of a package when running tests, which allows one to use non-exported objects in the package without having to resort to the triple colon \code{\link{:::}} trick. } \details{ The tests are assumed to be under the \file{testit/} or \file{tests/testit/} directory by default (depending on your working directory is the package root directory or the \file{tests/} directory). This function also looks for the \file{tests/testit/} directory under the package installation directory when the user-provided \code{dir} does not exist. The test scripts must be named of the form \samp{test-*.R}; other R scripts will not be treated as test files (but may also be useful, e.g. you can \code{\link{source}()} them in tests). For \command{R CMD check}, this means the test R scripts (\file{test-*.R} are under \file{pkg_root/tests/testit/}. The R scripts are executed with \code{\link{sys.source}} in the namespace of the package to be tested; when an R script is executed, the working directory is the same as the directory containing this script, and all existing objects in the test environment will be removed before the code is executed. } \note{ All test scripts (\samp{test-*.R}) must be encoded in UTF-8 if they contain any multibyte characters. } \examples{ \dontrun{ test_pkg("testit") } } \seealso{ The \pkg{testthat} package (much more sophisticated). } testit/DESCRIPTION0000644000176200001440000000150414035577302013302 0ustar liggesusersPackage: testit Type: Package Title: A Simple Package for Testing R Packages Version: 0.13 Authors@R: c( person("Yihui", "Xie", role = c("aut", "cre"), email = "xie@yihui.name", comment = c(ORCID = "0000-0003-0645-5666")), person("Steven", "Mortimer", role = "ctb", email="reportmort@gmail.com") ) Description: Provides two convenience functions assert() and test_pkg() to facilitate testing R packages. License: GPL-3 URL: https://github.com/yihui/testit BugReports: https://github.com/yihui/testit/issues Suggests: rstudioapi, covr RoxygenNote: 7.1.1 Encoding: UTF-8 NeedsCompilation: no Packaged: 2021-04-14 13:30:17 UTC; yihui Author: Yihui Xie [aut, cre] (), Steven Mortimer [ctb] Maintainer: Yihui Xie Repository: CRAN Date/Publication: 2021-04-14 14:40:02 UTC testit/tests/0000755000176200001440000000000013732272702012735 5ustar liggesuserstestit/tests/test-all.R0000644000176200001440000000011113727322335014600 0ustar liggesuserslibrary(testit) test_pkg('testit') try(test_pkg('testit', 'test-error')) testit/tests/test-error/0000755000176200001440000000000013357673757015065 5ustar liggesuserstestit/tests/test-error/test-error.R0000644000176200001440000000003612661414653017276 0ustar liggesusersstop('An intentional error!') testit/tests/testit/0000755000176200001440000000000013357673757014273 5ustar liggesuserstestit/tests/testit/test-assert.R0000644000176200001440000000217113326651121016646 0ustar liggesuserslibrary(testit) assert('assert works', 1 == 1) # Okay, that is kind of cheating assert( 'assert() should signal an error if a condition does not hold', has_error(assert('this should produce an error', 1 == 2)) ) # a meaningless test in terms of R (failure is irrelevant to Frequentist or Bayesian) try(assert( 'Frequentists must be correct (http://xkcd.com/1132/)', 'The sun has exploded!', sample(6, 2) == c(6, 6) ), silent = !interactive()) # fail logical(0) assert( 'assert() should stop on logical(0)', has_error(assert('1 equals integer(0)', 1 == integer(0))) ) assert( 'the infix operator %==% works', 1 %==% 1, !(1 %==% 1L) ) assert( 'has_warning() works', has_warning(warning('An intentional warning')), has_warning((function() {1:2 + 1:3})()) ) assert( 'has_error() works', has_error(stop('An intentional error')), has_error(1 + 'a') ) assert( 'has_error() can suppress error message', has_error(stop('An intentional error'), silent = TRUE), has_error(1 + 'a', silent = FALSE) ) assert('tests can be written in () in a single {}', { (1 == 1L) z = 1:10 (rev(z) %==% 10:1) !!TRUE }) testit/tests/testit/test-utils.R0000644000176200001440000000145613326651315016517 0ustar liggesuserslibrary(testit) # no need to use testit:::available_dir() assert( 'available_dir() should find an existing directory', file.exists( available_dir(c('foobar', 'whatever', '~', system.file('man', package = 'testit'))) ), has_error(available_dir('asdfasdf')) ) exprs = parse(text = 'if (TRUE) {T&F}\n1+1') assert( 'deparse_key() fetches the n-1 element if code is in {}', deparse_key(exprs[[1]]) == 'if (TRUE) { .... T & F' ) assert( 'deparse_key() returns the parsed code if length == 1', deparse_key(exprs[[2]]) == '1 + 1' ) assert('insert_identical() should not work in a non-interactive R session', { if (!interactive()) has_error(insert_identical()) }) assert('sys.source2() works on empty files', { f = tempfile() writeLines(' ', f) (sys.source2(f, environment()) %==% NULL) }) testit/R/0000755000176200001440000000000014035566723012002 5ustar liggesuserstestit/R/utils.R0000644000176200001440000000347513727316246013276 0ustar liggesusers# an internal environment to store objects .env = new.env(parent = emptyenv()) # has the package been installed once in test_pkg()? .env$installed = FALSE # find an available dir available_dir = function(dirs) { for (i in dirs) { if (utils::file_test('-d', i)) return(i) } stop('none of the directories exists:\n', paste(utils::formatUL(dirs), collapse = '\n')) } # tailored for assert(): extract the expression that is likely to be useful for # diagnostics if possible deparse_key = function(expr) { x = deparse(expr, width.cutoff = 100L) if ((n <- length(x)) <= 1) return(x) # if expression is in {}, fetch the line n-1, otherwise use the first line paste(x[1], '....', if (x[n] == '}') sub('^\\s*', '', x[n - 1L])) } # whether every element of x is strictly TRUE all_true = function(x) { is.logical(x) && length(x) && !any(is.na(x)) && all(x) } insert_identical = function() { rstudioapi::insertText(text = ' %==% ') } # This function is a modification of base::sys.source. It allows to specify # the top-level environment, which is by default "envir" (the same as in # base::sys.source), but for package testing it is desirable to use the # package namespace to mimic the environment structure used when packages # are running. This function assumes that chdir = FALSE and keep.source = TRUE. sys.source2 = function(file, envir, top.env = as.environment(envir)) { oop = options(keep.source = TRUE, topLevelEnvironment = top.env) on.exit(options(oop), add = TRUE) lines = readLines(file, warn = FALSE, encoding = 'UTF-8') srcfile = srcfilecopy(file, lines, file.mtime(file), isFile = TRUE) exprs = parse(text = lines, srcfile = srcfile, encoding = 'UTF-8') if (length(exprs) == 0L) return() owd = setwd(dirname(file)); on.exit(setwd(owd), add = TRUE) for (i in seq_along(exprs)) eval(exprs[i], envir) } testit/R/zzz.R0000644000176200001440000000041113732271700012745 0ustar liggesusers# clean up the temp library created in test_pkg() at the end of the R session .onLoad = function(libname, pkgname) { reg.finalizer(.env, function(e) { unlink(e$lib_new, recursive = TRUE) if (!is.null(e$lib_old)) .libPaths(e$lib_old) }, onexit = TRUE) } testit/R/testit.R0000644000176200001440000002422114035566723013442 0ustar liggesusers#' Assertions with an optional message #' #' The function \code{assert()} was inspired by \code{\link{stopifnot}()}. It #' emits a message in case of errors, which can be a helpful hint for diagnosing #' the errors (\code{stopifnot()} only prints the possibly truncated source code #' of the expressions). #' #' For the \code{...} argument, it should be a single R expression wrapped in #' \code{{}}. This expression may contain multiple sub-expressions. A #' sub-expression is treated as a test condition if it is wrapped in \code{()} #' (meaning its value will be checked to see if it is a logical vector #' containing any \code{FALSE} values) , otherwise it is evaluated in the normal #' way and its value will not be checked. If the value of the last #' sub-expression is logical, it will also be treated as a test condition. #' @param fact a message for the assertions when any of them fails; treated the #' same way as expressions in \code{...} if it is not a character string, #' which means you do not have to provide a message to this function #' @param ... an R expression; see Details #' @return For \code{assert()}, invisible \code{NULL} if all expressions #' returned \code{TRUE}, otherwise an error is signaled and the user-provided #' message is emitted. For \code{\%==\%}, \code{TRUE} or \code{FALSE}. #' @note The internal implementation of \code{assert()} is different with the #' \code{stopifnot()} function in R \pkg{base}: (1) the custom message #' \code{fact} is emitted if an error occurs; (2) \code{assert()} requires the #' logical values to be non-empty (\code{logical(0)} will trigger an error); #' (3) if \code{...} contains a compound expression in \code{{}} that returns #' \code{FALSE} (e.g., \code{if (TRUE) {1+1; FALSE}}), the first and the last #' but one line of the source code from \code{\link{deparse}()} are printed in #' the error message, otherwise the first line is printed; (4) the arguments #' in \code{...} are evaluated sequentially, and \code{assert()} will signal #' an error upon the first failed assertion, and will ignore the rest of #' assertions. #' @export #' @examples #' ## The first way to write assertions ------------------- #' #' assert('T is bad for TRUE, and so is F for FALSE', {T=FALSE;F=TRUE #' (T!=TRUE) # note the parentheses #' (F!=FALSE)}) #' #' assert('A Poisson random number is non-negative', { #' x = rpois(1, 10) #' (x >= 0) #' (x > -1) # () is optional because it's the last expression #' }) #' #' #' ## The second way to write assertions -------------------- #' #' assert('one equals one', 1==1) #' assert('seq and : produce equal sequences', seq(1L, 10L) == 1L:10L) #' assert('seq and : produce identical sequences', identical(seq(1L, 10L), 1L:10L)) #' #' # multiple tests #' T=FALSE; F=TRUE #' assert('T is bad for TRUE, and so is F for FALSE', T!=TRUE, F!=FALSE) #' #' # a mixture of tests #' assert("Let's pray all of them will pass", 1==1, 1!=2, letters[4]=='d', rev(rev(letters))==letters) #' #' # logical(0) cannot pass assert(), although stopifnot() does not care #' try(assert('logical(0) cannot pass', 1==integer(0))) #' stopifnot(1==integer(0)) # it's OK! #' #' # a compound expression #' try(assert('this if statement returns TRUE', if(TRUE){x=1;x==2})) #' #' # no message #' assert(!FALSE, TRUE, is.na(NA)) assert = function(fact, ...) { opt = options(testit.asserting = TRUE); on.exit(options(opt), add = TRUE) mc = match.call() # match.call() uses the arg order in the func def, so fact is always 1st arg fact = NULL if (is.character(mc[[2]])) { fact = mc[[2]]; mc = mc[-2] } one = one_expression(mc) assert2(fact, if (one) mc[[2]][-1] else mc[-1], parent.frame(), !one) } # whether the argument of a function call is a single expression in {} one_expression = function(call) { length(call) == 2 && length(call[[2]]) >= 1 && identical(call[[c(2, 1)]], as.symbol('{')) } assert2 = function(fact, exprs, envir, all = TRUE) { n = length(exprs) for (i in seq_len(n)) { expr = exprs[[i]] val = eval(expr, envir = envir, enclos = NULL) # special case: fact is an expression instead of a string constant in assert() if (is.null(fact) && all && i == 1 && is.character(val)) { fact = val; next } # check all values in case of multiple arguments, o/w only check values in () if (all || (i == n && is.logical(val)) || (length(expr) >= 1 && identical(expr[[1]], as.symbol('(')))) { if (all_true(val)) next if (!is.null(fact)) message('assertion failed: ', fact) stop(sprintf( ngettext(length(val), '%s is not TRUE', '%s are not all TRUE'), deparse_key(expr) ), call. = FALSE, domain = NA) } } } #' @description The infix operator \code{\%==\%} is simply an alias of the #' \code{\link{identical}()} function to make it slightly easier and intuitive #' to write test conditions. \code{x \%==\% y} is the same as #' \code{identical(x, y)}. When it is used inside \code{assert()}, a message #' will be printed if the returned value is not \code{TRUE}, to show the #' values of the LHS (\code{x}) and RHS (\code{y}) via \code{\link{str}()}, #' which can be helpful for you to check why the assertion failed. #' @param x,y two R objects to be compared #' @rdname assert #' @import utils #' @export `%==%` = function(x, y) { res = identical(x, y) if (!res && isTRUE(getOption('testit.asserting', FALSE))) { mc = match.call() info = paste(capture.output({ cat(deparse_key(mc[[2]]), '(LHS) ==>\n') str(x) cat('----------\n') str(y) cat('<== (RHS)', deparse_key(mc[[3]]), '\n') }), collapse = '\n') message(info) } res } #' Run the tests of a package in its namespace #' #' The main purpose of this function is to expose the namespace of a package #' when running tests, which allows one to use non-exported objects in the #' package without having to resort to the triple colon \code{\link{:::}} trick. #' #' The tests are assumed to be under the \file{testit/} or \file{tests/testit/} #' directory by default (depending on your working directory is the package root #' directory or the \file{tests/} directory). This function also looks for the #' \file{tests/testit/} directory under the package installation directory when #' the user-provided \code{dir} does not exist. The test scripts must be named #' of the form \samp{test-*.R}; other R scripts will not be treated as test #' files (but may also be useful, e.g. you can \code{\link{source}()} them in #' tests). #' #' For \command{R CMD check}, this means the test R scripts (\file{test-*.R} are #' under \file{pkg_root/tests/testit/}. The R scripts are executed with #' \code{\link{sys.source}} in the namespace of the package to be tested; when #' an R script is executed, the working directory is the same as the directory #' containing this script, and all existing objects in the test environment will #' be removed before the code is executed. #' @param package the package name #' @param dir the directory of the test files; by default, it is the directory #' \file{testit/} or \file{tests/testit/} under the current working directory #' @return \code{NULL}. All test files are executed, unless an error occurs. #' @note All test scripts (\samp{test-*.R}) must be encoded in UTF-8 if they #' contain any multibyte characters. #' @seealso The \pkg{testthat} package (much more sophisticated). #' @export #' @examples \dontrun{test_pkg('testit')} test_pkg = function(package, dir = c('testit', 'tests/testit')) { # install the source package before running tests when this function is called # in a non-interactive R session that is not `R CMD check` install = !.env$installed && !interactive() && file.exists(desc <- file.path('../DESCRIPTION')) && is.na(Sys.getenv('_R_CHECK_PACKAGE_NAME_', NA)) && !is.na(p <- read.dcf(desc, fields = 'Package')[1, 1]) && p == package if (install) { .env$lib_old = lib_old = .libPaths() .env$lib_new = lib_new = tempfile('R-lib-', '.'); dir.create(lib_new) res = system2( file.path(R.home('bin'), 'R'), c( 'CMD', 'INSTALL', paste0('--library=', lib_new), '--no-help', '--no-staged-install', '--no-test-load', '..' ) ) if (res == 0) { .libPaths(c(lib_new, lib_old)) .env$installed = TRUE } } if (!is.na(i <- match(paste0('package:', package), search()))) detach(pos = i, unload = TRUE, force = TRUE) library(package, character.only = TRUE) path = available_dir(c(dir, system.file('tests', 'testit', package = package))) fs = list.files(path, full.names = TRUE) # clean up new files/dirs generated during testing if (getOption('testit.cleanup', TRUE)) on.exit({ unlink(setdiff(list.files(path, full.names = TRUE), fs), recursive = TRUE) }, add = TRUE) rs = fs[grep('^test-.+[.][rR]$', basename(fs))] # make all objects in the package visible to tests env = new.env(parent = getNamespace(package)) for (r in rs) { rm(list = ls(env, all.names = TRUE), envir = env) withCallingHandlers( sys.source2(r, envir = env, top.env = getNamespace(package)), error = function(e) { z = .traceback(5) if (length(z) == 0) return() z = z[[1]] n = length(z) s = if (!is.null(srcref <- attr(z, 'srcref'))) { paste0(' at ', basename(attr(srcref, 'srcfile')$filename), '#', srcref[1]) } cat('Error from', z[1], if (n > 1) '...', s, '\n') } ) } } #' Check if an R expression produces warnings or errors #' #' The two functions \code{has_warning()} and \code{has_error()} check if an #' expression produces warnings and errors, respectively. #' @param expr an R expression #' @param silent logical: should the report of error messages be suppressed? #' @return A logical value. #' @export #' @rdname has_message #' @examples has_warning(1+1); has_warning(1:2+1:3) #' #' has_error(2-3); has_error(1+'a'); has_error(stop("err"), silent = TRUE) has_warning = function(expr) { warn = FALSE op = options(warn = -1); on.exit(options(op)) withCallingHandlers(expr, warning = function(w) { warn <<- TRUE invokeRestart('muffleWarning') }) warn } #' @export #' @rdname has_message has_error = function(expr, silent = !interactive()) { inherits(try(force(expr), silent = silent), 'try-error') } testit/MD50000644000176200001440000000131114035577302012100 0ustar liggesusers419b281df18c6b3b8f23e434bf77cadf *DESCRIPTION 78c5938d8bfd6df28866607550a87a6b *NAMESPACE 699d59694a3fe3bdd3d3db0040dfd5c7 *R/testit.R 16333d65e9f4dd53d50ee3cba5d0aba3 *R/utils.R 2441fdcc8adda58063777f331393cd8a *R/zzz.R df271e1b16c7d1fd4eae3599f10e716b *inst/NEWS.Rd 4aae9b69bfa4f9baa95bd50f98274ba3 *inst/rstudio/addins.dcf 7d219d592188867d38ccfe155abcead6 *man/assert.Rd 31e04410a6e767062af385680c9d9b6c *man/has_message.Rd 768b639bfcf9290b21550404635fbfae *man/test_pkg.Rd fc95de596ff143707ce5da2874086a92 *tests/test-all.R 5330c047ff926dcaf08d9266db9a71f8 *tests/test-error/test-error.R 19586fa6d7b802908a529efc2d81a8f0 *tests/testit/test-assert.R d0e552278f4254984e245eae8b556f76 *tests/testit/test-utils.R testit/inst/0000755000176200001440000000000014035566656012563 5ustar liggesuserstestit/inst/rstudio/0000755000176200001440000000000012660730017014236 5ustar liggesuserstestit/inst/rstudio/addins.dcf0000644000176200001440000000017012660730065016157 0ustar liggesusersName: Insert %==% Description: Insert the infix operator %==% from testit. Binding: insert_identical Interactive: false testit/inst/NEWS.Rd0000644000176200001440000000043414035566656013627 0ustar liggesusers\name{NEWS} \title{News for Package 'testit'} \section{CHANGES IN testit VERSION 999.999}{ \itemize{ \item This NEWS file is only a placeholder. The version 999.999 does not really exist. Please read the NEWS on Github: \url{https://github.com/yihui/testit/releases} } }