├── .Rbuildignore ├── .github └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ └── test-coverage.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── opts.R ├── register-s3.R ├── roxygen2.R ├── testex.R ├── testthat.R ├── use.R ├── utils.R ├── utils_rd.R ├── utils_srcref.R └── zzz.R ├── README.md ├── codecov.yml ├── inst └── pkg.example │ ├── DESCRIPTION │ ├── LICENSE │ ├── NAMESPACE │ ├── R │ └── fn.R │ ├── man │ ├── fn.Rd │ ├── fn_roxygen.Rd │ ├── fn_roxygen_multiple.Rd │ └── fn_roxygen_testthat.Rd │ └── tests │ ├── testthat.R │ └── testthat │ ├── test-fn.R │ └── test-testex.R ├── man ├── as.srcref.Rd ├── deparse_indent.Rd ├── deparse_pretty.Rd ├── fallback_expect_no_error.Rd ├── file_line_nchar.Rd ├── get_example_value.Rd ├── is_r_cmd_check.Rd ├── package-file-helpers.Rd ├── s3_register.Rd ├── split_srcref.Rd ├── srclocs.Rd ├── srcref_key.Rd ├── srcref_nlines.Rd ├── string_newline_count.Rd ├── test_examples_as_testthat.Rd ├── test_files.Rd ├── testex-options.Rd ├── testex-rd-example-helpers.Rd ├── testex-roxygen-tags.Rd ├── testex-testthat.Rd ├── testex.Rd ├── use_testex.Rd ├── use_testex_as_testthat.Rd ├── uses_roxygen2.Rd ├── vapplys.Rd ├── with_attached.Rd ├── with_srcref.Rd └── wrap_expect_no_error.Rd ├── pkgdown ├── _pkgdown.yml └── extra.scss ├── tests ├── spelling.R ├── testthat.R └── testthat │ ├── setup.R │ ├── test-escape-infotex.R │ ├── test-options.R │ ├── test-pkgexample.R │ ├── test-rd-utils.R │ ├── test-roxygen2-expect.R │ ├── test-roxygen2-parse-text.R │ ├── test-roxygen2-testthat.R │ ├── test-srcref-key.R │ ├── test-testex.R │ ├── test-testthat.R │ └── test-use.R └── vignettes ├── .gitignore ├── configuration.Rmd └── interface_layers.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | \.github 2 | codecov\.yml 3 | LICENSE\.md 4 | pkgdown 5 | docs 6 | 7 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | branches: [main, master] 12 | 13 | name: R-CMD-check 14 | 15 | jobs: 16 | R-CMD-check: 17 | runs-on: ${{ matrix.config.os }} 18 | 19 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 20 | 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | config: 25 | - {os: macos-latest, r: 'release'} 26 | 27 | - {os: windows-latest, r: 'release'} 28 | # Use 3.6 to trigger usage of RTools35 29 | - {os: windows-latest, r: '3.6'} 30 | # use 4.1 to check with rtools40's older compiler 31 | - {os: windows-latest, r: '4.1'} 32 | 33 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 34 | - {os: ubuntu-latest, r: 'release'} 35 | - {os: ubuntu-latest, r: 'oldrel-1'} 36 | - {os: ubuntu-latest, r: 'oldrel-2'} 37 | - {os: ubuntu-latest, r: 'oldrel-3'} 38 | - {os: ubuntu-latest, r: 'oldrel-4'} 39 | 40 | env: 41 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 42 | R_KEEP_PKG_SOURCE: yes 43 | 44 | steps: 45 | - uses: actions/checkout@v3 46 | 47 | - uses: r-lib/actions/setup-pandoc@v2 48 | 49 | - uses: r-lib/actions/setup-r@v2 50 | with: 51 | r-version: ${{ matrix.config.r }} 52 | http-user-agent: ${{ matrix.config.http-user-agent }} 53 | use-public-rspm: true 54 | 55 | - uses: r-lib/actions/setup-r-dependencies@v2 56 | with: 57 | extra-packages: any::rcmdcheck 58 | needs: check 59 | 60 | - uses: r-lib/actions/check-r-package@v2 61 | with: 62 | upload-snapshots: true 63 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown 13 | 14 | jobs: 15 | pkgdown: 16 | runs-on: ubuntu-latest 17 | # Only restrict concurrency for non-PR jobs 18 | concurrency: 19 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 20 | env: 21 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - uses: r-lib/actions/setup-pandoc@v2 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::pkgdown, local::. 34 | needs: website 35 | 36 | - name: Build site 37 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 38 | shell: Rscript {0} 39 | 40 | - name: Deploy to GitHub pages 🚀 41 | if: github.event_name != 'pull_request' 42 | uses: JamesIves/github-pages-deploy-action@v4.4.1 43 | with: 44 | clean: false 45 | branch: gh-pages 46 | folder: docs 47 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage 10 | 11 | jobs: 12 | test-coverage: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - uses: r-lib/actions/setup-r@v2 21 | with: 22 | use-public-rspm: true 23 | 24 | - uses: r-lib/actions/setup-r-dependencies@v2 25 | with: 26 | extra-packages: any::covr 27 | needs: coverage 28 | 29 | - name: Test coverage 30 | run: | 31 | covr::codecov( 32 | quiet = FALSE, 33 | clean = FALSE, 34 | install_path = file.path(Sys.getenv("RUNNER_TEMP"), "package") 35 | ) 36 | shell: Rscript {0} 37 | 38 | - name: Show testthat output 39 | if: always() 40 | run: | 41 | ## -------------------------------------------------------------------- 42 | find ${{ runner.temp }}/package -name 'testthat.Rout*' -exec cat '{}' \; || true 43 | shell: bash 44 | 45 | - name: Upload test results 46 | if: failure() 47 | uses: actions/upload-artifact@v3 48 | with: 49 | name: coverage-test-failures 50 | path: ${{ runner.temp }}/package 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs 2 | inst/doc 3 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: testex 2 | Title: Add Tests to Examples 3 | Version: 0.2.0.9000 4 | Authors@R: 5 | c( 6 | person( 7 | "Doug", "Kelkhoff", 8 | email = "doug.kelkhoff@gmail.com", 9 | role = c("aut", "cre") 10 | ) 11 | ) 12 | Description: 13 | Add tests in-line in examples. Provides standalone functions for 14 | facilitating easier test writing in Rd files. However, a more familiar 15 | interface is provided using 'roxygen2' tags. Tools are also provided for 16 | facilitating package configuration and use with 'testthat'. 17 | URL: https://github.com/dgkf/testex, https://dgkf.github.io/testex/ 18 | License: MIT + file LICENSE 19 | Depends: 20 | R (>= 3.2.0) 21 | Imports: 22 | utils 23 | Suggests: 24 | testthat, 25 | withr, 26 | callr, 27 | roxygen2, 28 | spelling, 29 | knitr, 30 | rmarkdown 31 | Encoding: UTF-8 32 | Roxygen: list(markdown = TRUE) 33 | RoxygenNote: 7.3.1 34 | Language: en-US 35 | VignetteBuilder: knitr 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: testex authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2022 testex authors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(roxygen2::roxy_tag_parse,roxy_tag_test) 4 | export(fallback_expect_no_error) 5 | export(s3_register) 6 | export(test_examples_as_testthat) 7 | export(testex) 8 | export(use_testex) 9 | export(use_testex_as_testthat) 10 | export(uses_roxygen2) 11 | export(with_attached) 12 | export(with_srcref) 13 | importFrom(utils,getSrcFilename) 14 | importFrom(utils,getSrcLocation) 15 | importFrom(utils,getSrcref) 16 | importFrom(utils,head) 17 | importFrom(utils,packageName) 18 | importFrom(utils,packageVersion) 19 | importFrom(utils,tail) 20 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # testex (development) 2 | 3 | > **Life-cycle Policy Prior to `v1.0.0`** 4 | > 5 | > Be aware that this package produces code that enters into your package's 6 | > R documentation files. Until `testex` reaches `1.0.0`, there are no 7 | > guarantees for a stable interface, which means your package's tests written 8 | > in documentation files may fail if interface changes. 9 | > 10 | 11 | # testex 0.2.0 12 | 13 | ## Breaking Changes 14 | 15 | > Documentation syntax changes. Documentation will be need to be 16 | > re-`roxygenize`'d or otherwise updated. 17 | 18 | * Changes syntax of tests to minimize reliance on `testex` namespace 19 | consistency across versions. Instead of using `testex(with_srcref(..))` and 20 | `testthat_block(test_that(.., with_srcref(..)))`, both interfaces are now 21 | handled via `testex()` with an added `style` parameter: 22 | 23 | ```r 24 | testex(style = "testthat", srcref = "fn.R:10:11", { code }) 25 | ``` 26 | 27 | This syntax is intended to be more resilient to changes to keep your 28 | tests from relying too heavily on an unchanging `testex` function interface. 29 | 30 | ## New Features 31 | 32 | * Adds configuration (`Config/testex/options`) field `"version"`, which is 33 | automatically updated when a newer version of `testex` is first used. 34 | 35 | This field is checked to decide whether the currently loaded version of 36 | `testex` is capable of re-running your tests. 37 | 38 | Currently, a conservative approach is taken. If there is a version mismatch, 39 | `testex` will suggest updating when run independently using a testing 40 | framework and will disable `testex` testing during `R CMD check` to avoid 41 | causing downstream test failures as the API changes. However, this means 42 | that `testex` tests will be ineffective if your package is out-of-date 43 | with the released `testex` version on `CRAN` 44 | 45 | Past a `v1.0.0` release, this behavior will be relaxed to check for a 46 | compatible major version. 47 | 48 | # testex 0.1.0 49 | 50 | * Initial CRAN submission 51 | -------------------------------------------------------------------------------- /R/opts.R: -------------------------------------------------------------------------------- 1 | .testex_options <- new.env(parent = baseenv()) 2 | 3 | 4 | 5 | #' Cached retrieval of testex options from package DESCRIPTION 6 | #' 7 | #' As long as the `fingerprint` has not changed, the package `DESCRIPTION` will 8 | #' be read only once to parse and retrieve configuration options. If the 9 | #' `DESCRIPTION` file is modified or if run from a separate process, the 10 | #' configured settings will be refreshed based on the most recent version of 11 | #' the file. 12 | #' 13 | #' @param path A path in which to search for a package `DESCRIPTION` 14 | #' @param fingerprint An object used to indicate when the cached values have 15 | #' been invalidated 16 | #' 17 | #' @return The test options environment, invisibly. 18 | #' 19 | #' @name testex-options 20 | #' @keywords internal 21 | memoise_testex_desc <- function(path, fingerprint, ...) { 22 | if (identical(fingerprint, .testex_options$.fingerprint)) { 23 | return(invisible(.testex_options)) 24 | } 25 | 26 | desc_opts <- read_testex_options(path, ...) 27 | 28 | # clean and re-load memoised options 29 | rm(list = names(.testex_options), envir = .testex_options) 30 | for (n in names(desc_opts)) .testex_options[[n]] <- desc_opts[[n]] 31 | 32 | .testex_options$.fingerprint <- fingerprint 33 | invisible(.testex_options) 34 | } 35 | 36 | 37 | 38 | read_testex_options <- function(path, warn = TRUE, update = FALSE) { 39 | desc <- read.dcf(file = path, all = TRUE) 40 | desc <- read.dcf(file = path, keep.white = colnames(desc)) 41 | 42 | field <- "Config/testex/options" 43 | desc_opts <- if (field %in% colnames(desc)) desc[, field][[1]] else "" 44 | 45 | # the field name is erroneously parsed with the contents on R <4.1 in CMD check 46 | desc_opts <- gsub(paste0(field, ": "), "", desc_opts, fixed = TRUE) 47 | pkg_opts <- pkg_opts_orig <- eval(parse(text = desc_opts), envir = baseenv()) 48 | loaded_version <- packageVersion(packageName()) 49 | loaded_version_str <- as.character(loaded_version) 50 | 51 | warn_mismatch_msg <- cliless( 52 | "{.pkg testex} {.code version} in {.file DESCRIPTION} does not match ", 53 | "currently loaded version. Consider updating to avoid unexpected test ", 54 | "failures. Execution during {.code R CMD check} disabled." 55 | ) 56 | 57 | if (update) { 58 | # update registered version if necessary 59 | if (is.null(pkg_opts$version) || pkg_opts$version < loaded_version) { 60 | pkg_opts$version <- loaded_version_str 61 | } 62 | 63 | # only write if field was modified 64 | if (!identical(pkg_opts, pkg_opts_orig)) { 65 | if (!field %in% colnames(desc)) { 66 | field_col <- matrix(nrow = nrow(desc), dimnames = list(c(), field)) 67 | desc <- cbind(desc, field_col) 68 | } 69 | 70 | desc[, field] <- deparse(pkg_opts) 71 | 72 | write.dcf( 73 | desc, 74 | file = path, 75 | keep.white = colnames(desc), 76 | width = 80L, 77 | indent = 2L 78 | ) 79 | } 80 | } 81 | 82 | if (!identical(pkg_opts$version, loaded_version_str)) { 83 | if (warn) warning(warn_mismatch_msg) 84 | pkg_opts$check <- FALSE 85 | } 86 | 87 | pkg_opts 88 | } 89 | 90 | 91 | 92 | #' @describeIn testex-options 93 | #' 94 | #' @return The test options environment as a list 95 | #' 96 | testex_options <- function(path = package_desc(), ...) { 97 | path <- package_desc(path) 98 | 99 | if (is_r_cmd_check()) { 100 | fingerprint <- list(rcmdcheck = TRUE, pid = Sys.getpid()) 101 | 102 | # don't warn or update description during checking 103 | return(as.list(memoise_testex_desc( 104 | path, 105 | fingerprint, 106 | warn = FALSE, 107 | update = FALSE 108 | ))) 109 | } 110 | 111 | if (!is.null(path) && file.exists(path)) { 112 | fingerprint <- list( 113 | desc = TRUE, 114 | path = path, 115 | mtime = file.info(path)[["mtime"]] 116 | ) 117 | 118 | return(as.list(memoise_testex_desc(path, fingerprint, ...))) 119 | } 120 | 121 | return(as.list(.testex_options)) 122 | } 123 | -------------------------------------------------------------------------------- /R/register-s3.R: -------------------------------------------------------------------------------- 1 | # This source code file is licensed under the `unlicense` license 2 | # https://unlicense.org 3 | 4 | #' Register a method for a suggested dependency 5 | #' 6 | #' Generally, the recommend way to register an S3 method is to use the 7 | #' `S3Method()` namespace directive (often generated automatically by the 8 | #' `@export` `roxygen2` tag). However, this technique requires that the generic 9 | #' be in an imported package, and sometimes you want to suggest a package, 10 | #' and only provide a method when that package is loaded. `s3_register()` 11 | #' can be called from your package's `.onLoad()` to dynamically register 12 | #' a method only if the generic's package is loaded. 13 | #' 14 | #' For R 3.5.0 and later, `s3_register()` is also useful when demonstrating 15 | #' class creation in a vignette, since method lookup no longer always involves 16 | #' the lexical scope. For R 3.6.0 and later, you can achieve a similar effect 17 | #' by using "delayed method registration", i.e. placing the following in your 18 | #' `NAMESPACE` file: 19 | #' 20 | #' ``` 21 | #' if (getRversion() >= "3.6.0") { 22 | #' S3method(package::generic, class) 23 | #' } 24 | #' ``` 25 | #' 26 | #' @section Usage in other packages: 27 | #' To avoid taking a dependency on `vctrs`, you copy the source of 28 | #' [`s3_register()`](https://github.com/r-lib/vctrs/blob/main/R/register-s3.R) 29 | #' into your own package. It is licensed under the permissive 30 | #' [`unlicense`](https://choosealicense.com/licenses/unlicense/) to make it 31 | #' crystal clear that we're happy for you to do this. There's no need to include 32 | #' the license or even credit us when using this function. 33 | #' 34 | #' @usage NULL 35 | #' @param generic Name of the generic in the form `pkg::generic`. 36 | #' @param class Name of the class 37 | #' @param method Optionally, the implementation of the method. By default, 38 | #' this will be found by looking for a function called `generic.class` 39 | #' in the package environment. 40 | #' 41 | #' Note that providing `method` can be dangerous if you use 42 | #' `devtools`. When the namespace of the method is reloaded by 43 | #' `devtools::load_all()`, the function will keep inheriting from 44 | #' the old namespace. This might cause crashes because of dangling 45 | #' `.Call()` pointers. 46 | #' 47 | #' @return No return value, called for side-effect of registering an S3 48 | #' generic. 49 | #' 50 | #' @examples 51 | #' # A typical use case is to dynamically register tibble/pillar methods 52 | #' # for your class. That way you avoid creating a hard dependency on packages 53 | #' # that are not essential, while still providing finer control over 54 | #' # printing when they are used. 55 | #' 56 | #' .onLoad <- function(...) { 57 | #' s3_register("pillar::pillar_shaft", "vctrs_vctr") 58 | #' s3_register("tibble::type_sum", "vctrs_vctr") 59 | #' } 60 | #' 61 | #' @export 62 | #' @keywords internal 63 | # nocov start 64 | s3_register <- function(generic, class, method = NULL) { 65 | stopifnot(is.character(generic), length(generic) == 1) 66 | stopifnot(is.character(class), length(class) == 1) 67 | 68 | pieces <- strsplit(generic, "::")[[1]] 69 | stopifnot(length(pieces) == 2) 70 | package <- pieces[[1]] 71 | generic <- pieces[[2]] 72 | 73 | caller <- parent.frame() 74 | 75 | get_method_env <- function() { 76 | top <- topenv(caller) 77 | if (isNamespace(top)) { 78 | asNamespace(environmentName(top)) 79 | } else { 80 | caller 81 | } 82 | } 83 | get_method <- function(method) { 84 | if (is.null(method)) { 85 | get(paste0(generic, ".", class), envir = get_method_env()) 86 | } else { 87 | method 88 | } 89 | } 90 | 91 | register <- function(...) { 92 | envir <- asNamespace(package) 93 | 94 | # Refresh the method each time, it might have been updated by 95 | # `devtools::load_all()` 96 | method_fn <- get_method(method) 97 | stopifnot(is.function(method_fn)) 98 | 99 | # Only register if generic can be accessed 100 | if (exists(generic, envir)) { 101 | registerS3method(generic, class, method_fn, envir = envir) 102 | } else if (identical(Sys.getenv("NOT_CRAN"), "true")) { 103 | warning(c( 104 | sprintf( 105 | "Can't find generic `%s` in package %s to register S3 method.", 106 | generic, 107 | package 108 | ) 109 | )) 110 | } 111 | } 112 | 113 | # Always register hook in case package is later unloaded & reloaded 114 | setHook(packageEvent(package, "onLoad"), function(...) { 115 | register() 116 | }) 117 | 118 | # For compatibility with R < 4.0 where base isn't locked 119 | is_sealed <- function(pkg) { 120 | identical(pkg, "base") || environmentIsLocked(asNamespace(pkg)) 121 | } 122 | 123 | # Avoid registration failures during loading (pkgload or regular). 124 | # Check that environment is locked because the registering package 125 | # might be a dependency of the package that exports the generic. In 126 | # that case, the exports (and the generic) might not be populated 127 | # yet (#1225). 128 | if (isNamespaceLoaded(package) && is_sealed(package)) { 129 | register() 130 | } 131 | 132 | invisible() 133 | } 134 | #nocov end 135 | -------------------------------------------------------------------------------- /R/roxygen2.R: -------------------------------------------------------------------------------- 1 | #' [`testex`] `roxygen2` tags 2 | #' 3 | #' [`testex`] provides two new `roxygen2` tags, `@test` and `@testthat`. 4 | #' 5 | #' @section tags: 6 | #' [testex] tags are all sub-tags meant to be used within an 7 | #' `@examples` block. They should be considered as tags \emph{within} the 8 | #' `@examples` block and used to construct blocks of testing code within 9 | #' example code. 10 | #' 11 | #' \describe{ 12 | #' \item{`@test`: }{ 13 | #' In-line expectations to test the output of the previous command within an 14 | #' example. If `.` is used within the test expression, it will be used to 15 | #' refer to the output of the previous example command. Otherwise, the 16 | #' result of the expression is expected to be identical to the previous 17 | #' output. 18 | #' 19 | #' #' @examples 20 | #' #' 1 + 2 21 | #' #' @test 3 22 | #' #' @test . == 3 23 | #' #' 24 | #' #' @examples 25 | #' #' 3 + 4 26 | #' #' @test identical(., 7) 27 | #' } 28 | #' } 29 | #' 30 | #' \describe{ 31 | #' \item{`@testthat`: }{ 32 | #' Similar to `@test`, `@testthat` can be used to make in-line 33 | #' assertions using `testthat` expectations. `testthat` expectations 34 | #' follow a convention where the first argument is an object to compare 35 | #' against an expected value or characteristic. Since the value will always 36 | #' be the result of the previous example, this part of the code is 37 | #' implicitly constructed for you. 38 | #' 39 | #' If you want to use the example result elsewhere in your expectation, you 40 | #' can refer to it with a `.`. When used in this way, [testex] will 41 | #' not do any further implicit modification of your expectation. 42 | #' 43 | #' #' @examples 44 | #' #' 1 + 2 45 | #' #' @testthat expect_equal(3) 46 | #' #' @testthat expect_gt(0) 47 | #' #' 48 | #' #' @examples 49 | #' #' 3 + 4 50 | #' #' @testthat expect_equal(., 7) 51 | #' } 52 | #' } 53 | #' 54 | #' @name testex-roxygen-tags 55 | NULL 56 | 57 | 58 | 59 | #' @importFrom utils head tail 60 | #' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_test 61 | roxy_tag_parse.roxy_tag_test <- function(x) { 62 | testex_options(path = x$file, warn = TRUE, update = TRUE) 63 | x$raw <- x$val <- format_tag_expect_test(x) 64 | as_example(x) 65 | } 66 | 67 | #' @importFrom utils head tail 68 | #' @exportS3Method roxygen2::roxy_tag_parse roxy_tag_test 69 | roxy_tag_parse.roxy_tag_testthat <- function(x) { 70 | testex_options(path = x$file, warn = TRUE, update = TRUE) 71 | x$raw <- x$val <- format_tag_testthat_test(x) 72 | as_example(x) 73 | } 74 | 75 | 76 | 77 | #' Convert a `roxygen2` Tag to an `@examples` Tag 78 | #' 79 | #' Allows for converting testing tags into additional `@examples` tags, which 80 | #' `roxygen2` will joint together into a single examples section. 81 | #' 82 | #' @param tag A `roxygen2` tag, whose class should be converted into an 83 | #' `@examples` tag. 84 | #' @return The tag with an appropriate examples s3 class. 85 | #' 86 | #' @noRd 87 | #' @keywords internal 88 | as_example <- function(tag) { 89 | class(tag) <- class(tag)[!startsWith(class(tag), "roxy_tag_")] 90 | class(tag) <- c("roxy_tag_examples", class(tag)) 91 | roxygen2::tag_examples(tag) 92 | } 93 | 94 | 95 | 96 | #' Format An `@test` Tag 97 | #' 98 | #' @param tag A `roxygen2` `@test` tag. 99 | #' @return A formatted string of R documentation `\testonly{}` code. 100 | #' 101 | #' @noRd 102 | #' @keywords internal 103 | format_tag_expect_test <- function(tag) { # nolint 104 | parsed_test <- parse(text = tag$raw, n = 1, keep.source = TRUE) 105 | test <- populate_test_dot(parsed_test) 106 | n <- first_expr_end(parsed_test) 107 | 108 | test_str <- trimws(substring(tag$raw, 0, n), "right") 109 | n_newlines <- nchar(gsub("[^\n]", "", test_str)) 110 | 111 | srcref_str <- paste0( 112 | basename(tag$file), 113 | ":", tag$line, ":", tag$line + n_newlines 114 | ) 115 | 116 | paste0( 117 | "\\testonly{\n", 118 | "testex::testex(srcref = ", deparse(srcref_str), ", \n", 119 | deparse_pretty(test), 120 | ")}", 121 | trimws(substring(tag$raw, n + 1L), "right") 122 | ) 123 | } 124 | 125 | #' Populate An Implicit `@test` Lambda Function 126 | #' 127 | #' When a `@test` tag does not contain a `.` object, its result is considered 128 | #' an an implicit test for an identical object. 129 | #' 130 | #' @param expr A (possibly) implicity lambda function 131 | #' @return A new expression, calling identical if needed. 132 | #' 133 | #' @noRd 134 | #' @keywords internal 135 | populate_test_dot <- function(expr) { 136 | if (is.expression(expr)) expr <- expr[[1]] 137 | if (!"." %in% all.names(expr)) { 138 | expr <- bquote(identical(., .(expr))) 139 | } 140 | expr 141 | } 142 | 143 | 144 | 145 | #' Format An `@testthat` Tag 146 | #' 147 | #' @param tag A `roxygen2` `@testthat` tag. 148 | #' @return A formatted string of R documentation `\testonly{}` code. 149 | #' 150 | #' @noRd 151 | #' @keywords internal 152 | format_tag_testthat_test <- function(tag) { # nolint 153 | parsed_test <- parse(text = tag$raw, n = 1, keep.source = TRUE) 154 | test <- populate_testthat_dot(parsed_test) 155 | 156 | n <- first_expr_end(parsed_test) 157 | test_str <- substring(tag$raw, 1L, n) 158 | 159 | nlines <- string_newline_count(trimws(test_str, "right")) 160 | lines <- tag$line + c(0L, nlines) 161 | src <- paste0(basename(tag$file), ":", lines[[1]], ":", lines[[2]]) 162 | 163 | paste0( 164 | "\\testonly{\n", 165 | "testex::testex(style = \"testthat\", srcref = ", deparse(src), ", \n", 166 | deparse_pretty(test), 167 | ")}", 168 | trimws(substring(tag$raw, n + 1L), "right") 169 | ) 170 | } 171 | 172 | #' Populate An Implicit `@testthat` Lambda Function 173 | #' 174 | #' When a `testthat` tag does not contain a `.` object, its result is 175 | #' onsidered an an implicit `testthat` expectation, which should be injected 176 | #' with a `.` as a first argument. 177 | #' 178 | #' @param expr A (possibly) implicity lambda function 179 | #' @return A new expression, injecting a `.` argument if needed. 180 | #' 181 | #' @noRd 182 | #' @keywords internal 183 | populate_testthat_dot <- function(expr) { 184 | if (is.expression(expr)) expr <- expr[[1]] 185 | if (!"." %in% all.names(expr)) { 186 | expr <- as.call(append(as.list(expr), quote(.), after = 1L)) 187 | } 188 | expr 189 | } 190 | 191 | 192 | 193 | #' Find The Last Character of the First Expression 194 | #' 195 | #' @param x A parsed expression with [`srcref`]. 196 | #' @return An integer representing the character position of the end of the 197 | #' first call in a in a parsed expression. 198 | #' 199 | #' @noRd 200 | #' @keywords internal 201 | first_expr_end <- function(x) { 202 | if (!is.null(sr <- attr(x[[1]], "wholeSrcref"))) { 203 | nchar(paste0(as.character(sr), collapse = "\n")) 204 | } else if (!is.null(sr <- attr(x, "wholeSrcref"))) { 205 | nchar(paste0(as.character(sr), collapse = "\n")) 206 | } 207 | } 208 | 209 | 210 | 211 | #' Escape R Documentation `\\testonly` Strings 212 | #' 213 | #' @param x A `character` value 214 | #' @return An escaped string, where any `\` is converted to `\\` 215 | #' 216 | #' @noRd 217 | #' @family roclet_process_helpers 218 | #' @keywords internal 219 | escape_infotex <- function(x) { 220 | gsub("\\\\", "\\\\\\\\", x) 221 | } 222 | -------------------------------------------------------------------------------- /R/testex.R: -------------------------------------------------------------------------------- 1 | #' A syntactic helper for writing quick and easy example tests 2 | #' 3 | #' A wrapper around `stopifnot` that allows you to use `.` to refer to 4 | #' `.Last.value` and preserve the last non-test output from an example. 5 | #' 6 | #' @section Documenting with `testex`: 7 | #' 8 | #' `testex` is a simple wrapper around execution that propagates the 9 | #' `.Last.value` returned before running, allowing you to chain tests 10 | #' more easily. 11 | #' 12 | #' ## Use in `Rd` files: 13 | #' 14 | #' \preformatted{ 15 | #' \examples{ 16 | #' f <- function(a, b) a + b 17 | #' f(3, 4) 18 | #' \testonly{ 19 | #' testex::testex( 20 | #' is.numeric(.), 21 | #' identical(., 7) 22 | #' ) 23 | #' } 24 | #' } 25 | #' } 26 | #' 27 | #' But `Rd` files are generally regarded as being a bit cumbersome to author 28 | #' directly. Instead, `testex` provide helpers that generate this style of 29 | #' documentation, which use this function internally. 30 | #' 31 | #' ## Use with `roxygen2` 32 | #' 33 | #' Within a `roxygen2` `@examples` block you can instead use the `@test` tag 34 | #' which will generate Rd code as shown above. 35 | #' 36 | #' \preformatted{ 37 | #' #' @examples 38 | #' #' f <- function(a, b) a + b 39 | #' #' f(3, 4) 40 | #' #' @test is.numeric(.) 41 | #' #' @test identical(., 7) 42 | #' } 43 | #' 44 | #' @param ... Expressions to evaluated. `.` will be replaced with the 45 | #' expression passed to `val`, and may be used as a shorthand for the 46 | #' last example result. 47 | #' @param value A value to test against. By default, this will use the example's 48 | #' `.Last.value`. 49 | #' @param example_srcref An option `srcref_key` string used to indicate where 50 | #' the relevant example code originated from. 51 | #' @param srcref An option `srcref_key` string used to indicate where the 52 | #' relevant test code originated from. 53 | #' @param envir An environment in which tests should be evaluated. By default 54 | #' the parent environment where tests are evaluated. 55 | #' @param style A syntactic style used by the test. Defaults to `"standalone"`, 56 | #' which expects `TRUE` and uses a `.`-notation. Accepts one of 57 | #' `"standalone"` or `"testthat"`. By default, styles will be implicitly 58 | #' converted to accommodate known testing frameworks, though this can be 59 | #' disabled by passing the style `"AsIs"` with [I()]. 60 | #' 61 | #' @return Invisibly returns the `.Last.value` as it existed prior to evaluating 62 | #' the test. 63 | #' 64 | #' @export 65 | testex <- function( 66 | ..., 67 | srcref = NULL, 68 | example_srcref = NULL, 69 | value = get_example_value(), 70 | envir = parent.frame(), 71 | style = "standalone") { 72 | opts <- testex_options() 73 | if (is_r_cmd_check() && isFALSE(opts$check)) { 74 | return(invisible(.Last.value)) 75 | } 76 | 77 | if (!missing(value)) { 78 | value <- substitute(value) 79 | } 80 | 81 | is_testthat_running <- requireNamespace("testthat", quietly = TRUE) && 82 | testthat::is_testing() 83 | 84 | exprs <- substitute(...()) 85 | expr <- if (is_testthat_running && identical(style, "standalone")) { 86 | testex_standalone_as_testthat(exprs, srcref, value) 87 | } else if (identical(style, "testthat")) { 88 | testex_testthat(exprs, srcref, value) 89 | } else { 90 | testex_standalone(exprs, srcref, value) 91 | } 92 | 93 | eval(expr, envir = envir) 94 | } 95 | 96 | testex_standalone_as_testthat <- function(exprs, ...) { 97 | # wrap @test tests in expect_true when running with `testthat` 98 | exprs <- lapply(exprs, function(expr) bquote(testthat::expect_true(.(expr)))) 99 | testex_testthat(exprs, ...) 100 | } 101 | 102 | testex_testthat <- function(exprs, srcref, value) { 103 | # bind srcref if provided 104 | if (!is.null(srcref)) { 105 | exprs <- lapply(exprs, function(expr) { 106 | bquote(testex::with_srcref(.(srcref), .(expr))) 107 | }) 108 | } 109 | 110 | # build block, handling last output, example execution 111 | expr <- exprs_as_call(exprs) 112 | bquote(local(testex::with_attached("testthat", { 113 | . <- .(value) 114 | skip_if(inherits(., "error"), "previous example produced an error") 115 | .(expr) 116 | invisible(.) 117 | }))) 118 | } 119 | 120 | testex_standalone <- function(exprs, srcref, value) { 121 | expr <- bquote({ 122 | . <- .(value) 123 | .(as.call(append(list(as.name("stopifnot")), exprs))) 124 | invisible(.) 125 | }) 126 | } 127 | 128 | exprs_as_call <- function(exprs) { 129 | if (length(exprs) > 1) { 130 | as.call(append(list(as.name("{")), exprs)) 131 | } else { 132 | exprs[[1]] 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /R/testthat.R: -------------------------------------------------------------------------------- 1 | #' Support for `testthat` Expectations 2 | #' 3 | #' `testthat` support is managed through a "style" provided to [`testex`]. 4 | #' When using the `testthat` style (automatically when using the `@testthat` 5 | #' tag), expectations are processed such that they always refer to the previous 6 | #' example. Special care is taken to manage propagation of this value through 7 | #' your test code, regardless of how `testthat` is executed. 8 | #' 9 | #' @examplesIf requireNamespace("testthat", quietly = TRUE) 10 | #' # example code 11 | #' 1 + 2 12 | #' 13 | #' # within `testex` block, test code refers to previous result with `.` 14 | #' testex(style = "testthat", srcref = "abc.R:1:3", { \dontshow{ 15 | #' . <- 3 # needed because roxygen2 @examplesIf mutates .Last.value 16 | #' } 17 | #' test_that("addition holds up", { 18 | #' expect_equal(., 3) 19 | #' }) 20 | #' }) 21 | #' 22 | #' @name testex-testthat 23 | NULL 24 | 25 | 26 | 27 | #' Raise `testthat` Expectations With A Known Source Reference 28 | #' 29 | #' Retroactively assigns a source file and location to a expectation. This 30 | #' allows `testthat` to report an origin for any code that raised an example 31 | #' test failure from the source `roxygen2` code, even though the test code is 32 | #' reconstructed from package documentation files. 33 | #' 34 | #' @param src A `srcref_key` which is parsed to produce an artificial [`srcref`] 35 | #' for the expectation signaled messages. 36 | #' @param expr An expression to be evaluated. If an `expectation` condition is 37 | #' raised during its evaluation, its [`srcref`] is converted to `src`. 38 | #' @param envir An environment in which to evaluate `expr`. 39 | #' 40 | #' @return The result of evaluating `expr`, or an expectation with appended 41 | #' [`srcref`] information if an expectation is raised. 42 | #' 43 | #' @export 44 | with_srcref <- function(src, expr, envir = parent.frame()) { 45 | expr <- substitute(expr) 46 | withCallingHandlers( 47 | eval(expr, envir = envir), 48 | expectation = function(e) { 49 | srcref <- as.srcref(src) 50 | e[["srcref"]] <- srcref 51 | testthat::exp_signal(e) 52 | invokeRestart(computeRestarts()[[1L]]) 53 | } 54 | ) 55 | } 56 | 57 | 58 | 59 | #' Expect no Error 60 | #' 61 | #' @note This is a stop-gap implementation, and will only be used for legacy 62 | #' versions of `testthat` before this was properly supported. 63 | #' 64 | #' A `testthat` expectation that the provided code can be evaluated without 65 | #' producing an error. This is the most basic expectation one should expect of 66 | #' any example code. Further expectations are provided in subsequent `testthat` 67 | #' code. 68 | #' 69 | #' @param object An expression to evaluate 70 | #' @param ... Additional arguments unused 71 | #' 72 | #' @return The value produced by the expectation code 73 | #' 74 | #' @export 75 | fallback_expect_no_error <- function(object, ...) { 76 | object <- substitute(object) 77 | act <- list( 78 | val = tryCatch(eval(object, envir = parent.frame()), error = identity), 79 | lab = deparse(object) 80 | ) 81 | 82 | testthat::expect( 83 | !inherits(act$val, "error"), 84 | failure_message = sprintf( 85 | "Example %s threw an error during execution.", 86 | act$lab 87 | ), 88 | ... 89 | ) 90 | 91 | invisible(act$val) 92 | } 93 | 94 | #' Return appropriate call name provided testthat version 95 | #' @noRd 96 | expect_no_error_call <- function() { 97 | if (packageVersion("testthat") >= "3.1.5") { 98 | quote(testthat::expect_no_error) 99 | } else { 100 | quote(testex::fallback_expect_no_error) 101 | } 102 | } 103 | 104 | 105 | 106 | #' Execute examples from Rd files as `testthat` tests 107 | #' 108 | #' Reads examples from Rd files and constructs `testthat`-style tests. 109 | #' `testthat` expectations are built such that 110 | #' 111 | #' 1. Each example expression is expected to run without error 112 | #' 1. Any `testex` expectations are expected to pass 113 | #' 114 | #' Generally, you won't need to use this function directly. Instead, it 115 | #' is called by a file generated by [`use_testex_as_testthat()`] which will add 116 | #' any `testex` example tests to your existing `testthat` testing suite. 117 | #' 118 | #' @note 119 | #' It is assumed that this function is used within a `testthat` run, when 120 | #' the necessary packages are already installed and loaded. 121 | #' 122 | #' @param package A package name whose examples should be tested 123 | #' @param path Optionally, a path to a source code directory to use. Will only 124 | #' have an effect if parameter `package` is missing. 125 | #' @param test_dir An option directory where test files should be written. 126 | #' Defaults to a temporary directory. 127 | #' @param clean Whether the `test_dir` should be removed upon completion of test 128 | #' execution. Defaults to `TRUE`. 129 | #' @param overwrite Whether files should be overwritten if `test_dir` already 130 | #' exists. Defaults to `TRUE`. 131 | #' @param roxygenize Whether R documentation files should be re-written using 132 | #' `roxygen2` prior to testing. When not `FALSE`, tests written in `roxygen2` 133 | #' tags will be used to update R documentation files prior to testing to use 134 | #' the most up-to-date example tests. May be `TRUE`, or a `list` of arguments 135 | #' passed to [`roxygen2::roxygenize`]. By default, only enabled when running 136 | #' outside of `R CMD check` and while taking `roxygen2` as a dependency. 137 | #' @param ... Additional argument unused 138 | #' @param reporter A `testthat` reporter to use. Defaults to the active 139 | #' reporter in the `testthat` environment or default reporter. 140 | #' 141 | #' @return The result of [`testthat::source_file()`], after iterating over 142 | #' generated test files. 143 | #' 144 | #' @examplesIf requireNamespace("testthat", quietly = TRUE) 145 | #' \donttest{ 146 | #' # library(pkg.example) 147 | #' path <- system.file("pkg.example", package = "testex") 148 | #' test_examples_as_testthat(path = path) 149 | #' } 150 | #' 151 | #' @export 152 | test_examples_as_testthat <- function( 153 | package, 154 | path, 155 | ..., 156 | test_dir = file.path(tempdir(), "testex-tests"), 157 | clean = TRUE, 158 | overwrite = TRUE, 159 | roxygenize = !is_r_cmd_check() && uses_roxygen2(path), 160 | reporter = testthat::get_reporter()) { 161 | requireNamespace("testthat") 162 | testthat_envvar_val <- Sys.getenv("TESTTHAT") 163 | Sys.setenv(TESTTHAT = "true") 164 | on.exit(Sys.setenv(TESTTHAT = testthat_envvar_val), add = TRUE) 165 | 166 | if (missing(path)) { 167 | path <- find_package_root(testthat::test_path()) 168 | } 169 | 170 | if (isTRUE(roxygenize)) roxygenize <- list() 171 | if (is.list(roxygenize) && requireNamespace("roxygen2", quietly = TRUE)) { 172 | args <- roxygenize 173 | args$package.dir <- path 174 | context <- cliless("{.pkg testex} re-roxygenizing examples") 175 | testthat::context_start_file(context) 176 | testthat::expect_invisible(suppressMessages({ 177 | do.call(getExportedValue("roxygen2", "roxygenize"), args) 178 | })) 179 | } 180 | 181 | rds <- find_package_rds(package, path) 182 | test_dir_exists <- dir.exists(test_dir) 183 | 184 | if (!test_dir_exists) { 185 | dir.create(test_dir) 186 | if (clean) on.exit(unlink(test_dir), add = TRUE) 187 | } 188 | 189 | if (test_dir_exists && !overwrite) { 190 | test_files <- list.files(test_dir, full.names = TRUE) 191 | context <- cliless("{.pkg testex} testing examples") 192 | test_files(test_files, context, chdir = FALSE) 193 | return() 194 | } 195 | 196 | # find example sections and convert them to tests 197 | rd_examples <- Filter(Negate(is.null), lapply(rds, rd_extract_examples)) 198 | test_files <- lapply(seq_along(rd_examples), function(i) { 199 | rd_filename <- names(rd_examples[i]) 200 | rd_example <- rd_examples[[i]] 201 | 202 | # break up examples into examples and test, wrap examples in expectations 203 | exprs <- split_testonly_as_expr(rd_example) 204 | is_ex <- names(exprs) != "\\testonly" 205 | exprs[is_ex] <- lapply( 206 | exprs[is_ex], 207 | wrap_expect_no_error, 208 | value = quote(..Last.value) # can't use base::.Last.value in testthat env 209 | ) 210 | 211 | # write out test code to file in test dir 212 | path <- file.path(test_dir, rd_filename) 213 | example_code <- vcapply(exprs, deparse_pretty) 214 | 215 | writeLines(paste(example_code, collapse = "\n\n"), path) 216 | path 217 | }) 218 | 219 | context <- cliless("{.pkg testex} testing examples") 220 | test_files(test_files, context, chdir = FALSE) 221 | } 222 | 223 | 224 | 225 | #' Test a list of files 226 | #' 227 | #' @param files A collection of file paths to test 228 | #' @param context An optional context message to display in `testthat` reporters 229 | #' @param ... Additional arguments passed to `testhat::source_file` 230 | #' 231 | #' @return The result of [testthat::source_file()], after iterating over 232 | #' generated test files. 233 | #' 234 | #' @keywords internal 235 | test_files <- function(files, context, ...) { 236 | testthat::context_start_file(context) 237 | for (file in files) testthat::source_file(file, ...) 238 | } 239 | 240 | 241 | 242 | #' Wraps an example expression in a `testthat` expectation to not error 243 | #' 244 | #' @param expr An expression to wrap in a `expect_no_error()` expectation. Uses 245 | #' `testthat`s version if recent enough version is available, or provides 246 | #' a fallback otherwise. 247 | #' @param value A symbol to use to store the result of `expr` 248 | #' 249 | #' @return A [testthat::test_that()] call 250 | #' 251 | #' @importFrom utils packageVersion 252 | #' @keywords internal 253 | wrap_expect_no_error <- function(expr, value) { 254 | srckey <- srcref_key(expr, path = "root") 255 | # nocov start 256 | bquote(testthat::test_that("example executes without error", { 257 | testex::with_srcref(.(srckey), { 258 | .(value) <<- .(expect_no_error_call())(.(expr)) 259 | }) 260 | })) 261 | # nocov end 262 | } 263 | 264 | 265 | 266 | #' Determine which symbol to use by default when testing examples 267 | #' 268 | #' @return The value of the last test expression 269 | #' 270 | #' @keywords internal 271 | get_example_value <- function() { 272 | if (testthat::is_testing()) { 273 | quote(..Last.value) 274 | } else { 275 | quote(.Last.value) 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /R/use.R: -------------------------------------------------------------------------------- 1 | #' Add [`testex`] tags and configure package to fully use [`testex`] features 2 | #' 3 | #' @note 4 | #' The [`testex`] `roxygen2` tags behave similarly to `roxygen2` `@examples` 5 | #' tags, with the minor addition of some wrapping code to manage the tests. This 6 | #' means that they will be integrated into your `@examples` and can be 7 | #' intermixed between `@examples` tags 8 | #' 9 | #' @param path A package source code working directory 10 | #' @param check A `logical` value indicating whether tests should be 11 | #' executing during `R CMD check`. 12 | #' @param quiet Whether output should be suppressed 13 | #' 14 | #' @return The result of [`write.dcf()`] upon modifying the package 15 | #' `DESCRIPTION` file. 16 | #' 17 | #' @export 18 | use_testex <- function(path = getwd(), check = TRUE, quiet = FALSE) { 19 | path <- file.path(find_package_root(path), "DESCRIPTION") 20 | desc <- read.dcf(path) 21 | desc <- read.dcf(path, keep.white = colnames(desc)) 22 | 23 | report <- report(quiet) 24 | desc <- update_desc_roxygen(desc, report) 25 | desc <- update_desc_suggests(desc, report) 26 | desc <- update_desc_config_options(desc, list(check = check), report) 27 | update_testthat(path, report) 28 | report$show("Configuring {.pkg testex}:") 29 | 30 | write.dcf( 31 | desc, 32 | path, 33 | keep.white = setdiff(colnames(desc), "Roxygen"), 34 | width = 80L, 35 | indent = 2L 36 | ) 37 | } 38 | 39 | 40 | 41 | #' A Simple Stateful Reporter Class 42 | #' 43 | #' @param quiet Whether output should be shown 44 | #' @return A class-like environment with a few reporting methods 45 | #' 46 | #' @noRd 47 | report <- function(quiet) { 48 | messages <- character(0L) 49 | add <- function(..., .envir = parent.frame()) { 50 | messages <<- append(messages, cliless(..., .envir = .envir)) 51 | } 52 | show <- function(title) { 53 | if (quiet) return() 54 | title <- cliless(title) 55 | if (length(messages) > 0) { 56 | cat(title, "\n", paste0(" * ", messages, collapse = "\n"), "\n", sep = "") 57 | } else { 58 | cat(title, "You're already set up!\n") 59 | } 60 | } 61 | environment() 62 | } 63 | 64 | 65 | 66 | #' Update [`roxygen2`] Settings Field in DESCRIPTION 67 | #' 68 | #' @param desc A parsed DESCRIPTION matrix 69 | #' @param report A reporter to aggregate output 70 | #' @return Used for side-effects of updating DESCRIPTION and reporter 71 | #' 72 | #' @noRd 73 | update_desc_roxygen <- function(desc, report) { 74 | # update Roxygen settings 75 | roxygen_orig <- if (!"Roxygen" %in% colnames(desc)) { 76 | list() 77 | } else { 78 | eval( 79 | parse(text = desc[1L, "Roxygen"], keep.source = FALSE), 80 | envir = new.env(parent = baseenv()) 81 | ) 82 | } 83 | 84 | # add testex to packages 85 | roxygen <- roxygen_orig 86 | roxygen$packages <- unique(c(roxygen$packages, packageName())) 87 | if (!identical(roxygen, roxygen_orig)) { 88 | report$add( 89 | "Including {.code package = \"{packageName()}\"} in ", 90 | "{.code Roxygen} {.file DESCRIPTION} field" 91 | ) 92 | } 93 | 94 | roxygen_str <- paste0("\n ", deparse(roxygen), collapse = "") 95 | desc_update(desc, Roxygen = roxygen_str) 96 | } 97 | 98 | #' Update Suggests field to DESCRIPTION 99 | #' 100 | #' @param desc A parsed DESCRIPTION matrix 101 | #' @param report A reporter to aggregate output 102 | #' @return Used for side-effects of updating DESCRIPTION and reporter 103 | #' 104 | #' @noRd 105 | update_desc_suggests <- function(desc, report) { 106 | # add testex to Suggests 107 | suggests <- if (!"Suggests" %in% colnames(desc)) { 108 | "" 109 | } else { 110 | desc[1L, "Suggests"] 111 | } 112 | 113 | package_re <- paste0("\\b", packageName(), "\\b") 114 | if (!any(grepl(package_re, suggests))) { 115 | lines <- Filter(nchar, strsplit(suggests, "\n")[[1]]) 116 | ws <- min(nchar(gsub("[^ ].*", "", lines)), 4) 117 | package <- paste0(strrep(" ", ws), packageName()) 118 | suggests <- paste0("\n", paste(c(lines, package), collapse = ",\n")) 119 | report$add("Adding {.code Suggests} package {.pkg {packageName()}}") 120 | } 121 | 122 | desc_update(desc, Suggests = suggests) 123 | } 124 | 125 | #' Add `Config/pkg/options` field to DESCRIPTION 126 | #' 127 | #' @param desc A parsed DESCRIPTION matrix 128 | #' @param options Options to use 129 | #' @param report A reporter to aggregate output 130 | #' @return Used for side-effects of updating DESCRIPTION and reporter 131 | #' 132 | #' @noRd 133 | update_desc_config_options <- function(desc, options, report) { 134 | config <- paste("Config", packageName(), "options", sep = "/") 135 | options_orig <- if (!config %in% colnames(desc)) { 136 | list() 137 | } else { 138 | eval( 139 | parse(text = desc[1L, config], keep.source = FALSE), 140 | envir = new.env(parent = baseenv()) 141 | ) 142 | } 143 | 144 | options_new <- options_orig 145 | options_new[names(options)] <- options[names(options)] 146 | if (!identical(options_new, options_orig)) { 147 | desc <- cbind(desc, matrix(NA_character_, dimnames = list(c(), config))) 148 | desc[1L, config] <- paste0("\n ", deparse(options_new)) 149 | report$add( 150 | "Configuring {.file DESCRIPTION} {.file {config}} with ", 151 | "{.code {deparse(options_new)}}" 152 | ) 153 | } 154 | 155 | desc 156 | } 157 | 158 | #' Add `testthat` test for running example tests 159 | #' 160 | #' @param path A directory path to use as basis for finding testing suite 161 | #' @param report A reporter to aggregate output 162 | #' @return Used for side-effects of adding files and updating reporter 163 | #' 164 | #' @noRd 165 | update_testthat <- function(path, report) { 166 | tryCatch( 167 | { 168 | f <- use_testex_as_testthat(path = path, quiet = TRUE) 169 | if (!is.null(f)) report$add("Adding test file {.file {f}}") 170 | }, 171 | error = function(e) NULL 172 | ) 173 | } 174 | 175 | 176 | 177 | #' Update Fields in the DESCRIPTION file 178 | #' 179 | #' @param desc A Parsed `DESCRPITION` file matrix 180 | #' @param ... Named fields to update 181 | #' @return A `DESCRIPTION` matrix 182 | #' 183 | #' @noRd 184 | desc_update <- function(desc, ...) { 185 | cols <- list(...) 186 | new_cols <- setdiff(names(cols), colnames(desc)) 187 | desc <- cbind( 188 | desc, 189 | matrix( 190 | NA_character_, 191 | nrow = nrow(desc), 192 | ncol = length(new_cols), 193 | dimnames = list(c(), new_cols) 194 | ) 195 | ) 196 | 197 | for (col in names(cols)) { 198 | desc[, col] <- cols[[col]] 199 | } 200 | 201 | desc 202 | } 203 | 204 | 205 | 206 | #' {cli}less 207 | #' 208 | #' Pretty format text without cli? As if! Call cli if available, or use a 209 | #' heavily simplified version of glue, used as fallback. 210 | #' 211 | #' @param ... Used to form input string. 212 | #' @param .envir Environment in which to evaluate expressions. 213 | #' @return A formatted string. 214 | #' 215 | #' @noRd 216 | cliless <- function(..., .envir = parent.frame(), .less = FALSE) { 217 | if (!.less && !is.null(tryCatch(ns <- getNamespace("cli"), error = function(e) NULL))) { 218 | return(ns$format_inline(..., .envir = .envir)) 219 | } 220 | 221 | re <- "{(?:\\.([^{} ]+) )?([^{}]+|[^{]*(?R)[^}]*)}" 222 | str <- paste0(..., collapse = "") 223 | m <- gregexec(re, str, perl = TRUE)[[1]] 224 | if (!is.matrix(m)) return(str) 225 | l <- attr(m, "match.length") 226 | 227 | if (ncol(m) == 1 && m[1, 1] == 1 && l[1, 1] == nchar(str)) { 228 | # when entire string is a glue-ish cli expression 229 | style <- substring(str, s <- m[2, 1], s + l[2, 1] - 1L) 230 | expr <- substring(str, s <- m[3, 1], s + l[3, 1] - 1L) 231 | return(switch(style, 232 | "code" = paste0("`", cliless(expr, .envir = .envir, .less = .less), "`"), 233 | "file" = paste0("'", cliless(expr, .envir = .envir, .less = .less), "'"), 234 | "pkg" = paste0("{", cliless(expr, .envir = .envir, .less = .less), "}"), 235 | { 236 | text <- cliless(expr, .envir = .envir, .less = .less) 237 | format(eval(parse(text = text), envir = .envir)) 238 | } 239 | )) 240 | } 241 | 242 | for (col in rev(seq_len(ncol(m)))) { 243 | start <- m[1, col] 244 | end <- start + l[1, col] - 1L 245 | str <- paste0( 246 | substring(str, 1L, start - 1L), 247 | cliless(substring(str, start, end), .envir = .envir, .less = .less), 248 | substring(str, end + 1L) 249 | ) 250 | } 251 | 252 | str 253 | } 254 | 255 | 256 | 257 | #' Run examples as `testthat` expectations 258 | #' 259 | #' @param path A package source code working directory 260 | #' @param context A `testthat` test context to use as the basis for a new test 261 | #' filename. 262 | #' @param quiet Whether to emit output messages. 263 | #' 264 | #' @return The result of [`writeLines()`] after writing a new `testthat` file. 265 | #' 266 | #' @family use 267 | #' 268 | #' @importFrom utils packageName 269 | #' @export 270 | use_testex_as_testthat <- function( 271 | path = getwd(), context = "testex", quiet = FALSE) { 272 | path <- find_package_root(path) 273 | testthat_path <- file.path(path, "tests", "testthat") 274 | test_file <- file.path(testthat_path, paste0("test-", context, ".R")) 275 | 276 | if (!dir.exists(testthat_path)) { 277 | if (!quiet) { 278 | stop( 279 | "It looks like you don't have any testthat tests yet. Start ", 280 | "by setting up your package to use testthat, then try again." 281 | ) 282 | } 283 | return() 284 | } 285 | 286 | if (file.exists(test_file)) { 287 | if (!quiet) { 288 | stop(sprintf( 289 | "testthat test file '%s' already exists.", 290 | basename(test_file) 291 | )) 292 | } 293 | return() 294 | } 295 | 296 | test_contents <- c( 297 | paste0(packageName(), "::test_examples_as_testthat()") 298 | ) 299 | 300 | writeLines(test_contents, test_file) 301 | test_file 302 | } 303 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | `%||%` <- function(lhs, rhs) { 2 | if (is.null(lhs)) rhs else lhs 3 | } 4 | 5 | 6 | 7 | `%|NA|%` <- function(lhs, rhs) { 8 | if (is.na(lhs)) rhs else lhs 9 | } 10 | 11 | 12 | 13 | #' Temporarily attach a namespace 14 | #' 15 | #' This function is primarily for managing attaching of namespaces needed for 16 | #' testing internally. It is exported only because it is needed in code 17 | #' generated within `Rd` files, but is otherwise unlikely to be needed. 18 | #' 19 | #' @param ns A namespace or namespace name to attach 20 | #' @param expr An expression to evaluate while the namespace is attached 21 | #' 22 | #' @return The result of evaluating `expr` 23 | #' 24 | #' @export 25 | with_attached <- function(ns, expr) { 26 | nsname <- if (isNamespace(ns)) getNamespaceName(ns) else ns 27 | if (paste0("package:", nsname) %in% search()) { 28 | return(eval(expr)) 29 | } 30 | 31 | if (is.character(ns)) { 32 | requireNamespace(ns) 33 | } 34 | 35 | try(silent = TRUE, { 36 | attached <- attachNamespace(ns) 37 | on.exit(detach(attr(attached, "name"), character.only = TRUE)) 38 | }) 39 | 40 | expr <- substitute(expr) 41 | eval(expr) 42 | } 43 | 44 | 45 | 46 | #' Test whether currently executing R checks 47 | #' 48 | #' @return A logical indicating whether `R CMD check` is currently running 49 | #' 50 | #' @keywords internal 51 | is_r_cmd_check <- function() { 52 | !is.na(Sys.getenv("_R_CHECK_PACKAGE_NAME_", unset = NA_character_)) 53 | } 54 | 55 | 56 | 57 | #' Package source file helpers 58 | #' 59 | #' Discover specific package related file paths 60 | #' 61 | #' @param path A path within a package source or install directory 62 | #' @param quiet Whether to suppress output 63 | #' 64 | #' @return NULL, invisibly 65 | #' 66 | #' @name package-file-helpers 67 | #' @keywords internal 68 | #' 69 | find_package_root <- function(path = ".", quiet = FALSE) { 70 | if (path == ".") path <- getwd() 71 | while (dirname(path) != path) { 72 | if (file.exists(file.path(path, "DESCRIPTION"))) { 73 | # package source directory 74 | return(path) 75 | } else if (endsWith(basename(path), ".Rcheck")) { 76 | # installed package, as during R CMD check 77 | file <- basename(path) 78 | package <- substring(file, 1, nchar(file) - nchar(".Rcheck")) 79 | return(file.path(path, package)) 80 | } 81 | path <- dirname(path) 82 | } 83 | 84 | if (!quiet) stop("Unable to find package root") 85 | invisible(NULL) 86 | } 87 | 88 | 89 | 90 | #' Find and return a package's Rd db 91 | #' 92 | #' @param package A package name 93 | #' @param path A file path within a package's source code or installation 94 | #' directory. Only considered if `package` is missing. 95 | #' 96 | #' @return A list of package Rd objects, as returned by [`tools::Rd_db()`] 97 | #' 98 | #' @name package-file-helpers 99 | find_package_rds <- function(package, path = getwd()) { 100 | if (!missing(package)) { 101 | package_path <- find.package(package, quiet = TRUE) 102 | } else { 103 | package_path <- find_package_root(path) 104 | } 105 | 106 | desc <- file.path(package_path, "DESCRIPTION") 107 | package <- read.dcf(desc, fields = "Package")[[1L]] 108 | 109 | has_r_dir <- isTRUE(dir.exists(file.path(package_path, "R"))) 110 | has_meta_dir <- isTRUE(dir.exists(file.path(package_path, "Meta"))) 111 | 112 | if (has_r_dir && !has_meta_dir) { 113 | return(tools::Rd_db(dir = package_path)) 114 | } 115 | 116 | if (has_meta_dir) { 117 | return(tools::Rd_db(package = package, lib.loc = dirname(package_path))) 118 | } 119 | 120 | tools::Rd_db(package) 121 | } 122 | 123 | 124 | 125 | #' @name package-file-helpers 126 | package_desc <- function(path = getwd()) { 127 | x <- Sys.getenv("_R_CHECK_PACKAGE_NAME_", unset = NA_character_) 128 | if (!is.na(x)) { 129 | return(file.path(find.package(x), "DESCRIPTION")) 130 | } 131 | 132 | x <- find_package_root(path, quiet = TRUE) 133 | if (!is.null(x)) { 134 | return(file.path(x, "DESCRIPTION")) 135 | } 136 | 137 | invisible(NULL) 138 | } 139 | 140 | 141 | 142 | #' `vapply` shorthand alternatives 143 | #' 144 | #' Simple wrappers around `vapply` for common data types 145 | #' 146 | #' @param ... Arguments passed to [`vapply`] 147 | #' @param FUN.VALUE A preset signature for the flavor of [`vapply`]. This is 148 | #' exposed for transparency, but modifying it would break the implicit 149 | #' contract in the function name about the return type. 150 | #' 151 | #' @return The result of [`vapply`] 152 | #' 153 | #' @rdname vapplys 154 | #' @keywords internal 155 | vlapply <- function(..., FUN.VALUE = logical(1L)) { # nolint 156 | vapply(..., FUN.VALUE = FUN.VALUE) 157 | } 158 | 159 | #' @rdname vapplys 160 | vcapply <- function(..., FUN.VALUE = character(1L)) { # nolint 161 | vapply(..., FUN.VALUE = FUN.VALUE) 162 | } 163 | 164 | #' @rdname vapplys 165 | vnapply <- function(..., FUN.VALUE = numeric(1L)) { # nolint 166 | vapply(..., FUN.VALUE = FUN.VALUE) 167 | } 168 | 169 | 170 | 171 | #' Deparse pretty 172 | #' 173 | #' Deparse to a single string with two spaces of indentation 174 | #' 175 | #' @param expr An expression to deparse 176 | #' 177 | #' @return A pretty-formatted string representation of `expr`. 178 | #' 179 | #' @keywords internal 180 | deparse_pretty <- function(expr) { 181 | lines <- deparse(expr, width.cutoff = 120L) 182 | paste0(gsub("^( +)\\1", "\\1", lines), collapse = "\n") 183 | } 184 | 185 | 186 | 187 | #' Deparse an expression and indent for pretty-printing 188 | #' 189 | #' @param x A `code` object 190 | #' @param indent An `integer` number of spaces or a string to prefix each 191 | #' line of the deparsed output. 192 | #' 193 | #' @return An indented version of the deparsed string from `x`. 194 | #' 195 | #' @keywords internal 196 | deparse_indent <- function(x, indent = 0L) { 197 | if (is.numeric(indent)) indent <- strrep(" ", indent) 198 | paste0(indent, deparse(unclass(x)), collapse = "\n") 199 | } 200 | 201 | 202 | 203 | #' Get String Line Count 204 | #' 205 | #' @param x A character value 206 | #' 207 | #' @return The number of newline characters in a multiline string 208 | #' 209 | #' @keywords internal 210 | string_newline_count <- function(x) { 211 | nchar(gsub("[^\n]", "", x)) 212 | } 213 | 214 | 215 | 216 | #' Return the number of characters in a line of a file 217 | #' 218 | #' @param file A file to use as reference 219 | #' @param line A line number to retrieve the length of 220 | #' 221 | #' @return The number of characters in line `line`. 222 | #' 223 | #' @keywords internal 224 | file_line_nchar <- function(file, line) { 225 | bn <- basename(file) 226 | if (!file.exists(file) || (startsWith(bn, "<") && endsWith(bn, ">"))) { 227 | return(10000) 228 | } 229 | nchar(scan(file, what = character(), skip = line - 1, n = 1, sep = "\n", quiet = TRUE)) 230 | } 231 | 232 | 233 | 234 | #' Checks for use of `roxygen2` 235 | #' 236 | #' @param path A file path to a package source code directory 237 | #' @return A logical value indicating whether a package takes `roxygen2` as 238 | #' a dependency. 239 | #' 240 | #' @export 241 | uses_roxygen2 <- function(path) { 242 | x <- find_package_root(path, quiet = TRUE) 243 | desc <- file.path(x, "DESCRIPTION") 244 | deps <- read.dcf(desc, fields = c("Depends", "Imports", "Suggests")) 245 | deps <- trimws(unlist(strsplit(deps, ","))) 246 | isTRUE(any(grepl("^roxygen2( |$)", deps))) 247 | } 248 | -------------------------------------------------------------------------------- /R/utils_rd.R: -------------------------------------------------------------------------------- 1 | #' Rd Example Parsing Helpers 2 | #' 3 | #' @param rd An Rd object 4 | #' 5 | #' @name testex-rd-example-helpers 6 | #' @keywords internal 7 | #' 8 | NULL 9 | 10 | 11 | 12 | #' @describeIn testex-rd-example-helpers 13 | #' Extract examples tag from an Rd file 14 | #' 15 | #' @return The examples section of an Rd object 16 | #' 17 | rd_extract_examples <- function(rd) { 18 | rd_tags <- vapply(rd, attr, character(1L), "Rd_tag") 19 | rd_ex <- which(rd_tags == "\\examples") 20 | if (length(rd_ex) == 0L) return(NULL) 21 | rd[[rd_ex]] 22 | } 23 | 24 | 25 | 26 | #' @describeIn testex-rd-example-helpers 27 | #' Convert an Rd example to string 28 | #' 29 | #' @return A formatted Rd example 30 | #' 31 | rd_code_as_string <- function(rd) { 32 | if (inherits(rd, "\\dontrun")) 33 | paste(gsub("\\S", "", unlist(rd)), collapse = "") 34 | else 35 | paste(unlist(rd), collapse = "") 36 | } 37 | 38 | 39 | 40 | #' @describeIn testex-rd-example-helpers 41 | #' Split sections of an example into evaluated example code blocks and code 42 | #' blocks wrapped in `\testonly` `Rd_tag`s, reassigning [`srcref`]s as the 43 | #' example code is split. 44 | #' 45 | #' @return An interlaced list of expressions, either representing example 46 | #' code or tests. The names of the list are either `\testonly` or `RDCODE` 47 | #' depending on the originating source of the expression. 48 | #' 49 | split_testonly_as_expr <- function(rd) { 50 | rds <- split_testonly(rd) 51 | 52 | # convert Rd tag lists to strings (including \dontrun, converted to ws only) 53 | all_seg <- lapply(rds, rd_code_as_string) 54 | n <- length(all_seg) 55 | 56 | # resegment to combine any non-testonly sections 57 | resegment <- names(all_seg) == "\\testonly" 58 | resegment[-1] <- resegment[-n] | resegment[-1] 59 | resegment <- cumsum(resegment) 60 | code_seg <- split(all_seg, resegment) 61 | 62 | # preserver \testonly names, everything else can now be considered RCODE 63 | names(code_seg) <- names(all_seg)[!duplicated(resegment)] 64 | names(code_seg) <- ifelse( 65 | names(code_seg) == "\\testonly", 66 | "\\testonly", 67 | "RCODE" 68 | ) 69 | 70 | code_seg <- lapply(code_seg, rd_code_as_string) 71 | code_seg_lines <- vnapply(code_seg, string_newline_count) 72 | 73 | # filter out any unused lines 74 | segment_has_expr <- grepl("\\S", code_seg) 75 | code_seg <- code_seg[segment_has_expr] 76 | code_seg_lines <- code_seg_lines[segment_has_expr] 77 | code_exprs <- lapply(code_seg, function(seg) { 78 | expr <- str2expression(seg) 79 | if (length(expr) == 1) expr[[1]] 80 | else as.call(append(list(as.symbol("{")), as.list(expr))) 81 | }) 82 | 83 | # split original srcref into srcrefs for individual expressions 84 | code_srcrefs <- split_srcref(utils::getSrcref(rds), cumsum(code_seg_lines)) 85 | for (i in seq_along(code_seg)) { 86 | attr(code_exprs[[i]], "srcref") <- code_srcrefs[[i]] 87 | } 88 | 89 | code_exprs 90 | } 91 | 92 | 93 | 94 | #' @describeIn testex-rd-example-helpers 95 | #' Split sections of an example into lists of `Rd_tag`s. Note that [`srcref`]s 96 | #' are split by line number. If a line is split between two sections, it is 97 | #' attributed to the first section. As this is used primarily for giving line 98 | #' numbers to test messages, this is sufficient for providing test failures 99 | #' locations. 100 | #' 101 | #' @return A list of Rd tag contents 102 | #' 103 | split_testonly <- function(rd) { 104 | attrs <- attributes(rd) 105 | n <- length(rd) 106 | 107 | tags <- vapply(rd, attr, character(1L), "Rd_tag") 108 | is_cons <- logical(n) 109 | is_cons[-1] <- tags[-1] == tags[-n] 110 | cumsum(!is_cons) 111 | 112 | res <- split(rd, cumsum(!is_cons)) 113 | split_tags <- tags[!is_cons] 114 | 115 | # rd tags of each split are applied as a subclass 116 | for (i in seq_along(res)) { 117 | class(res[[i]]) <- c(split_tags[[i]], class(res[[i]])) 118 | } 119 | 120 | attributes(res) <- attrs 121 | names(res) <- vapply(res, function(i) attr(i[[1]], "Rd_tag"), character(1L)) 122 | res 123 | } 124 | -------------------------------------------------------------------------------- /R/utils_srcref.R: -------------------------------------------------------------------------------- 1 | #' Convert a [`srcref`] to a [`character`] representation 2 | #' 3 | #' @param x A [`srcref`] object 4 | #' @param nloc The number of locations ([`utils::getSrcLocation`]) to use. 5 | #' Defaults to 2, indicating starting and ending line number. 6 | #' @param path A form of file path to use for the key. One of `"base"` for only 7 | #' the basename of the source file path, `"root"` for a path relative to a 8 | #' package root directory if found, or `"full"` for the full file path. 9 | #' 10 | #' @return A string hash of a [srcref] 11 | #' 12 | #' @keywords internal 13 | #' @importFrom utils getSrcref getSrcFilename 14 | srcref_key <- function(x, nloc = 2, path = c("base", "root", "full")) { 15 | path <- match.arg(path) 16 | 17 | stopifnot(nloc %in% c(2, 4, 6, 8)) 18 | nloc_indxs <- list(c(1, 3), 1:4, 1:6, 1:8) 19 | nloc <- match(nloc, c(2, 4, 6, 8)) 20 | nloc <- nloc_indxs[[nloc]] 21 | loc <- paste(as.numeric(utils::getSrcref(x))[nloc], collapse = ":") 22 | 23 | srcpath <- utils::getSrcFilename(x, full.names = TRUE) 24 | pkgroot <- find_package_root(srcpath, quiet = TRUE) 25 | if (!length(pkgroot)) { 26 | pkgroot <- "" 27 | } else { 28 | pkgroot <- paste0(pkgroot, .Platform$file.sep) 29 | } 30 | 31 | srcpath <- switch(path, 32 | "full" = srcpath, 33 | "base" = basename(srcpath), 34 | "root" = { 35 | if (isTRUE(startsWith(srcpath, pkgroot))) { 36 | substring(srcpath, nchar(pkgroot) + 1) 37 | } else { 38 | srcpath 39 | } 40 | } 41 | ) 42 | 43 | paste0(srcpath, ":", loc) 44 | } 45 | 46 | 47 | 48 | #' Convert to [srcref] 49 | #' 50 | #' @param x an object to coerce 51 | #' @return A [srcref] 52 | #' 53 | #' @name as.srcref 54 | #' @keywords internal 55 | as.srcref <- function(x) { 56 | UseMethod("as.srcref") 57 | } 58 | 59 | 60 | 61 | #' @describeIn as.srcref 62 | #' Convert from a `srcref_key` to a [srcref] object 63 | #' 64 | as.srcref.character <- function(x) { 65 | m <- regexpr("(?.*?)(?(:\\d+)+)", x, perl = TRUE) 66 | m <- matrix( 67 | substring(x, s <- attr(m, "capture.start"), s + attr(m, "capture.length") - 1), 68 | nrow = length(x), 69 | dimnames = list(x, colnames(s)) 70 | ) 71 | 72 | filename <- m[, "filename"] 73 | pkgroot <- find_package_root(quiet = TRUE) 74 | 75 | filepath <- if (is.null(pkgroot)) { 76 | filename 77 | } else if (file.exists(f <- file.path(pkgroot, filename))) { 78 | f 79 | } else if (file.exists(f <- file.path(pkgroot, "R", filename))) { 80 | f 81 | } else { 82 | filename 83 | } 84 | 85 | location <- srclocs( 86 | as.numeric(strsplit(m[, "location"], ":")[[1]][-1]), 87 | filename 88 | ) 89 | 90 | src_file <- srcfilealias(filename, srcfile(filepath)) 91 | srcref(src_file, location) 92 | } 93 | 94 | 95 | 96 | #' Build a source location from a minimal numeric vector 97 | #' 98 | #' Build a length four source location from a length two source location. The 99 | #' starting column on the first line is assumed to be 1, and the final column is 100 | #' taken to be the length of the line if the source file exists, or 1 as a 101 | #' fallback. 102 | #' 103 | #' @param x A numeric vector of at least length 2 104 | #' @param file A file to use to determine the length of the final line 105 | #' 106 | #' @return A numeric vector similar to a [`utils::getSrcLocation`] object 107 | #' 108 | #' @keywords internal 109 | srclocs <- function(x, file) { 110 | if (length(x) < 4) { 111 | line <- x[[2]] 112 | x[[3]] <- x[[2]] 113 | x[[2]] <- 1 114 | x[[4]] <- if (file.exists(file)) file_line_nchar(file, line) else 1 115 | } 116 | x 117 | } 118 | 119 | 120 | 121 | #' Split a Source Reference at specific lines 122 | #' 123 | #' @param sr An original [srcref] object 124 | #' @param where A numeric vector of line numbers where the [srcref] should be 125 | #' split 126 | #' 127 | #' @return A list of [srcref] 128 | #' 129 | #' @importFrom utils getSrcFilename 130 | #' @keywords internal 131 | split_srcref <- function(sr, where) { 132 | if (is.null(sr)) { 133 | return(rep_len(sr, length(where))) 134 | } 135 | file <- utils::getSrcFilename(sr, full.names = TRUE) 136 | 137 | # allocate a list of new [srcref]s 138 | refs <- list() 139 | length(refs) <- length(where) 140 | 141 | # starting from start of collective srcref, offset local lines 142 | start <- getSrcLocation(sr) 143 | where <- start + where 144 | 145 | # create new [srcref]s of regions, divided by "where" lines 146 | for (i in seq_along(where)) { 147 | locs <- srclocs(c(start, where[[i]]), file) 148 | refs[[i]] <- srcref(srcfile(file), locs) 149 | start <- where[[i]] + 1 150 | } 151 | 152 | refs 153 | } 154 | 155 | 156 | 157 | #' Determine the number of source code lines of a given [srcref] 158 | #' 159 | #' @param x A [srcref] object 160 | #' @return The number of lines in the original source of a [srcref] 161 | #' 162 | #' @importFrom utils getSrcLocation 163 | #' @keywords internal 164 | srcref_nlines <- function(x) { 165 | getSrcLocation(x, "line", first = FALSE) - getSrcLocation(x, "line") + 1L 166 | } 167 | -------------------------------------------------------------------------------- /R/zzz.R: -------------------------------------------------------------------------------- 1 | .onLoad <- function(libname, pkgname) { 2 | s3_register("roxygen2::roxy_tag_parse", "roxy_tag_test") 3 | s3_register("roxygen2::roxy_tag_parse", "roxy_tag_testthat") 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `testex` ***test examples*** 2 | 3 | [![CRAN](https://img.shields.io/cran/v/testex.svg)](https://cran.r-project.org/package=testex) 4 | [![`R CMD check`](https://github.com/dgkf/testex/workflows/R-CMD-check/badge.svg)](https://github.com/dgkf/testex/actions?query=workflow%3AR-CMD-check) 5 | [![coverage](https://img.shields.io/codecov/c/github/dgkf/testex/main.svg)](https://app.codecov.io/gh/dgkf/testex) 6 | 7 | Add tests and assertions in-line in examples 8 | 9 | ## Quick Start 10 | 11 | Set up your package to use `testex` using 12 | 13 | ```r 14 | testex::use_testex() 15 | ``` 16 | 17 | and then start adding tests! 18 | 19 | ```r 20 | #' Hello, World! 21 | #' 22 | #' @examples 23 | #' hello("World") 24 | #' @test "Hello, World!" 25 | #' 26 | #' hello("darkness my old friend") 27 | #' @test grepl("darkness", .) 28 | #' 29 | #' @export 30 | hello <- function(who) { 31 | paste0("Hello, ", who, "!") 32 | } 33 | ``` 34 | 35 | If you were already using `testthat`, you'll immediately see a new test 36 | context for testing your examples. And if you aren't using `testthat`, then 37 | you'll find that your tests are being run with your examples when you run 38 | `R CMD check` 39 | 40 | ## `roxygen2` tags 41 | 42 | ### `@test` 43 | 44 | will check that the result of the last example is identical to your test. You 45 | can use the example output in a function using a `.`. 46 | 47 | ```r 48 | #' @examples 49 | #' sum(1:10) 50 | #' @test 55 51 | #' @test is.numeric(.) 52 | ``` 53 | 54 | ### `@testthat` 55 | 56 | is similar, but has the added benefit of automatically inserting a `.` into 57 | `testthat::expect_*` functions. 58 | 59 | ```r 60 | #' @examples 61 | #' sum(1:10) 62 | #' @testthat expect_equal(55) 63 | #' @testthat expect_vector(numeric()) 64 | ``` 65 | 66 | ## Prior Art 67 | 68 | - [`roxytest`](https://github.com/mikldk/roxytest) 69 | A slightly different approach. Allows tests to be written in-line, but 70 | generates test files used directly by a testing framework. 71 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 1% 9 | informational: true 10 | patch: 11 | default: 12 | target: auto 13 | threshold: 1% 14 | informational: true 15 | -------------------------------------------------------------------------------- /inst/pkg.example/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: pkg.example 2 | Title: A showcase of how testex can be used to test examples 3 | Version: 0.0.0.9000 4 | Authors@R: 5 | person("First", "Last", , "first.last@example.com", role = c("aut", "cre")) 6 | Description: What the package does (one paragraph). 7 | Suggests: 8 | testthat (>= 3.0.0), 9 | roxygen2, 10 | testex 11 | Encoding: UTF-8 12 | Roxygen: list(markdown = TRUE, packages = "testex") 13 | RoxygenNote: 7.3.1 14 | License: MIT + file LICENSE 15 | Config/testthat/edition: 3 16 | Config/testex/options: list(check = TRUE, version = "0.2.0.9000") 17 | -------------------------------------------------------------------------------- /inst/pkg.example/LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2022 2 | COPYRIGHT HOLDER: testex authors 3 | -------------------------------------------------------------------------------- /inst/pkg.example/NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | export(fn) 4 | export(fn_roxygen) 5 | export(fn_roxygen_multiple1) 6 | export(fn_roxygen_multiple2) 7 | export(fn_roxygen_testthat) 8 | -------------------------------------------------------------------------------- /inst/pkg.example/R/fn.R: -------------------------------------------------------------------------------- 1 | #' Test Function 2 | #' 3 | #' This example showcases how you might write "raw" tests within your examples. 4 | #' You could use `\testonly` directly, or use `testex::testex()` to use 5 | #' `.`-syntax. 6 | #' 7 | #' @param x A thing 8 | #' 9 | #' @return The pasted thing 10 | #' 11 | #' @examples 12 | #' fn("testing") 13 | #' 14 | #' \testonly{testex::testex( 15 | #' . == "testing 1 2 3", 16 | #' startsWith(., "testing") 17 | #' )} 18 | #' 19 | #' \testonly{testex::testex(style = "testthat", 20 | #' testthat::test_that("fn gives expected results", { 21 | #' testthat::expect_equal(., "testing 1 2 3") 22 | #' }) 23 | #' )} 24 | #' 25 | #' @export 26 | fn <- function(x) { 27 | paste(x, "1 2 3") 28 | } 29 | 30 | 31 | 32 | #' Test Function 33 | #' 34 | #' This example introduces the `@test` tag, either a value or an expression 35 | #' using the `.`-syntax to test the last example result. 36 | #' 37 | #' @param x A thing 38 | #' 39 | #' @return The pasted thing 40 | #' 41 | #' @examples 42 | #' \dontshow{ 43 | #' value <- "testing" 44 | #' } 45 | #' 46 | #' fn_roxygen(value) 47 | #' @test "testing 1 2 3" 48 | #' 49 | #' \dontrun{ 50 | #' stop("this won't work") 51 | #' } 52 | #' 53 | #' fn_roxygen("testing") 54 | #' @test grepl("\\d", .) 55 | #' @test startsWith(., "testing") 56 | #' 57 | #' fn_roxygen("testing") 58 | #' @test { 59 | #' "testing 1 2 3" 60 | #' } 61 | #' 62 | #' fn_roxygen("testing") 63 | #' # untested trailing example 64 | #' 65 | #' @export 66 | fn_roxygen <- function(x) { 67 | paste(x, "1 2 3") 68 | } 69 | 70 | 71 | 72 | #' Test Function 73 | #' 74 | #' This example introduces `testthat`-style tests using in-line `@testthat` 75 | #' `roxygen2` tags. 76 | #' 77 | #' @param x A thing 78 | #' 79 | #' @return The pasted thing 80 | #' 81 | #' @examples 82 | #' fn_roxygen_testthat("testing") 83 | #' @testthat expect_equal("testing 1 2 3") 84 | #' @testthat expect_match("^testing") 85 | #' 86 | #' fn_roxygen_testthat("testing") 87 | #' @testthat expect_equal("testing 1 2 3") 88 | #' @testthat expect_match("^testing") 89 | #' 90 | #' @export 91 | fn_roxygen_testthat <- function(x) { 92 | paste(x, "1 2 3") 93 | } 94 | 95 | 96 | 97 | 98 | #' Test Topic Covering Multiple Functions 99 | #' 100 | #' This example composes an examples section from multiple blocks. 101 | #' 102 | #' @param x A thing 103 | #' @return The pasted thing 104 | #' 105 | #' @name fn_roxygen_multiple 106 | NULL 107 | 108 | #' @describeIn fn_roxygen_multiple 109 | #' Ensure multiple objects' examples are combined into a single topic 110 | #' 111 | #' @examples 112 | #' fn_roxygen_multiple1("testing") 113 | #' @test grepl("\\d", .) 114 | #' @test startsWith(., "testing") 115 | #' 116 | #' @export 117 | fn_roxygen_multiple1 <- function(x) { 118 | paste(x, "1 2 3") 119 | } 120 | 121 | #' @describeIn fn_roxygen_multiple 122 | #' Ensure multiple objects' examples are combined into a single topic 123 | #' 124 | #' @examples 125 | #' fn_roxygen_multiple2("testing") 126 | #' @test grepl("\\d", .) 127 | #' @test startsWith(., "testing") 128 | #' 129 | #' @export 130 | fn_roxygen_multiple2 <- function(x) { 131 | paste(x, "1 2 3") 132 | } 133 | -------------------------------------------------------------------------------- /inst/pkg.example/man/fn.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fn.R 3 | \name{fn} 4 | \alias{fn} 5 | \title{Test Function} 6 | \usage{ 7 | fn(x) 8 | } 9 | \arguments{ 10 | \item{x}{A thing} 11 | } 12 | \value{ 13 | The pasted thing 14 | } 15 | \description{ 16 | This example showcases how you might write "raw" tests within your examples. 17 | You could use \verb{\testonly} directly, or use \code{testex::testex()} to use 18 | \code{.}-syntax. 19 | } 20 | \examples{ 21 | fn("testing") 22 | 23 | \testonly{testex::testex( 24 | . == "testing 1 2 3", 25 | startsWith(., "testing") 26 | )} 27 | 28 | \testonly{testex::testex(style = "testthat", 29 | testthat::test_that("fn gives expected results", { 30 | testthat::expect_equal(., "testing 1 2 3") 31 | }) 32 | )} 33 | 34 | } 35 | -------------------------------------------------------------------------------- /inst/pkg.example/man/fn_roxygen.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fn.R 3 | \name{fn_roxygen} 4 | \alias{fn_roxygen} 5 | \title{Test Function} 6 | \usage{ 7 | fn_roxygen(x) 8 | } 9 | \arguments{ 10 | \item{x}{A thing} 11 | } 12 | \value{ 13 | The pasted thing 14 | } 15 | \description{ 16 | This example introduces the \verb{@test} tag, either a value or an expression 17 | using the \code{.}-syntax to test the last example result. 18 | } 19 | \examples{ 20 | \dontshow{ 21 | value <- "testing" 22 | } 23 | 24 | fn_roxygen(value) 25 | \testonly{ 26 | testex::testex(srcref = "fn.R:47:47", 27 | identical(., "testing 1 2 3"))} 28 | \dontrun{ 29 | stop("this won't work") 30 | } 31 | 32 | fn_roxygen("testing") 33 | \testonly{ 34 | testex::testex(srcref = "fn.R:54:54", 35 | grepl("\\\\d", .))} 36 | \testonly{ 37 | testex::testex(srcref = "fn.R:55:55", 38 | startsWith(., "testing"))} 39 | fn_roxygen("testing") 40 | \testonly{ 41 | testex::testex(srcref = "fn.R:58:60", 42 | identical(., { 43 | "testing 1 2 3" 44 | }))} 45 | 46 | fn_roxygen("testing") 47 | # untested trailing example 48 | } 49 | -------------------------------------------------------------------------------- /inst/pkg.example/man/fn_roxygen_multiple.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fn.R 3 | \name{fn_roxygen_multiple} 4 | \alias{fn_roxygen_multiple} 5 | \alias{fn_roxygen_multiple1} 6 | \alias{fn_roxygen_multiple2} 7 | \title{Test Topic Covering Multiple Functions} 8 | \usage{ 9 | fn_roxygen_multiple1(x) 10 | 11 | fn_roxygen_multiple2(x) 12 | } 13 | \arguments{ 14 | \item{x}{A thing} 15 | } 16 | \value{ 17 | The pasted thing 18 | } 19 | \description{ 20 | This example composes an examples section from multiple blocks. 21 | } 22 | \section{Functions}{ 23 | \itemize{ 24 | \item \code{fn_roxygen_multiple1()}: Ensure multiple objects' examples are combined into a single topic 25 | 26 | \item \code{fn_roxygen_multiple2()}: Ensure multiple objects' examples are combined into a single topic 27 | 28 | }} 29 | \examples{ 30 | fn_roxygen_multiple1("testing") 31 | \testonly{ 32 | testex::testex(srcref = "fn.R:113:113", 33 | grepl("\\\\d", .))} 34 | \testonly{ 35 | testex::testex(srcref = "fn.R:114:114", 36 | startsWith(., "testing"))} 37 | fn_roxygen_multiple2("testing") 38 | \testonly{ 39 | testex::testex(srcref = "fn.R:126:126", 40 | grepl("\\\\d", .))} 41 | \testonly{ 42 | testex::testex(srcref = "fn.R:127:127", 43 | startsWith(., "testing"))} 44 | } 45 | -------------------------------------------------------------------------------- /inst/pkg.example/man/fn_roxygen_testthat.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/fn.R 3 | \name{fn_roxygen_testthat} 4 | \alias{fn_roxygen_testthat} 5 | \title{Test Function} 6 | \usage{ 7 | fn_roxygen_testthat(x) 8 | } 9 | \arguments{ 10 | \item{x}{A thing} 11 | } 12 | \value{ 13 | The pasted thing 14 | } 15 | \description{ 16 | This example introduces \code{testthat}-style tests using in-line \verb{@testthat} 17 | \code{roxygen2} tags. 18 | } 19 | \examples{ 20 | fn_roxygen_testthat("testing") 21 | \testonly{ 22 | testex::testex(style = "testthat", srcref = "fn.R:83:83", 23 | expect_equal(., "testing 1 2 3"))} 24 | \testonly{ 25 | testex::testex(style = "testthat", srcref = "fn.R:84:84", 26 | expect_match(., "^testing"))} 27 | fn_roxygen_testthat("testing") 28 | \testonly{ 29 | testex::testex(style = "testthat", srcref = "fn.R:87:87", 30 | expect_equal(., "testing 1 2 3"))} 31 | \testonly{ 32 | testex::testex(style = "testthat", srcref = "fn.R:88:88", 33 | expect_match(., "^testing"))} 34 | } 35 | -------------------------------------------------------------------------------- /inst/pkg.example/tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(pkg.example) 3 | 4 | test_check("pkg.example") 5 | -------------------------------------------------------------------------------- /inst/pkg.example/tests/testthat/test-fn.R: -------------------------------------------------------------------------------- 1 | test_that("fn works", { 2 | expect_equal(fn("abc"), "abc 1 2 3") 3 | }) 4 | -------------------------------------------------------------------------------- /inst/pkg.example/tests/testthat/test-testex.R: -------------------------------------------------------------------------------- 1 | testex::test_examples_as_testthat() 2 | -------------------------------------------------------------------------------- /man/as.srcref.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_srcref.R 3 | \name{as.srcref} 4 | \alias{as.srcref} 5 | \alias{as.srcref.character} 6 | \title{Convert to \link{srcref}} 7 | \usage{ 8 | as.srcref(x) 9 | 10 | \method{as.srcref}{character}(x) 11 | } 12 | \arguments{ 13 | \item{x}{an object to coerce} 14 | } 15 | \value{ 16 | A \link{srcref} 17 | } 18 | \description{ 19 | Convert to \link{srcref} 20 | } 21 | \section{Methods (by class)}{ 22 | \itemize{ 23 | \item \code{as.srcref(character)}: Convert from a \code{srcref_key} to a \link{srcref} object 24 | 25 | }} 26 | \keyword{internal} 27 | -------------------------------------------------------------------------------- /man/deparse_indent.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{deparse_indent} 4 | \alias{deparse_indent} 5 | \title{Deparse an expression and indent for pretty-printing} 6 | \usage{ 7 | deparse_indent(x, indent = 0L) 8 | } 9 | \arguments{ 10 | \item{x}{A \code{code} object} 11 | 12 | \item{indent}{An \code{integer} number of spaces or a string to prefix each 13 | line of the deparsed output.} 14 | } 15 | \value{ 16 | An indented version of the deparsed string from \code{x}. 17 | } 18 | \description{ 19 | Deparse an expression and indent for pretty-printing 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/deparse_pretty.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{deparse_pretty} 4 | \alias{deparse_pretty} 5 | \title{Deparse pretty} 6 | \usage{ 7 | deparse_pretty(expr) 8 | } 9 | \arguments{ 10 | \item{expr}{An expression to deparse} 11 | } 12 | \value{ 13 | A pretty-formatted string representation of \code{expr}. 14 | } 15 | \description{ 16 | Deparse to a single string with two spaces of indentation 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/fallback_expect_no_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{fallback_expect_no_error} 4 | \alias{fallback_expect_no_error} 5 | \title{Expect no Error} 6 | \usage{ 7 | fallback_expect_no_error(object, ...) 8 | } 9 | \arguments{ 10 | \item{object}{An expression to evaluate} 11 | 12 | \item{...}{Additional arguments unused} 13 | } 14 | \value{ 15 | The value produced by the expectation code 16 | } 17 | \description{ 18 | Expect no Error 19 | } 20 | \note{ 21 | This is a stop-gap implementation, and will only be used for legacy 22 | versions of \code{testthat} before this was properly supported. 23 | 24 | A \code{testthat} expectation that the provided code can be evaluated without 25 | producing an error. This is the most basic expectation one should expect of 26 | any example code. Further expectations are provided in subsequent \code{testthat} 27 | code. 28 | } 29 | -------------------------------------------------------------------------------- /man/file_line_nchar.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{file_line_nchar} 4 | \alias{file_line_nchar} 5 | \title{Return the number of characters in a line of a file} 6 | \usage{ 7 | file_line_nchar(file, line) 8 | } 9 | \arguments{ 10 | \item{file}{A file to use as reference} 11 | 12 | \item{line}{A line number to retrieve the length of} 13 | } 14 | \value{ 15 | The number of characters in line \code{line}. 16 | } 17 | \description{ 18 | Return the number of characters in a line of a file 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /man/get_example_value.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{get_example_value} 4 | \alias{get_example_value} 5 | \title{Determine which symbol to use by default when testing examples} 6 | \usage{ 7 | get_example_value() 8 | } 9 | \value{ 10 | The value of the last test expression 11 | } 12 | \description{ 13 | Determine which symbol to use by default when testing examples 14 | } 15 | \keyword{internal} 16 | -------------------------------------------------------------------------------- /man/is_r_cmd_check.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{is_r_cmd_check} 4 | \alias{is_r_cmd_check} 5 | \title{Test whether currently executing R checks} 6 | \usage{ 7 | is_r_cmd_check() 8 | } 9 | \value{ 10 | A logical indicating whether \verb{R CMD check} is currently running 11 | } 12 | \description{ 13 | Test whether currently executing R checks 14 | } 15 | \keyword{internal} 16 | -------------------------------------------------------------------------------- /man/package-file-helpers.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{package-file-helpers} 4 | \alias{package-file-helpers} 5 | \alias{find_package_root} 6 | \alias{find_package_rds} 7 | \alias{package_desc} 8 | \title{Package source file helpers} 9 | \usage{ 10 | find_package_root(path = ".", quiet = FALSE) 11 | 12 | find_package_rds(package, path = getwd()) 13 | 14 | package_desc(path = getwd()) 15 | } 16 | \arguments{ 17 | \item{path}{A file path within a package's source code or installation 18 | directory. Only considered if \code{package} is missing.} 19 | 20 | \item{quiet}{Whether to suppress output} 21 | 22 | \item{package}{A package name} 23 | } 24 | \value{ 25 | NULL, invisibly 26 | 27 | A list of package Rd objects, as returned by \code{\link[tools:Rdutils]{tools::Rd_db()}} 28 | } 29 | \description{ 30 | Discover specific package related file paths 31 | } 32 | \keyword{internal} 33 | -------------------------------------------------------------------------------- /man/s3_register.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/register-s3.R 3 | \name{s3_register} 4 | \alias{s3_register} 5 | \title{Register a method for a suggested dependency} 6 | \arguments{ 7 | \item{generic}{Name of the generic in the form \code{pkg::generic}.} 8 | 9 | \item{class}{Name of the class} 10 | 11 | \item{method}{Optionally, the implementation of the method. By default, 12 | this will be found by looking for a function called \code{generic.class} 13 | in the package environment. 14 | 15 | Note that providing \code{method} can be dangerous if you use 16 | \code{devtools}. When the namespace of the method is reloaded by 17 | \code{devtools::load_all()}, the function will keep inheriting from 18 | the old namespace. This might cause crashes because of dangling 19 | \code{.Call()} pointers.} 20 | } 21 | \value{ 22 | No return value, called for side-effect of registering an S3 23 | generic. 24 | } 25 | \description{ 26 | Generally, the recommend way to register an S3 method is to use the 27 | \code{S3Method()} namespace directive (often generated automatically by the 28 | \verb{@export} \code{roxygen2} tag). However, this technique requires that the generic 29 | be in an imported package, and sometimes you want to suggest a package, 30 | and only provide a method when that package is loaded. \code{s3_register()} 31 | can be called from your package's \code{.onLoad()} to dynamically register 32 | a method only if the generic's package is loaded. 33 | } 34 | \details{ 35 | For R 3.5.0 and later, \code{s3_register()} is also useful when demonstrating 36 | class creation in a vignette, since method lookup no longer always involves 37 | the lexical scope. For R 3.6.0 and later, you can achieve a similar effect 38 | by using "delayed method registration", i.e. placing the following in your 39 | \code{NAMESPACE} file: 40 | 41 | \if{html}{\out{
}}\preformatted{if (getRversion() >= "3.6.0") \{ 42 | S3method(package::generic, class) 43 | \} 44 | }\if{html}{\out{
}} 45 | } 46 | \section{Usage in other packages}{ 47 | 48 | To avoid taking a dependency on \code{vctrs}, you copy the source of 49 | \href{https://github.com/r-lib/vctrs/blob/main/R/register-s3.R}{\code{s3_register()}} 50 | into your own package. It is licensed under the permissive 51 | \href{https://choosealicense.com/licenses/unlicense/}{\code{unlicense}} to make it 52 | crystal clear that we're happy for you to do this. There's no need to include 53 | the license or even credit us when using this function. 54 | } 55 | 56 | \examples{ 57 | # A typical use case is to dynamically register tibble/pillar methods 58 | # for your class. That way you avoid creating a hard dependency on packages 59 | # that are not essential, while still providing finer control over 60 | # printing when they are used. 61 | 62 | .onLoad <- function(...) { 63 | s3_register("pillar::pillar_shaft", "vctrs_vctr") 64 | s3_register("tibble::type_sum", "vctrs_vctr") 65 | } 66 | 67 | } 68 | \keyword{internal} 69 | -------------------------------------------------------------------------------- /man/split_srcref.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_srcref.R 3 | \name{split_srcref} 4 | \alias{split_srcref} 5 | \title{Split a Source Reference at specific lines} 6 | \usage{ 7 | split_srcref(sr, where) 8 | } 9 | \arguments{ 10 | \item{sr}{An original \link{srcref} object} 11 | 12 | \item{where}{A numeric vector of line numbers where the \link{srcref} should be 13 | split} 14 | } 15 | \value{ 16 | A list of \link{srcref} 17 | } 18 | \description{ 19 | Split a Source Reference at specific lines 20 | } 21 | \keyword{internal} 22 | -------------------------------------------------------------------------------- /man/srclocs.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_srcref.R 3 | \name{srclocs} 4 | \alias{srclocs} 5 | \title{Build a source location from a minimal numeric vector} 6 | \usage{ 7 | srclocs(x, file) 8 | } 9 | \arguments{ 10 | \item{x}{A numeric vector of at least length 2} 11 | 12 | \item{file}{A file to use to determine the length of the final line} 13 | } 14 | \value{ 15 | A numeric vector similar to a \code{\link[utils:sourceutils]{utils::getSrcLocation}} object 16 | } 17 | \description{ 18 | Build a length four source location from a length two source location. The 19 | starting column on the first line is assumed to be 1, and the final column is 20 | taken to be the length of the line if the source file exists, or 1 as a 21 | fallback. 22 | } 23 | \keyword{internal} 24 | -------------------------------------------------------------------------------- /man/srcref_key.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_srcref.R 3 | \name{srcref_key} 4 | \alias{srcref_key} 5 | \title{Convert a \code{\link{srcref}} to a \code{\link{character}} representation} 6 | \usage{ 7 | srcref_key(x, nloc = 2, path = c("base", "root", "full")) 8 | } 9 | \arguments{ 10 | \item{x}{A \code{\link{srcref}} object} 11 | 12 | \item{nloc}{The number of locations (\code{\link[utils:sourceutils]{utils::getSrcLocation}}) to use. 13 | Defaults to 2, indicating starting and ending line number.} 14 | 15 | \item{path}{A form of file path to use for the key. One of \code{"base"} for only 16 | the basename of the source file path, \code{"root"} for a path relative to a 17 | package root directory if found, or \code{"full"} for the full file path.} 18 | } 19 | \value{ 20 | A string hash of a \link{srcref} 21 | } 22 | \description{ 23 | Convert a \code{\link{srcref}} to a \code{\link{character}} representation 24 | } 25 | \keyword{internal} 26 | -------------------------------------------------------------------------------- /man/srcref_nlines.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_srcref.R 3 | \name{srcref_nlines} 4 | \alias{srcref_nlines} 5 | \title{Determine the number of source code lines of a given \link{srcref}} 6 | \usage{ 7 | srcref_nlines(x) 8 | } 9 | \arguments{ 10 | \item{x}{A \link{srcref} object} 11 | } 12 | \value{ 13 | The number of lines in the original source of a \link{srcref} 14 | } 15 | \description{ 16 | Determine the number of source code lines of a given \link{srcref} 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/string_newline_count.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{string_newline_count} 4 | \alias{string_newline_count} 5 | \title{Get String Line Count} 6 | \usage{ 7 | string_newline_count(x) 8 | } 9 | \arguments{ 10 | \item{x}{A character value} 11 | } 12 | \value{ 13 | The number of newline characters in a multiline string 14 | } 15 | \description{ 16 | Get String Line Count 17 | } 18 | \keyword{internal} 19 | -------------------------------------------------------------------------------- /man/test_examples_as_testthat.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{test_examples_as_testthat} 4 | \alias{test_examples_as_testthat} 5 | \title{Execute examples from Rd files as \code{testthat} tests} 6 | \usage{ 7 | test_examples_as_testthat( 8 | package, 9 | path, 10 | ..., 11 | test_dir = file.path(tempdir(), "testex-tests"), 12 | clean = TRUE, 13 | overwrite = TRUE, 14 | roxygenize = !is_r_cmd_check() && uses_roxygen2(path), 15 | reporter = testthat::get_reporter() 16 | ) 17 | } 18 | \arguments{ 19 | \item{package}{A package name whose examples should be tested} 20 | 21 | \item{path}{Optionally, a path to a source code directory to use. Will only 22 | have an effect if parameter \code{package} is missing.} 23 | 24 | \item{...}{Additional argument unused} 25 | 26 | \item{test_dir}{An option directory where test files should be written. 27 | Defaults to a temporary directory.} 28 | 29 | \item{clean}{Whether the \code{test_dir} should be removed upon completion of test 30 | execution. Defaults to \code{TRUE}.} 31 | 32 | \item{overwrite}{Whether files should be overwritten if \code{test_dir} already 33 | exists. Defaults to \code{TRUE}.} 34 | 35 | \item{roxygenize}{Whether R documentation files should be re-written using 36 | \code{roxygen2} prior to testing. When not \code{FALSE}, tests written in \code{roxygen2} 37 | tags will be used to update R documentation files prior to testing to use 38 | the most up-to-date example tests. May be \code{TRUE}, or a \code{list} of arguments 39 | passed to \code{\link[roxygen2:roxygenize]{roxygen2::roxygenize}}. By default, only enabled when running 40 | outside of \verb{R CMD check} and while taking \code{roxygen2} as a dependency.} 41 | 42 | \item{reporter}{A \code{testthat} reporter to use. Defaults to the active 43 | reporter in the \code{testthat} environment or default reporter.} 44 | } 45 | \value{ 46 | The result of \code{\link[testthat:source_file]{testthat::source_file()}}, after iterating over 47 | generated test files. 48 | } 49 | \description{ 50 | Reads examples from Rd files and constructs \code{testthat}-style tests. 51 | \code{testthat} expectations are built such that 52 | } 53 | \details{ 54 | \enumerate{ 55 | \item Each example expression is expected to run without error 56 | \item Any \code{testex} expectations are expected to pass 57 | } 58 | 59 | Generally, you won't need to use this function directly. Instead, it 60 | is called by a file generated by \code{\link[=use_testex_as_testthat]{use_testex_as_testthat()}} which will add 61 | any \code{testex} example tests to your existing \code{testthat} testing suite. 62 | } 63 | \note{ 64 | It is assumed that this function is used within a \code{testthat} run, when 65 | the necessary packages are already installed and loaded. 66 | } 67 | \examples{ 68 | \dontshow{if (requireNamespace("testthat", quietly = TRUE)) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 69 | \donttest{ 70 | # library(pkg.example) 71 | path <- system.file("pkg.example", package = "testex") 72 | test_examples_as_testthat(path = path) 73 | } 74 | \dontshow{\}) # examplesIf} 75 | } 76 | -------------------------------------------------------------------------------- /man/test_files.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{test_files} 4 | \alias{test_files} 5 | \title{Test a list of files} 6 | \usage{ 7 | test_files(files, context, ...) 8 | } 9 | \arguments{ 10 | \item{files}{A collection of file paths to test} 11 | 12 | \item{context}{An optional context message to display in \code{testthat} reporters} 13 | 14 | \item{...}{Additional arguments passed to \code{testhat::source_file}} 15 | } 16 | \value{ 17 | The result of \code{\link[testthat:source_file]{testthat::source_file()}}, after iterating over 18 | generated test files. 19 | } 20 | \description{ 21 | Test a list of files 22 | } 23 | \keyword{internal} 24 | -------------------------------------------------------------------------------- /man/testex-options.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/opts.R 3 | \name{testex-options} 4 | \alias{testex-options} 5 | \alias{memoise_testex_desc} 6 | \alias{testex_options} 7 | \title{Cached retrieval of testex options from package DESCRIPTION} 8 | \usage{ 9 | memoise_testex_desc(path, fingerprint, ...) 10 | 11 | testex_options(path = package_desc(), ...) 12 | } 13 | \arguments{ 14 | \item{path}{A path in which to search for a package \code{DESCRIPTION}} 15 | 16 | \item{fingerprint}{An object used to indicate when the cached values have 17 | been invalidated} 18 | } 19 | \value{ 20 | The test options environment, invisibly. 21 | 22 | The test options environment as a list 23 | } 24 | \description{ 25 | As long as the \code{fingerprint} has not changed, the package \code{DESCRIPTION} will 26 | be read only once to parse and retrieve configuration options. If the 27 | \code{DESCRIPTION} file is modified or if run from a separate process, the 28 | configured settings will be refreshed based on the most recent version of 29 | the file. 30 | } 31 | \section{Functions}{ 32 | \itemize{ 33 | \item \code{testex_options()}: 34 | 35 | }} 36 | \keyword{internal} 37 | -------------------------------------------------------------------------------- /man/testex-rd-example-helpers.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils_rd.R 3 | \name{testex-rd-example-helpers} 4 | \alias{testex-rd-example-helpers} 5 | \alias{rd_extract_examples} 6 | \alias{rd_code_as_string} 7 | \alias{split_testonly_as_expr} 8 | \alias{split_testonly} 9 | \title{Rd Example Parsing Helpers} 10 | \usage{ 11 | rd_extract_examples(rd) 12 | 13 | rd_code_as_string(rd) 14 | 15 | split_testonly_as_expr(rd) 16 | 17 | split_testonly(rd) 18 | } 19 | \arguments{ 20 | \item{rd}{An Rd object} 21 | } 22 | \value{ 23 | The examples section of an Rd object 24 | 25 | A formatted Rd example 26 | 27 | An interlaced list of expressions, either representing example 28 | code or tests. The names of the list are either \verb{\testonly} or \code{RDCODE} 29 | depending on the originating source of the expression. 30 | 31 | A list of Rd tag contents 32 | } 33 | \description{ 34 | Rd Example Parsing Helpers 35 | } 36 | \section{Functions}{ 37 | \itemize{ 38 | \item \code{rd_extract_examples()}: Extract examples tag from an Rd file 39 | 40 | \item \code{rd_code_as_string()}: Convert an Rd example to string 41 | 42 | \item \code{split_testonly_as_expr()}: Split sections of an example into evaluated example code blocks and code 43 | blocks wrapped in \verb{\testonly} \code{Rd_tag}s, reassigning \code{\link{srcref}}s as the 44 | example code is split. 45 | 46 | \item \code{split_testonly()}: Split sections of an example into lists of \code{Rd_tag}s. Note that \code{\link{srcref}}s 47 | are split by line number. If a line is split between two sections, it is 48 | attributed to the first section. As this is used primarily for giving line 49 | numbers to test messages, this is sufficient for providing test failures 50 | locations. 51 | 52 | }} 53 | \keyword{internal} 54 | -------------------------------------------------------------------------------- /man/testex-roxygen-tags.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/roxygen2.R 3 | \name{testex-roxygen-tags} 4 | \alias{testex-roxygen-tags} 5 | \title{\code{\link{testex}} \code{roxygen2} tags} 6 | \description{ 7 | \code{\link{testex}} provides two new \code{roxygen2} tags, \verb{@test} and \verb{@testthat}. 8 | } 9 | \section{tags}{ 10 | 11 | \link{testex} tags are all sub-tags meant to be used within an 12 | \verb{@examples} block. They should be considered as tags \emph{within} the 13 | \verb{@examples} block and used to construct blocks of testing code within 14 | example code. 15 | 16 | \describe{ 17 | \item{\verb{@test}: }{ 18 | In-line expectations to test the output of the previous command within an 19 | example. If \code{.} is used within the test expression, it will be used to 20 | refer to the output of the previous example command. Otherwise, the 21 | result of the expression is expected to be identical to the previous 22 | output. 23 | 24 | \if{html}{\out{
}}\preformatted{#' @examples 25 | #' 1 + 2 26 | #' @test 3 27 | #' @test . == 3 28 | #' 29 | #' @examples 30 | #' 3 + 4 31 | #' @test identical(., 7) 32 | }\if{html}{\out{
}} 33 | 34 | } 35 | } 36 | 37 | \describe{ 38 | \item{\verb{@testthat}: }{ 39 | Similar to \verb{@test}, \verb{@testthat} can be used to make in-line 40 | assertions using \code{testthat} expectations. \code{testthat} expectations 41 | follow a convention where the first argument is an object to compare 42 | against an expected value or characteristic. Since the value will always 43 | be the result of the previous example, this part of the code is 44 | implicitly constructed for you. 45 | 46 | If you want to use the example result elsewhere in your expectation, you 47 | can refer to it with a \code{.}. When used in this way, \link{testex} will 48 | not do any further implicit modification of your expectation. 49 | 50 | \if{html}{\out{
}}\preformatted{#' @examples 51 | #' 1 + 2 52 | #' @testthat expect_equal(3) 53 | #' @testthat expect_gt(0) 54 | #' 55 | #' @examples 56 | #' 3 + 4 57 | #' @testthat expect_equal(., 7) 58 | }\if{html}{\out{
}} 59 | 60 | } 61 | } 62 | } 63 | 64 | -------------------------------------------------------------------------------- /man/testex-testthat.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{testex-testthat} 4 | \alias{testex-testthat} 5 | \title{Support for \code{testthat} Expectations} 6 | \description{ 7 | \code{testthat} support is managed through a "style" provided to \code{\link{testex}}. 8 | When using the \code{testthat} style (automatically when using the \verb{@testthat} 9 | tag), expectations are processed such that they always refer to the previous 10 | example. Special care is taken to manage propagation of this value through 11 | your test code, regardless of how \code{testthat} is executed. 12 | } 13 | \examples{ 14 | \dontshow{if (requireNamespace("testthat", quietly = TRUE)) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} 15 | # example code 16 | 1 + 2 17 | 18 | # within `testex` block, test code refers to previous result with `.` 19 | testex(style = "testthat", srcref = "abc.R:1:3", { \dontshow{ 20 | . <- 3 # needed because roxygen2 @examplesIf mutates .Last.value 21 | } 22 | test_that("addition holds up", { 23 | expect_equal(., 3) 24 | }) 25 | }) 26 | \dontshow{\}) # examplesIf} 27 | } 28 | -------------------------------------------------------------------------------- /man/testex.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testex.R 3 | \name{testex} 4 | \alias{testex} 5 | \title{A syntactic helper for writing quick and easy example tests} 6 | \usage{ 7 | testex( 8 | ..., 9 | srcref = NULL, 10 | example_srcref = NULL, 11 | value = get_example_value(), 12 | envir = parent.frame(), 13 | style = "standalone" 14 | ) 15 | } 16 | \arguments{ 17 | \item{...}{Expressions to evaluated. \code{.} will be replaced with the 18 | expression passed to \code{val}, and may be used as a shorthand for the 19 | last example result.} 20 | 21 | \item{srcref}{An option \code{srcref_key} string used to indicate where the 22 | relevant test code originated from.} 23 | 24 | \item{example_srcref}{An option \code{srcref_key} string used to indicate where 25 | the relevant example code originated from.} 26 | 27 | \item{value}{A value to test against. By default, this will use the example's 28 | \code{.Last.value}.} 29 | 30 | \item{envir}{An environment in which tests should be evaluated. By default 31 | the parent environment where tests are evaluated.} 32 | 33 | \item{style}{A syntactic style used by the test. Defaults to \code{"standalone"}, 34 | which expects \code{TRUE} and uses a \code{.}-notation. Accepts one of 35 | \code{"standalone"} or \code{"testthat"}. By default, styles will be implicitly 36 | converted to accommodate known testing frameworks, though this can be 37 | disabled by passing the style \code{"AsIs"} with \code{\link[=I]{I()}}.} 38 | } 39 | \value{ 40 | Invisibly returns the \code{.Last.value} as it existed prior to evaluating 41 | the test. 42 | } 43 | \description{ 44 | A wrapper around \code{stopifnot} that allows you to use \code{.} to refer to 45 | \code{.Last.value} and preserve the last non-test output from an example. 46 | } 47 | \section{Documenting with \code{testex}}{ 48 | 49 | 50 | \code{testex} is a simple wrapper around execution that propagates the 51 | \code{.Last.value} returned before running, allowing you to chain tests 52 | more easily. 53 | \subsection{Use in \code{Rd} files:}{ 54 | 55 | \preformatted{ 56 | \examples{ 57 | f <- function(a, b) a + b 58 | f(3, 4) 59 | \testonly{ 60 | testex::testex( 61 | is.numeric(.), 62 | identical(., 7) 63 | ) 64 | } 65 | } 66 | } 67 | 68 | But \code{Rd} files are generally regarded as being a bit cumbersome to author 69 | directly. Instead, \code{testex} provide helpers that generate this style of 70 | documentation, which use this function internally. 71 | } 72 | 73 | \subsection{Use with \code{roxygen2}}{ 74 | 75 | Within a \code{roxygen2} \verb{@examples} block you can instead use the \verb{@test} tag 76 | which will generate Rd code as shown above. 77 | 78 | \preformatted{ 79 | #' @examples 80 | #' f <- function(a, b) a + b 81 | #' f(3, 4) 82 | #' @test is.numeric(.) 83 | #' @test identical(., 7) 84 | } 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /man/use_testex.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use.R 3 | \name{use_testex} 4 | \alias{use_testex} 5 | \title{Add \code{\link{testex}} tags and configure package to fully use \code{\link{testex}} features} 6 | \usage{ 7 | use_testex(path = getwd(), check = TRUE, quiet = FALSE) 8 | } 9 | \arguments{ 10 | \item{path}{A package source code working directory} 11 | 12 | \item{check}{A \code{logical} value indicating whether tests should be 13 | executing during \verb{R CMD check}.} 14 | 15 | \item{quiet}{Whether output should be suppressed} 16 | } 17 | \value{ 18 | The result of \code{\link[=write.dcf]{write.dcf()}} upon modifying the package 19 | \code{DESCRIPTION} file. 20 | } 21 | \description{ 22 | Add \code{\link{testex}} tags and configure package to fully use \code{\link{testex}} features 23 | } 24 | \note{ 25 | The \code{\link{testex}} \code{roxygen2} tags behave similarly to \code{roxygen2} \verb{@examples} 26 | tags, with the minor addition of some wrapping code to manage the tests. This 27 | means that they will be integrated into your \verb{@examples} and can be 28 | intermixed between \verb{@examples} tags 29 | } 30 | -------------------------------------------------------------------------------- /man/use_testex_as_testthat.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/use.R 3 | \name{use_testex_as_testthat} 4 | \alias{use_testex_as_testthat} 5 | \title{Run examples as \code{testthat} expectations} 6 | \usage{ 7 | use_testex_as_testthat(path = getwd(), context = "testex", quiet = FALSE) 8 | } 9 | \arguments{ 10 | \item{path}{A package source code working directory} 11 | 12 | \item{context}{A \code{testthat} test context to use as the basis for a new test 13 | filename.} 14 | 15 | \item{quiet}{Whether to emit output messages.} 16 | } 17 | \value{ 18 | The result of \code{\link[=writeLines]{writeLines()}} after writing a new \code{testthat} file. 19 | } 20 | \description{ 21 | Run examples as \code{testthat} expectations 22 | } 23 | \concept{use} 24 | -------------------------------------------------------------------------------- /man/uses_roxygen2.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{uses_roxygen2} 4 | \alias{uses_roxygen2} 5 | \title{Checks for use of \code{roxygen2}} 6 | \usage{ 7 | uses_roxygen2(path) 8 | } 9 | \arguments{ 10 | \item{path}{A file path to a package source code directory} 11 | } 12 | \value{ 13 | A logical value indicating whether a package takes \code{roxygen2} as 14 | a dependency. 15 | } 16 | \description{ 17 | Checks for use of \code{roxygen2} 18 | } 19 | -------------------------------------------------------------------------------- /man/vapplys.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{vlapply} 4 | \alias{vlapply} 5 | \alias{vcapply} 6 | \alias{vnapply} 7 | \title{\code{vapply} shorthand alternatives} 8 | \usage{ 9 | vlapply(..., FUN.VALUE = logical(1L)) 10 | 11 | vcapply(..., FUN.VALUE = character(1L)) 12 | 13 | vnapply(..., FUN.VALUE = numeric(1L)) 14 | } 15 | \arguments{ 16 | \item{...}{Arguments passed to \code{\link{vapply}}} 17 | 18 | \item{FUN.VALUE}{A preset signature for the flavor of \code{\link{vapply}}. This is 19 | exposed for transparency, but modifying it would break the implicit 20 | contract in the function name about the return type.} 21 | } 22 | \value{ 23 | The result of \code{\link{vapply}} 24 | } 25 | \description{ 26 | Simple wrappers around \code{vapply} for common data types 27 | } 28 | \keyword{internal} 29 | -------------------------------------------------------------------------------- /man/with_attached.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils.R 3 | \name{with_attached} 4 | \alias{with_attached} 5 | \title{Temporarily attach a namespace} 6 | \usage{ 7 | with_attached(ns, expr) 8 | } 9 | \arguments{ 10 | \item{ns}{A namespace or namespace name to attach} 11 | 12 | \item{expr}{An expression to evaluate while the namespace is attached} 13 | } 14 | \value{ 15 | The result of evaluating \code{expr} 16 | } 17 | \description{ 18 | This function is primarily for managing attaching of namespaces needed for 19 | testing internally. It is exported only because it is needed in code 20 | generated within \code{Rd} files, but is otherwise unlikely to be needed. 21 | } 22 | -------------------------------------------------------------------------------- /man/with_srcref.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{with_srcref} 4 | \alias{with_srcref} 5 | \title{Raise \code{testthat} Expectations With A Known Source Reference} 6 | \usage{ 7 | with_srcref(src, expr, envir = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{src}{A \code{srcref_key} which is parsed to produce an artificial \code{\link{srcref}} 11 | for the expectation signaled messages.} 12 | 13 | \item{expr}{An expression to be evaluated. If an \code{expectation} condition is 14 | raised during its evaluation, its \code{\link{srcref}} is converted to \code{src}.} 15 | 16 | \item{envir}{An environment in which to evaluate \code{expr}.} 17 | } 18 | \value{ 19 | The result of evaluating \code{expr}, or an expectation with appended 20 | \code{\link{srcref}} information if an expectation is raised. 21 | } 22 | \description{ 23 | Retroactively assigns a source file and location to a expectation. This 24 | allows \code{testthat} to report an origin for any code that raised an example 25 | test failure from the source \code{roxygen2} code, even though the test code is 26 | reconstructed from package documentation files. 27 | } 28 | -------------------------------------------------------------------------------- /man/wrap_expect_no_error.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/testthat.R 3 | \name{wrap_expect_no_error} 4 | \alias{wrap_expect_no_error} 5 | \title{Wraps an example expression in a \code{testthat} expectation to not error} 6 | \usage{ 7 | wrap_expect_no_error(expr, value) 8 | } 9 | \arguments{ 10 | \item{expr}{An expression to wrap in a \code{expect_no_error()} expectation. Uses 11 | \code{testthat}s version if recent enough version is available, or provides 12 | a fallback otherwise.} 13 | 14 | \item{value}{A symbol to use to store the result of \code{expr}} 15 | } 16 | \value{ 17 | A \code{\link[testthat:test_that]{testthat::test_that()}} call 18 | } 19 | \description{ 20 | Wraps an example expression in a \code{testthat} expectation to not error 21 | } 22 | \keyword{internal} 23 | -------------------------------------------------------------------------------- /pkgdown/_pkgdown.yml: -------------------------------------------------------------------------------- 1 | development: 2 | mode: auto 3 | 4 | url: ~ 5 | template: 6 | bootstrap: 5 7 | theme: arrow-light #atom-one-light 8 | 9 | bslib: 10 | fg: "#404040" 11 | bg: "#F4F4F4" 12 | navbar-dark-active-color: var(--bs-fg) 13 | navbar-dark-color: var(--bs-fg) 14 | navbar-dark-brand-color: var(--bs-fg) 15 | navbar-dark-brand-hover-color: white 16 | navbar-dark-hover-color: white 17 | primary: "#9A1FF4" 18 | 19 | navbar: 20 | bg: primary 21 | -------------------------------------------------------------------------------- /pkgdown/extra.scss: -------------------------------------------------------------------------------- 1 | [data-bs-theme="dark"] { 2 | --bs-code-color: rgba(var(--bs-primary-rgb), 0.8); 3 | } 4 | 5 | h1, h2, h3, h4, h5, h6 { 6 | @extend %headings !optional; 7 | } 8 | 9 | %headings { 10 | margin-top: 1rem; 11 | } 12 | 13 | pre { 14 | padding: 1rem; 15 | background: #fefbfa; 16 | border-style: none none none solid; 17 | border-color: rgba(var(--bs-primary-rgb), 0.6); 18 | border-radius: 0; 19 | border-width: 0.2em; 20 | } 21 | 22 | .btn-copy-ex { 23 | padding: 0.5em 1em; 24 | } 25 | 26 | // for dev version tag, which can be hard to read with bright navbars 27 | .nav-text.text-danger { 28 | background-color: rgba(255, 255, 255, 0.8); 29 | padding: 0.2em 0.8em; 30 | border-radius: 1em; 31 | } 32 | 33 | .navbar-dark { 34 | --bs-navbar-color: rgba(var(--bs-body-bg-rgb), 0.8); 35 | --bs-navbar-hover-color: rgba(var(--bs-body-bg-rgb), 1.0); 36 | font-weight: bolder; 37 | 38 | .navbar-nav { 39 | .active > .nav-link { 40 | background: rgba(255, 255, 255, 0.2); 41 | } 42 | } 43 | 44 | input[type="search"] { 45 | border: none; 46 | border-radius: 0.1em; 47 | background-color: rgba(var(--bs-light-rgb), 0.2); 48 | color: rgba(var(--bs-primary-rgb), 0.6); 49 | &:focus { background-color: rgba(var(--bs-light-rgb), 0.3); } 50 | } 51 | } 52 | 53 | .text-muted { 54 | color: rgba(var(--bs-light-rgb), 0.6) !important; 55 | } 56 | -------------------------------------------------------------------------------- /tests/spelling.R: -------------------------------------------------------------------------------- 1 | if (requireNamespace("spelling", quietly = TRUE)) { 2 | spelling::spell_check_test( 3 | vignettes = TRUE, 4 | error = FALSE, 5 | skip_on_cran = TRUE 6 | ) 7 | } 8 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(testex) 3 | 4 | test_check("testex") 5 | -------------------------------------------------------------------------------- /tests/testthat/setup.R: -------------------------------------------------------------------------------- 1 | pkg_example_dir <- if (length(find.package(packageName(), quiet = TRUE)) > 0) { 2 | system.file("pkg.example", package = packageName()) 3 | } else { 4 | file.path(testthat::test_path(), "..", "..", "inst", "pkg.example") 5 | } 6 | 7 | as_r_cmd_check <- function(expr, pkg = "pkg") { 8 | withr::with_envvar( 9 | list("_R_CHECK_PACKAGE_NAME_" = pkg), 10 | expr 11 | ) 12 | } 13 | 14 | as_not_r_cmd_check <- function(expr) { 15 | withr::with_envvar( 16 | list("_R_CHECK_PACKAGE_NAME_" = NA), 17 | expr 18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /tests/testthat/test-escape-infotex.R: -------------------------------------------------------------------------------- 1 | test_that("escape_infotex doubles infotex backslashes", { 2 | expect_equal(escape_infotex("\\testonly"), "\\\\testonly") 3 | }) 4 | -------------------------------------------------------------------------------- /tests/testthat/test-options.R: -------------------------------------------------------------------------------- 1 | test_that(paste0( 2 | "update_testex_desc reads and caches options set in Config/testex/options", 3 | "warning when version is mismatched and disabling checks" 4 | ), { 5 | desc <- " 6 | Package: example 7 | Config/testex/options: list(a = 1) 8 | " 9 | 10 | dir.create(test_dir <- tempfile("testex")) 11 | desc_path <- file.path(test_dir, "DESCRIPTION") 12 | withr::defer(unlink(test_dir, recursive = TRUE)) 13 | 14 | writeLines(trimws(desc), desc_path) 15 | 16 | as_not_r_cmd_check({ 17 | expect_warning(testex_options(desc_path), "version") 18 | expect_silent(orig <- testex_options("")) 19 | }) 20 | 21 | expect_length(orig, 2) 22 | expect_identical(orig$a, 1) 23 | expect_identical(orig$check, FALSE) 24 | orig_mtime <- .testex_options$.fingerprint$mtime 25 | 26 | # during R CMD check, process ID is used for fingerprint instead of mtime 27 | skip_if(is_r_cmd_check(), "on R CMD check") 28 | Sys.sleep(1) # give time for mtime to update 29 | 30 | # without updating file, cache fingerprint unchanged 31 | expect_silent(testex_options(desc_path)) 32 | expect_true(.testex_options$.fingerprint$mtime == orig_mtime) 33 | 34 | desc <- " 35 | Package: example 36 | Config/testex/options: list(a = 1, b = 2) 37 | " 38 | 39 | # expect invalidation of cached value and new values stored 40 | writeLines(trimws(desc), desc_path) 41 | 42 | as_not_r_cmd_check({ 43 | expect_warning(testex_options(desc_path)) 44 | expect_silent(updated <- testex_options("")) 45 | }) 46 | 47 | expect_true(.testex_options$.fingerprint$mtime != orig_mtime) 48 | expect_length(updated, 3) 49 | expect_identical(updated$a, 1) 50 | expect_identical(updated$b, 2) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/testthat/test-pkgexample.R: -------------------------------------------------------------------------------- 1 | test_that("test_examples_as_testthat converts examples to tests and executes test suite", { 2 | dir.create(test_lib <- tempfile("testex_test_lib")) 3 | withr::defer(unlink(test_lib, recursive = TRUE)) 4 | 5 | install.packages( 6 | system.file(package = "testex", "pkg.example"), 7 | lib = test_lib, 8 | repos = NULL, 9 | type = "source", 10 | INSTALL_opts = "--install-tests", 11 | quiet = testthat::is_testing() 12 | ) 13 | 14 | expect_silent({ 15 | res <- callr::r( 16 | function() { 17 | library(testthat) 18 | library(testex) 19 | library(pkg.example) 20 | 21 | with_reporter(ListReporter$new(), { 22 | test_examples_as_testthat( 23 | path = find.package("pkg.example"), 24 | roxygenize = FALSE # pkgload works weirdly here... 25 | ) 26 | }) 27 | }, 28 | libpath = c(test_lib, .libPaths()), 29 | env = c(TESTTHAT = "true") 30 | ) 31 | 32 | test_res <- res$results$as_list() 33 | }) 34 | 35 | # resurface tests from example package as integration tests of testthat eval 36 | for (test in test_res) { 37 | for (expectation in test$results) { 38 | testthat::exp_signal(expectation) 39 | } 40 | } 41 | }) 42 | -------------------------------------------------------------------------------- /tests/testthat/test-rd-utils.R: -------------------------------------------------------------------------------- 1 | test_that("rd_code_as_string collapses code into string", { 2 | rd_raw <- " 3 | \\examples{ 4 | 1 + 2 5 | 6 | 4 7 | } 8 | " 9 | 10 | dir.create(test_dir <- tempfile("testex")) 11 | test_path <- file.path(test_dir, "rd") 12 | withr::defer(unlink(test_dir, recursive = TRUE)) 13 | 14 | writeLines(rd_raw, test_path) 15 | rd <- tools::parse_Rd(test_path) 16 | 17 | expect_silent(rd_string <- rd_code_as_string(rd)) 18 | expect_match(rd_string, "1 \\+ 2\n\n[ ]*4") 19 | }) 20 | 21 | test_that("rd_extract_examples pulls out \\examples Rd tags", { 22 | rd_raw <- " 23 | \\title{test} 24 | \\description{testing!} 25 | \\examples{ 26 | 1 + 2 27 | \\testonly{ 28 | identical(.Last.value, 3) 29 | } 30 | 31 | 4 32 | } 33 | " 34 | 35 | dir.create(test_dir <- tempfile("testex")) 36 | test_path <- file.path(test_dir, "rd") 37 | withr::defer(unlink(test_dir, recursive = TRUE)) 38 | 39 | writeLines(rd_raw, test_path) 40 | rd <- tools::parse_Rd(test_path) 41 | 42 | expect_silent(examples_rd <- rd_extract_examples(rd)) 43 | expect_equal(attr(examples_rd, "Rd_tag"), "\\examples") 44 | }) 45 | 46 | test_that("split_testonly splits examples block into components", { 47 | rd_raw <- "\\examples{ 48 | 1 + 2 49 | \\testonly{ 50 | identical(.Last.value, 3) 51 | } 52 | 53 | 3 + 4 54 | \\testonly{ 55 | identical(.Last.value, 7) 56 | } 57 | 58 | 5 59 | }" 60 | 61 | dir.create(test_dir <- tempfile("testex")) 62 | test_path <- file.path(test_dir, "rd") 63 | withr::defer(unlink(test_dir, recursive = TRUE)) 64 | 65 | writeLines(rd_raw, test_path) 66 | rd <- tools::parse_Rd(test_path)[[1]] 67 | 68 | expect_silent(rd_example_sections <- split_testonly(rd)) 69 | expect_length(rd_example_sections, 5) 70 | expect_named(rd_example_sections) 71 | expect_equal( 72 | names(rd_example_sections), 73 | c("RCODE", "\\testonly", "RCODE", "\\testonly", "RCODE") 74 | ) 75 | }) 76 | 77 | test_that("split_testonly_as_expr parses example components and preserves srcrefs", { 78 | rd_raw <- "\\examples{ 79 | 1 + 2 80 | \\testonly{ 81 | identical(.Last.value, 3) 82 | } 83 | 84 | 3 + 4 85 | \\testonly{ 86 | identical(.Last.value, 7) 87 | } 88 | 89 | 5 90 | }" 91 | 92 | dir.create(test_dir <- tempfile("testex")) 93 | test_path <- file.path(test_dir, "rd") 94 | withr::defer(unlink(test_dir, recursive = TRUE)) 95 | 96 | writeLines(rd_raw, test_path) 97 | rd <- tools::parse_Rd(test_path)[[1]] 98 | 99 | expect_silent(rd_example_sections <- split_testonly_as_expr(rd)) 100 | expect_length(rd_example_sections, 5) 101 | expect_true(all(vlapply(rd_example_sections, is.language) | vlapply(rd_example_sections, is.atomic))) 102 | expect_true(!any(vlapply(lapply(rd_example_sections, getSrcref), is.null))) 103 | }) 104 | 105 | test_that("split_testonly_as_expr handles \\dontrun, \\dontshow Rd tags", { 106 | rd_raw <- "\\examples{ 107 | \\dontshow{ 108 | print('hi mom!') 109 | } 110 | 111 | 1 + 2 112 | \\testonly{ 113 | identical(.Last.value, 3) 114 | } 115 | 116 | \\dontrun{ 117 | stop('whoops!') 118 | } 119 | 120 | 3 + 4 121 | \\testonly{ 122 | identical(.Last.value, 7) 123 | } 124 | 125 | 5 126 | }" 127 | 128 | dir.create(test_dir <- tempfile("testex")) 129 | test_path <- file.path(test_dir, "rd") 130 | withr::defer(unlink(test_dir, recursive = TRUE)) 131 | 132 | writeLines(rd_raw, test_path) 133 | rd <- tools::parse_Rd(test_path)[[1]] 134 | 135 | expect_silent(rd_example_sections <- split_testonly_as_expr(rd)) 136 | expect_length(rd_example_sections, 5) 137 | expect_named(rd_example_sections) 138 | expect_equal(names(rd_example_sections), c("RCODE", "\\testonly", "RCODE", "\\testonly", "RCODE")) 139 | }) 140 | -------------------------------------------------------------------------------- /tests/testthat/test-roxygen2-expect.R: -------------------------------------------------------------------------------- 1 | test_that("@test tags produce \\testonly blocks", { 2 | roxy_text <- " 3 | #' Title 4 | #' 5 | #' Description. 6 | #' 7 | #' @param x,y parameters 8 | #' 9 | #' @examples 10 | #' 1 + 2 11 | #' @test 3 12 | #' 13 | #' @export 14 | f <- function(x, y) x + y 15 | " 16 | 17 | block <- roxygen2::parse_text(roxy_text)[[1]] 18 | expect_tag <- block$tags[[5]] 19 | 20 | expect_equal(expect_tag$tag, "test") 21 | expect_s3_class(expect_tag, "roxy_tag_examples") 22 | 23 | expect_true(any(grepl("\\\\testonly\\{", expect_tag$val))) 24 | expect_true(any(grepl("testex::testex\\(", expect_tag$val))) 25 | expect_true(any(grepl("identical\\(\\., 3\\)", expect_tag$val))) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/testthat/test-roxygen2-parse-text.R: -------------------------------------------------------------------------------- 1 | test_that("roxygen2 can parse testex tags without raising conditions", { 2 | roxy_text <- " 3 | #' Title 4 | #' 5 | #' Description. 6 | #' 7 | #' @param x,y parameters 8 | #' 9 | #' @examples 10 | #' 1 + 2 11 | #' @test 3 12 | #' 13 | #' @export 14 | f <- function(x, y) x + y 15 | " 16 | 17 | expect_silent(block <- roxygen2::parse_text(roxy_text)[[1]]) 18 | }) 19 | -------------------------------------------------------------------------------- /tests/testthat/test-roxygen2-testthat.R: -------------------------------------------------------------------------------- 1 | test_that("@expect tags produce \\testonly blocks", { 2 | roxy_text <- " 3 | #' Title 4 | #' 5 | #' Description. 6 | #' 7 | #' @param x,y parameters 8 | #' 9 | #' @examples 10 | #' 1 + 2 11 | #' @testthat expect_equals(3) 12 | #' 13 | #' @export 14 | f <- function(x, y) x + y 15 | " 16 | 17 | block <- roxygen2::parse_text(roxy_text)[[1]] 18 | testthat_tag <- block$tags[[5]] 19 | 20 | expect_equal(testthat_tag$tag, "testthat") 21 | expect_s3_class(testthat_tag, "roxy_tag_examples") 22 | 23 | expect_true(any(grepl("\\\\testonly\\{", testthat_tag$val))) 24 | expect_true(any(grepl("testex::testex\\(", testthat_tag$val))) 25 | expect_true(any(grepl("expect_equals\\(\\., 3\\)", testthat_tag$val))) 26 | }) 27 | -------------------------------------------------------------------------------- /tests/testthat/test-srcref-key.R: -------------------------------------------------------------------------------- 1 | # first expression, used for testing 2 | { 3 | 1 + 2 4 | } 5 | 6 | test_that("srcrefs can round-trip through a string srcref key", { 7 | this_file <- file.path(testthat::test_path(), "test-srcref-key.R") 8 | exprs <- parse(this_file, n = 1, keep.source = TRUE) 9 | expr <- exprs[[1]] 10 | attr(expr, "srcref") <- getSrcref(exprs)[[1]] 11 | 12 | # ensure key includes filename and start-end lines 13 | expect_equal(srcref_key(expr), "test-srcref-key.R:2:4") 14 | 15 | # srcref keys can be parsed back into srcrefs 16 | expect_equal( 17 | as.srcref(srcref_key(expr)), 18 | srcref( 19 | srcfilealias("test-srcref-key.R", srcfile("test-srcref-key.R")), 20 | c(2, 1, 4, 1) 21 | ) 22 | ) 23 | }) 24 | 25 | test_that("srcrefs for package files find actual file full path", { 26 | src_file <- file.path(find.package("testex"), "tests", "testthat", "test-srcref-key.R") 27 | skip_if_not(file.exists(src_file), "tests not installed") 28 | 29 | exprs <- parse(src_file, n = 1, keep.source = TRUE) 30 | expr <- exprs[[1]] 31 | attr(expr, "srcref") <- getSrcref(exprs)[[1]] 32 | 33 | # ensure key includes filename and start-end lines 34 | expect_match(srcref_key(expr), "test-srcref-key.R:\\d+:\\d+") 35 | 36 | # srcref key file paths can be absolute when a package root is found 37 | expect_true(!is.null(find_package_root(quiet = FALSE))) 38 | expect_silent(src_key_file <- getSrcFilename(as.srcref(srcref_key(expr, path = "full")), full.names = TRUE)) 39 | expect_equal(src_key_file, tools::file_path_as_absolute(src_key_file)) 40 | }) 41 | 42 | test_that("srcref keys can be customized to include more detailed locations", { 43 | src_file <- file.path(find.package("testex"), "fakedir", "file.R") 44 | expr <- expression(1) 45 | attr(expr, "srcref") <- srcref(srcfile(src_file), 1:8) 46 | 47 | # ensure key includes filename and start-end lines 48 | expect_match(srcref_key(expr, nloc = 4), ".*(:\\d+){4}") 49 | expect_equal(as.numeric(as.srcref(srcref_key(expr, nloc = 4)))[1:4], 1:4) 50 | expect_match(srcref_key(expr, nloc = 6), ".*(:\\d+){6}") 51 | expect_equal(as.numeric(as.srcref(srcref_key(expr, nloc = 6)))[1:6], 1:6) 52 | expect_match(srcref_key(expr, nloc = 8), ".*(:\\d+){8}") 53 | expect_equal(as.numeric(as.srcref(srcref_key(expr, nloc = 8)))[1:8], 1:8) 54 | }) 55 | 56 | 57 | test_that("srcrefs using root package path produce full paths", { 58 | src_file <- file.path(find.package("testex"), "fakedir", "file.R") 59 | expr <- expression(1) 60 | attr(expr, "srcref") <- srcref(srcfile(src_file), 1:8) 61 | 62 | expect_silent(src_key <- srcref_key(expr, path = "base")) 63 | expect_equal(gsub(":.*", "", src_key), "file.R") 64 | expect_silent(src_key_file <- getSrcFilename(as.srcref(src_key), full.names = TRUE)) 65 | expect_equal(src_key_file, "file.R") 66 | 67 | expect_silent(src_key <- srcref_key(expr, path = "root")) 68 | expect_equal(gsub(":.*", "", src_key), file.path("fakedir", "file.R")) 69 | expect_silent(src_key_file <- getSrcFilename(as.srcref(src_key), full.names = TRUE)) 70 | expect_equal(src_key_file, file.path("fakedir", "file.R")) 71 | 72 | expect_silent(src_key <- srcref_key(expr, path = "full")) 73 | expect_match(src_key, src_file, fixed = TRUE) 74 | expect_silent(src_key_file <- getSrcFilename(as.srcref(src_key), full.names = TRUE)) 75 | expect_equal(src_key_file, src_file) 76 | }) 77 | -------------------------------------------------------------------------------- /tests/testthat/test-testex.R: -------------------------------------------------------------------------------- 1 | test_that("testex code blocks evaluate expectations against target symbol", { 2 | # style = I("standalone") used to avoid converting test style to accommodate 3 | # running testthat suite. 4 | 5 | expect_silent(withr::with_dir(pkg_example_dir, as_not_r_cmd_check({ 6 | ..Last.value <- 3 7 | testex(style = I("standalone"), identical(., 3), value = ..Last.value) 8 | }))) 9 | 10 | expect_error(withr::with_dir(pkg_example_dir, as_not_r_cmd_check({ 11 | ..Last.value <- 3 12 | testex(style = I("standalone"), identical(., 4), value = ..Last.value) 13 | }))) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/testthat/test-testthat.R: -------------------------------------------------------------------------------- 1 | test_that("with_attached temporarily attaches a packages", { 2 | expect_true(suppressPackageStartupMessages({ 3 | with_attached("roxygen2", any(grepl("roxygen2", search()))) 4 | })) 5 | 6 | expect_true(!any(grepl("roxygen2", search()))) 7 | }) 8 | 9 | test_that("with_srcref binds srcref to testthat condition expectations", { 10 | as.srcref.character(":1:2") 11 | 12 | expect_match( 13 | paste(collapse = "\n", capture.output( 14 | with_reporter(LocationReporter$new(), { 15 | test_that("example", with_srcref(":1:2", expect_true(FALSE))) 16 | }) 17 | )), 18 | ":1" # error reported at "start" of srcref 19 | ) 20 | }) 21 | 22 | test_that("wrap_expect_no_error adds srcref, wraps code in expect_no_error expectation and assigns result to value", { 23 | expr <- quote(1 + 2) 24 | attr(expr, "srcref") <- srcref(srcfile(""), 1:4) 25 | 26 | expect_silent(res_expr <- wrap_expect_no_error(expr, value = quote(..Last.value))) 27 | res_str <- paste0(deparse(res_expr), collapse = "\n") 28 | 29 | # unexpected whitespace may be introduced between langauge elements, due to 30 | # covr traces in quoted code 31 | expect_match(res_str, "testthat::test_that") 32 | expect_match(res_str, "testex::with_srcref(\":1:3\"", fixed = TRUE) 33 | expect_match(res_str, "..Last.value\\s+<<-\\s+") 34 | expect_match(res_str, "testthat::expect_no_error\\(\\s*1\\s+\\+\\s+2") 35 | }) 36 | 37 | test_that("expect_no_error reports when testthat errors occurs while evaluating an expression", { 38 | expect_condition( 39 | fallback_expect_no_error(stop(1)), 40 | class = "expectation" 41 | ) 42 | 43 | expect_silent(fallback_expect_no_error(1 + 2)) 44 | }) 45 | 46 | test_that("testthat_block returns last value from previous expression", { 47 | expect_silent(withr::with_dir(pkg_example_dir, as_not_r_cmd_check({ 48 | ..Last.value <- 3 49 | out <- testex(style = "testthat", expect_equal(., 3), value = ..Last.value) 50 | }))) 51 | 52 | expect_equal(out, 3) 53 | }) 54 | 55 | test_that("testthat_block skips if example throws error", { 56 | expect_silent(withr::with_dir(pkg_example_dir, as_not_r_cmd_check({ 57 | cond <- tryCatch( 58 | { 59 | ..Last.value <- errorCondition("whoops!") 60 | testex(style = "testthat", expect_equal(., 3), value = ..Last.value) 61 | }, 62 | condition = identity 63 | ) 64 | }))) 65 | 66 | expect_s3_class(cond, "skip") 67 | }) 68 | -------------------------------------------------------------------------------- /tests/testthat/test-use.R: -------------------------------------------------------------------------------- 1 | test_that(paste0( 2 | "use_testex adds testex to Suggests and Roxygen roclets specification ", 3 | "if it does not yet exist" 4 | ), { 5 | ex_pkg_inst <- system.file(package = "testex", "pkg.example") 6 | 7 | dir.create(test_dir <- tempfile("testex")) 8 | ex_pkg_path <- file.path(test_dir, basename(ex_pkg_inst)) 9 | file.copy(ex_pkg_inst, test_dir, recursive = TRUE) 10 | 11 | desc <- " 12 | Title: pkg.example 13 | Version: 1.2.3 14 | " 15 | 16 | desc_path <- file.path(ex_pkg_path, "DESCRIPTION") 17 | writeLines(desc, desc_path) 18 | withr::defer(unlink(test_dir, recursive = TRUE)) 19 | 20 | expect_equal(read.dcf(desc_path, fields = "Roxygen")[1, ][[1]], NA_character_) 21 | expect_silent(use_testex(ex_pkg_path, quiet = TRUE)) 22 | expect_match(read.dcf(desc_path, fields = "Roxygen")[1, ][[1]], "^list\\(") 23 | expect_match(read.dcf(desc_path, fields = "Roxygen")[1, ][[1]], "packages = \"testex\"") 24 | expect_match(read.dcf(desc_path, fields = "Suggests")[1, ][[1]], "\\btestex\\b") 25 | }) 26 | 27 | test_that("use_testex_as_testthat adds test-testex.R when testthat already in use", { 28 | ex_pkg_inst <- system.file(package = "testex", "pkg.example") 29 | 30 | dir.create(test_dir <- tempfile("testex")) 31 | ex_pkg_path <- file.path(test_dir, basename(ex_pkg_inst)) 32 | file.copy(ex_pkg_inst, test_dir, recursive = TRUE) 33 | testthat_testex_file <- file.path(ex_pkg_path, "tests", "testthat", "test-testex.R") 34 | file.remove(testthat_testex_file) 35 | withr::defer(unlink(test_dir, recursive = TRUE)) 36 | 37 | expect_silent(use_testex_as_testthat(ex_pkg_path)) 38 | expect_true(file.exists(testthat_testex_file)) 39 | expect_true(any(grepl("testex::test_examples", readLines(testthat_testex_file)))) 40 | }) 41 | 42 | test_that("use_testex_as_testthat aborts when testthat not in use", { 43 | ex_pkg_inst <- system.file(package = "testex", "pkg.example") 44 | 45 | dir.create(test_dir <- tempfile("testex")) 46 | ex_pkg_path <- file.path(test_dir, basename(ex_pkg_inst)) 47 | file.copy(ex_pkg_inst, test_dir, recursive = TRUE) 48 | testthat_dir <- file.path(ex_pkg_path, "tests", "testthat") 49 | unlink(testthat_dir, recursive = TRUE) 50 | withr::defer(unlink(test_dir, recursive = TRUE)) 51 | 52 | expect_error(use_testex_as_testthat(ex_pkg_path), "use testthat") 53 | }) 54 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/configuration.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Configuration" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Configuration} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | ```{r, include = FALSE} 11 | knitr::opts_chunk$set( 12 | collapse = TRUE, 13 | comment = "#>" 14 | ) 15 | ``` 16 | 17 | To make the most of `testex`, there are a few configuration steps you might 18 | consider. These are made simple by using: 19 | 20 | ```r 21 | testex::use_testex() 22 | ``` 23 | 24 | which will : 25 | 26 | - [x] Add `packages = "testex"` to the `Roxygen` field in `DESCRIPTION`, 27 | allowing `roxygen2` to make use of the `testex` tags. 28 | - [x] Add `testex` as a `Suggests` dependency 29 | - [x] Add settings to the `Config/testex/options` field in `DESCRIPTION`, 30 | enabling example tests during `R CMD check` by default. 31 | - [x] Add a `test-testex.R` test file if you're using `testthat`, 32 | enabling example tests during `testthat` test evaluation. 33 | 34 | Though if you prefer you can configure all of this yourself: 35 | 36 | # `testthat` 37 | 38 | Running tests using `testthat` is simple. Just use 39 | 40 | ```r 41 | testex::use_testex_as_testthat() 42 | ``` 43 | 44 | This will add a simple, one-line file to your `tests/testthat` directory 45 | containing 46 | 47 | ```r 48 | testex::test_examples_as_testthat() 49 | ``` 50 | 51 | By adding this single line to a `testthat` test file (such as 52 | `tests/testthat/test-testex.R`), your example tests will be included as part 53 | of your test suite. 54 | 55 | When run this way, `testex` tests are embedded with additional metadata 56 | including the original file location of the examples so that `testthat` is 57 | able to provide more informative error messages. 58 | 59 | # `R CMD check` 60 | 61 | By default, your tests will run when your run examples using `R CMD check`. 62 | However, `R CMD check` will stop on the first error and truncates error output, 63 | which can be inconvenient for debugging. If you'd prefer not to run tests 64 | during checking, you can add the following line to your `DESCRIPTION`. 65 | 66 | ``` 67 | Config/testex/options: list(check = FALSE) 68 | ``` 69 | -------------------------------------------------------------------------------- /vignettes/interface_layers.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Interface Layers" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Interface Layers} 6 | %\VignetteEngine{knitr::rmarkdown} 7 | %\VignetteEncoding{UTF-8} 8 | --- 9 | 10 | `testex` provides a multi-tiered API, allowing you to adopt it at in a way 11 | that makes the most sense for your package -- whether you're minimizing your 12 | dependency footprint or rely on a specific set of tools. Whatever your 13 | workflow, `testex` has something to offer. 14 | 15 | # Base building-blocks 16 | 17 | R examples can include test code! Even if you're not using `testex`, 18 | you can already add tests to your examples! 19 | 20 | ```latex 21 | \examples{ 22 | identity("hello, world") 23 | \testonly{ 24 | stopifnot(.Last.value == "hello, world") 25 | } 26 | } 27 | ``` 28 | 29 | Here we use `.Last.value` to grab the result of our last example and test it 30 | against an expected value. Though, as you might expect, you can't easily add 31 | _another_ test because `.Last.value` will have changed. 32 | 33 | `testex` provides a familiar interface for managing just this: 34 | 35 | ```latex 36 | \examples{ 37 | identity("hello, world") 38 | \testonly{testex::testex( 39 | is.character(.), 40 | . == "hello, world") 41 | )} 42 | } 43 | ``` 44 | 45 | Already `testex` is doing a bit of work to make our lives easier. The 46 | `.Last.value` is propagated to each of the tests and we can use the convenient 47 | shorthand `.` to refer to the value we want to test. 48 | 49 | # Use a `roxygen2` tag! 50 | 51 | If you're already using `roxygen2`, then things get even easier! `roxygen2` 52 | can make use of new tags provided by `testex`: 53 | 54 | ```r 55 | #' Hello, World! 56 | #' 57 | #' @examples 58 | #' 59 | #' hello("World") 60 | #' @test "Hello, World!" 61 | #' 62 | #' hello("darkness my old friend") 63 | #' @test grepl("darkness", .) 64 | #' 65 | #' @export 66 | hello <- function(who) { 67 | paste0("Hello, ", who, "!") 68 | } 69 | ``` 70 | 71 | After running `roxygen2::roxygenize()`, you can take a peak at the `Rd` files 72 | and see how the code has been translated to `testex` tests. 73 | 74 | # Leverage `testthat` expectations 75 | 76 | A convenience tag is also provide for those that prefer the `testthat` style of 77 | testing. `testthat` provides a wealth of expectation functions, which can be 78 | used in conjunction with `testex` to write more familiar tests. 79 | 80 | ```r 81 | #' Hello, World! 82 | #' 83 | #' @examples 84 | #' 85 | #' hello("World") 86 | #' @testthat expect_equal("Hello, World!") 87 | #' 88 | #' hello("testthat my old friend") 89 | #' @testthat expect_match("testthat") 90 | #' 91 | #' @export 92 | hello <- function(who) { 93 | paste0("Hello, ", who, "!") 94 | } 95 | ``` 96 | 97 | The `@testthat` tag will automatically insert the `.Last.value` from the 98 | previous example into the first argument of each expectation. Multiple 99 | consecutive `@testthat` expectations will all test the previous example output. 100 | 101 | # Other Test Suites? 102 | 103 | There are, of course, plenty of other flavors of testing suites. Thankfully, 104 | `testex` is quite versatile because `Rd` code is used as the foundation of 105 | everything else. 106 | 107 | If you want to see support for another framework, please open an issue! 108 | --------------------------------------------------------------------------------