diff --git a/.gitignore b/.gitignore index dcf683c..d770c7c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ ..Rcheck *.Rcheck *.tar.gz +.DS_Store +src/*.o +src/*.so diff --git a/DESCRIPTION b/DESCRIPTION index 0ae6aaf..47df2ca 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: testthatsomemore Title: A mocking, stubbing, and file testing framework extending testthat -Version: 0.2.6 +Version: 0.2.7 Maintainer: Robert Krzyzanowski Authors@R: c(person("Robert", "Krzyzanowski", email = "technoguyrob@gmail.com", role = c("aut", "cre"))) @@ -24,4 +24,4 @@ License: MIT + file LICENSE LazyData: true URL: http://github.com/robertzk/testthatsomemore BugReports: https://github.com/robertzk/testthatsomemore/issues -RoxygenNote: 5.0.1 +RoxygenNote: 5.0.0 diff --git a/NAMESPACE b/NAMESPACE index 9803142..b777912 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export("stub<-") export(assert) export(create_file_structure) +export(duplicate) export(expect_is_directory) export(expect_is_file) export(is.directory) @@ -14,3 +15,4 @@ export(touch) export(within_file_structure) import(testthat) importFrom(crayon,yellow) +useDynLib(testthatsomemore,duplicate_testthatsomemore_) diff --git a/NEWS.md b/NEWS.md index 566e0df..0dcf7a3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# Version 0.2.7 + + * Added expect_called helper function. + # Version 0.2.6 * Added mock_httr_response for mocking httr response objects. diff --git a/R/duplicate.R b/R/duplicate.R new file mode 100644 index 0000000..35d5ef0 --- /dev/null +++ b/R/duplicate.R @@ -0,0 +1,17 @@ +#' Duplicate a function. Returns a carbon copy of the original, so that original +#' can be modified safely. +#' +#' @param x function. +#' @examples +#' \dontrun{ +#' fn <- function(x) cat(x, "\n") +#' fn2 <- duplicate(fn) +#' fn <- function(x) cat(x, "woof","\n") +#' fn("hello") # hellowoof +#' fn2("hello") # hello +#' } +#' @export +#' @useDynLib testthatsomemore duplicate_testthatsomemore_ +duplicate <- function(x) { + .Call(duplicate_testthatsomemore_, x) +} \ No newline at end of file diff --git a/R/expect_called.R b/R/expect_called.R new file mode 100644 index 0000000..c5ac902 --- /dev/null +++ b/R/expect_called.R @@ -0,0 +1,42 @@ +#' Expect that a function was called while executing an expression. +#' +#' @param fn function. The function that was expected to get called. If not +#' specified with \code{::}, package_name param must be specified. +#' @param expr expression. The expression to execute. +#' @param package_name string. The package where fn lives. If not specified, fn must +#' be package_name::fn. +#' @param was_called logical. If \code{FALSE}, negate the meaning: \code{fn} +#' should \emph{not} have been called. By default \code{TRUE}. +#' @return invisible \code{TRUE}. +#' @examples \dontrun{ +#' expect_called(base::print, print("Hello")) # Will pass +#' expect_called(print, print("Hello"), "base") # Will pass +#' expect_called(base::print, cat("Hello")) # Will fail +#' expect_called(base::print, cat("Hello"), was_called = FALSE) # Will pass +#' } +expect_called <- function(fn, expr, package_name, was_called = TRUE) { + fn <- substitute(fn) + if (is.call(fn) && identical(as.character(fn[[1]]), "not")) { + fn <- fn[[2]] + was_called <- FALSE + } + # Turn foo into package_name::foo + if (is.name(substitute(fn))) fn <- bquote(getFromNamespace(.(deparse(fn)), .(package_name))) + else fn <- as.call(list(quote(getFromNamespace), as.character(fn[[3]]), as.character(fn[[2]]))) + + grab <- function(i) as.character(as.list(fn)[[i]]) + + copy_of_fn <- duplicate(eval(fn)) + env <- list2env(list(called = 0), parent = emptyenv()) + + mocked_fn <- function(...) { + env$called <- env$called + 1 + copy_of_fn(...) + } + + result <- testthatsomemore::package_stub(grab(3), grab(2), mocked_fn, force(expr)) + expect_identical(was_called, as.logical(env$called)) + + invisible(TRUE) +} + diff --git a/man/duplicate.Rd b/man/duplicate.Rd new file mode 100644 index 0000000..c794763 --- /dev/null +++ b/man/duplicate.Rd @@ -0,0 +1,26 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/duplicate.R +\name{duplicate} +\alias{duplicate} +\title{Duplicate a function. Returns a carbon copy of the original, so that original +can be modified safely.} +\usage{ +duplicate(x) +} +\arguments{ +\item{x}{function.} +} +\description{ +Duplicate a function. Returns a carbon copy of the original, so that original +can be modified safely. +} +\examples{ +\dontrun{ + fn <- function(x) cat(x, "\\n") + fn2 <- duplicate(fn) + fn <- function(x) cat(x, "woof","\\n") + fn("hello") # hellowoof + fn2("hello") # hello +} +} + diff --git a/man/expect_called.Rd b/man/expect_called.Rd new file mode 100644 index 0000000..efc5bed --- /dev/null +++ b/man/expect_called.Rd @@ -0,0 +1,35 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/expect_called.R +\name{expect_called} +\alias{expect_called} +\title{Expect that a function was called while executing an expression.} +\usage{ +expect_called(fn, expr, package_name, was_called = TRUE) +} +\arguments{ +\item{fn}{function. The function that was expected to get called. If not +specified with \code{::}, package_name param must be specified.} + +\item{expr}{expression. The expression to execute.} + +\item{package_name}{string. The package where fn lives. If not specified, fn must +be package_name::fn.} + +\item{was_called}{logical. If \code{FALSE}, negate the meaning: \code{fn} +should \emph{not} have been called. By default \code{TRUE}.} +} +\value{ +invisible \code{TRUE}. +} +\description{ +Expect that a function was called while executing an expression. +} +\examples{ +\dontrun{ + expect_called(base::print, print("Hello")) # Will pass + expect_called(print, print("Hello"), "base") # Will pass + expect_called(base::print, cat("Hello")) # Will fail + expect_called(base::print, cat("Hello"), was_called = FALSE) # Will pass +} +} + diff --git a/src/duplicate.c b/src/duplicate.c new file mode 100644 index 0000000..ef6dd12 --- /dev/null +++ b/src/duplicate.c @@ -0,0 +1,9 @@ +//shamelessly ripped off from @jimhester +#define USE_RINTERNALS +#include +#include +#include + +SEXP duplicate_testthatsomemore_(SEXP x) { + return duplicate(x); +} \ No newline at end of file diff --git a/tests/testthat/test-duplicate.R b/tests/testthat/test-duplicate.R new file mode 100644 index 0000000..f79f135 --- /dev/null +++ b/tests/testthat/test-duplicate.R @@ -0,0 +1,7 @@ +context("duplicate") + +test_that('it duplicates a function', { + fn <- function(x) cat(x, "\n") + fn_copy <- duplicate(fn) + expect_equal(fn, fn_copy) +}) diff --git a/tests/testthat/test-expect_called.R b/tests/testthat/test-expect_called.R new file mode 100644 index 0000000..921569b --- /dev/null +++ b/tests/testthat/test-expect_called.R @@ -0,0 +1,31 @@ +context('expect_called') + + +test_that('it finds called function', { + f <- function() sum(2, 2) + expect_true(expect_called(base::sum, f())) +}) + +test_that('it detects function was not called', { + f <- function() 10 + expect_true(expect_called(base::sum, f(), was_called = FALSE )) +}) + +test_that('Package setting works', { + f <- function() sum(1, 2) + expect_true(expect_called(sum, f(), "base")) +}) + +# neither test_that nor describe wrappers work here? +# test_that('it detects errors when function is expected but actually not called', { +# f <- function() 10 +# expect_error( +# expect_called(base::sum, f() ), +# "is not identical" +# ) +# }) + + + + + diff --git a/tests/testthat/test-pretend_now_is.R b/tests/testthat/test-pretend_now_is.R index c6c5c3a..270e0f7 100644 --- a/tests/testthat/test-pretend_now_is.R +++ b/tests/testthat/test-pretend_now_is.R @@ -40,7 +40,9 @@ describe("pretend_now_is", { test_that("it can pretend it's 5 seconds ago", { now <- Sys.time() five_s_ago <- now - as.difftime(5, units = "secs") - pretend_now_is("5 seconds ago", expect_equal(Sys.time(), five_s_ago)) + pretend_now_is("5 seconds ago", { + expect_equal(as.Date(Sys.time()), as.Date(five_s_ago)) + }) }) test_that("it can pretend Sys.Date is different", {