├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ ├── R-CMD-check-dev.yaml │ ├── R-CMD-check-status.yaml │ ├── R-CMD-check.yaml │ ├── check │ └── action.yml │ ├── commit │ └── action.yml │ ├── covr │ └── action.yml │ ├── dep-matrix │ └── action.yml │ ├── dep-suggests-matrix │ ├── action.R │ └── action.yml │ ├── fledge.yaml │ ├── get-extra │ └── action.yml │ ├── git-identity │ └── action.yml │ ├── install │ └── action.yml │ ├── lock.yaml │ ├── matrix-check │ └── action.yml │ ├── pkgdown-build │ └── action.yml │ ├── pkgdown-deploy │ └── action.yml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ ├── rate-limit │ └── action.yml │ ├── revdep.yaml │ ├── roxygenize │ └── action.yml │ ├── style │ └── action.yml │ ├── update-snapshots │ └── action.yml │ └── versions-matrix │ ├── action.R │ └── action.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── env.R ├── eval.R ├── local.R ├── mock.R ├── mockr-package.R ├── test.R ├── utils.R └── with-mock.R ├── README.Rmd ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── man ├── get_mock_env.Rd ├── local_mock.Rd └── mockr-package.Rd ├── mockr.Rproj ├── tests ├── testthat.R └── testthat │ └── test-mock.R └── vignettes ├── .gitignore └── mockr.Rmd /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^README\.Rmd$ 4 | ^README-.*\.png$ 5 | ^codecov\.yml$ 6 | ^_pkgdown\.yml$ 7 | ^cran-comments\.md$ 8 | ^\.github$ 9 | ^CODE_OF_CONDUCT\.md$ 10 | ^CRAN-SUBMISSION$ 11 | ^LICENSE\.md$ 12 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | /pkg.lock 2 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-dev.yaml: -------------------------------------------------------------------------------- 1 | # This workflow calls the GitHub API very frequently. 2 | # Can't be run as part of commits 3 | on: 4 | schedule: 5 | - cron: "0 5 * * *" # 05:00 UTC every day only run on main branch 6 | push: 7 | branches: 8 | - "cran-*" 9 | tags: 10 | - "v*" 11 | 12 | name: rcc dev 13 | 14 | jobs: 15 | matrix: 16 | runs-on: ubuntu-22.04 17 | outputs: 18 | matrix: ${{ steps.set-matrix.outputs.matrix }} 19 | 20 | name: Collect deps 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | - uses: ./.github/workflows/rate-limit 26 | with: 27 | token: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - uses: r-lib/actions/setup-r@v2 30 | 31 | - id: set-matrix 32 | uses: ./.github/workflows/dep-matrix 33 | 34 | check-matrix: 35 | runs-on: ubuntu-22.04 36 | needs: matrix 37 | 38 | name: Check deps 39 | 40 | steps: 41 | - name: Install json2yaml 42 | run: | 43 | sudo npm install -g json2yaml 44 | 45 | - name: Check matrix definition 46 | run: | 47 | matrix='${{ needs.matrix.outputs.matrix }}' 48 | echo $matrix 49 | echo $matrix | jq . 50 | echo $matrix | json2yaml 51 | 52 | R-CMD-check-base: 53 | runs-on: ubuntu-22.04 54 | 55 | name: base 56 | 57 | # Begin custom: services 58 | # End custom: services 59 | 60 | strategy: 61 | fail-fast: false 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | 66 | - uses: ./.github/workflows/custom/before-install 67 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 68 | 69 | - uses: ./.github/workflows/install 70 | with: 71 | cache-version: rcc-dev-base-1 72 | needs: build, check 73 | extra-packages: "any::rcmdcheck any::remotes ." 74 | token: ${{ secrets.GITHUB_TOKEN }} 75 | 76 | - name: Session info 77 | run: | 78 | options(width = 100) 79 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 80 | pkgs <- installed.packages()[, "Package"] 81 | sessioninfo::session_info(pkgs, include_base = TRUE) 82 | shell: Rscript {0} 83 | 84 | - uses: ./.github/workflows/custom/after-install 85 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 86 | 87 | - uses: ./.github/workflows/update-snapshots 88 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 89 | 90 | - uses: ./.github/workflows/check 91 | with: 92 | results: ${{ matrix.package }} 93 | 94 | R-CMD-check-dev: 95 | needs: 96 | - matrix 97 | - R-CMD-check-base 98 | 99 | runs-on: ubuntu-22.04 100 | 101 | name: 'rcc-dev: ${{ matrix.package }}' 102 | 103 | # Begin custom: services 104 | # End custom: services 105 | 106 | strategy: 107 | fail-fast: false 108 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 109 | 110 | steps: 111 | - uses: actions/checkout@v4 112 | 113 | - uses: ./.github/workflows/custom/before-install 114 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 115 | 116 | - uses: ./.github/workflows/install 117 | with: 118 | cache-version: rcc-dev-${{ matrix.package }}-1 119 | needs: build, check 120 | extra-packages: "any::rcmdcheck any::remotes ." 121 | token: ${{ secrets.GITHUB_TOKEN }} 122 | 123 | - name: Install dev version of ${{ matrix.package }} 124 | env: 125 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 126 | run: | 127 | remotes::install_dev("${{ matrix.package }}", "https://cloud.r-project.org", upgrade = "always") 128 | shell: Rscript {0} 129 | 130 | - name: Session info 131 | run: | 132 | options(width = 100) 133 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 134 | pkgs <- installed.packages()[, "Package"] 135 | sessioninfo::session_info(pkgs, include_base = TRUE) 136 | shell: Rscript {0} 137 | 138 | - uses: ./.github/workflows/custom/after-install 139 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 140 | 141 | - uses: ./.github/workflows/update-snapshots 142 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 143 | 144 | - uses: ./.github/workflows/check 145 | with: 146 | results: ${{ matrix.package }} 147 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check-status.yaml: -------------------------------------------------------------------------------- 1 | # Workflow to update the status of a commit for the R-CMD-check workflow 2 | # Necessary because remote PRs cannot update the status of the commit 3 | on: 4 | workflow_run: 5 | workflows: 6 | - rcc 7 | types: 8 | - requested 9 | - completed 10 | 11 | name: rcc-status 12 | 13 | jobs: 14 | rcc-status: 15 | runs-on: ubuntu-24.04 16 | 17 | name: "Update commit status" 18 | 19 | permissions: 20 | contents: read 21 | statuses: write 22 | 23 | steps: 24 | - name: "Update commit status" 25 | # Only run if triggered by rcc workflow 26 | if: github.event.workflow_run.name == 'rcc' 27 | env: 28 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | run: | 30 | set -x 31 | 32 | if [ "${{ github.event.workflow_run.status }}" == "completed" ]; then 33 | if [ "${{ github.event.workflow_run.conclusion }}" == "success" ]; then 34 | state="success" 35 | else 36 | state="failure" 37 | fi 38 | 39 | # Read artifact ID 40 | artifact_id=$(gh api \ 41 | -H "Accept: application/vnd.github+json" \ 42 | -H "X-GitHub-Api-Version: 2022-11-28" \ 43 | repos/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}/artifacts | jq -r '.artifacts[] | select(.name == "rcc-smoke-sha") | .id') 44 | 45 | if [ -n "${artifact_id}" ]; then 46 | # Download artifact 47 | curl -L -o rcc-smoke-sha.zip \ 48 | -H "Accept: application/vnd.github+json" \ 49 | -H "Authorization: Bearer ${GH_TOKEN}" \ 50 | -H "X-GitHub-Api-Version: 2022-11-28" \ 51 | https://api.github.com/repos/${{ github.repository }}/actions/artifacts/${artifact_id}/zip 52 | 53 | # Unzip artifact 54 | unzip rcc-smoke-sha.zip 55 | 56 | # Read artifact 57 | sha=$(cat rcc-smoke-sha.txt) 58 | 59 | # Clean up 60 | rm rcc-smoke-sha.zip rcc-smoke-sha.txt 61 | fi 62 | else 63 | state="pending" 64 | fi 65 | 66 | if [ -z "${sha}" ]; then 67 | sha=${{ github.event.workflow_run.head_sha }} 68 | fi 69 | 70 | html_url=${{ github.event.workflow_run.html_url }} 71 | description=${{ github.event.workflow_run.name }} 72 | 73 | gh api \ 74 | --method POST \ 75 | -H "Accept: application/vnd.github+json" \ 76 | -H "X-GitHub-Api-Version: 2022-11-28" \ 77 | repos/${{ github.repository }}/statuses/${sha} \ 78 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 79 | shell: bash 80 | -------------------------------------------------------------------------------- /.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: 10 | - main 11 | - master 12 | - release 13 | - cran-* 14 | pull_request: 15 | branches: 16 | - main 17 | - master 18 | workflow_dispatch: 19 | inputs: 20 | ref: 21 | description: "Branch, tag, or commit to check out" 22 | required: false 23 | default: "main" 24 | versions-matrix: 25 | description: "Create a matrix of R versions" 26 | type: boolean 27 | default: false 28 | dep-suggests-matrix: 29 | description: "Create a matrix of suggested dependencies" 30 | type: boolean 31 | default: false 32 | merge_group: 33 | types: 34 | - checks_requested 35 | schedule: 36 | - cron: "10 1 * * *" 37 | 38 | concurrency: 39 | group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.ref || github.head_ref || github.sha }}-${{ github.base_ref || '' }} 40 | cancel-in-progress: true 41 | 42 | name: rcc 43 | 44 | jobs: 45 | rcc-smoke: 46 | runs-on: ubuntu-24.04 47 | 48 | outputs: 49 | sha: ${{ steps.commit.outputs.sha }} 50 | versions-matrix: ${{ steps.versions-matrix.outputs.matrix }} 51 | dep-suggests-matrix: ${{ steps.dep-suggests-matrix.outputs.matrix }} 52 | 53 | name: "Smoke test: stock R" 54 | 55 | permissions: 56 | contents: write 57 | statuses: write 58 | pull-requests: write 59 | actions: write 60 | 61 | # Begin custom: services 62 | # End custom: services 63 | 64 | steps: 65 | - uses: actions/checkout@v4 66 | with: 67 | ref: ${{ inputs.ref }} 68 | 69 | - name: Update status for rcc 70 | # FIXME: Wrap into action 71 | if: github.event_name == 'workflow_dispatch' 72 | env: 73 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 74 | run: | 75 | # Check status of this workflow 76 | state="pending" 77 | sha=${{ inputs.ref }} 78 | if [ -z "${sha}" ]; then 79 | sha=${{ github.head_ref }} 80 | fi 81 | if [ -z "${sha}" ]; then 82 | sha=${{ github.sha }} 83 | fi 84 | sha=$(git rev-parse ${sha}) 85 | 86 | html_url=$(gh api \ 87 | -H "Accept: application/vnd.github+json" \ 88 | -H "X-GitHub-Api-Version: 2022-11-28" \ 89 | repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) 90 | 91 | description="${{ github.workflow }} / ${{ github.job }}" 92 | 93 | gh api \ 94 | --method POST \ 95 | -H "Accept: application/vnd.github+json" \ 96 | -H "X-GitHub-Api-Version: 2022-11-28" \ 97 | repos/${{ github.repository }}/statuses/${sha} \ 98 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 99 | shell: bash 100 | 101 | - uses: ./.github/workflows/rate-limit 102 | with: 103 | token: ${{ secrets.GITHUB_TOKEN }} 104 | 105 | - uses: ./.github/workflows/git-identity 106 | 107 | - uses: ./.github/workflows/custom/before-install 108 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 109 | 110 | - uses: ./.github/workflows/install 111 | with: 112 | token: ${{ secrets.GITHUB_TOKEN }} 113 | cache-version: rcc-smoke-2 114 | needs: build, check, website 115 | # Beware of using dev pkgdown here, has brought in dev dependencies in the past 116 | extra-packages: any::rcmdcheck r-lib/roxygen2 any::decor r-lib/styler r-lib/pkgdown deps::. 117 | 118 | - uses: ./.github/workflows/custom/after-install 119 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 120 | 121 | # Must come after the custom after-install workflow 122 | - name: Install package 123 | run: | 124 | _R_SHLIB_STRIP_=true R CMD INSTALL . 125 | shell: bash 126 | 127 | - id: versions-matrix 128 | # Only run for pull requests if the base repo is different from the head repo, not for workflow_dispatch if not requested, always run for other events 129 | if: (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository) && (github.event_name != 'workflow_dispatch' || inputs.versions-matrix) 130 | uses: ./.github/workflows/versions-matrix 131 | 132 | - id: dep-suggests-matrix 133 | # Not for workflow_dispatch if not requested, always run for other events 134 | if: github.event_name != 'workflow_dispatch' || inputs.dep-suggests-matrix 135 | uses: ./.github/workflows/dep-suggests-matrix 136 | 137 | - uses: ./.github/workflows/update-snapshots 138 | with: 139 | base: ${{ inputs.ref || github.head_ref }} 140 | 141 | - uses: ./.github/workflows/style 142 | 143 | - uses: ./.github/workflows/roxygenize 144 | 145 | - name: Remove config files from previous iteration 146 | run: | 147 | rm -f .github/dep-suggests-matrix.json .github/versions-matrix.json 148 | shell: bash 149 | 150 | - id: commit 151 | uses: ./.github/workflows/commit 152 | with: 153 | token: ${{ secrets.GITHUB_TOKEN }} 154 | 155 | - uses: ./.github/workflows/check 156 | with: 157 | results: ${{ runner.os }}-smoke-test 158 | 159 | - uses: ./.github/workflows/pkgdown-build 160 | if: github.event_name != 'push' 161 | 162 | - uses: ./.github/workflows/pkgdown-deploy 163 | if: github.event_name == 'push' 164 | 165 | # Upload sha as artifact 166 | - run: | 167 | echo -n "${{ steps.commit.outputs.sha }}" > rcc-smoke-sha.txt 168 | shell: bash 169 | 170 | - uses: actions/upload-artifact@v4 171 | with: 172 | name: rcc-smoke-sha 173 | path: rcc-smoke-sha.txt 174 | 175 | - name: Update status for rcc 176 | # FIXME: Wrap into action 177 | if: always() && github.event_name == 'workflow_dispatch' 178 | env: 179 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 180 | run: | 181 | # Check status of this workflow 182 | if [ "${{ job.status }}" == "success" ]; then 183 | state="success" 184 | else 185 | state="failure" 186 | fi 187 | 188 | sha=${{ steps.commit.outputs.sha }} 189 | if [ -z "${sha}" ]; then 190 | sha=${{ inputs.ref }} 191 | fi 192 | if [ -z "${sha}" ]; then 193 | sha=${{ github.head_ref }} 194 | fi 195 | if [ -z "${sha}" ]; then 196 | sha=${{ github.sha }} 197 | fi 198 | sha=$(git rev-parse ${sha}) 199 | 200 | html_url=$(gh api \ 201 | -H "Accept: application/vnd.github+json" \ 202 | -H "X-GitHub-Api-Version: 2022-11-28" \ 203 | repos/${{ github.repository }}/actions/runs/${{ github.run_id }} | jq -r .html_url) 204 | 205 | description="${{ github.workflow }} / ${{ github.job }}" 206 | 207 | gh api \ 208 | --method POST \ 209 | -H "Accept: application/vnd.github+json" \ 210 | -H "X-GitHub-Api-Version: 2022-11-28" \ 211 | repos/${{ github.repository }}/statuses/${sha} \ 212 | -f "state=${state}" -f "target_url=${html_url}" -f "description=${description}" -f "context=rcc" 213 | shell: bash 214 | 215 | rcc-smoke-check-matrix: 216 | runs-on: ubuntu-24.04 217 | 218 | name: "Check matrix" 219 | 220 | needs: 221 | - rcc-smoke 222 | 223 | steps: 224 | - uses: actions/checkout@v4 225 | with: 226 | ref: ${{ needs.rcc-smoke.outputs.sha }} 227 | 228 | - uses: ./.github/workflows/matrix-check 229 | with: 230 | matrix: ${{ needs.rcc-smoke.outputs.versions-matrix }} 231 | 232 | - uses: ./.github/workflows/matrix-check 233 | with: 234 | matrix: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix }} 235 | 236 | rcc-full: 237 | needs: 238 | - rcc-smoke 239 | 240 | runs-on: ${{ matrix.os }} 241 | 242 | if: ${{ needs.rcc-smoke.outputs.versions-matrix != '' }} 243 | 244 | name: 'rcc: ${{ matrix.os }} (${{ matrix.r }}) ${{ matrix.desc }}' 245 | 246 | # Begin custom: services 247 | # End custom: services 248 | 249 | strategy: 250 | fail-fast: false 251 | matrix: ${{fromJson(needs.rcc-smoke.outputs.versions-matrix)}} 252 | 253 | steps: 254 | - uses: actions/checkout@v4 255 | with: 256 | ref: ${{ needs.rcc-smoke.outputs.sha }} 257 | 258 | - uses: ./.github/workflows/custom/before-install 259 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 260 | 261 | - uses: ./.github/workflows/install 262 | with: 263 | r-version: ${{ matrix.r }} 264 | cache-version: rcc-full-1 265 | token: ${{ secrets.GITHUB_TOKEN }} 266 | needs: build, check 267 | 268 | - uses: ./.github/workflows/custom/after-install 269 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 270 | 271 | - name: Must allow NOTEs if packages are missing, even with _R_CHECK_FORCE_SUGGESTS_ 272 | run: | 273 | if (Sys.getenv("RCMDCHECK_ERROR_ON") %in% c("", "note")) { 274 | pkgs <- setdiff(desc::desc_get_deps()$package, "R") 275 | installable <- vapply(pkgs, FUN.VALUE = logical(1), requireNamespace, quietly = TRUE) 276 | if (any(!installable)) { 277 | message("Missing packages: ", paste(pkgs[!installable], collapse = ", ")) 278 | cat('RCMDCHECK_ERROR_ON="warning"\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) 279 | } 280 | } 281 | shell: Rscript {0} 282 | 283 | - uses: ./.github/workflows/update-snapshots 284 | if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository 285 | 286 | - uses: ./.github/workflows/check 287 | if: ${{ ! matrix.covr }} 288 | with: 289 | results: ${{ runner.os }}-r${{ matrix.r }} 290 | 291 | - uses: ./.github/workflows/covr 292 | if: ${{ matrix.covr }} 293 | with: 294 | token: ${{ secrets.CODECOV_TOKEN }} 295 | 296 | # The status update is taken care of by R-CMD-check-status.yaml 297 | 298 | rcc-suggests: 299 | needs: 300 | - rcc-smoke 301 | 302 | runs-on: ubuntu-22.04 303 | 304 | if: ${{ needs.rcc-smoke.outputs.dep-suggests-matrix != '' }} 305 | 306 | name: Without ${{ matrix.package }} 307 | 308 | # Begin custom: services 309 | # End custom: services 310 | 311 | strategy: 312 | fail-fast: false 313 | matrix: ${{fromJson(needs.rcc-smoke.outputs.dep-suggests-matrix)}} 314 | 315 | steps: 316 | - uses: actions/checkout@v4 317 | 318 | - uses: ./.github/workflows/custom/before-install 319 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 320 | 321 | - uses: ./.github/workflows/install 322 | with: 323 | cache-version: rcc-dev-${{ matrix.package }}-1 324 | needs: build, check 325 | extra-packages: "any::rcmdcheck any::remotes ." 326 | token: ${{ secrets.GITHUB_TOKEN }} 327 | 328 | - name: Remove ${{ matrix.package }} and all strong dependencies 329 | run: | 330 | pkg <- "${{ matrix.package }}" 331 | pkgs <- tools::package_dependencies(pkg, reverse = TRUE)[[1]] 332 | installed <- rownames(utils::installed.packages()) 333 | to_remove <- c(pkg, intersect(pkgs, installed)) 334 | print(to_remove) 335 | remove.packages(to_remove) 336 | shell: Rscript {0} 337 | 338 | - name: Session info 339 | run: | 340 | options(width = 100) 341 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 342 | pkgs <- installed.packages()[, "Package"] 343 | sessioninfo::session_info(pkgs, include_base = TRUE) 344 | shell: Rscript {0} 345 | 346 | - uses: ./.github/workflows/custom/after-install 347 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 348 | 349 | - name: Must allow NOTEs, even with _R_CHECK_FORCE_SUGGESTS_ 350 | run: | 351 | if (Sys.getenv("RCMDCHECK_ERROR_ON") %in% c("", "note")) { 352 | cat('RCMDCHECK_ERROR_ON="warning"\n', file = Sys.getenv("GITHUB_ENV"), append = TRUE) 353 | } 354 | shell: Rscript {0} 355 | 356 | - name: Check env vars 357 | run: | 358 | print(Sys.getenv('_R_CHECK_FORCE_SUGGESTS_')) 359 | print(Sys.getenv('RCMDCHECK_ERROR_ON')) 360 | shell: Rscript {0} 361 | 362 | - uses: ./.github/workflows/check 363 | with: 364 | results: ${{ matrix.package }} 365 | 366 | # The status update is taken care of by R-CMD-check-status.yaml 367 | -------------------------------------------------------------------------------- /.github/workflows/check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check an R package" 2 | inputs: 3 | results: 4 | description: Slug for check results 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - uses: r-lib/actions/check-r-package@v2 11 | with: 12 | # Fails on R 3.6 on Windows, remove when this job is removed? 13 | args: 'c("--no-manual", "--as-cran", "--no-multiarch")' 14 | error-on: ${{ env.RCMDCHECK_ERROR_ON || '"note"' }} 15 | 16 | - name: Show test output 17 | if: always() 18 | run: | 19 | ## -- Show test output -- 20 | echo "::group::Test output" 21 | find check -name '*.Rout*' -exec head -n 1000000 '{}' \; || true 22 | echo "::endgroup::" 23 | shell: bash 24 | 25 | - name: Upload check results 26 | if: failure() 27 | uses: actions/upload-artifact@main 28 | with: 29 | name: ${{ inputs.results }}-results 30 | path: check 31 | -------------------------------------------------------------------------------- /.github/workflows/commit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to commit changes to the repository" 2 | inputs: 3 | token: 4 | description: "GitHub token" 5 | required: true 6 | outputs: 7 | sha: 8 | description: "SHA of generated commit" 9 | value: ${{ steps.commit.outputs.sha }} 10 | 11 | runs: 12 | using: "composite" 13 | steps: 14 | - name: Commit if changed, create a PR if protected 15 | id: commit 16 | env: 17 | GITHUB_TOKEN: ${{ inputs.token }} 18 | run: | 19 | set -x 20 | if [ -n "$(git status --porcelain)" ]; then 21 | echo "Changed" 22 | protected=${{ github.ref_protected }} 23 | foreign=${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} 24 | if [ "${foreign}" = "true" ]; then 25 | # https://github.com/krlmlr/actions-sync/issues/44 26 | echo "Can't push to foreign branch" 27 | elif [ "${protected}" = "true" ]; then 28 | current_branch=$(git branch --show-current) 29 | new_branch=gha-commit-$(git rev-parse --short HEAD) 30 | git checkout -b ${new_branch} 31 | git add . 32 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 33 | # Force-push, used in only one place 34 | # Alternative: separate branch names for each usage 35 | git push -u origin HEAD -f 36 | 37 | existing_pr=$(gh pr list --state open --base main --head ${new_branch} --json number --jq '.[] | .number') 38 | if [ -n "${existing_pr}" ]; then 39 | echo "Existing PR: ${existing_pr}" 40 | else 41 | gh pr create --base main --head ${new_branch} --title "chore: Auto-update from GitHub Actions" --body "Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 42 | fi 43 | 44 | gh workflow run rcc -f ref=$(git rev-parse HEAD) 45 | gh pr merge --merge --auto 46 | else 47 | git fetch 48 | if [ -n "${GITHUB_HEAD_REF}" ]; then 49 | git add . 50 | git stash save 51 | git switch ${GITHUB_HEAD_REF} 52 | git merge origin/${GITHUB_BASE_REF} --no-edit 53 | git stash pop 54 | fi 55 | git add . 56 | git commit -m "chore: Auto-update from GitHub Actions"$'\n'$'\n'"Run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" 57 | git push -u origin HEAD 58 | 59 | # Only set output if changed 60 | echo sha=$(git rev-parse HEAD) >> $GITHUB_OUTPUT 61 | fi 62 | fi 63 | shell: bash 64 | -------------------------------------------------------------------------------- /.github/workflows/covr/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run covr for an R package" 2 | inputs: 3 | token: 4 | description: codecov token 5 | required: false 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Run coverage check 11 | run: | 12 | if (dir.exists("tests/testthat")) { 13 | cov <- covr::package_coverage( 14 | quiet = FALSE, 15 | clean = FALSE, 16 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 17 | ) 18 | covr::to_cobertura(cov) 19 | } else { 20 | message("No tests found, coverage not tested.") 21 | } 22 | shell: Rscript {0} 23 | 24 | - uses: codecov/codecov-action@v5 25 | with: 26 | # Fail if token is given 27 | fail_ci_if_error: ${{ inputs.token != '' }} 28 | files: ./cobertura.xml 29 | plugins: noop 30 | disable_search: true 31 | token: ${{ inputs.token }} 32 | 33 | - name: Show testthat output 34 | if: always() 35 | run: | 36 | ## -------------------------------------------------------------------- 37 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 38 | shell: bash 39 | 40 | - name: Upload test results 41 | if: failure() 42 | uses: actions/upload-artifact@v4 43 | with: 44 | name: coverage-test-failures 45 | path: ${{ runner.temp }}/package 46 | -------------------------------------------------------------------------------- /.github/workflows/dep-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all dependent packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | # Determine package dependencies 13 | # From remotes 14 | read_dcf <- function(path) { 15 | fields <- colnames(read.dcf(path)) 16 | as.list(read.dcf(path, keep.white = fields)[1, ]) 17 | } 18 | 19 | re_match <- function(text, pattern, perl = TRUE, ...) { 20 | 21 | stopifnot(is.character(pattern), length(pattern) == 1, !is.na(pattern)) 22 | text <- as.character(text) 23 | 24 | match <- regexpr(pattern, text, perl = perl, ...) 25 | 26 | start <- as.vector(match) 27 | length <- attr(match, "match.length") 28 | end <- start + length - 1L 29 | 30 | matchstr <- substring(text, start, end) 31 | matchstr[ start == -1 ] <- NA_character_ 32 | 33 | res <- data.frame( 34 | stringsAsFactors = FALSE, 35 | .text = text, 36 | .match = matchstr 37 | ) 38 | 39 | if (!is.null(attr(match, "capture.start"))) { 40 | 41 | gstart <- attr(match, "capture.start") 42 | glength <- attr(match, "capture.length") 43 | gend <- gstart + glength - 1L 44 | 45 | groupstr <- substring(text, gstart, gend) 46 | groupstr[ gstart == -1 ] <- NA_character_ 47 | dim(groupstr) <- dim(gstart) 48 | 49 | res <- cbind(groupstr, res, stringsAsFactors = FALSE) 50 | } 51 | 52 | names(res) <- c(attr(match, "capture.names"), ".text", ".match") 53 | class(res) <- c("tbl_df", "tbl", class(res)) 54 | res 55 | } 56 | 57 | dev_split_ref <- function(x) { 58 | re_match(x, "^(?[^@#]+)(?[@#].*)?$") 59 | } 60 | 61 | has_dev_dep <- function(package) { 62 | cran_url <- "https://cloud.r-project.org" 63 | 64 | refs <- dev_split_ref(package) 65 | url <- file.path(cran_url, "web", "packages", refs[["pkg"]], "DESCRIPTION") 66 | 67 | f <- tempfile() 68 | on.exit(unlink(f)) 69 | 70 | utils::download.file(url, f) 71 | desc <- read_dcf(f) 72 | 73 | url_fields <- c(desc$URL, desc$BugReports) 74 | 75 | if (length(url_fields) == 0) { 76 | return(FALSE) 77 | } 78 | 79 | pkg_urls <- unlist(strsplit(url_fields, "[[:space:]]*,[[:space:]]*")) 80 | 81 | # Remove trailing "/issues" from the BugReports URL 82 | pkg_urls <- sub("/issues$", "", pkg_urls) 83 | 84 | valid_domains <- c("github[.]com", "gitlab[.]com", "bitbucket[.]org") 85 | 86 | parts <- 87 | re_match(pkg_urls, 88 | sprintf("^https?://(?%s)/(?%s)/(?%s)(?:/(?%s))?", 89 | domain = paste0(valid_domains, collapse = "|"), 90 | username = "[^/]+", 91 | repo = "[^/@#]+", 92 | subdir = "[^/@$ ]+" 93 | ) 94 | )[c("domain", "username", "repo", "subdir")] 95 | 96 | # Remove cases which don't match and duplicates 97 | 98 | parts <- unique(stats::na.omit(parts)) 99 | 100 | nrow(parts) == 1 101 | } 102 | 103 | if (!requireNamespace("desc", quietly = TRUE)) { 104 | install.packages("desc") 105 | } 106 | 107 | deps_df <- desc::desc_get_deps() 108 | deps_df <- deps_df[deps_df$type %in% c("Depends", "Imports", "LinkingTo", "Suggests"), ] 109 | 110 | packages <- sort(deps_df$package) 111 | packages <- intersect(packages, rownames(available.packages())) 112 | 113 | valid_dev_dep <- vapply(packages, has_dev_dep, logical(1)) 114 | 115 | # https://github.com/r-lib/remotes/issues/576 116 | valid_dev_dep[packages %in% c("igraph", "duckdb", "logging")] <- FALSE 117 | 118 | deps <- packages[valid_dev_dep] 119 | if (any(!valid_dev_dep)) { 120 | msg <- paste0( 121 | "Could not determine development repository for packages: ", 122 | paste(packages[!valid_dev_dep], collapse = ", ") 123 | ) 124 | writeLines(paste0("::warning::", msg)) 125 | } 126 | 127 | json <- paste0( 128 | '{"package":[', 129 | paste0('"', deps, '"', collapse = ","), 130 | ']}' 131 | ) 132 | writeLines(json) 133 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 134 | shell: Rscript {0} 135 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.R: -------------------------------------------------------------------------------- 1 | # FIXME: Dynamic lookup by parsing https://svn.r-project.org/R/tags/ 2 | get_deps <- function() { 3 | # Determine package dependencies 4 | if (!requireNamespace("desc", quietly = TRUE)) { 5 | install.packages("desc") 6 | } 7 | 8 | deps_df <- desc::desc_get_deps() 9 | deps_df_optional <- deps_df$package[deps_df$type %in% c("Suggests", "Enhances")] 10 | deps_df_hard <- deps_df$package[deps_df$type %in% c("Depends", "Imports", "LinkingTo")] 11 | deps_df_base <- unlist(tools::standard_package_names(), use.names = FALSE) 12 | 13 | packages <- sort(deps_df_optional) 14 | packages <- intersect(packages, rownames(available.packages())) 15 | 16 | # Too big to fail, or can't be avoided: 17 | off_limits <- c("testthat", "rmarkdown", "rcmdcheck", deps_df_hard, deps_df_base) 18 | off_limits_dep <- unlist(tools::package_dependencies(off_limits, recursive = TRUE, which = "strong")) 19 | setdiff(packages, c(off_limits, off_limits_dep)) 20 | } 21 | 22 | if (Sys.getenv("GITHUB_BASE_REF") != "") { 23 | print(Sys.getenv("GITHUB_BASE_REF")) 24 | system("git fetch origin ${GITHUB_BASE_REF}") 25 | # Use .. to avoid having to fetch the entire history 26 | # https://github.com/krlmlr/actions-sync/issues/45 27 | diff_cmd <- "git diff origin/${GITHUB_BASE_REF}.. -- R/ tests/ | egrep '^[+][^+]' | grep -q ::" 28 | diff_lines <- system(diff_cmd, intern = TRUE) 29 | if (length(diff_lines) > 0) { 30 | writeLines("Changes using :: in R/ or tests/:") 31 | writeLines(diff_lines) 32 | packages <- get_deps() 33 | } else { 34 | writeLines("No changes using :: found in R/ or tests/, not checking without suggested packages") 35 | packages <- character() 36 | } 37 | } else { 38 | writeLines("No GITHUB_BASE_REF, checking without suggested packages") 39 | packages <- get_deps() 40 | } 41 | 42 | if (length(packages) > 0) { 43 | json <- paste0( 44 | '{"package":[', 45 | paste0('"', packages, '"', collapse = ","), 46 | "]}" 47 | ) 48 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 49 | writeLines(json) 50 | } else { 51 | writeLines("No suggested packages found.") 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/dep-suggests-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all suggested packages" 2 | outputs: 3 | matrix: 4 | description: "Generated matrix" 5 | value: ${{ steps.set-matrix.outputs.matrix }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - id: set-matrix 11 | run: | 12 | Rscript ./.github/workflows/dep-suggests-matrix/action.R 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/fledge.yaml: -------------------------------------------------------------------------------- 1 | name: fledge 2 | 3 | on: 4 | # for manual triggers 5 | workflow_dispatch: 6 | inputs: 7 | pr: 8 | description: "Create PR" 9 | required: false 10 | type: boolean 11 | default: false 12 | # daily run 13 | schedule: 14 | - cron: "30 0 * * *" 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || '' }}-${{ github.base_ref || '' }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | check_fork: 22 | runs-on: ubuntu-24.04 23 | outputs: 24 | is_forked: ${{ steps.check.outputs.is_forked }} 25 | steps: 26 | - name: Check if the repo is forked 27 | id: check 28 | env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | is_forked=$(gh api repos/${{ github.repository }} | jq .fork) 32 | echo "is_forked=${is_forked}" >> $GITHUB_OUTPUT 33 | shell: bash 34 | 35 | fledge: 36 | runs-on: ubuntu-24.04 37 | needs: check_fork 38 | if: needs.check_fork.outputs.is_forked == 'false' 39 | permissions: 40 | contents: write 41 | pull-requests: write 42 | actions: write 43 | env: 44 | FLEDGE_GHA_CI: true 45 | steps: 46 | - uses: actions/checkout@v4 47 | with: 48 | fetch-depth: 0 49 | fetch-tags: true 50 | 51 | - name: Configure Git identity 52 | run: | 53 | env | sort 54 | git config --local user.name "$GITHUB_ACTOR" 55 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 56 | shell: bash 57 | 58 | - name: Update apt 59 | run: | 60 | sudo apt-get update 61 | shell: bash 62 | 63 | - uses: r-lib/actions/setup-r@v2 64 | with: 65 | use-public-rspm: true 66 | 67 | - uses: r-lib/actions/setup-r-dependencies@v2 68 | env: 69 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 70 | with: 71 | pak-version: devel 72 | packages: cynkra/fledge 73 | cache-version: fledge-1 74 | 75 | - name: Count rulesets 76 | # Assume that branch is protected if ruleset exists 77 | id: rulesets 78 | env: 79 | GH_TOKEN: ${{ github.token }} 80 | run: | 81 | n_rulesets=$(gh api repos/${{ github.repository }}/rulesets -q length) 82 | echo "count=${n_rulesets}" >> $GITHUB_OUTPUT 83 | shell: bash 84 | 85 | - name: Switch to branch if branch protection is enabled 86 | if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 87 | run: | 88 | git checkout -b fledge 89 | git push -f -u origin HEAD 90 | shell: bash 91 | 92 | - name: Bump version 93 | env: 94 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 95 | run: | 96 | check_default_branch <- ("${{ github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 }}" != "true") 97 | if (fledge::bump_version(which = "dev", no_change_behavior = "noop", check_default_branch = check_default_branch)) { 98 | fledge::finalize_version(push = TRUE) 99 | } 100 | shell: Rscript {0} 101 | 102 | - name: Create and merge PR if branch protection is enabled 103 | if: github.ref_protected == 'true' || inputs.pr == 'true' || steps.rulesets.outputs.count > 0 104 | env: 105 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 106 | run: | 107 | set -ex 108 | if [ -n "$(git diff main --numstat)" ]; then 109 | gh pr create --base main --head fledge --fill-first 110 | gh workflow run rcc -f ref=$(git rev-parse HEAD) 111 | gh pr merge --squash --auto 112 | else 113 | echo "No changes." 114 | fi 115 | shell: bash 116 | 117 | - name: Check release 118 | env: 119 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 120 | run: | 121 | fledge:::release_after_cran_built_binaries() 122 | shell: Rscript {0} 123 | -------------------------------------------------------------------------------- /.github/workflows/get-extra/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to determine extra packages to be installed" 2 | outputs: 3 | packages: 4 | description: "List of extra packages" 5 | value: ${{ steps.get-extra.outputs.packages }} 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Get extra packages 11 | id: get-extra 12 | run: | 13 | set -x 14 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) 15 | echo packages=$packages >> $GITHUB_OUTPUT 16 | shell: bash 17 | -------------------------------------------------------------------------------- /.github/workflows/git-identity/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to set up a Git identity" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Configure Git identity 7 | run: | 8 | env | sort 9 | git config --local user.name "$GITHUB_ACTOR" 10 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 11 | shell: bash 12 | -------------------------------------------------------------------------------- /.github/workflows/install/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to run for installing R packages" 2 | inputs: 3 | token: 4 | description: GitHub token, set to secrets.GITHUB_TOKEN 5 | required: true 6 | r-version: 7 | description: Passed on to r-lib/actions/setup-r@v2 8 | required: false 9 | default: release 10 | install-r: 11 | description: Passed on to r-lib/actions/setup-r@v2 12 | required: false 13 | default: true 14 | needs: 15 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 16 | required: false 17 | default: "" 18 | packages: 19 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 20 | required: false 21 | default: deps::., any::sessioninfo 22 | extra-packages: 23 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 24 | required: false 25 | default: any::rcmdcheck 26 | cache-version: 27 | description: Passed on to r-lib/actions/setup-r-dependencies@v2 28 | required: false 29 | default: 1 30 | 31 | runs: 32 | using: "composite" 33 | steps: 34 | - name: Set environment variables 35 | run: | 36 | echo "R_REMOTES_NO_ERRORS_FROM_WARNINGS=true" | tee -a $GITHUB_ENV 37 | echo "R_KEEP_PKG_SOURCE=yes" | tee -a $GITHUB_ENV 38 | echo "_R_CHECK_SYSTEM_CLOCK_=false" | tee -a $GITHUB_ENV 39 | echo "_R_CHECK_FUTURE_FILE_TIMESTAMPS_=false" | tee -a $GITHUB_ENV 40 | # prevent rgl issues because no X11 display is available 41 | echo "RGL_USE_NULL=true" | tee -a $GITHUB_ENV 42 | # from https://github.com/r-devel/r-dev-web/blob/main/CRAN/QA/Kurt/lib/R/Scripts/check_CRAN_incoming.R 43 | echo "_R_CHECK_CRAN_INCOMING_CHECK_FILE_URIS_=true" | tee -a $GITHUB_ENV 44 | echo "_R_CHECK_CRAN_INCOMING_NOTE_GNU_MAKE_=true" | tee -a $GITHUB_ENV 45 | echo "_R_CHECK_PACKAGE_DEPENDS_IGNORE_MISSING_ENHANCES_=true" | tee -a $GITHUB_ENV 46 | echo "_R_CHECK_CODE_CLASS_IS_STRING_=true" | tee -a $GITHUB_ENV 47 | echo "_R_CHECK_CODOC_VARIABLES_IN_USAGES_=true" | tee -a $GITHUB_ENV 48 | echo "_R_CHECK_CONNECTIONS_LEFT_OPEN_=true" | tee -a $GITHUB_ENV 49 | echo "_R_CHECK_DATALIST_=true" | tee -a $GITHUB_ENV 50 | echo "_R_CHECK_NEWS_IN_PLAIN_TEXT_=true" | tee -a $GITHUB_ENV 51 | echo "_R_CHECK_PACKAGES_USED_CRAN_INCOMING_NOTES_=true" | tee -a $GITHUB_ENV 52 | echo "_R_CHECK_RD_CONTENTS_KEYWORDS_=true" | tee -a $GITHUB_ENV 53 | echo "_R_CHECK_R_DEPENDS_=warn" | tee -a $GITHUB_ENV 54 | echo "_R_CHECK_S3_METHODS_SHOW_POSSIBLE_ISSUES_=true" | tee -a $GITHUB_ENV 55 | echo "_R_CHECK_THINGS_IN_TEMP_DIR_=true" | tee -a $GITHUB_ENV 56 | echo "_R_CHECK_UNDOC_USE_ALL_NAMES_=true" | tee -a $GITHUB_ENV 57 | echo "_R_CHECK_URLS_SHOW_301_STATUS_=true" | tee -a $GITHUB_ENV 58 | echo "_R_CXX_USE_NO_REMAP_=true" | tee -a $GITHUB_ENV 59 | # There is no way to disable recency and frequency checks when the incoming checks are run 60 | # echo "_R_CHECK_CRAN_INCOMING_=true" | tee -a $GITHUB_ENV 61 | echo "_R_CHECK_CRAN_INCOMING_SKIP_LARGE_VERSION_=true" | tee -a $GITHUB_ENV 62 | echo "_R_CHECK_FORCE_SUGGESTS_=false" | tee -a $GITHUB_ENV 63 | shell: bash 64 | 65 | - name: Set environment variables (non-Windows only) 66 | if: runner.os != 'Windows' 67 | run: | 68 | echo "_R_CHECK_BASHISMS_=true" | tee -a $GITHUB_ENV 69 | shell: bash 70 | 71 | - name: Update apt 72 | if: runner.os == 'Linux' 73 | run: | 74 | sudo apt-get update 75 | sudo apt-get install -y aspell 76 | echo "_R_CHECK_CRAN_INCOMING_USE_ASPELL_=true" | tee -a $GITHUB_ENV 77 | shell: bash 78 | 79 | - name: Remove pkg-config@0.29.2 80 | if: runner.os == 'macOS' 81 | run: | 82 | brew uninstall pkg-config@0.29.2 || true 83 | shell: bash 84 | 85 | - uses: r-lib/actions/setup-pandoc@v2 86 | 87 | - uses: r-lib/actions/setup-r@v2 88 | with: 89 | r-version: ${{ inputs.r-version }} 90 | install-r: ${{ inputs.install-r }} 91 | http-user-agent: ${{ matrix.config.http-user-agent }} 92 | use-public-rspm: true 93 | 94 | - id: get-extra 95 | run: | 96 | set -x 97 | packages=$( ( grep Config/gha/extra-packages DESCRIPTION || true ) | cut -d " " -f 2) 98 | echo packages=$packages >> $GITHUB_OUTPUT 99 | shell: bash 100 | 101 | - uses: r-lib/actions/setup-r-dependencies@v2 102 | env: 103 | GITHUB_PAT: ${{ inputs.token }} 104 | with: 105 | pak-version: stable 106 | needs: ${{ inputs.needs }} 107 | packages: ${{ inputs.packages }} 108 | extra-packages: ${{ inputs.extra-packages }} ${{ ( matrix.covr && 'covr xml2' ) || '' }} ${{ steps.get-extra.outputs.packages }} 109 | cache-version: ${{ inputs.cache-version }} 110 | 111 | - name: Add pkg.lock to .gitignore 112 | run: | 113 | set -x 114 | if ! [ -f .github/.gitignore ] || [ -z "$(grep '^/pkg.lock$' .github/.gitignore)" ]; then 115 | echo /pkg.lock >> .github/.gitignore 116 | fi 117 | shell: bash 118 | 119 | - name: Add fake qpdf and checkbashisms 120 | if: runner.os == 'Linux' 121 | run: | 122 | sudo ln -s $(which true) /usr/local/bin/qpdf 123 | sudo ln -s $(which true) /usr/local/bin/checkbashisms 124 | shell: bash 125 | 126 | - name: Install ccache 127 | uses: krlmlr/ccache-action@parallel-dir 128 | with: 129 | max-size: 10G 130 | verbose: 1 131 | save: false 132 | restore: false 133 | 134 | - name: Use ccache for compiling R code, and parallelize 135 | run: | 136 | mkdir -p ~/.R 137 | echo 'CC := ccache $(CC)' >> ~/.R/Makevars 138 | echo 'CXX := ccache $(CXX)' >> ~/.R/Makevars 139 | echo 'CXX11 := ccache $(CXX11)' >> ~/.R/Makevars 140 | echo 'CXX14 := ccache $(CXX14)' >> ~/.R/Makevars 141 | echo 'CXX17 := ccache $(CXX17)' >> ~/.R/Makevars 142 | echo 'MAKEFLAGS = -j2' >> ~/.R/Makevars 143 | cat ~/.R/Makevars 144 | 145 | echo 'CCACHE_SLOPPINESS=locale,time_macros' | tee -a $GITHUB_ENV 146 | 147 | # echo 'CCACHE_DEBUG=true' | tee -a $GITHUB_ENV 148 | # echo "CCACHE_DEBUGDIR=$(dirname $(pwd))/ccache-debug" | tee -a $GITHUB_ENV 149 | # mkdir -p $(dirname $(pwd))/.ccache-debug 150 | 151 | echo 'PKG_BUILD_EXTRA_FLAGS=false' | tee -a $GITHUB_ENV 152 | 153 | # Repair 154 | git rm -rf .ccache || true 155 | rm -rf .ccache 156 | shell: bash 157 | 158 | - name: Show R CMD config --all 159 | run: | 160 | R CMD config --all 161 | shell: bash 162 | -------------------------------------------------------------------------------- /.github/workflows/lock.yaml: -------------------------------------------------------------------------------- 1 | name: "Lock threads" 2 | permissions: 3 | issues: write 4 | pull-requests: write 5 | discussions: write 6 | on: 7 | workflow_dispatch: 8 | schedule: 9 | - cron: "37 2 * * *" 10 | 11 | jobs: 12 | lock: 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: krlmlr/lock-threads@patch-1 16 | with: 17 | github-token: ${{ github.token }} 18 | issue-inactive-days: "365" 19 | issue-lock-reason: "" 20 | issue-comment: > 21 | This old thread has been automatically locked. If you think you have 22 | found something related to this, please open a new issue and link to this 23 | old issue if necessary. 24 | -------------------------------------------------------------------------------- /.github/workflows/matrix-check/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to check a matrix with all R and OS versions, computed with the versions-matrix action" 2 | inputs: 3 | matrix: 4 | description: "Generated matrix" 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Install json2yaml 11 | run: | 12 | sudo npm install -g json2yaml 13 | shell: bash 14 | 15 | - run: | 16 | matrix='${{ inputs.matrix }}' 17 | if [ -n "${matrix}" ]; then 18 | echo $matrix | jq . 19 | echo $matrix | json2yaml 20 | else 21 | echo "No matrix found" 22 | fi 23 | shell: bash 24 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to build a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Build site 7 | run: | 8 | pkgdown::build_site() 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown-deploy/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to deploy a pkgdown website" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Deploy site 7 | uses: nick-fields/retry@v3 8 | with: 9 | timeout_minutes: 15 10 | max_attempts: 10 11 | command: | 12 | R -q -e 'pkgdown::deploy_to_branch(new_process = FALSE)' 13 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Also included in R-CMD-check.yaml, this workflow only listens to pushes to branches 3 | # that start with "docs*" or "cran-*" and does not need to act on pushes to the main branch. 4 | on: 5 | push: 6 | branches: 7 | - "docs*" 8 | - "cran-*" 9 | # The main branch is excluded here, it is handled by the R-CMD-check workflow. 10 | # This workflow is only for handling pushes to designated branches. 11 | workflow_dispatch: 12 | 13 | name: pkgdown 14 | 15 | concurrency: 16 | group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.sha }}-${{ github.base_ref || '' }} 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | pkgdown: 21 | runs-on: ubuntu-24.04 22 | 23 | name: "pkgdown" 24 | 25 | # Begin custom: services 26 | # End custom: services 27 | 28 | steps: 29 | - uses: actions/checkout@v4 30 | 31 | - uses: ./.github/workflows/rate-limit 32 | with: 33 | token: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - uses: ./.github/workflows/git-identity 36 | if: github.event_name == 'push' 37 | 38 | - uses: ./.github/workflows/custom/before-install 39 | if: hashFiles('.github/workflows/custom/before-install/action.yml') != '' 40 | 41 | - uses: ./.github/workflows/install 42 | with: 43 | token: ${{ secrets.GITHUB_TOKEN }} 44 | cache-version: pkgdown-2 45 | needs: website 46 | extra-packages: r-lib/pkgdown local::. 47 | 48 | - uses: ./.github/workflows/custom/after-install 49 | if: hashFiles('.github/workflows/custom/after-install/action.yml') != '' 50 | 51 | - uses: ./.github/workflows/pkgdown-build 52 | if: github.event_name != 'push' 53 | 54 | - uses: ./.github/workflows/pkgdown-deploy 55 | if: github.event_name == 'push' 56 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Commands 5 | jobs: 6 | document: 7 | if: startsWith(github.event.comment.body, '/document') 8 | name: document 9 | # macos is actually better here due to native binary packages 10 | runs-on: macos-latest 11 | env: 12 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: r-lib/actions/pr-fetch@v2 16 | with: 17 | repo-token: ${{ secrets.GITHUB_TOKEN }} 18 | - uses: r-lib/actions/setup-r@v2 19 | - name: Configure Git identity 20 | run: | 21 | env | sort 22 | git config --local user.name "$GITHUB_ACTOR" 23 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 24 | shell: bash 25 | - name: Install dependencies 26 | run: | 27 | install.packages(c("remotes", "roxygen2"), type = "binary") 28 | remotes::install_deps(dependencies = TRUE) 29 | shell: Rscript {0} 30 | - name: Document 31 | run: | 32 | roxygen2::roxygenise() 33 | shell: Rscript {0} 34 | - name: commit 35 | run: | 36 | if [ -n "$(git status --porcelain man/ NAMESPACE)" ]; then 37 | git add man/ NAMESPACE 38 | git commit -m 'Document' 39 | fi 40 | - uses: r-lib/actions/pr-push@v2 41 | with: 42 | repo-token: ${{ secrets.GITHUB_TOKEN }} 43 | style: 44 | if: startsWith(github.event.comment.body, '/style') 45 | name: style 46 | # macos is actually better here due to native binary packages 47 | runs-on: macos-latest 48 | env: 49 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 50 | steps: 51 | - uses: actions/checkout@v4 52 | - uses: r-lib/actions/pr-fetch@v2 53 | with: 54 | repo-token: ${{ secrets.GITHUB_TOKEN }} 55 | - uses: r-lib/actions/setup-r@v2 56 | - name: Configure Git identity 57 | run: | 58 | env | sort 59 | git config --local user.name "$GITHUB_ACTOR" 60 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 61 | shell: bash 62 | - name: Install dependencies 63 | run: | 64 | install.packages(c("styler", "roxygen2"), type = "binary") 65 | shell: Rscript {0} 66 | - name: Style 67 | run: | 68 | styler::style_pkg(strict = FALSE) 69 | shell: Rscript {0} 70 | - name: commit 71 | run: | 72 | if [ -n "$(git status --porcelain '*.R' '*.Rmd')" ]; then 73 | git add '*.R' '*.Rmd' 74 | git commit -m 'Style' 75 | fi 76 | - uses: r-lib/actions/pr-push@v2 77 | with: 78 | repo-token: ${{ secrets.GITHUB_TOKEN }} 79 | merge: 80 | if: startsWith(github.event.comment.body, '/merge') 81 | name: merge 82 | runs-on: ubuntu-22.04 83 | steps: 84 | - name: Create and merge pull request 85 | run: | 86 | set -exo pipefail 87 | PR_DETAILS=$( curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} ) 88 | echo "$PR_DETAILS" | jq . 89 | PR_BASE=$(echo "$PR_DETAILS" | jq -r .base.ref) 90 | PR_HEAD=$(echo "$PR_DETAILS" | jq -r .head.ref) 91 | PR_URL=$(curl -s -X POST --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" --data '{ "head": "'$PR_BASE'", "base": "'$PR_HEAD'", "title": "Merge back PR target branch", "body": "Target: #${{ github.event.issue.number }}" }' https://api.github.com/repos/${{ github.repository }}/pulls | jq -r .url ) 92 | echo $PR_URL 93 | # Merging here won't run CI/CD 94 | # curl -s -X PUT --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" $PR_URL/merge 95 | # A mock job just to ensure we have a successful build status 96 | finish: 97 | runs-on: ubuntu-22.04 98 | steps: 99 | - run: true 100 | -------------------------------------------------------------------------------- /.github/workflows/rate-limit/action.yml: -------------------------------------------------------------------------------- 1 | name: "Check GitHub rate limits" 2 | inputs: 3 | token: # id of input 4 | description: GitHub token, pass secrets.GITHUB_TOKEN 5 | required: true 6 | 7 | runs: 8 | using: "composite" 9 | steps: 10 | - name: Check rate limits 11 | run: | 12 | curl -s --header "authorization: Bearer ${{ inputs.token }}" https://api.github.com/rate_limit 13 | shell: bash 14 | -------------------------------------------------------------------------------- /.github/workflows/revdep.yaml: -------------------------------------------------------------------------------- 1 | # This workflow creates many jobs, run only when a branch is created 2 | on: 3 | push: 4 | branches: 5 | - "revdep*" # never run automatically on main branch 6 | 7 | name: revdep 8 | 9 | jobs: 10 | matrix: 11 | runs-on: ubuntu-22.04 12 | outputs: 13 | matrix: ${{ steps.set-matrix.outputs.matrix }} 14 | 15 | name: Collect revdeps 16 | 17 | env: 18 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 19 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 20 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 21 | # prevent rgl issues because no X11 display is available 22 | RGL_USE_NULL: true 23 | # Begin custom: env vars 24 | # End custom: env vars 25 | 26 | steps: 27 | - name: Check rate limits 28 | run: | 29 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 30 | shell: bash 31 | 32 | - uses: actions/checkout@v4 33 | 34 | # FIXME: Avoid reissuing succesful jobs 35 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#list-jobs-for-a-workflow-run 36 | # https://docs.github.com/en/free-pro-team@latest/rest/reference/actions#workflow-runs 37 | - id: set-matrix 38 | run: | 39 | package <- read.dcf("DESCRIPTION")[, "Package"][[1]] 40 | deps <- tools:::package_dependencies(package, reverse = TRUE, which = c("Depends", "Imports", "LinkingTo", "Suggests"))[[1]] 41 | json <- paste0( 42 | '{"package":[', 43 | paste0('"', deps, '"', collapse = ","), 44 | ']}' 45 | ) 46 | writeLines(json) 47 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 48 | shell: Rscript {0} 49 | 50 | check-matrix: 51 | runs-on: ubuntu-22.04 52 | needs: matrix 53 | steps: 54 | - name: Install json2yaml 55 | run: | 56 | sudo npm install -g json2yaml 57 | 58 | - name: Check matrix definition 59 | run: | 60 | matrix='${{ needs.matrix.outputs.matrix }}' 61 | echo $matrix 62 | echo $matrix | jq . 63 | echo $matrix | json2yaml 64 | 65 | R-CMD-check: 66 | needs: matrix 67 | 68 | runs-on: ubuntu-22.04 69 | 70 | name: 'revdep: ${{ matrix.package }}' 71 | 72 | # Begin custom: services 73 | # End custom: services 74 | 75 | strategy: 76 | fail-fast: false 77 | matrix: ${{fromJson(needs.matrix.outputs.matrix)}} 78 | 79 | env: 80 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 81 | RSPM: https://packagemanager.rstudio.com/cran/__linux__/bionic/latest 82 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 83 | # prevent rgl issues because no X11 display is available 84 | RGL_USE_NULL: true 85 | # Begin custom: env vars 86 | # End custom: env vars 87 | 88 | steps: 89 | - name: Check rate limits 90 | run: | 91 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 92 | shell: bash 93 | 94 | - uses: actions/checkout@v4 95 | 96 | # Begin custom: before install 97 | # End custom: before install 98 | 99 | - name: Use RSPM 100 | run: | 101 | mkdir -p /home/runner/work/_temp/Library 102 | echo 'local({release <- system2("lsb_release", "-sc", stdout = TRUE); options(repos=c(CRAN = paste0("https://packagemanager.rstudio.com/all/__linux__/", release, "/latest")), HTTPUserAgent = sprintf("R/%s R (%s)", getRversion(), paste(getRversion(), R.version$platform, R.version$arch, R.version$os)))}); .libPaths("/home/runner/work/_temp/Library")' | sudo tee /etc/R/Rprofile.site 103 | 104 | - name: Install remotes 105 | run: | 106 | if (!requireNamespace("curl", quietly = TRUE)) install.packages("curl") 107 | if (!requireNamespace("remotes", quietly = TRUE)) install.packages("remotes") 108 | shell: Rscript {0} 109 | 110 | - uses: r-lib/actions/setup-pandoc@v2 111 | 112 | - name: Install system dependencies 113 | if: runner.os == 'Linux' 114 | run: | 115 | sudo apt-get update -y 116 | Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "22.04")); package <- "${{ matrix.package }}"; deps <- tools::package_dependencies(package, which = "Suggests")[[1]]; lapply(c(package, deps), function(x) { writeLines(remotes::system_requirements("ubuntu", "22.04", package = x)) })' | sort | uniq > .github/deps.sh 117 | cat .github/deps.sh 118 | sudo sh < .github/deps.sh 119 | 120 | - name: Install package 121 | run: | 122 | package <- "${{ matrix.package }}" 123 | install.packages(package, dependencies = TRUE) 124 | remotes::install_cran("rcmdcheck") 125 | shell: Rscript {0} 126 | 127 | - name: Session info old 128 | run: | 129 | options(width = 100) 130 | if (!requireNamespace("sessioninfo", quietly = TRUE)) install.packages("sessioninfo") 131 | pkgs <- installed.packages()[, "Package"] 132 | sessioninfo::session_info(pkgs, include_base = TRUE) 133 | shell: Rscript {0} 134 | 135 | # Begin custom: after install 136 | # End custom: after install 137 | 138 | - name: Check old 139 | env: 140 | _R_CHECK_CRAN_INCOMING_: false 141 | _R_CHECK_SYSTEM_CLOCK_: false 142 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 143 | # Avoid downloading binary package from RSPM 144 | run: | 145 | package <- "${{ matrix.package }}" 146 | options(HTTPUserAgent = "gha") 147 | path <- download.packages(package, destdir = ".github")[, 2] 148 | print(path) 149 | 150 | dir <- file.path("revdep", package) 151 | dir.create(dir, showWarnings = FALSE, recursive = TRUE) 152 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 153 | file.rename(file.path(dir, "check"), file.path(dir, "old")) 154 | saveRDS(check, file.path(dir, "old.rds")) 155 | shell: Rscript {0} 156 | 157 | - name: Install local package 158 | run: | 159 | remotes::install_local(".", force = TRUE) 160 | shell: Rscript {0} 161 | 162 | - name: Session info new 163 | run: | 164 | options(width = 100) 165 | pkgs <- installed.packages()[, "Package"] 166 | sessioninfo::session_info(pkgs, include_base = TRUE) 167 | shell: Rscript {0} 168 | 169 | - name: Check new 170 | env: 171 | _R_CHECK_CRAN_INCOMING_: false 172 | _R_CHECK_SYSTEM_CLOCK_: false 173 | _R_CHECK_FUTURE_FILE_TIMESTAMPS_: false 174 | run: | 175 | package <- "${{ matrix.package }}" 176 | path <- dir(".github", pattern = paste0("^", package), full.names = TRUE)[[1]] 177 | print(path) 178 | 179 | dir <- file.path("revdep", package) 180 | check <- rcmdcheck::rcmdcheck(path, args = c("--no-manual", "--as-cran"), error_on = "never", check_dir = file.path(dir, "check")) 181 | file.rename(file.path(dir, "check"), file.path(dir, "new")) 182 | saveRDS(check, file.path(dir, "new.rds")) 183 | shell: Rscript {0} 184 | 185 | - name: Compare 186 | run: | 187 | package <- "${{ matrix.package }}" 188 | dir <- file.path("revdep", package) 189 | old <- readRDS(file.path(dir, "old.rds")) 190 | new <- readRDS(file.path(dir, "new.rds")) 191 | compare <- rcmdcheck::compare_checks(old, new) 192 | compare 193 | cmp <- compare$cmp 194 | if (!identical(cmp[cmp$which == "old", "output"], cmp[cmp$which == "new", "output"])) { 195 | if (!requireNamespace("waldo", quietly = TRUE)) install.packages("waldo") 196 | print(waldo::compare(old, new)) 197 | 198 | stop("Check output differs.") 199 | } 200 | shell: Rscript {0} 201 | 202 | - name: Upload check results 203 | if: failure() 204 | uses: actions/upload-artifact@main 205 | with: 206 | name: ${{ matrix.package }}-results 207 | path: revdep/${{ matrix.package }} 208 | 209 | - name: Check rate limits 210 | if: always() 211 | run: | 212 | curl -s --header "authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" https://api.github.com/rate_limit 213 | shell: bash 214 | -------------------------------------------------------------------------------- /.github/workflows/roxygenize/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create documentation with roxygen2" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Roxygenize 7 | run: | 8 | try(roxygen2::roxygenize()) 9 | shell: Rscript {0} 10 | -------------------------------------------------------------------------------- /.github/workflows/style/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to auto-style a package" 2 | 3 | runs: 4 | using: "composite" 5 | steps: 6 | - name: Check styler options 7 | id: check 8 | run: | 9 | set -x 10 | scope=$( ( grep Config/autostyle/scope DESCRIPTION || true ) | cut -d " " -f 2) 11 | strict=$( ( grep Config/autostyle/strict DESCRIPTION || true ) | cut -d " " -f 2) 12 | rmd=$( ( grep Config/autostyle/rmd DESCRIPTION || true ) | cut -d " " -f 2) 13 | echo scope=$scope >> $GITHUB_OUTPUT 14 | echo strict=$strict >> $GITHUB_OUTPUT 15 | echo rmd=$rmd >> $GITHUB_OUTPUT 16 | shell: bash 17 | 18 | - uses: actions/cache@v4 19 | if: ${{ steps.check.outputs.scope }} 20 | with: 21 | path: | 22 | ~/.cache/R/R.cache 23 | key: ${{ runner.os }}-2-${{ github.run_id }}- 24 | restore-keys: | 25 | ${{ runner.os }}-2- 26 | 27 | - name: Imprint run ID 28 | if: ${{ steps.check.outputs.scope }} 29 | run: | 30 | mkdir -p ~/.cache/R/R.cache/styler 31 | touch ~/.cache/R/R.cache/${{ github.run_id }} 32 | shell: bash 33 | 34 | - name: Show cache 35 | if: ${{ steps.check.outputs.scope }} 36 | run: | 37 | ls -l ~/.cache/R/R.cache 38 | ls -l ~/.cache/R/R.cache/styler 39 | shell: bash 40 | 41 | - name: Enable styler cache 42 | if: ${{ steps.check.outputs.scope }} 43 | run: | 44 | styler::cache_activate(verbose = TRUE) 45 | shell: Rscript {0} 46 | 47 | - name: Run styler 48 | if: ${{ steps.check.outputs.scope }} 49 | run: | 50 | strict <- as.logical("${{ steps.check.outputs.strict }}") 51 | if (is.na(strict)) { 52 | strict <- FALSE 53 | } 54 | rmd <- as.logical("${{ steps.check.outputs.rmd }}") 55 | if (is.na(rmd)) { 56 | rmd <- TRUE 57 | } 58 | styler::style_pkg( 59 | scope = "${{ steps.check.outputs.scope }}", 60 | strict = strict, 61 | filetype = c("R", "Rprofile", if (rmd) c("Rmd", "Rmarkdown", "Rnw", "Qmd")) 62 | ) 63 | shell: Rscript {0} 64 | 65 | - name: Show cache again 66 | if: ${{ steps.check.outputs.scope }} 67 | run: | 68 | ls -l ~/.cache/R/R.cache 69 | ls -l ~/.cache/R/R.cache/styler 70 | gdu -s --inodes ~/.cache/R/R.cache/styler/* || du -s --inodes ~/.cache/R/R.cache/styler/* 71 | shell: bash 72 | -------------------------------------------------------------------------------- /.github/workflows/update-snapshots/action.yml: -------------------------------------------------------------------------------- 1 | name: "Action to create pull requests for updated testthat snapshots" 2 | description: > 3 | This action will run `testthat::test_local()` for tests that seem to use snapshots, 4 | this is determined by reading and grepping the test files. 5 | If the tests are failing, snapshots are updated, and a pull request is opened. 6 | inputs: 7 | base: 8 | description: "The base branch to create the pull request against." 9 | required: false 10 | default: "main" 11 | 12 | runs: 13 | using: "composite" 14 | steps: 15 | - name: Run tests on test files that use snapshots 16 | id: run-tests 17 | run: | 18 | ## -- Run tests on test files that use snapshots -- 19 | rx <- "^test-(.*)[.][rR]$" 20 | files <- dir("tests/testthat", pattern = rx) 21 | has_snapshot <- vapply(files, function(.x) any(grepl("snapshot", readLines(file.path("tests/testthat", .x)), fixed = TRUE)), logical(1)) 22 | if (any(has_snapshot)) { 23 | patterns <- gsub(rx, "^\\1$", files[has_snapshot]) 24 | pattern <- paste0(patterns, collapse = "|") 25 | tryCatch( 26 | { 27 | result <- as.data.frame(testthat::test_local(pattern = pattern, reporter = "silent", stop_on_failure = FALSE)) 28 | print(result) 29 | failures <- result[result$failed + result$warning > 0, ] 30 | if (nrow(failures) > 0) { 31 | writeLines("Snapshot tests failed/warned.") 32 | print(failures[names(failures) != "result"]) 33 | print(failures$result) 34 | testthat::snapshot_accept() 35 | writeLines("changed=true", Sys.getenv("GITHUB_OUTPUT")) 36 | } else { 37 | writeLines("Snapshot tests ran successfully.") 38 | } 39 | }, 40 | error = print 41 | ) 42 | } else { 43 | writeLines("No snapshots found.") 44 | } 45 | shell: Rscript {0} 46 | 47 | - name: Add snapshots to Git 48 | if: ${{ steps.run-tests.outputs.changed }} 49 | run: | 50 | ## -- Add snapshots to Git -- 51 | mkdir -p tests/testthat/_snaps 52 | git add -- tests/testthat/_snaps 53 | shell: bash 54 | 55 | - name: Check changed files 56 | if: ${{ steps.run-tests.outputs.changed }} 57 | id: check-changed 58 | run: | 59 | echo "changed=$(git status --porcelain -- tests/testthat/_snaps | head -n 1)" >> $GITHUB_OUTPUT 60 | shell: bash 61 | 62 | - name: Derive branch name 63 | if: ${{ steps.check-changed.outputs.changed }} 64 | id: matrix-desc 65 | run: | 66 | config=$(echo '${{ toJSON(matrix) }}' | jq -c .) 67 | echo "text=$(echo ${config})" >> $GITHUB_OUTPUT 68 | echo "branch=$(echo ${config} | sed -r 's/[^0-9a-zA-Z]+/-/g;s/^-//;s/-$//')" >> $GITHUB_OUTPUT 69 | shell: bash 70 | 71 | - name: Create pull request 72 | if: ${{ steps.check-changed.outputs.changed }} 73 | id: cpr 74 | uses: peter-evans/create-pull-request@v6 75 | with: 76 | base: ${{ inputs.base }} 77 | branch: snapshot-${{ inputs.base }}-${{ github.job }}-${{ steps.matrix-desc.outputs.branch }} 78 | delete-branch: true 79 | title: "test: Snapshot updates for ${{ github.job }} (${{ steps.matrix-desc.outputs.text }})" 80 | body: "Automated changes by [create-pull-request](https://github.com/peter-evans/create-pull-request) GitHub action${{ github.event.number && format(' for #{0}', github.event.number) || '' }}." 81 | add-paths: | 82 | tests/testthat/_snaps 83 | 84 | - name: Fail if pull request created 85 | if: ${{ steps.cpr.outputs.pull-request-number }} 86 | run: | 87 | false 88 | shell: bash 89 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.R: -------------------------------------------------------------------------------- 1 | # Determine active versions of R to test against 2 | tags <- xml2::read_html("https://svn.r-project.org/R/tags/") 3 | 4 | bullets <- 5 | tags |> 6 | xml2::xml_find_all("//li") |> 7 | xml2::xml_text() 8 | 9 | version_bullets <- grep("^R-([0-9]+-[0-9]+-[0-9]+)/$", bullets, value = TRUE) 10 | versions <- unique(gsub("^R-([0-9]+)-([0-9]+)-[0-9]+/$", "\\1.\\2", version_bullets)) 11 | 12 | r_release <- head(sort(as.package_version(versions), decreasing = TRUE), 5) 13 | 14 | deps <- desc::desc_get_deps() 15 | r_crit <- deps$version[deps$package == "R"] 16 | if (length(r_crit) == 1) { 17 | min_r <- as.package_version(gsub("^>= ([0-9]+[.][0-9]+)(?:.*)$", "\\1", r_crit)) 18 | r_release <- r_release[r_release >= min_r] 19 | } 20 | 21 | r_versions <- c("devel", as.character(r_release)) 22 | 23 | macos <- data.frame(os = "macos-latest", r = r_versions[2:3]) 24 | windows <- data.frame(os = "windows-latest", r = r_versions[1:3]) 25 | linux_devel <- data.frame(os = "ubuntu-22.04", r = r_versions[1], `http-user-agent` = "release", check.names = FALSE) 26 | linux <- data.frame(os = "ubuntu-22.04", r = r_versions[-1]) 27 | covr <- data.frame(os = "ubuntu-22.04", r = r_versions[2], covr = "true", desc = "with covr") 28 | 29 | include_list <- list(macos, windows, linux_devel, linux, covr) 30 | 31 | if (file.exists(".github/versions-matrix.R")) { 32 | custom <- source(".github/versions-matrix.R")$value 33 | if (is.data.frame(custom)) { 34 | custom <- list(custom) 35 | } 36 | include_list <- c(include_list, custom) 37 | } 38 | 39 | print(include_list) 40 | 41 | filter <- read.dcf("DESCRIPTION")[1, ]["Config/gha/filter"] 42 | if (!is.na(filter)) { 43 | filter_expr <- parse(text = filter)[[1]] 44 | subset_fun_expr <- bquote(function(x) subset(x, .(filter_expr))) 45 | subset_fun <- eval(subset_fun_expr) 46 | include_list <- lapply(include_list, subset_fun) 47 | print(include_list) 48 | } 49 | 50 | to_json <- function(x) { 51 | if (nrow(x) == 0) return(character()) 52 | parallel <- vector("list", length(x)) 53 | for (i in seq_along(x)) { 54 | parallel[[i]] <- paste0('"', names(x)[[i]], '":"', x[[i]], '"') 55 | } 56 | paste0("{", do.call(paste, c(parallel, sep = ",")), "}") 57 | } 58 | 59 | configs <- unlist(lapply(include_list, to_json)) 60 | json <- paste0('{"include":[', paste(configs, collapse = ","), "]}") 61 | 62 | if (Sys.getenv("GITHUB_OUTPUT") != "") { 63 | writeLines(paste0("matrix=", json), Sys.getenv("GITHUB_OUTPUT")) 64 | } 65 | writeLines(json) 66 | -------------------------------------------------------------------------------- /.github/workflows/versions-matrix/action.yml: -------------------------------------------------------------------------------- 1 | name: "Actions to compute a matrix with all R and OS versions" 2 | 3 | outputs: 4 | matrix: 5 | description: "Generated matrix" 6 | value: ${{ steps.set-matrix.outputs.matrix }} 7 | 8 | runs: 9 | using: "composite" 10 | steps: 11 | - name: Install json2yaml 12 | run: | 13 | sudo npm install -g json2yaml 14 | shell: bash 15 | 16 | - id: set-matrix 17 | run: | 18 | Rscript ./.github/workflows/versions-matrix/action.R 19 | shell: bash 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | inst/doc 5 | CRAN-SUBMISSION 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards 42 | of acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies 54 | when an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail 56 | address, posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at coc@cynkra.com. 63 | All complaints will be reviewed and investigated promptly and fairly. 64 | 65 | All community leaders are obligated to respect the privacy and security of the 66 | reporter of any incident. 67 | 68 | ## Enforcement Guidelines 69 | 70 | Community leaders will follow these Community Impact Guidelines in determining 71 | the consequences for any action they deem in violation of this Code of Conduct: 72 | 73 | ### 1. Correction 74 | 75 | **Community Impact**: Use of inappropriate language or other behavior deemed 76 | unprofessional or unwelcome in the community. 77 | 78 | **Consequence**: A private, written warning from community leaders, providing 79 | clarity around the nature of the violation and an explanation of why the 80 | behavior was inappropriate. A public apology may be requested. 81 | 82 | ### 2. Warning 83 | 84 | **Community Impact**: A violation through a single incident or series of 85 | actions. 86 | 87 | **Consequence**: A warning with consequences for continued behavior. No 88 | interaction with the people involved, including unsolicited interaction with 89 | those enforcing the Code of Conduct, for a specified period of time. This 90 | includes avoiding interactions in community spaces as well as external channels 91 | like social media. Violating these terms may lead to a temporary or permanent 92 | ban. 93 | 94 | ### 3. Temporary Ban 95 | 96 | **Community Impact**: A serious violation of community standards, including 97 | sustained inappropriate behavior. 98 | 99 | **Consequence**: A temporary ban from any sort of interaction or public 100 | communication with the community for a specified period of time. No public or 101 | private interaction with the people involved, including unsolicited interaction 102 | with those enforcing the Code of Conduct, is allowed during this period. 103 | Violating these terms may lead to a permanent ban. 104 | 105 | ### 4. Permanent Ban 106 | 107 | **Community Impact**: Demonstrating a pattern of violation of community 108 | standards, including sustained inappropriate behavior, harassment of an 109 | individual, or aggression toward or disparagement of classes of individuals. 110 | 111 | **Consequence**: A permanent ban from any sort of public interaction within the 112 | community. 113 | 114 | ## Attribution 115 | 116 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 117 | version 2.0, 118 | available at . 119 | 120 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 121 | enforcement ladder](https://github.com/mozilla/diversity). 122 | 123 | [homepage]: https://www.contributor-covenant.org 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | . Translations are available at . 127 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: mockr 2 | Title: Mocking in R 3 | Version: 0.2.2 4 | Date: 2025-05-01 5 | Authors@R: 6 | person(given = "Kirill", 7 | family = "M\u00fcller", 8 | role = c("aut", "cre"), 9 | email = "kirill@cynkra.com") 10 | Description: Provides a means to mock a package function, i.e., 11 | temporarily substitute it for testing. Designed as a drop-in 12 | replacement for the now deprecated 'testthat::with_mock()' and 13 | 'testthat::local_mock()'. 14 | License: MIT + file LICENSE 15 | URL: https://krlmlr.github.io/mockr/, 16 | https://github.com/krlmlr/mockr 17 | BugReports: https://github.com/krlmlr/mockr/issues 18 | Imports: 19 | rlang, 20 | withr 21 | Suggests: 22 | covr, 23 | fs, 24 | knitr, 25 | pkgload, 26 | rmarkdown, 27 | testthat, 28 | usethis 29 | VignetteBuilder: 30 | knitr 31 | Encoding: UTF-8 32 | Roxygen: list(markdown = TRUE) 33 | RoxygenNote: 7.3.2.9000 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2025 2 | COPYRIGHT HOLDER: mockr authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2025 mockr 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 | export(get_mock_env) 4 | export(local_mock) 5 | export(with_mock) 6 | import(rlang) 7 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # mockr 0.2.2 (2025-05-01) 4 | 5 | ## License 6 | 7 | - Relicense as MIT. 8 | 9 | ## Bug fixes 10 | 11 | - Avoid rendering vignette with usethis missing. 12 | 13 | - More careful querying of functions to be mocked, to avoid errors for `.onLoad()` when testing interactively (#29). 14 | 15 | 16 | # mockr 0.2.1 (2023-01-30) 17 | 18 | ## Bug fixes 19 | 20 | - More careful querying of functions to be mocked, to avoid errors for `.onLoad()` when testing interactively (#29). 21 | 22 | ## Chore 23 | 24 | - Change maintainer e-mail address. 25 | 26 | 27 | # mockr 0.2.0 (2022-04-02) 28 | 29 | ## Breaking changes 30 | 31 | - `with_mock()` now requires braces (so that error locations can be reported more accurately) and supports only one expression (#15). 32 | 33 | ## Features 34 | 35 | - Functions declared in evaluation environments are now also replaced, with a warning (#5). 36 | - New `local_mock()` (#6). 37 | - `with_mock()` works when running a `testthat::test_that()` block interactively (#7). 38 | - New `get_mock_env()` to make the mocking environment explicit (#7). 39 | - Functions that start with a dot can be mocked (#3, #4). 40 | 41 | 42 | ## Documentation 43 | 44 | - Add "Getting started" vignette (#22). 45 | 46 | ## Internal 47 | 48 | - Switch to rlang (#13). 49 | - Switch to GitHub Actions (#10). 50 | 51 | 52 | # mockr 0.1 (2017-04-28) 53 | 54 | Initial CRAN release. 55 | 56 | - `with_mock()` modeled closely after `testthat::with_mock()`, can only mock in the package under test but avoids fiddling with R's internals. 57 | - The `.env` argument now can be a character, but using this argument may lead to different results than `testthat::with_mock()`. 58 | -------------------------------------------------------------------------------- /R/env.R: -------------------------------------------------------------------------------- 1 | #' Get environment for mocking 2 | #' 3 | #' Called by default from [with_mock()] to determine 4 | #' the environment where to update mocked functions. 5 | #' This function is exported to help troubleshooting. 6 | #' 7 | #' This function works differently depending on 8 | #' [testthat::is_testing()]. 9 | #' 10 | #' Outside testthat, `topenv(.parent)` is returned. 11 | #' This was the default for mockr < 0.1.0 and works for many cases. 12 | #' 13 | #' In testthat, `asNamespace("")` for the tested package is returned. 14 | #' The tested package is determined via [testthat::testing_package()]. 15 | #' If this is empty (e.g. if a `test_that()` block is run in interactive mode), 16 | #' this function looks in the search path for packages loaded by 17 | #' [pkgload::load_all()]. 18 | #' 19 | #' @inheritParams with_mock 20 | #' 21 | #' @export 22 | get_mock_env <- function(.parent = parent.frame()) { 23 | top <- topenv(.parent) 24 | 25 | testing <- is_installed("testthat") && testthat::is_testing() 26 | if (!testing) { 27 | return(top) 28 | } 29 | 30 | pkg <- testthat::testing_package() 31 | if (pkg != "") { 32 | return(asNamespace(pkg)) 33 | } 34 | 35 | env <- parent.env(top) 36 | 37 | for (i in 1:1000) { 38 | name <- attr(env, "name") 39 | 40 | if (!is.null(name)) { 41 | if (grepl("^package:", name)) { 42 | ns <- sub("^package:", "", name) 43 | ns_env <- asNamespace(ns) 44 | 45 | if (exists(".__DEVTOOLS__", ns_env)) { 46 | return(ns_env) 47 | } 48 | } 49 | } 50 | 51 | env <- parent.env(env) 52 | if (identical(env, empty_env())) { 53 | break 54 | } 55 | } 56 | 57 | warn("No package loaded, using `topenv()` as mocking environment.") 58 | top 59 | } 60 | 61 | check_dots_env <- function(dots, .parent) { 62 | same <- vlapply(dots, quo_is_env, .parent) 63 | if (!all(same)) { 64 | abort("Can only evaluate expressions in the parent environment.") 65 | } 66 | } 67 | 68 | quo_is_env <- function(quo, env) { 69 | quo_env <- quo_get_env(quo) 70 | identical(quo_env, env) || identical(quo_env, rlang::empty_env()) 71 | } 72 | 73 | create_mock_env <- function(dots, .env, .parent, .defer_env = parent.frame()) { 74 | if (is.character(.env)) .env <- asNamespace(.env) 75 | 76 | new_funcs <- extract_new_funcs(dots, .env) 77 | 78 | # check if functions exist in parent environment, replace those instead 79 | eval_env_funcs <- mget(names(new_funcs), .parent, mode = "function", ifnotfound = list(NULL)) 80 | eval_env_funcs <- eval_env_funcs[!vlapply(eval_env_funcs, is.null)] 81 | 82 | if (length(eval_env_funcs) > 0) { 83 | warn(paste0( 84 | "Replacing functions in evaluation environment: ", 85 | paste0("`", names(eval_env_funcs), "()`", collapse = ", ") 86 | )) 87 | 88 | withr::defer(populate_env(.parent, eval_env_funcs), envir = .defer_env) 89 | populate_env(.parent, new_funcs[names(eval_env_funcs)]) 90 | 91 | new_funcs <- new_funcs[!(names(new_funcs) %in% names(eval_env_funcs))] 92 | } 93 | 94 | mock_env <- create_mock_env_with_old_funcs(new_funcs, .env, .parent) 95 | populate_env(mock_env, new_funcs) 96 | mock_env 97 | } 98 | 99 | extract_new_funcs <- function(dots, .env) { 100 | mocks <- extract_mocks(dots = dots, env = .env) 101 | new_func_names <- lapply(mocks, "[[", "name") 102 | new_funcs <- lapply(mocks, "[[", "new_value") 103 | names(new_funcs) <- new_func_names 104 | new_funcs 105 | } 106 | 107 | create_mock_env_with_old_funcs <- function(new_funcs, .env, .parent) { 108 | # retrieve all functions not mocked 109 | old_funcs <- as.list(.env, all.names = TRUE) 110 | old_funcs <- old_funcs[vlapply(old_funcs, is.function)] 111 | old_funcs <- old_funcs[!(names(old_funcs) %in% names(new_funcs))] 112 | 113 | # Query value visible from .parent to support nesting. 114 | # For some reason, this doesn't always exist (#29). 115 | for (i in seq_along(old_funcs)) { 116 | old_funcs[[i]] <- get0( 117 | names(old_funcs)[[i]], 118 | .parent, 119 | mode = "function", 120 | inherits = TRUE, 121 | ifnotfound = old_funcs[[i]] 122 | ) 123 | } 124 | 125 | # create and populate mocking environment 126 | mock_env <- new.env(parent = parent.env(.parent)) 127 | old_funcs <- lapply(old_funcs, `environment<-`, mock_env) 128 | populate_env(mock_env, old_funcs) 129 | 130 | mock_env 131 | } 132 | 133 | populate_env <- function(env, funcs) { 134 | lapply(names(funcs), function(x) env[[x]] <- funcs[[x]]) 135 | } 136 | -------------------------------------------------------------------------------- /R/eval.R: -------------------------------------------------------------------------------- 1 | evaluate_code <- function(code, .parent) { 2 | # Special treatment of last element, shortcut is important! 3 | if (length(code) == 0L) { 4 | return(invisible(NULL)) 5 | } 6 | 7 | if (length(code) > 1) { 8 | warn("Passing multiple pieces of code to `with_mock()` is discouraged, use a braced expression instead.") 9 | } else if (!is_call(quo_get_expr(code[[1]]), quote(`{`))) { 10 | warn("The code passed to `with_mock()` must be a braced expression to get accurate file-line information for failures.") 11 | } 12 | 13 | # Evaluate the code 14 | for (expression in code[-length(code)]) { 15 | # Can't use eval_tidy(), otherwise changes to variables 16 | # are not visible outside 17 | # https://github.com/r-lib/rlang/issues/1077 18 | eval(quo_get_expr(expression), .parent) 19 | } 20 | eval(quo_get_expr(code[[length(code)]]), .parent) 21 | } 22 | -------------------------------------------------------------------------------- /R/local.R: -------------------------------------------------------------------------------- 1 | local_mock_env <- function(mock_env, .parent, env = parent.frame()) { 2 | old_parent <- parent.env(.parent) 3 | withr::defer(parent.env(.parent) <- old_parent, env) 4 | parent.env(.parent) <- mock_env 5 | 6 | invisible() 7 | } 8 | -------------------------------------------------------------------------------- /R/mock.R: -------------------------------------------------------------------------------- 1 | extract_mocks <- function(dots, env) { 2 | lapply(stats::setNames(nm = names(dots)), 3 | function(qual_name) extract_mock(qual_name, dots[[qual_name]], env)) 4 | } 5 | 6 | extract_mock <- function(qual_name, dot, env) { 7 | name <- extract_mock_name(qual_name) 8 | check_mock(name, env) 9 | mock(name = name, new = eval_tidy(dot)) 10 | } 11 | 12 | extract_mock_name <- function(qual_name) { 13 | pkg_rx <- ".*[^:]" 14 | colons_rx <- "::(?:[:]?)" 15 | name_rx <- ".*" 16 | pkg_and_name_rx <- sprintf("^(?:(%s)%s)?(%s)$", pkg_rx, colons_rx, name_rx) 17 | 18 | pkg_name <- gsub(pkg_and_name_rx, "\\1", qual_name) 19 | if (pkg_name != "") { 20 | warn("`with_mock()` cannot mock functions defined in other packages.") 21 | } 22 | 23 | name <- gsub(pkg_and_name_rx, "\\2", qual_name) 24 | name 25 | } 26 | 27 | check_mock <- function(name, env) { 28 | orig <- mget(name, envir = env, ifnotfound = list(NULL))[[1]] 29 | if (is.null(orig)) { 30 | abort(paste0(name, " not found in environment ", environmentName(env), ".")) 31 | } 32 | if (!is.function(orig)) { 33 | abort(paste0(name, " is not a function in environment ", environmentName(env), ".")) 34 | } 35 | } 36 | 37 | mock <- function(name, new) { 38 | structure(list(name = as.name(name), new_value = new), class = "mock") 39 | } 40 | -------------------------------------------------------------------------------- /R/mockr-package.R: -------------------------------------------------------------------------------- 1 | #' @import rlang 2 | #' @keywords internal 3 | "_PACKAGE" 4 | -------------------------------------------------------------------------------- /R/test.R: -------------------------------------------------------------------------------- 1 | some_symbol <- 42 2 | 3 | mocker <- function() mockee() 4 | 5 | .mocker <- function() .mockee() 6 | 7 | mockee <- function() stop("Not mocking") 8 | 9 | mockee2 <- function() stop("Not mocking (2)") 10 | 11 | mockee3 <- function() mockee() 12 | 13 | .mockee <- function() stop("Not mocking (3)") 14 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | vlapply <- function(X, FUN, ..., USE.NAMES = TRUE) { 2 | vapply(X = X, FUN = FUN, FUN.VALUE = logical(1L), ..., USE.NAMES = USE.NAMES) 3 | } 4 | -------------------------------------------------------------------------------- /R/with-mock.R: -------------------------------------------------------------------------------- 1 | #' Mock functions in a package 2 | #' 3 | #' `local_mock()` temporarily substitutes implementations of package functions. 4 | #' This is useful for testing code that relies on functions that are 5 | #' slow, have unintended side effects or access resources that may not be 6 | #' available when testing. 7 | #' 8 | #' This works by adding a shadow environment as a parent of the environment 9 | #' in which the expressions are evaluated. Everything happens at the R level, 10 | #' but only functions in your own package can be mocked. 11 | #' Otherwise, the implementation is modeled after the original version in the 12 | #' `testthat` package, which is now deprecated. 13 | #' 14 | #' @param ... `[any]`\cr Named arguments redefine mocked functions. 15 | #' An unnamed argument containing code in braces (`{}`) should be provided 16 | #' to `with_mock()`, 17 | #' it will be evaluated after mocking the functions. 18 | #' Use `:=` to mock functions that start with a dot 19 | #' to avoid potential collision with current or future arguments 20 | #' to `with_mock()` or `local_mock()`. 21 | #' Passing more than one unnamed argument to `with_mock()`, 22 | #' or code that is not inside braces, gives a warning. 23 | #' @param .parent `[environment]`\cr the environment in which to evaluate the expressions, 24 | #' defaults to [parent.frame()]. Usually doesn't need to be changed. 25 | #' @param .env `[environment]`\cr the environment in which to patch the functions, 26 | #' defaults to [topenv()]. Usually doesn't need to be changed. 27 | #' @param .defer_env `[environment]`\cr 28 | #' Attach exit handlers to this environment. 29 | #' Typically, this should be either the current environment 30 | #' or a parent frame (accessed through [parent.frame()]). 31 | #' This argument is passed on as `envir` to [withr::defer()]. 32 | #' @return 33 | #' `local_mock()` returns `NULL`, invisibly. 34 | #' @references Suraj Gupta (2012): [How R Searches And Finds Stuff](https://blog.thatbuthow.com/how-r-searches-and-finds-stuff/) 35 | #' @export 36 | #' @examples 37 | #' some_func <- function() stop("oops") 38 | #' some_other_func <- function() some_func() 39 | #' my_env <- environment() 40 | #' 41 | #' tester_func <- function() { 42 | #' # The default for .env works well most of the time, 43 | #' # unfortunately not in examples 44 | #' local_mock(some_func = function() 42, .env = my_env) 45 | #' some_other_func() 46 | #' } 47 | #' try(some_other_func()) 48 | #' tester_func() 49 | local_mock <- function(..., 50 | .parent = parent.frame(), 51 | .env = get_mock_env(.parent), 52 | .defer_env = parent.frame()) { 53 | dots <- enquos(...) 54 | 55 | check_dots_env(dots, .parent) 56 | 57 | if (length(get_code_dots(dots, warn = FALSE)) > 0) { 58 | abort("All arguments to `local_mock()` must be named.") 59 | } 60 | 61 | mock_funs <- get_mock_dots(dots) 62 | if (length(mock_funs) == 0) { 63 | return() 64 | } 65 | 66 | mock_env <- create_mock_env( 67 | mock_funs, .env = .env, .parent = .parent, .defer_env = .defer_env 68 | ) 69 | 70 | local_mock_env(mock_env, .parent, .defer_env) 71 | invisible() 72 | } 73 | 74 | #' @description 75 | #' `with_mock()` substitutes, runs code locally, and restores in one go. 76 | #' @return 77 | #' `with_mock()` returns the result of the last unnamed argument. 78 | #' Visibility is preserved. 79 | #' @rdname local_mock 80 | #' @export 81 | #' @examples 82 | #' 83 | #' tester_func_with <- function() { 84 | #' with_mock( 85 | #' some_func = function() 42, 86 | #' .env = my_env, 87 | #' { 88 | #' some_other_func() 89 | #' } 90 | #' ) 91 | #' } 92 | #' tester_func_with() 93 | with_mock <- function(..., 94 | .parent = parent.frame(), 95 | .env = get_mock_env(.parent)) { 96 | dots <- enquos(...) 97 | 98 | check_dots_env(dots, .parent) 99 | 100 | mock_funs <- get_mock_dots(dots) 101 | mock_env <- create_mock_env(mock_funs, .env = .env, .parent = .parent) 102 | 103 | local_mock_env(mock_env, .parent) 104 | evaluate_code(get_code_dots(dots), .parent) 105 | } 106 | 107 | get_mock_dots <- function(dots) { 108 | mock_qual_names <- names2(dots) 109 | 110 | if (all(mock_qual_names == "")) { 111 | warn("Not mocking anything. Please use named arguments to specify the functions you want to mock.") 112 | list() 113 | } else { 114 | dots[mock_qual_names != ""] 115 | } 116 | } 117 | 118 | get_code_dots <- function(dots, warn = TRUE) { 119 | mock_qual_names <- names2(dots) 120 | 121 | if (all(mock_qual_names != "")) { 122 | if (warn) { 123 | warn("Not evaluating anything. Please use unnamed arguments to specify expressions you want to evaluate.") 124 | } 125 | list() 126 | } else { 127 | dots[mock_qual_names == ""] 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: downlit::readme_document 3 | --- 4 | 5 | 6 | 7 | ```{r setup, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | 15 | pkgload::load_all() 16 | ``` 17 | 18 | # mockr 19 | 20 | 21 | [![rcc](https://github.com/krlmlr/mockr/workflows/rcc/badge.svg)](https://github.com/krlmlr/mockr/actions) 22 | [![CRAN_Status_Badge](https://www.r-pkg.org/badges/version/mockr)](https://cran.r-project.org/package=mockr) 23 | [![Codecov test coverage](https://codecov.io/gh/krlmlr/mockr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/krlmlr/mockr?branch=main) 24 | 25 | 26 | 27 | The goal of mockr is to provide a drop-in replacement for `testthat::local_mock()` and `testthat::with_mock()` which is deprecated in testthat 3.0.0. 28 | The functions `mockr::local_mock()` and `mockr::with_mock()` are modeled closely after the original implementation, but now only allow mocking functions in the package under test. 29 | In contrast to the original implementation, no fiddling 30 | with R's internals is needed, and the implementation plays well with byte-compiled code. 31 | There are some caveats, though: 32 | 33 | 1. Mocking external functions (in other packages) doesn't work anymore. This is by design. 34 | - If you need to mock an external function, write a wrapper. 35 | - If that external function is called by third-party code, you'll need to perhaps mock that third-party code, or look for a different way of implementing this test or organizing your code. 36 | 2. You cannot refer to functions in your package via `your.package::` or `your.package:::` anymore. 37 | - Remove the `your.package:::`, your code and tests should run just fine without that. 38 | 39 | If you encounter other problems, please [file an issue](https://github.com/krlmlr/mockr/issues). 40 | 41 | ## Example 42 | 43 | ```{r example, error = TRUE} 44 | library(mockr) 45 | 46 | access_resource <- function() { 47 | message("Trying to access resource...") 48 | # For some reason we can't access the resource in our tests. 49 | stop("Can't access resource now.") 50 | } 51 | 52 | work_with_resource <- function() { 53 | resource <- access_resource() 54 | message("Fetched resource: ", resource) 55 | invisible(resource) 56 | } 57 | 58 | # Calling this function gives an error 59 | work_with_resource() 60 | 61 | local({ 62 | # Here, we override the function that raises the error 63 | local_mock(access_resource = function() 42) 64 | 65 | # No error raised 66 | work_with_resource() 67 | }) 68 | ``` 69 | 70 | 71 | ## Installation 72 | 73 | Install from CRAN via 74 | 75 | ```r 76 | install.packages("mockr") 77 | ``` 78 | 79 | --- 80 | 81 | ## Code of Conduct 82 | 83 | Please note that the mockr project is released with a [Contributor Code of Conduct](https://krlmlr.github.io/mockr/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # mockr 4 | 5 | 6 | 7 | [![rcc](https://github.com/krlmlr/mockr/workflows/rcc/badge.svg)](https://github.com/krlmlr/mockr/actions) [![CRAN\_Status\_Badge](https://www.r-pkg.org/badges/version/mockr)](https://cran.r-project.org/package=mockr) [![Codecov test coverage](https://codecov.io/gh/krlmlr/mockr/branch/main/graph/badge.svg)](https://app.codecov.io/gh/krlmlr/mockr?branch=main) 8 | 9 | 10 | 11 | The goal of mockr is to provide a drop-in replacement for [`testthat::local_mock()`](https://testthat.r-lib.org/reference/with_mock.html) and [`testthat::with_mock()`](https://testthat.r-lib.org/reference/with_mock.html) which is deprecated in testthat 3.0.0. The functions [`mockr::local_mock()`](https://krlmlr.github.io/mockr/reference/local_mock.html) and [`mockr::with_mock()`](https://krlmlr.github.io/mockr/reference/local_mock.html) are modeled closely after the original implementation, but now only allow mocking functions in the package under test. In contrast to the original implementation, no fiddling with R’s internals is needed, and the implementation plays well with byte-compiled code. There are some caveats, though: 12 | 13 | 1. Mocking external functions (in other packages) doesn’t work anymore. This is by design. 14 | - If you need to mock an external function, write a wrapper. 15 | - If that external function is called by third-party code, you’ll need to perhaps mock that third-party code, or look for a different way of implementing this test or organizing your code. 16 | 2. You cannot refer to functions in your package via `your.package::` or `your.package:::` anymore. 17 | - Remove the `your.package:::`, your code and tests should run just fine without that. 18 | 19 | If you encounter other problems, please [file an issue](https://github.com/krlmlr/mockr/issues). 20 | 21 | ## Example 22 | 23 |
24 | library(mockr)
25 | 
26 | access_resource <- function() {
27 |   message("Trying to access resource...")
28 |   # For some reason we can't access the resource in our tests.
29 |   stop("Can't access resource now.")
30 | }
31 | 
32 | work_with_resource <- function() {
33 |   resource <- access_resource()
34 |   message("Fetched resource: ", resource)
35 |   invisible(resource)
36 | }
37 | 
38 | # Calling this function gives an error
39 | work_with_resource()
40 | #> Trying to access resource...
41 | #> Error in access_resource(): Can't access resource now.
42 | 
43 | local({
44 |   # Here, we override the function that raises the error
45 |   local_mock(access_resource = function() 42)
46 | 
47 |   # No error raised
48 |   work_with_resource()
49 | })
50 | #> Fetched resource: 42
51 | 52 | ## Installation 53 | 54 | Install from CRAN via 55 | 56 |
57 | install.packages("mockr")
58 | 59 | ------------------------------------------------------------------------ 60 | 61 | ## Code of Conduct 62 | 63 | Please note that the mockr project is released with a [Contributor Code of Conduct](https://krlmlr.github.io/mockr/CODE_OF_CONDUCT.html). By contributing to this project, you agree to abide by its terms. 64 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://krlmlr.github.io/mockr 2 | 3 | template: 4 | params: 5 | bootswatch: flatly # https://bootswatch.com/flatly/ 6 | 7 | reference: 8 | - title: Substituting implementations 9 | contents: 10 | - local_mock 11 | - get_mock_env 12 | 13 | authors: 14 | Kirill Müller: 15 | href: https://krlmlr.info 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /cran-comments.md: -------------------------------------------------------------------------------- 1 | mockr 0.2.2 2 | 3 | ## Cran Repository Policy 4 | 5 | - [x] Reviewed CRP last edited 2024-08-27. 6 | -------------------------------------------------------------------------------- /man/get_mock_env.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/env.R 3 | \name{get_mock_env} 4 | \alias{get_mock_env} 5 | \title{Get environment for mocking} 6 | \usage{ 7 | get_mock_env(.parent = parent.frame()) 8 | } 9 | \arguments{ 10 | \item{.parent}{\verb{[environment]}\cr the environment in which to evaluate the expressions, 11 | defaults to \code{\link[=parent.frame]{parent.frame()}}. Usually doesn't need to be changed.} 12 | } 13 | \description{ 14 | Called by default from \code{\link[=with_mock]{with_mock()}} to determine 15 | the environment where to update mocked functions. 16 | This function is exported to help troubleshooting. 17 | } 18 | \details{ 19 | This function works differently depending on 20 | \code{\link[testthat:is_testing]{testthat::is_testing()}}. 21 | 22 | Outside testthat, \code{topenv(.parent)} is returned. 23 | This was the default for mockr < 0.1.0 and works for many cases. 24 | 25 | In testthat, \code{asNamespace("")} for the tested package is returned. 26 | The tested package is determined via \code{\link[testthat:is_testing]{testthat::testing_package()}}. 27 | If this is empty (e.g. if a \code{test_that()} block is run in interactive mode), 28 | this function looks in the search path for packages loaded by 29 | \code{\link[pkgload:load_all]{pkgload::load_all()}}. 30 | } 31 | -------------------------------------------------------------------------------- /man/local_mock.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/with-mock.R 3 | \name{local_mock} 4 | \alias{local_mock} 5 | \alias{with_mock} 6 | \title{Mock functions in a package} 7 | \usage{ 8 | local_mock( 9 | ..., 10 | .parent = parent.frame(), 11 | .env = get_mock_env(.parent), 12 | .defer_env = parent.frame() 13 | ) 14 | 15 | with_mock(..., .parent = parent.frame(), .env = get_mock_env(.parent)) 16 | } 17 | \arguments{ 18 | \item{...}{\verb{[any]}\cr Named arguments redefine mocked functions. 19 | An unnamed argument containing code in braces (\code{{}}) should be provided 20 | to \code{with_mock()}, 21 | it will be evaluated after mocking the functions. 22 | Use \verb{:=} to mock functions that start with a dot 23 | to avoid potential collision with current or future arguments 24 | to \code{with_mock()} or \code{local_mock()}. 25 | Passing more than one unnamed argument to \code{with_mock()}, 26 | or code that is not inside braces, gives a warning.} 27 | 28 | \item{.parent}{\verb{[environment]}\cr the environment in which to evaluate the expressions, 29 | defaults to \code{\link[=parent.frame]{parent.frame()}}. Usually doesn't need to be changed.} 30 | 31 | \item{.env}{\verb{[environment]}\cr the environment in which to patch the functions, 32 | defaults to \code{\link[=topenv]{topenv()}}. Usually doesn't need to be changed.} 33 | 34 | \item{.defer_env}{\verb{[environment]}\cr 35 | Attach exit handlers to this environment. 36 | Typically, this should be either the current environment 37 | or a parent frame (accessed through \code{\link[=parent.frame]{parent.frame()}}). 38 | This argument is passed on as \code{envir} to \code{\link[withr:defer]{withr::defer()}}.} 39 | } 40 | \value{ 41 | \code{local_mock()} returns \code{NULL}, invisibly. 42 | 43 | \code{with_mock()} returns the result of the last unnamed argument. 44 | Visibility is preserved. 45 | } 46 | \description{ 47 | \code{local_mock()} temporarily substitutes implementations of package functions. 48 | This is useful for testing code that relies on functions that are 49 | slow, have unintended side effects or access resources that may not be 50 | available when testing. 51 | 52 | \code{with_mock()} substitutes, runs code locally, and restores in one go. 53 | } 54 | \details{ 55 | This works by adding a shadow environment as a parent of the environment 56 | in which the expressions are evaluated. Everything happens at the R level, 57 | but only functions in your own package can be mocked. 58 | Otherwise, the implementation is modeled after the original version in the 59 | \code{testthat} package, which is now deprecated. 60 | } 61 | \examples{ 62 | some_func <- function() stop("oops") 63 | some_other_func <- function() some_func() 64 | my_env <- environment() 65 | 66 | tester_func <- function() { 67 | # The default for .env works well most of the time, 68 | # unfortunately not in examples 69 | local_mock(some_func = function() 42, .env = my_env) 70 | some_other_func() 71 | } 72 | try(some_other_func()) 73 | tester_func() 74 | 75 | tester_func_with <- function() { 76 | with_mock( 77 | some_func = function() 42, 78 | .env = my_env, 79 | { 80 | some_other_func() 81 | } 82 | ) 83 | } 84 | tester_func_with() 85 | } 86 | \references{ 87 | Suraj Gupta (2012): \href{https://blog.thatbuthow.com/how-r-searches-and-finds-stuff/}{How R Searches And Finds Stuff} 88 | } 89 | -------------------------------------------------------------------------------- /man/mockr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/mockr-package.R 3 | \docType{package} 4 | \name{mockr-package} 5 | \alias{mockr} 6 | \alias{mockr-package} 7 | \title{mockr: Mocking in R} 8 | \description{ 9 | Provides a means to mock a package function, i.e., temporarily substitute it for testing. Designed as a drop-in replacement for the now deprecated 'testthat::with_mock()' and 'testthat::local_mock()'. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://krlmlr.github.io/mockr/} 15 | \item \url{https://github.com/krlmlr/mockr} 16 | \item Report bugs at \url{https://github.com/krlmlr/mockr/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Kirill Müller \email{kirill@cynkra.com} 22 | 23 | } 24 | \keyword{internal} 25 | -------------------------------------------------------------------------------- /mockr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | ProjectId: 4ee541d7-048b-486d-8c0f-60fb0a644644 3 | 4 | RestoreWorkspace: No 5 | SaveWorkspace: No 6 | AlwaysSaveHistory: Default 7 | 8 | EnableCodeIndexing: Yes 9 | UseSpacesForTab: Yes 10 | NumSpacesForTab: 2 11 | Encoding: UTF-8 12 | 13 | RnwWeave: knitr 14 | LaTeX: pdfLaTeX 15 | 16 | AutoAppendNewline: Yes 17 | StripTrailingWhitespace: Yes 18 | 19 | BuildType: Package 20 | PackageUseDevtools: Yes 21 | PackageInstallArgs: --no-multiarch --with-keep.source 22 | PackageRoxygenize: rd,collate,namespace 23 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(mockr) 3 | 4 | test_check("mockr") 5 | -------------------------------------------------------------------------------- /tests/testthat/test-mock.R: -------------------------------------------------------------------------------- 1 | context("Mock") 2 | 3 | test_that("direct mocking via with_mock()", { 4 | with_mock( 5 | mockee = function() 42, 6 | { 7 | expect_equal(mockee(), 42) 8 | } 9 | ) 10 | }) 11 | 12 | test_that("direct mocking via local_mock()", { 13 | local({ 14 | local_mock(mockee = function() 42) 15 | 16 | expect_equal(mockee(), 42) 17 | }) 18 | 19 | expect_error(mockee()) 20 | }) 21 | 22 | test_that("direct and indirect mocking, also with depth", { 23 | local_mock(mockee = function() 42) 24 | 25 | expect_equal(mockee(), 42) 26 | expect_equal(mocker(), 42) 27 | expect_equal(mockee3(), 42) 28 | }) 29 | 30 | test_that("direct and indirect mocking with dot (#4)", { 31 | local_mock(.mockee = function() 42) 32 | 33 | expect_equal(.mockee(), 42) 34 | expect_equal(.mocker(), 42) 35 | }) 36 | 37 | test_that("infinite depth", { 38 | call_mockee <- function() mockee() 39 | 40 | local_mock(mockee = function() 42) 41 | 42 | expect_equal(call_mockee(), 42) 43 | }) 44 | 45 | test_that("mocked function is restored on error", { 46 | expect_error( 47 | with_mock( 48 | mockee = function(x, y, ...) list(equal = TRUE, message = "TRUE"), 49 | { 50 | stop("Simulated error") 51 | } 52 | ), 53 | "Simulated error" 54 | ) 55 | 56 | expect_error(mockee()) 57 | }) 58 | 59 | test_that("non-empty mock with return value", { 60 | expect_true( 61 | with_mock( 62 | mockee = function(x, y, ...) list(equal = TRUE, message = "TRUE"), 63 | { 64 | TRUE 65 | } 66 | ) 67 | ) 68 | }) 69 | 70 | test_that("nested local_mock()", { 71 | local({ 72 | local_mock(mockee = function() mockee2()) 73 | local_mock(mockee2 = function() 42) 74 | expect_equal(mockee(), 42) 75 | }) 76 | 77 | expect_error(mockee()) 78 | expect_error(mockee2()) 79 | }) 80 | 81 | test_that("nested with_mock()", { 82 | with_mock( 83 | mockee = function() mockee2(), 84 | { 85 | with_mock( 86 | mockee2 = function() 42, 87 | { 88 | expect_equal(mockee(), 42) 89 | } 90 | ) 91 | expect_error(mockee2()) 92 | } 93 | ) 94 | expect_error(mockee()) 95 | expect_error(mockee2()) 96 | }) 97 | 98 | test_that("qualified mock names warn", { 99 | expect_warning( 100 | local_mock("mockr::mockee" = function() 42), 101 | "cannot mock functions defined in other packages" 102 | ) 103 | }) 104 | 105 | test_that("can't mock non-existing", { 106 | expect_error(local_mock(..bogus.. = identity), "[.][.]bogus[.][.] not found in environment mockr") 107 | }) 108 | 109 | test_that("can't mock non-function", { 110 | expect_error(local_mock(some_symbol = FALSE), "some_symbol is not a function in environment mockr") 111 | }) 112 | 113 | test_that("empty or no-op mock", { 114 | expect_warning(local_mock(), "Not mocking anything") 115 | 116 | expect_warning(expect_null(with_mock()), 117 | "Not (?:mocking|evaluating) anything", all = TRUE) 118 | expect_warning(expect_true(with_mock(TRUE)), 119 | "Not mocking anything") 120 | expect_warning(expect_null(with_mock(mockee = function() {})), 121 | "Not evaluating anything") 122 | expect_warning(expect_false(withVisible(with_mock(invisible(5)))$visible), 123 | "Not mocking anything") 124 | }) 125 | 126 | test_that("multi local_mock()", { 127 | local_mock( 128 | mockee = function() 1, 129 | mockee2 = function() 2 130 | ) 131 | expect_equal(mockee(), 1) 132 | expect_equal(mockee2(), 2) 133 | expect_equal(mockee3(), 1) 134 | }) 135 | 136 | test_that("multi-mock", { 137 | expect_equal( 138 | with_mock( 139 | mockee = function() 1, 140 | mockee2 = function() 2, 141 | { 142 | mockee() 143 | } 144 | ), 145 | 1 146 | ) 147 | expect_equal( 148 | with_mock( 149 | mockee = function() 1, 150 | mockee2 = function() 2, 151 | { 152 | mockee2() 153 | } 154 | ), 155 | 2 156 | ) 157 | expect_equal( 158 | with_mock( 159 | mockee = function() 1, 160 | mockee2 = function() 2, 161 | { 162 | mockee3() 163 | } 164 | ), 165 | 1 166 | ) 167 | }) 168 | 169 | test_that("un-braced (#15)", { 170 | expect_warning( 171 | expect_true(with_mock(TRUE, mockee = identity)), 172 | "braced expression" 173 | ) 174 | }) 175 | 176 | test_that("multiple return values", { 177 | expect_warning( 178 | expect_true(with_mock(FALSE, TRUE, mockee = identity)), 179 | "multiple" 180 | ) 181 | expect_warning( 182 | expect_equal(with_mock({ 3 }, mockee = identity, 5), { 5 }), 183 | "multiple" 184 | ) 185 | }) 186 | 187 | test_that("can access variables defined in function", { 188 | x <- 5 189 | expect_equal(with_mock({ x }, mockee = identity), 5) 190 | }) 191 | 192 | test_that("changes to variables are preserved between calls and visible outside", { 193 | x <- 1 194 | expect_warning(with_mock( 195 | mockee = identity, 196 | x <- 3, 197 | expect_equal(x, 3) 198 | )) 199 | expect_equal(x, 3) 200 | }) 201 | 202 | test_that("mocks can access local variables", { 203 | value <- TRUE 204 | 205 | with_mock( 206 | { 207 | expect_true(mockee()) 208 | }, 209 | mockee = function() {value} 210 | ) 211 | }) 212 | 213 | test_that("mocks can update local variables", { 214 | value <- TRUE 215 | 216 | with_mock( 217 | { 218 | expect_false(mockee()) 219 | }, 220 | mockee = function() { value <<- FALSE; value } 221 | ) 222 | 223 | expect_false(value) 224 | }) 225 | 226 | test_that("mocks are overridden by local functons", { 227 | mockee <- function() stop("Still not mocking") 228 | 229 | expect_warning(local_mock(mockee = function() TRUE), "evaluation.*mockee") 230 | 231 | expect_true(mockee()) 232 | }) 233 | -------------------------------------------------------------------------------- /vignettes/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.R 3 | -------------------------------------------------------------------------------- /vignettes/mockr.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Mocking with mockr" 3 | output: rmarkdown::html_vignette 4 | vignette: > 5 | %\VignetteIndexEntry{Mocking with mockr} 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 | error = (.Platform$OS.type == "windows"), 15 | eval = requireNamespace("usethis", quietly = TRUE) 16 | ) 17 | 18 | set.seed(20201218) 19 | ``` 20 | 21 | The mockr package helps testing code that relies on functions that are slow, have unintended side effects or access resources that may not be available when testing. 22 | It allows replacing such functions with deterministic [*mock functions*](https://en.wikipedia.org/wiki/Mock_object). 23 | This article gives an overview and introduces a few techniques. 24 | 25 | ```{r setup} 26 | library(mockr) 27 | ``` 28 | 29 | ## General idea 30 | 31 | Let's assume a function `access_resource()` that accesses some resource. 32 | This works in normal circumstances, but not during tests. 33 | A function `work_with_resource()` works with that resource. 34 | How can we test `work_with_resource()` without adding too much logic to the implementation? 35 | 36 | ```{r fun-def} 37 | access_resource <- function() { 38 | message("Trying to access resource...") 39 | # For some reason we can't access the resource in our tests. 40 | stop("Can't access resource now.") 41 | } 42 | 43 | work_with_resource <- function() { 44 | resource <- access_resource() 45 | message("Fetched resource: ", resource) 46 | invisible(resource) 47 | } 48 | ``` 49 | 50 | In our example, calling the worker function gives an error: 51 | 52 | ```{r example-error, error = TRUE} 53 | work_with_resource() 54 | ``` 55 | 56 | We can use `local_mock()` to temporarily replace the implementation of `access_resource()` with one that doesn't throw an error: 57 | 58 | ```{r example-remedy} 59 | access_resource_for_test <- function() { 60 | # We return a value that's good enough for testing 61 | # and can be computed quickly: 62 | 42 63 | } 64 | 65 | local({ 66 | # Here, we override the function that raises the error 67 | local_mock(access_resource = access_resource_for_test) 68 | 69 | work_with_resource() 70 | }) 71 | ``` 72 | 73 | The use of `local()` here is required for technical reasons. 74 | This package is most useful in conjunction with testthat, the remainder of this article will focus on that use case. 75 | 76 | 77 | ## Create demo package 78 | 79 | We create a package called {mocktest} for demonstration. 80 | For this demo, the package is created in a temporary directory. 81 | A real project will live somewhere in your home directory. 82 | The `usethis::create_package()` function sets up a package project ready for development. 83 | The output shows the details of the package created. 84 | 85 | ```{r work-around-desc-bug-1, echo = FALSE} 86 | # Fixed in https://github.com/r-lib/desc/commit/daece0e5816e17a461969489bfdda2d50b4f5fe5, requires desc > 1.4.0 87 | desc_options <- options(cli.num_colors = 1) 88 | ``` 89 | 90 | ```{r create-package} 91 | pkg <- usethis::create_package(file.path(tempdir(), "mocktest")) 92 | ``` 93 | 94 | ```{r work-around-desc-bug-2, echo = FALSE} 95 | options(desc_options) 96 | ``` 97 | 98 | In an interactive RStudio session, a new window opens. 99 | Users of other environments would change the working directory manually. 100 | For this demo, we manually set the active project. 101 | 102 | ```{r set-focus, include = FALSE} 103 | wd <- getwd() 104 | 105 | knitr::knit_hooks$set( 106 | pkg = function(before, options, envir) { 107 | if (before) { 108 | wd <<- setwd(pkg) 109 | } else { 110 | setwd(wd) 111 | } 112 | 113 | invisible() 114 | } 115 | ) 116 | 117 | knitr::opts_chunk$set(pkg = TRUE) 118 | ``` 119 | 120 | ```{r pkg-location} 121 | usethis::proj_set() 122 | ``` 123 | 124 | The infrastructure files and directories that comprise a minimal R package are created: 125 | 126 | ```{r dir-tree} 127 | fs::dir_tree() 128 | ``` 129 | 130 | ## Import function 131 | 132 | We copy the functions from the previous example (under different names) into the package. 133 | Normally we would use a text editor: 134 | 135 | ```{bash import} 136 | cat > R/resource.R <<"EOF" 137 | access_resource_pkg <- function() { 138 | message("Trying to access resource...") 139 | # For some reason we can't access the resource in our tests. 140 | stop("Can't access resource now.") 141 | } 142 | 143 | work_with_resource_pkg <- function() { 144 | resource <- access_resource_pkg() 145 | message("Fetched resource: ", resource) 146 | invisible(resource) 147 | } 148 | EOF 149 | ``` 150 | 151 | Loading the package and calling the function gives the error we have seen before: 152 | 153 | ```{r run-pkg, error = TRUE} 154 | pkgload::load_all() 155 | work_with_resource_pkg() 156 | ``` 157 | 158 | ## Adding test with mock 159 | 160 | We create a test that tests `work_with_resource_pkg()`, mocking `access_resource_pkg()`. 161 | We need to prefix with the package name, because testthat provides its own `testthat::local_mock()` which is now deprecated. 162 | 163 | ```{r test} 164 | usethis::use_testthat() 165 | ``` 166 | 167 | ```{bash create-test} 168 | cat > tests/testthat/test-resource.R <<"EOF" 169 | test_that("Can work with resource", { 170 | mockr::local_mock(access_resource_pkg = function() { 171 | 42 172 | }) 173 | 174 | expect_message( 175 | expect_equal(work_with_resource_pkg(), 42) 176 | ) 177 | }) 178 | EOF 179 | ``` 180 | 181 | The test succeeds: 182 | 183 | ```{r error = TRUE} 184 | testthat::test_local(reporter = "location") 185 | ``` 186 | 187 | 188 | ## Run individual tests 189 | 190 | mockr is aware of testthat and will work even if executing the tests in the current session. 191 | This is especially handy if you want to troubleshoot single tests: 192 | 193 | ```{r test-manually} 194 | test_that("Can work with resource", { 195 | mockr::local_mock(access_resource_pkg = function() { 196 | 42 197 | }) 198 | 199 | expect_message( 200 | expect_equal(work_with_resource_pkg(), 42) 201 | ) 202 | }) 203 | ``` 204 | 205 | 206 | ## Write wrapper functions 207 | 208 | mockr can only mock functions in the current package. 209 | To substitute implementations of functions in other packages, create wrappers in your package and use these wrappers exclusively. 210 | 211 | The example below demonstrates a `d6()` function that is used to get the value of a random die throw. 212 | Instead of using `runif()` directly, this function uses `my_runif()` which wraps `runif()`. 213 | 214 | ```{bash runif} 215 | cat > R/runif.R <<"EOF" 216 | my_runif <- function(...) { 217 | runif(...) 218 | } 219 | 220 | d6 <- function() { 221 | trunc(my_runif(1, 0, 6)) + 1 222 | } 223 | EOF 224 | ``` 225 | 226 | ```{r} 227 | pkgload::load_all() 228 | ``` 229 | 230 | This allows testing the behavior of `d6()`: 231 | 232 | ```{r test-runif} 233 | test_that("d6() works correctly", { 234 | seq <- c(0.32, 5.4, 5, 2.99) 235 | my_runif_mock <- function(...) { 236 | on.exit(seq <<- seq[-1]) 237 | seq[[1]] 238 | } 239 | 240 | mockr::local_mock(my_runif = my_runif_mock) 241 | 242 | expect_equal(d6(), 1) 243 | expect_equal(d6(), 6) 244 | expect_equal(d6(), 6) 245 | expect_equal(d6(), 3) 246 | }) 247 | ``` 248 | 249 | 250 | ## Mock S3 methods 251 | 252 | mockr cannot substitute implementations of S3 methods. 253 | To substitute methods for a class `"foo"`, implement a subclass and add new methods only for that subclass. 254 | The pillar package contains [an example](https://github.com/r-lib/pillar/blob/fd6376eca74e9748ed616c49f906529eaee68df9/tests/testthat/helper-unknown-rows.R) where a class with changed behavior for `dim()` and `head()` for the sole purpose of testing. 255 | --------------------------------------------------------------------------------