Skip to content

Commit

Permalink
feat: simplify syntax; auto-roxygenize; improve lifecycle mgmt
Browse files Browse the repository at this point in the history
  • Loading branch information
dgkf authored Apr 14, 2024
1 parent ee5e7de commit 0edede5
Show file tree
Hide file tree
Showing 31 changed files with 567 additions and 337 deletions.
11 changes: 6 additions & 5 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: testex
Title: Add Tests to Examples
Version: 0.1.0
Version: 0.2.0
Authors@R:
c(
person(
Expand All @@ -9,18 +9,18 @@ Authors@R:
role = c("aut", "cre")
)
)
Description:
Description:
Add tests in-line in examples. Provides standalone functions for
facilitating easier test writing in Rd files. However, a more familiar
interface is provided using 'roxygen2' tags. Tools are also provided for
facilitating package configuration and use with 'testthat'.
URL: https://github.com/dgkf/testex, https://dgkf.github.io/testex/
License: MIT + file LICENSE
Depends:
Depends:
R (>= 3.2.0)
Imports:
Imports:
utils
Suggests:
Suggests:
testthat,
withr,
callr,
Expand All @@ -33,3 +33,4 @@ Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.1
Language: en-US
VignetteBuilder: knitr
Config/testex/options: list(version = "0.2.0")
2 changes: 1 addition & 1 deletion NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ export(fallback_expect_no_error)
export(s3_register)
export(test_examples_as_testthat)
export(testex)
export(testthat_block)
export(use_testex)
export(use_testex_as_testthat)
export(uses_roxygen2)
export(with_attached)
export(with_srcref)
importFrom(utils,getSrcFilename)
Expand Down
45 changes: 45 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,50 @@
# testex (development)

> **Life-cycle Policy Prior to `v1.0.0`**
>
> Be aware that this package produces code that enters into your package's
> R documentation files. Until `testex` reaches `1.0.0`, there are no
> guarantees for a stable interface, which means your package's tests written
> in documentation files may fail if interface changes.
>
# testex 0.2.0

## Breaking Changes

> Documentation syntax changes. Documentation will be need to be
> re-`roxygenize`'d or otherwise updated.
* Changes syntax of tests to minimize reliance on `testex` namespace
consistency across versions. Instead of using `testex(with_srcref(..))` and
`testthat_block(test_that(.., with_srcref(..)))`, both interfaces are now
handled via `testex()` with an added `style` parameter:

```r
testex(style = "testthat", srcref = "fn.R:10:11", { code })
```

This syntax is intended to be more resilient to changes to keep your
tests from relying to heavily on an unchanging `testex` function interface.

## New Features

* Adds configuration (`Config/testex/options`) field `"version"`, which is
automatically updated when a newer version of `testex` is first used.

This field is checked to decide whether the currently loaded version of
`testex` is capable of re-running your tests.

Currently, a conservative approach is taken. If there is a version mismatch,
`testex` will suggest updating when run independently using a testing
framework and will disable `testex` testing during `R CMD check` to avoid
causing downstream test failures as the API changes. However, this means
that `testex` tests will be ineffective if your package is out-of-date
with the released `testex` version on `CRAN`

Past a `v1.0.0` release, this behavior will be relaxed to check for a
compatible major version.

# testex 0.1.0

* Initial CRAN submission
88 changes: 72 additions & 16 deletions R/opts.R
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
#' As long as the `fingerprint` has not changed, the package `DESCRIPTION` will
#' be read only once to parse and retrieve configuration options. If the
#' `DESCRIPTION` file is modified or if run from a separate process, the
#' configured settings will be refreshed based on the most recent version of
#' configured settings will be refreshed based on the most recent version of
#' the file.
#'
#' @param path A path in which to search for a package `DESCRIPTION`
Expand All @@ -18,48 +18,104 @@
#'
#' @name testex-options
#' @keywords internal
update_testex_desc <- function(path, fingerprint) {
memoise_testex_desc <- function(path, fingerprint, ...) {
if (identical(fingerprint, .testex_options$.fingerprint)) {
return(invisible(.testex_options))
}

field <- "Config/testex/options"
desc_opts <- read.dcf(file = path, fields = field, keep.white = field)[[1L]]

# the field name is erroneously parsed with the contents on R <4.1 in CMD check
desc_opts <- gsub(paste0(field, ": "), "", desc_opts, fixed = TRUE)
desc_opts <- read_testex_options(path, ...)

desc_opts <- eval(parse(text = desc_opts), envir = baseenv())
# clean and re-load memoised options
rm(list = names(.testex_options), envir = .testex_options)
for (n in names(desc_opts)) .testex_options[[n]] <- desc_opts[[n]]

.testex_options$.fingerprint <- fingerprint
invisible(.testex_options)
}



read_testex_options <- function(path, warn = TRUE, update = FALSE) {
desc <- read.dcf(file = path, all = TRUE)
desc <- read.dcf(file = path, keep.white = colnames(desc))

field <- "Config/testex/options"
desc_opts <- if (field %in% colnames(desc)) desc[, field][[1]] else ""

# the field name is erroneously parsed with the contents on R <4.1 in CMD check
desc_opts <- gsub(paste0(field, ": "), "", desc_opts, fixed = TRUE)
pkg_opts <- pkg_opts_orig <- eval(parse(text = desc_opts), envir = baseenv())
loaded_version <- packageVersion(packageName())
loaded_version_str <- as.character(loaded_version)

warn_mismatch_msg <- cliless(
"{.pkg testex} {.code version} in {.file DESCRIPTION} does not match ",
"currently loaded version. Consider updating to avoid unexpected test ",
"failures. Execution during {.code R CMD check} disabled."
)

if (update) {
# update registered version if necessary
if (is.null(pkg_opts$version) || pkg_opts$version < loaded_version) {
pkg_opts$version <- loaded_version_str
}

# only write if field was modified
if (!identical(pkg_opts, pkg_opts_orig)) {
if (!field %in% colnames(desc)) {
field_col <- matrix(nrow = nrow(desc), dimnames = list(c(), field))
desc <- cbind(desc, field_col)
}

desc[, field] <- deparse(pkg_opts)

write.dcf(
desc,
file = path,
keep.white = colnames(desc),
width = 80L,
indent = 2L
)
}
}

if (!identical(pkg_opts$version, loaded_version_str)) {
if (warn) warning(warn_mismatch_msg)
pkg_opts$check <- FALSE
}

pkg_opts
}



#' @describeIn testex-options
#'
#' @return The test options environment as a list
#'
testex_options <- function(path = package_desc()) {
testex_options <- function(path = package_desc(), ...) {
path <- package_desc(path)

if (is_r_cmd_check()) {
fingerprint <- list(
rcmdcheck = TRUE,
pid = Sys.getpid()
)
fingerprint <- list(rcmdcheck = TRUE, pid = Sys.getpid())

return(as.list(update_testex_desc(path, fingerprint)))
# don't warn or update description during checking
return(as.list(memoise_testex_desc(
path,
fingerprint,
warn = FALSE,
update = FALSE
)))
}

if (file.exists(path)) {
if (!is.null(path) && file.exists(path)) {
fingerprint <- list(
desc = TRUE,
path = path,
mtime = file.info(path)[["mtime"]]
)

return(as.list(update_testex_desc(path, fingerprint)))
return(as.list(memoise_testex_desc(path, fingerprint, ...)))
}

return(as.list(.testex_options))
Expand Down
31 changes: 17 additions & 14 deletions R/roxygen2.R
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,15 @@ NULL
#' @importFrom utils head tail
#' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_test
roxy_tag_parse.roxy_tag_test <- function(x) {
testex_options(path = x$file, warn = TRUE, update = TRUE)
x$raw <- x$val <- format_tag_expect_test(x)
as_example(x)
}

#' @importFrom utils head tail
#' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_test
roxy_tag_parse.roxy_tag_testthat <- function(x) {
testex_options(path = x$file, warn = TRUE, update = TRUE)
x$raw <- x$val <- format_tag_testthat_test(x)
as_example(x)
}
Expand Down Expand Up @@ -98,14 +100,22 @@ as_example <- function(tag) {
#'
#' @noRd
#' @keywords internal
format_tag_expect_test <- function(tag) { # nolint
format_tag_expect_test <- function(tag) { # nolint
parsed_test <- parse(text = tag$raw, n = 1, keep.source = TRUE)
test <- populate_test_dot(parsed_test)
n <- first_expr_end(parsed_test)

test_str <- trimws(substring(tag$raw, 0, n), "right")
n_newlines <- nchar(gsub("[^\n]", "", test_str))

srcref_str <- paste0(
basename(tag$file),
":", tag$line, ":", tag$line + n_newlines
)

paste0(
"\\testonly{",
"testex::testex(",
"\\testonly{\n",
"testex::testex(srcref = ", deparse(srcref_str), ", \n",
deparse_pretty(test),
")}",
trimws(substring(tag$raw, n + 1L), "right")
Expand Down Expand Up @@ -139,7 +149,7 @@ populate_test_dot <- function(expr) {
#'
#' @noRd
#' @keywords internal
format_tag_testthat_test <- function(tag) { # nolint
format_tag_testthat_test <- function(tag) { # nolint
parsed_test <- parse(text = tag$raw, n = 1, keep.source = TRUE)
test <- populate_testthat_dot(parsed_test)

Expand All @@ -148,20 +158,13 @@ format_tag_testthat_test <- function(tag) { # nolint

nlines <- string_newline_count(trimws(test_str, "right"))
lines <- tag$line + c(0L, nlines)

src <- paste0(basename(tag$file), ":", lines[[1]], ":", lines[[2]])
desc <- sprintf("example tests at `%s`", src)

paste0(
"\\testonly{\n",
paste0("testex::testthat_block(test_that(", deparse(desc), ", {\n"),
paste0(
"testex::with_srcref(",
"\"", src, "\", ", deparse_pretty(test),
")\n"
),
"}))\n",
"}",
"testex::testex(style = \"testthat\", srcref = ", deparse(src), ", \n",
deparse_pretty(test),
")}",
trimws(substring(tag$raw, n + 1L), "right")
)
}
Expand Down
Loading

0 comments on commit 0edede5

Please sign in to comment.