├── .Rbuildignore ├── .github ├── .gitignore ├── CODE_OF_CONDUCT.md └── workflows │ ├── R-CMD-check.yaml │ ├── pkgdown.yaml │ ├── pr-commands.yaml │ └── test-coverage.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── LICENSE.note ├── NAMESPACE ├── NEWS.md ├── R ├── cpp11.R ├── embed-svglite.R ├── expect-doppelganger.R ├── svg.R ├── svglite │ ├── SVG.R │ ├── inlineSVG.R │ └── utils.R ├── utils.R └── vdiffr-package.R ├── README.md ├── _pkgdown.yml ├── codecov.yml ├── cran-comments.md ├── inst └── create_glyph_dims.R ├── man ├── expect_doppelganger.Rd ├── figures │ ├── lifecycle-archived.svg │ ├── lifecycle-defunct.svg │ ├── lifecycle-deprecated.svg │ ├── lifecycle-experimental.svg │ ├── lifecycle-maturing.svg │ ├── lifecycle-questioning.svg │ ├── lifecycle-stable.svg │ └── lifecycle-superseded.svg ├── vdiffr-package.Rd └── write_svg.Rd ├── revdep ├── .gitignore ├── README.md ├── cran.md ├── email.yml ├── failures.md └── problems.md ├── src ├── Makevars ├── Makevars.ucrt ├── Makevars.win ├── SvgStream.h ├── algo-it.h ├── compare.cpp ├── cpp11.cpp ├── devSVG.cpp ├── engine_version.cpp ├── engine_version.h ├── glyph_dims.h ├── tinyformat.h ├── utils.h └── vdiffr_types.h ├── tests ├── testthat.R └── testthat │ ├── _snaps │ ├── bar │ │ └── expect-doppelganger │ │ │ └── variant.svg │ ├── expect-doppelganger │ │ ├── base-doppelganger-with-symbol.svg │ │ ├── grid-doppelganger.svg │ │ ├── grob.svg │ │ ├── myplot.svg │ │ ├── myplot2.svg │ │ ├── page-error1.svg │ │ └── page-error2.svg │ ├── foo │ │ └── expect-doppelganger │ │ │ └── variant.svg │ └── ggplot │ │ ├── some-other-title.svg │ │ └── some-title.svg │ ├── helper-mock.R │ ├── helper-vdiffr.R │ ├── mock-pkg │ ├── DESCRIPTION │ └── tests │ │ ├── figs │ │ ├── base-doppelganger-with-symbol.svg │ │ ├── deps.txt │ │ ├── ggplot │ │ │ ├── some-other-title.svg │ │ │ └── some-title.svg │ │ ├── myplot.svg │ │ ├── myplot2.svg │ │ ├── orphaned1.svg │ │ ├── passed-plots │ │ │ └── grid-doppelganger.svg │ │ └── path2 │ │ │ └── orphaned2.svg │ │ ├── testthat.R │ │ └── testthat │ │ ├── _snaps │ │ ├── failed │ │ │ └── myplot.svg │ │ ├── ggplot │ │ │ ├── some-other-title.svg │ │ │ └── some-title.svg │ │ ├── new │ │ │ ├── alt1.svg │ │ │ ├── alt2.svg │ │ │ ├── context1.svg │ │ │ ├── context2.svg │ │ │ ├── new1.svg │ │ │ └── new2.svg │ │ └── passed │ │ │ ├── base-doppelganger-with-symbol.svg │ │ │ ├── grid-doppelganger.svg │ │ │ ├── myplot.svg │ │ │ └── myplot2.svg │ │ ├── test-failed.R │ │ ├── test-ggplot.R │ │ ├── test-new.R │ │ └── test-passed.R │ ├── test-expect-doppelganger.R │ ├── test-ggplot.R │ ├── test-log.R │ └── test-snapshot │ ├── _snaps │ └── snapshot │ │ ├── error-resets-snapshots.svg │ │ └── skip-resets-snapshots.svg │ └── test-snapshot.R ├── tools └── winlibs.R └── vdiffr.Rproj /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^scratch$ 4 | ^\.travis\.yml$ 5 | ^cran-comments\.md$ 6 | ^appveyor\.yml$ 7 | ^revdep 8 | ^vdiffr.Rproj$ 9 | ^CRAN-RELEASE$ 10 | ^_pkgdown\.yml$ 11 | ^docs$ 12 | ^pkgdown$ 13 | ^LICENSE\.md$ 14 | ^codecov\.yml$ 15 | ^\.github$ 16 | ^revdep$ 17 | ^CRAN-SUBMISSION$ 18 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/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, caste, color, religion, or sexual 10 | identity and 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 advances of 31 | 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 address, 35 | 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 of 42 | 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 when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | 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 codeofconduct@posit.co. 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.1, available at 118 | . 119 | 120 | Community Impact Guidelines were inspired by 121 | [Mozilla's code of conduct enforcement ladder][https://github.com/mozilla/inclusion]. 122 | 123 | For answers to common questions about this code of conduct, see the FAQ at 124 | . Translations are available at . 125 | 126 | [homepage]: https://www.contributor-covenant.org 127 | -------------------------------------------------------------------------------- /.github/workflows/R-CMD-check.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | # 4 | # NOTE: This workflow is overkill for most R packages and 5 | # check-standard.yaml is likely a better choice. 6 | # usethis::use_github_action("check-standard") will install it. 7 | on: 8 | push: 9 | branches: [main, master] 10 | pull_request: 11 | branches: [main, master] 12 | 13 | name: R-CMD-check.yaml 14 | 15 | permissions: read-all 16 | 17 | jobs: 18 | R-CMD-check: 19 | runs-on: ${{ matrix.config.os }} 20 | 21 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | config: 27 | - {os: macos-latest, r: 'release'} 28 | 29 | - {os: windows-latest, r: 'release'} 30 | # use 4.0 or 4.1 to check with rtools40's older compiler 31 | - {os: windows-latest, r: 'oldrel-4'} 32 | 33 | - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} 34 | - {os: ubuntu-latest, r: 'release'} 35 | - {os: ubuntu-latest, r: 'oldrel-1'} 36 | - {os: ubuntu-latest, r: 'oldrel-2'} 37 | - {os: ubuntu-latest, r: 'oldrel-3'} 38 | - {os: ubuntu-latest, r: 'oldrel-4'} 39 | 40 | env: 41 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 42 | R_KEEP_PKG_SOURCE: yes 43 | 44 | steps: 45 | - uses: actions/checkout@v4 46 | 47 | - uses: r-lib/actions/setup-pandoc@v2 48 | 49 | - uses: r-lib/actions/setup-r@v2 50 | with: 51 | r-version: ${{ matrix.config.r }} 52 | http-user-agent: ${{ matrix.config.http-user-agent }} 53 | use-public-rspm: true 54 | 55 | - uses: r-hub/actions/setup-r-sysreqs@v1 56 | with: 57 | type: minimal 58 | 59 | - uses: r-lib/actions/setup-r-dependencies@v2 60 | with: 61 | extra-packages: any::rcmdcheck 62 | needs: check 63 | 64 | - uses: r-lib/actions/check-r-package@v2 65 | with: 66 | upload-snapshots: true 67 | build_args: 'c("--no-manual","--compact-vignettes=gs+qpdf")' 68 | -------------------------------------------------------------------------------- /.github/workflows/pkgdown.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | release: 9 | types: [published] 10 | workflow_dispatch: 11 | 12 | name: pkgdown.yaml 13 | 14 | permissions: read-all 15 | 16 | jobs: 17 | pkgdown: 18 | runs-on: ubuntu-latest 19 | # Only restrict concurrency for non-PR jobs 20 | concurrency: 21 | group: pkgdown-${{ github.event_name != 'pull_request' || github.run_id }} 22 | env: 23 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 24 | permissions: 25 | contents: write 26 | steps: 27 | - uses: actions/checkout@v4 28 | 29 | - uses: r-lib/actions/setup-pandoc@v2 30 | 31 | - uses: r-lib/actions/setup-r@v2 32 | with: 33 | use-public-rspm: true 34 | 35 | - uses: r-lib/actions/setup-r-dependencies@v2 36 | with: 37 | extra-packages: any::pkgdown, local::. 38 | needs: website 39 | 40 | - name: Build site 41 | run: pkgdown::build_site_github_pages(new_process = FALSE, install = FALSE) 42 | shell: Rscript {0} 43 | 44 | - name: Deploy to GitHub pages 🚀 45 | if: github.event_name != 'pull_request' 46 | uses: JamesIves/github-pages-deploy-action@v4.5.0 47 | with: 48 | clean: false 49 | branch: gh-pages 50 | folder: docs 51 | -------------------------------------------------------------------------------- /.github/workflows/pr-commands.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | issue_comment: 5 | types: [created] 6 | 7 | name: pr-commands.yaml 8 | 9 | permissions: read-all 10 | 11 | jobs: 12 | document: 13 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/document') }} 14 | name: document 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | permissions: 19 | contents: write 20 | steps: 21 | - uses: actions/checkout@v4 22 | 23 | - uses: r-lib/actions/pr-fetch@v2 24 | with: 25 | repo-token: ${{ secrets.GITHUB_TOKEN }} 26 | 27 | - uses: r-lib/actions/setup-r@v2 28 | with: 29 | use-public-rspm: true 30 | 31 | - uses: r-lib/actions/setup-r-dependencies@v2 32 | with: 33 | extra-packages: any::roxygen2 34 | needs: pr-document 35 | 36 | - name: Document 37 | run: roxygen2::roxygenise() 38 | shell: Rscript {0} 39 | 40 | - name: commit 41 | run: | 42 | git config --local user.name "$GITHUB_ACTOR" 43 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 44 | git add man/\* NAMESPACE 45 | git commit -m 'Document' 46 | 47 | - uses: r-lib/actions/pr-push@v2 48 | with: 49 | repo-token: ${{ secrets.GITHUB_TOKEN }} 50 | 51 | style: 52 | if: ${{ github.event.issue.pull_request && (github.event.comment.author_association == 'MEMBER' || github.event.comment.author_association == 'OWNER') && startsWith(github.event.comment.body, '/style') }} 53 | name: style 54 | runs-on: ubuntu-latest 55 | env: 56 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 57 | permissions: 58 | contents: write 59 | steps: 60 | - uses: actions/checkout@v4 61 | 62 | - uses: r-lib/actions/pr-fetch@v2 63 | with: 64 | repo-token: ${{ secrets.GITHUB_TOKEN }} 65 | 66 | - uses: r-lib/actions/setup-r@v2 67 | 68 | - name: Install dependencies 69 | run: install.packages("styler") 70 | shell: Rscript {0} 71 | 72 | - name: Style 73 | run: styler::style_pkg() 74 | shell: Rscript {0} 75 | 76 | - name: commit 77 | run: | 78 | git config --local user.name "$GITHUB_ACTOR" 79 | git config --local user.email "$GITHUB_ACTOR@users.noreply.github.com" 80 | git add \*.R 81 | git commit -m 'Style' 82 | 83 | - uses: r-lib/actions/pr-push@v2 84 | with: 85 | repo-token: ${{ secrets.GITHUB_TOKEN }} 86 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage.yaml: -------------------------------------------------------------------------------- 1 | # Workflow derived from https://github.com/r-lib/actions/tree/v2/examples 2 | # Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help 3 | on: 4 | push: 5 | branches: [main, master] 6 | pull_request: 7 | branches: [main, master] 8 | 9 | name: test-coverage.yaml 10 | 11 | permissions: read-all 12 | 13 | jobs: 14 | test-coverage: 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - uses: r-lib/actions/setup-r@v2 23 | with: 24 | use-public-rspm: true 25 | 26 | - uses: r-lib/actions/setup-r-dependencies@v2 27 | with: 28 | extra-packages: any::covr, any::xml2 29 | needs: coverage 30 | 31 | - name: Test coverage 32 | run: | 33 | cov <- covr::package_coverage( 34 | quiet = FALSE, 35 | clean = FALSE, 36 | install_path = file.path(normalizePath(Sys.getenv("RUNNER_TEMP"), winslash = "/"), "package") 37 | ) 38 | covr::to_cobertura(cov) 39 | shell: Rscript {0} 40 | 41 | - uses: codecov/codecov-action@v4 42 | with: 43 | fail_ci_if_error: ${{ github.event_name != 'pull_request' && true || false }} 44 | file: ./cobertura.xml 45 | plugin: noop 46 | disable_search: true 47 | token: ${{ secrets.CODECOV_TOKEN }} 48 | 49 | - name: Show testthat output 50 | if: always() 51 | run: | 52 | ## -------------------------------------------------------------------- 53 | find '${{ runner.temp }}/package' -name 'testthat.Rout*' -exec cat '{}' \; || true 54 | shell: bash 55 | 56 | - name: Upload test results 57 | if: failure() 58 | uses: actions/upload-artifact@v4 59 | with: 60 | name: coverage-test-failures 61 | path: ${{ runner.temp }}/package 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | revdep/checks 5 | revdep/library 6 | src-i386/ 7 | src-x64/ 8 | docs/ 9 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: vdiffr 2 | Title: Visual Regression Testing and Graphical Diffing 3 | Version: 1.0.8.9000 4 | Authors@R: c( 5 | person("Lionel", "Henry", , "lionel@posit.co", role = c("cre", "aut")), 6 | person("Thomas Lin", "Pedersen", , "thomas.pedersen@posit.co", role = "aut", 7 | comment = c(ORCID = "0000-0002-5147-4711")), 8 | person("Posit Software, PBC", role = c("cph", "fnd")), 9 | person("T Jake", "Luciani", , "jake@apache.org", role = "aut", 10 | comment = "svglite"), 11 | person("Matthieu", "Decorde", , "matthieu.decorde@ens-lyon.fr", role = "aut", 12 | comment = "svglite"), 13 | person("Vaudor", "Lise", , "lise.vaudor@ens-lyon.fr", role = "aut", 14 | comment = "svglite"), 15 | person("Tony", "Plate", role = "ctb", 16 | comment = "svglite: Early line dashing code"), 17 | person("David", "Gohel", role = "ctb", 18 | comment = "svglite: Line dashing code and raster code"), 19 | person("Yixuan", "Qiu", role = "ctb", 20 | comment = "svglite: Improved styles; polypath implementation"), 21 | person("Håkon", "Malmedal", role = "ctb", 22 | comment = "svglite: Opacity code") 23 | ) 24 | Description: An extension to the 'testthat' package that makes it easy to 25 | add graphical unit tests. It provides a Shiny application to manage 26 | the test cases. 27 | License: MIT + file LICENSE 28 | URL: https://vdiffr.r-lib.org/, https://github.com/r-lib/vdiffr 29 | BugReports: https://github.com/r-lib/vdiffr/issues 30 | Depends: 31 | R (>= 4.0) 32 | Imports: 33 | diffobj, 34 | glue, 35 | grDevices, 36 | htmltools, 37 | lifecycle, 38 | rlang, 39 | testthat (>= 3.0.3), 40 | xml2 (>= 1.0.0) 41 | Suggests: 42 | covr, 43 | decor, 44 | ggplot2 (>= 3.2.0), 45 | roxygen2, 46 | withr 47 | LinkingTo: 48 | cpp11 49 | ByteCompile: true 50 | Config/Needs/website: tidyverse/tidytemplate 51 | Encoding: UTF-8 52 | LazyData: true 53 | Roxygen: list(markdown = TRUE) 54 | RoxygenNote: 7.3.2 55 | SystemRequirements: libpng 56 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2024 2 | COPYRIGHT HOLDER: vdiffr authors 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2024 vdiffr 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 | -------------------------------------------------------------------------------- /LICENSE.note: -------------------------------------------------------------------------------- 1 | vdiffr as a whole is released under MIT. It includes: 2 | 3 | - the jQuery library under the Apache 2.0 licence, 4 | - the js-imagediff library under the MIT licence, 5 | - the TwoFace library under the MIT licence, 6 | - the daff library under the MIT license, 7 | - the diff2html library under the MIT license, 8 | 9 | all of which are compatible with the MIT. 10 | 11 | 12 | js-imagediff 13 | ------------ 14 | 15 | Copyright (c) 2011 Carl Sutherland, Humble Software Development 16 | 17 | Permission is hereby granted, free of charge, to any person 18 | obtaining a copy of this software and associated documentation 19 | files (the "Software"), to deal in the Software without 20 | restriction, including without limitation the rights to use, 21 | copy, modify, merge, publish, distribute, sublicense, and/or sell 22 | copies of the Software, and to permit persons to whom the 23 | Software is furnished to do so, subject to the following 24 | conditions: 25 | 26 | The above copyright notice and this permission notice shall be 27 | included in all copies or substantial portions of the Software. 28 | 29 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 30 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 31 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 32 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 33 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 34 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 35 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 36 | OTHER DEALINGS IN THE SOFTWARE. 37 | 38 | 39 | TwoFace 40 | ------- 41 | 42 | Copyright (c) 2016 David Hong 43 | 44 | Permission is hereby granted, free of charge, to any person 45 | obtaining a copy of this software and associated documentation 46 | files (the "Software"), to deal in the Software without 47 | restriction, including without limitation the rights to use, 48 | copy, modify, merge, publish, distribute, sublicense, and/or sell 49 | copies of the Software, and to permit persons to whom the 50 | Software is furnished to do so, subject to the following 51 | conditions: 52 | 53 | The above copyright notice and this permission notice shall be 54 | included in all copies or substantial portions of the Software. 55 | 56 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 57 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 58 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 59 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 60 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 61 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 62 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 63 | OTHER DEALINGS IN THE SOFTWARE. 64 | 65 | 66 | jQuery 67 | ------ 68 | 69 | Copyright (c) 2016 jQuery Foundation and other contributors, https://jquery.org/ 70 | 71 | Licensed under the Apache License, Version 2.0 (the "License"); 72 | you may not use this file except in compliance with the License. 73 | You may obtain a copy of the License at 74 | 75 | http://www.apache.org/licenses/LICENSE-2.0 76 | 77 | Unless required by applicable law or agreed to in writing, software 78 | distributed under the License is distributed on an "AS IS" BASIS, 79 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 80 | See the License for the specific language governing permissions and 81 | limitations under the License. -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(print_plot,"function") 4 | S3method(print_plot,default) 5 | S3method(print_plot,ggplot) 6 | S3method(print_plot,grob) 7 | S3method(print_plot,recordedplot) 8 | export(expect_doppelganger) 9 | export(write_svg) 10 | import(rlang) 11 | importFrom(glue,glue) 12 | importFrom(lifecycle,deprecated) 13 | useDynLib(vdiffr, .registration = TRUE) 14 | -------------------------------------------------------------------------------- /R/cpp11.R: -------------------------------------------------------------------------------- 1 | # Generated by cpp11: do not edit by hand 2 | 3 | compare_files <- function(expected, test) { 4 | .Call(`_vdiffr_compare_files`, expected, test) 5 | } 6 | 7 | svglite_ <- function(file, bg, width, height, pointsize, standalone, always_valid) { 8 | .Call(`_vdiffr_svglite_`, file, bg, width, height, pointsize, standalone, always_valid) 9 | } 10 | 11 | svgstring_ <- function(env, bg, width, height, pointsize, standalone) { 12 | .Call(`_vdiffr_svgstring_`, env, bg, width, height, pointsize, standalone) 13 | } 14 | 15 | get_svg_content <- function(p) { 16 | .Call(`_vdiffr_get_svg_content`, p) 17 | } 18 | 19 | set_engine_version <- function(version) { 20 | invisible(.Call(`_vdiffr_set_engine_version`, version)) 21 | } 22 | -------------------------------------------------------------------------------- /R/embed-svglite.R: -------------------------------------------------------------------------------- 1 | svglite_path <- function(...) { 2 | file.path("R", "svglite", ...) 3 | } 4 | 5 | for (file in list.files(svglite_path(), pattern = "*.R")) { 6 | source(svglite_path(file), local = TRUE) 7 | } 8 | -------------------------------------------------------------------------------- /R/expect-doppelganger.R: -------------------------------------------------------------------------------- 1 | #' Does a figure look like its expected output? 2 | #' 3 | #' @description 4 | #' 5 | #' `expect_doppelganger()` is a testthat expectation for graphical 6 | #' plots. It generates SVG snapshots that you can review graphically 7 | #' with [testthat::snapshot_review()]. You will find more information 8 | #' about snapshotting in the [testthat snapshots 9 | #' vignette](https://testthat.r-lib.org/articles/snapshotting.html). 10 | #' 11 | #' Note that `expect_doppelgagner()` requires R version 4.1.0. If run 12 | #' on an earlier version of R, it emits a `testthat::skip()` so that you 13 | #' can still run other checks on old versions of R. 14 | #' 15 | #' @param title A brief description of what is being tested in the 16 | #' figure. For instance: "Points and lines overlap". 17 | #' 18 | #' If a ggplot2 figure doesn't have a title already, `title` is 19 | #' applied to the figure with `ggtitle()`. 20 | #' 21 | #' The title is also used as file name for storing SVG (in a 22 | #' sanitzed form, with special characters converted to `"-"`). 23 | #' @param fig A figure to test. This can be a ggplot object, a 24 | #' recordedplot, or more generally any object with a `print` method. 25 | #' 26 | #' If you need to test a plot with non-printable objects (e.g. base 27 | #' plots), `fig` can be a function that generates and prints the 28 | #' plot, e.g. `fig = function() plot(1:3)`. 29 | #' @param path,... `r lifecycle::badge('deprecated')`. 30 | #' @param writer A function that takes the plot, a target SVG file, 31 | #' and an optional plot title. It should transform the plot to SVG 32 | #' in a deterministic way and write it to the target file. See 33 | #' [write_svg()] (the default) for an example. 34 | #' @param cran If `FALSE` (the default), mismatched snapshots only 35 | #' cause a failure when you run tests locally or in your CI (Github 36 | #' Actions or any platform that sets the `CI` environment variable). 37 | #' If `TRUE`, failures may also occur on CRAN machines. 38 | #' 39 | #' Failures are disabled on CRAN by default because testing the 40 | #' appearance of a figure is inherently fragile. Changes in the R 41 | #' graphics engine or in ggplot2 may cause subtle differences in the 42 | #' aspect of a plot, such as a slightly smaller or larger margin. 43 | #' These changes will cause spurious failures because you need to 44 | #' update your snapshots to reflect the upstream changes. 45 | #' 46 | #' It would be distracting for both you and the CRAN maintainers if 47 | #' such changes systematically caused failures on CRAN. This is why 48 | #' snapshot expectations do not fail on CRAN by default and should 49 | #' be treated as a monitoring tool that allows you to quickly check 50 | #' how the appearance of your figures changes over time, and to 51 | #' manually assess whether changes reflect actual problems in your 52 | #' package. 53 | #' 54 | #' Internally, this argument is passed to 55 | #' [testthat::expect_snapshot_file()]. 56 | #' 57 | #' @inheritParams testthat::expect_snapshot_file 58 | #' 59 | #' @section Debugging: 60 | #' It is sometimes difficult to understand the cause of a failure. 61 | #' This usually indicates that the plot is not created 62 | #' deterministically. Potential culprits are: 63 | #' 64 | #' * Some of the plot components depend on random variation. Try 65 | #' setting a seed. 66 | #' 67 | #' * The plot depends on some system library. For instance sf plots 68 | #' depend on libraries like GEOS and GDAL. It might not be possible 69 | #' to test these plots with vdiffr. 70 | #' 71 | #' To help you understand the causes of a failure, vdiffr 72 | #' automatically logs the SVG diff of all failures when run under R 73 | #' CMD check. The log is located in `tests/vdiffr.Rout.fail` and 74 | #' should be displayed on Travis. 75 | #' 76 | #' You can also set the `VDIFFR_LOG_PATH` environment variable with 77 | #' `Sys.setenv()` to unconditionally (also interactively) log failures 78 | #' in the file pointed by the variable. 79 | #' 80 | #' @examples 81 | #' if (FALSE) { # Not run 82 | #' 83 | #' library("ggplot2") 84 | #' 85 | #' test_that("plots have known output", { 86 | #' disp_hist_base <- function() hist(mtcars$disp) 87 | #' expect_doppelganger("disp-histogram-base", disp_hist_base) 88 | #' 89 | #' disp_hist_ggplot <- ggplot(mtcars, aes(disp)) + geom_histogram() 90 | #' expect_doppelganger("disp-histogram-ggplot", disp_hist_ggplot) 91 | #' }) 92 | #' 93 | #' } 94 | #' @export 95 | expect_doppelganger <- function(title, 96 | fig, 97 | path = deprecated(), 98 | ..., 99 | writer = write_svg, 100 | cran = FALSE, 101 | variant = NULL) { 102 | testthat::local_edition(3) 103 | 104 | fig_name <- str_standardise(title) 105 | file <- paste0(fig_name, ".svg") 106 | 107 | # Announce snapshot file before touching `fig` in case evaluation 108 | # causes an error. This allows testthat to restore the files 109 | # (see r-lib/testthat#1393). 110 | testthat::announce_snapshot_file(name = file) 111 | 112 | testcase <- make_testcase_file(fig_name) 113 | writer(fig, testcase, title) 114 | 115 | if (!missing(...)) { 116 | lifecycle::deprecate_soft( 117 | "1.0.0", 118 | "vdiffr::expect_doppelganger(... = )", 119 | ) 120 | } 121 | if (lifecycle::is_present(path)) { 122 | lifecycle::deprecate_soft( 123 | "1.0.0", 124 | "vdiffr::expect_doppelganger(path = )", 125 | ) 126 | } 127 | 128 | if (is_graphics_engine_stale()) { 129 | testthat::skip(paste_line( 130 | "The R graphics engine is too old.", 131 | "Please update to R 4.1.0 and regenerate the vdiffr snapshots." 132 | )) 133 | } 134 | 135 | withCallingHandlers( 136 | testthat::expect_snapshot_file( 137 | testcase, 138 | name = file, 139 | cran = cran, 140 | variant = variant, 141 | compare = testthat::compare_file_text 142 | ), 143 | expectation_failure = function(cnd) { 144 | if (is_snapshot_stale(title, testcase)) { 145 | warn(paste_line( 146 | "SVG snapshot generated under a different vdiffr version.", 147 | "i" = "Please update your snapshots." 148 | )) 149 | } 150 | 151 | if (!is_null(snapshotter <- get_snapshotter())) { 152 | path_old <- snapshot_path(snapshotter, file) 153 | path_new <- snapshot_path(snapshotter, paste0(fig_name, ".new.svg")) 154 | 155 | if (all(file.exists(path_old, path_new))) { 156 | push_log(fig_name, path_old, path_new) 157 | } 158 | } 159 | } 160 | ) 161 | } 162 | 163 | # From testthat 164 | get_snapshotter <- function() { 165 | x <- getOption("testthat.snapshotter") 166 | if (is.null(x)) { 167 | return() 168 | } 169 | 170 | if (!x$is_active()) { 171 | return() 172 | } 173 | 174 | x 175 | } 176 | snapshot_path <- function(snapshotter, file) { 177 | file.path(snapshotter$snap_dir, snapshotter$file, file) 178 | } 179 | 180 | is_graphics_engine_stale <- function() { 181 | getRversion() < "4.1.0" 182 | } 183 | 184 | str_standardise <- function(s, sep = "-") { 185 | stopifnot(is_scalar_character(s)) 186 | s <- gsub("[^a-z0-9]", sep, tolower(s)) 187 | s <- gsub(paste0(sep, sep, "+"), sep, s) 188 | s <- gsub(paste0("^", sep, "|", sep, "$"), "", s) 189 | s 190 | } 191 | 192 | is_snapshot_stale <- function(title, testcase) { 193 | if (is_null(snapshotter <- get_snapshotter())) { 194 | return(FALSE) 195 | } 196 | 197 | file <- paste0(str_standardise(title), ".svg") 198 | path <- snapshot_path(snapshotter, file) 199 | 200 | if (!file.exists(path)) { 201 | return(FALSE) 202 | } 203 | 204 | lines <- readLines(path) 205 | 206 | match <- regexec( 207 | "data-engine-version='([0-9.]+)'", 208 | lines 209 | ) 210 | match <- Filter(length, regmatches(lines, match)) 211 | 212 | # Old vdiffr snapshot that doesn't embed a version 213 | if (!length(match)) { 214 | return(TRUE) 215 | } 216 | 217 | if (length(match) > 1) { 218 | abort("Found multiple vdiffr engine versions in SVG snapshot.") 219 | } 220 | 221 | snapshot_version <- match[[1]][[2]] 222 | svg_engine_ver() != snapshot_version 223 | } 224 | -------------------------------------------------------------------------------- /R/svg.R: -------------------------------------------------------------------------------- 1 | make_testcase_file <- function(fig_name) { 2 | file <- tempfile(fig_name, fileext = ".svg") 3 | structure(file, class = "vdiffr_testcase") 4 | } 5 | 6 | #' Default SVG writer 7 | #' 8 | #' This is the default SVG writer for vdiffr test cases. It uses 9 | #' embedded versions of [svglite](https://svglite.r-lib.org), 10 | #' [harfbuzz](https://harfbuzz.github.io/), and the Liberation and 11 | #' Symbola fonts in order to create deterministic SVGs. 12 | #' 13 | #' @param plot A plot object to convert to SVG. Can be a ggplot2 object, 14 | #' a [recorded plot][grDevices::recordPlot], or any object with a 15 | #' [print()][base::print] method. 16 | #' @param file The file to write the SVG to. 17 | #' @param title An optional title for the test case. 18 | #' 19 | #' @export 20 | write_svg <- function(plot, file, title = "") { 21 | svglite(file) 22 | on.exit(grDevices::dev.off()) 23 | print_plot(plot, title) 24 | } 25 | 26 | print_plot <- function(p, title = "") { 27 | UseMethod("print_plot") 28 | } 29 | 30 | #' @export 31 | print_plot.default <- function(p, title = "") { 32 | print(p) 33 | } 34 | 35 | #' @export 36 | print_plot.ggplot <- function(p, title = "") { 37 | if (title != "" && !"title" %in% names(p$labels)) { 38 | p <- p + ggplot2::ggtitle(title) 39 | } 40 | if (!length(p$theme)) { 41 | p <- p + ggplot2::theme_test() 42 | } 43 | print(p) 44 | } 45 | 46 | #' @export 47 | print_plot.grob <- function(p, title) { 48 | grid::grid.draw(p) 49 | } 50 | 51 | #' @export 52 | print_plot.recordedplot <- function(p, title) { 53 | grDevices::replayPlot(p) 54 | } 55 | 56 | #' @export 57 | print_plot.function <- function(p, title) { 58 | p() 59 | } 60 | -------------------------------------------------------------------------------- /R/svglite/SVG.R: -------------------------------------------------------------------------------- 1 | #' An SVG Graphics Driver 2 | #' 3 | #' This function produces graphics compliant to the current w3 svg XML 4 | #' standard. The driver output is currently NOT specifying a DOCTYPE DTD. 5 | #' 6 | #' svglite provides two ways of controlling fonts: system fonts 7 | #' aliases and user fonts aliases. Supplying a font alias has two 8 | #' effects. First it determines the \code{font-family} property of all 9 | #' text anchors in the SVG output. Secondly, the font is used to 10 | #' determine the dimensions of graphical elements and has thus an 11 | #' influence on the overall aspect of the plots. This means that for 12 | #' optimal display, the font must be available on both the computer 13 | #' used to create the svg, and the computer used to render the 14 | #' svg. See the \code{fonts} vignette for more information. 15 | #' 16 | #' @param filename The file where output will appear. 17 | #' @param height,width Height and width in inches. 18 | #' @param bg Default background color for the plot (defaults to "white"). 19 | #' @param pointsize Default point size. 20 | #' @param standalone Produce a standalone svg file? If \code{FALSE}, omits 21 | #' xml header and default namespace. 22 | #' @param always_valid Should the svgfile be a valid svg file while it is being 23 | #' written to? Setting this to `TRUE` will incur a considerable performance 24 | #' hit (>50% additional rendering time) so this should only be set to `TRUE` 25 | #' if the file is being parsed while it is still being written to. 26 | #' @param file Identical to `filename`. Provided for backward compatibility. 27 | #' @references \emph{W3C Scalable Vector Graphics (SVG)}: 28 | #' \url{http://www.w3.org/Graphics/SVG/} 29 | #' @author This driver was written by T Jake Luciani 30 | #' \email{jakeluciani@@yahoo.com} 2012: updated by Matthieu Decorde 31 | #' \email{matthieu.decorde@@ens-lyon.fr} 32 | #' @seealso \code{\link{pictex}}, \code{\link{postscript}}, \code{\link{Devices}} 33 | #' 34 | #' @examples 35 | #' # Save to file 36 | #' svglite(tempfile("Rplots.svg")) 37 | #' plot(1:11, (-5:5)^2, type = 'b', main = "Simple Example") 38 | #' dev.off() 39 | #' 40 | #' @keywords device 41 | #' @useDynLib svglite, .registration = TRUE 42 | #' @importFrom systemfonts match_font 43 | #' @export 44 | svglite <- function(filename = "Rplot%03d.svg", width = 10, height = 8, 45 | bg = "white", pointsize = 12, standalone = TRUE, 46 | always_valid = FALSE, file) { 47 | if (!missing(file)) { 48 | filename <- file 49 | } 50 | if (invalid_filename(filename)) 51 | stop("invalid 'file': ", filename) 52 | invisible(svglite_(filename, bg, width, height, pointsize, standalone, always_valid)) 53 | } 54 | 55 | #' Access current SVG as a string. 56 | #' 57 | #' This is a variation on \code{\link{svglite}} that makes it easy to access 58 | #' the current value as a string. 59 | #' 60 | #' See \code{\link{svglite}()} documentation for information about 61 | #' specifying fonts. 62 | #' 63 | #' @return A function with no arguments: call the function to get the 64 | #' current value of the string. 65 | #' @examples 66 | #' s <- svgstring(); s() 67 | #' 68 | #' plot.new(); s(); 69 | #' text(0.5, 0.5, "Hi!"); s() 70 | #' dev.off() 71 | #' 72 | #' s <- svgstring() 73 | #' plot(rnorm(5), rnorm(5)) 74 | #' s() 75 | #' dev.off() 76 | #' @inheritParams svglite 77 | #' @export 78 | svgstring <- function(width = 10, height = 8, bg = "white", 79 | pointsize = 12, standalone = TRUE) { 80 | env <- new.env(parent = emptyenv()) 81 | string_src <- svgstring_(env, width = width, height = height, bg = bg, 82 | pointsize = pointsize, standalone = standalone) 83 | 84 | function() { 85 | svgstr <- env$svg_string 86 | if(!env$is_closed) { 87 | svgstr <- c(svgstr, get_svg_content(string_src)) 88 | } 89 | structure(svgstr, class = "svg") 90 | } 91 | } 92 | 93 | #' @export 94 | print.svg <- function(x, ...) cat(x, sep = "\n") 95 | -------------------------------------------------------------------------------- /R/svglite/inlineSVG.R: -------------------------------------------------------------------------------- 1 | #' Run plotting code and view svg in RStudio Viewer or web browser. 2 | #' 3 | #' This is useful primarily for testing. Requires the \code{htmltools} 4 | #' package. 5 | #' 6 | #' @param code Plotting code to execute. 7 | #' @param ... Other arguments passed on to \code{\link{svglite}}. 8 | #' @keywords internal 9 | #' @export 10 | #' @examples 11 | #' if (interactive() && require("htmltools")) { 12 | #' htmlSVG(plot(1:10)) 13 | #' htmlSVG(hist(rnorm(100))) 14 | #' } 15 | htmlSVG <- function(code, ...) { 16 | svg <- inlineSVG(code, ...) 17 | htmltools::browsable( 18 | htmltools::HTML(svg) 19 | ) 20 | } 21 | 22 | #' Run plotting code and return svg 23 | #' 24 | #' This is useful primarily for testing. Requires the \code{xml2} package. 25 | #' 26 | #' @return A \code{xml2::xml_document} object. 27 | #' @inheritParams htmlSVG 28 | #' @inheritParams svglite 29 | #' @keywords internal 30 | #' @export 31 | #' @examples 32 | #' if (require("xml2")) { 33 | #' x <- xmlSVG(plot(1, axes = FALSE)) 34 | #' x 35 | #' xml_find_all(x, ".//text") 36 | #' } 37 | xmlSVG <- function(code, ..., standalone = FALSE, height = 7, width = 7) { 38 | plot <- inlineSVG(code, ..., 39 | standalone = standalone, 40 | height = height, 41 | width = width 42 | ) 43 | xml2::read_xml(plot) 44 | } 45 | 46 | #' Run plotting code and open svg in OS/system default svg viewer or editor. 47 | #' 48 | #' This is useful primarily for testing or post-processing the SVG. 49 | #' 50 | #' @inheritParams htmlSVG 51 | #' @inheritParams svglite 52 | #' @keywords internal 53 | #' @export 54 | #' @examples 55 | #' if (interactive()) { 56 | #' editSVG(plot(1:10)) 57 | #' editSVG(contour(volcano)) 58 | #' } 59 | editSVG <- function(code, ..., width = NA, height = NA) { 60 | dim <- plot_dim(c(width, height)) 61 | 62 | tmp <- tempfile(fileext = ".svg") 63 | svglite(tmp, width = dim[1], height = dim[2], ...) 64 | tryCatch(code, 65 | finally = grDevices::dev.off() 66 | ) 67 | 68 | system(sprintf("open %s", shQuote(tmp))) 69 | } 70 | 71 | #' Run plotting code and return svg as string 72 | #' 73 | #' This is useful primarily for testing but can be used as an 74 | #' alternative to \code{\link{svgstring}()}. 75 | #' 76 | #' @inheritParams htmlSVG 77 | #' @keywords internal 78 | #' @export 79 | #' @examples 80 | #' stringSVG(plot(1:10)) 81 | stringSVG <- function(code, ...) { 82 | svg <- inlineSVG(code, ...) 83 | structure(svg, class = "svg") 84 | } 85 | 86 | inlineSVG <- function(code, ..., width = NA, height = NA) { 87 | dim <- plot_dim(c(width, height)) 88 | 89 | svg <- svgstring(width = dim[1], height = dim[2], ...) 90 | tryCatch(code, 91 | finally = grDevices::dev.off() 92 | ) 93 | 94 | out <- svg() 95 | class(out) <- NULL 96 | out 97 | } 98 | -------------------------------------------------------------------------------- /R/svglite/utils.R: -------------------------------------------------------------------------------- 1 | 2 | mini_plot <- function(...) graphics::plot(..., axes = FALSE, xlab = "", ylab = "") 3 | 4 | plot_dim <- function(dim = c(NA, NA)) { 5 | if (any(is.na(dim))) { 6 | if (length(grDevices::dev.list()) == 0) { 7 | default_dim <- c(10, 8) 8 | } else { 9 | default_dim <- grDevices::dev.size() 10 | } 11 | 12 | dim[is.na(dim)] <- default_dim[is.na(dim)] 13 | dim_f <- prettyNum(dim, digits = 3) 14 | 15 | message("Saving ", dim_f[1], "\" x ", dim_f[2], "\" image") 16 | } 17 | 18 | dim 19 | } 20 | 21 | vapply_chr <- function(.x, .f, ...) { 22 | vapply(.x, .f, character(1), ...) 23 | } 24 | vapply_lgl <- function(.x, .f, ...) { 25 | vapply(.x, .f, logical(1), ...) 26 | } 27 | lapply_if <- function(.x, .p, .f, ...) { 28 | if (!is.logical(.p)) { 29 | .p <- vapply_lgl(.x, .p) 30 | } 31 | .x[.p] <- lapply(.x[.p], .f, ...) 32 | .x 33 | } 34 | keep <- function(.x, .p, ...) { 35 | .x[vapply_lgl(.x, .p, ...)] 36 | } 37 | compact <- function(x) { 38 | Filter(length, x) 39 | } 40 | `%||%` <- function(x, y) { 41 | if (is.null(x)) y else x 42 | } 43 | is_scalar_character <- function(x) { 44 | is.character(x) && length(x) == 1 45 | } 46 | names2 <- function(x) { 47 | names(x) %||% rep("", length(x)) 48 | } 49 | ilapply <- function(.x, .f, ...) { 50 | idx <- names(.x) %||% seq_along(.x) 51 | out <- Map(.f, names(.x), .x, ...) 52 | names(out) <- names(.x) 53 | out 54 | } 55 | ilapply_if <- function(.x, .p, .f, ...) { 56 | if (!is.logical(.p)) { 57 | .p <- vapply_lgl(.x, .p) 58 | } 59 | .x[.p] <- ilapply(.x[.p], .f, ...) 60 | .x 61 | } 62 | set_names <- function(x, nm = x) { 63 | stats::setNames(x, nm) 64 | } 65 | zip <- function(.l) { 66 | fields <- set_names(names(.l[[1]])) 67 | lapply(fields, function(i) { 68 | lapply(.l, .subset2, i) 69 | }) 70 | } 71 | 72 | svglite_manual_tests <- new.env() 73 | register_manual_test <- function(file) { 74 | testthat_dir <- getwd() 75 | testfile <- file.path(testthat_dir, file) 76 | assign(file, testfile, svglite_manual_tests) 77 | } 78 | init_manual_tests <- function() { 79 | remove(list = names(svglite_manual_tests), envir = svglite_manual_tests) 80 | } 81 | open_manual_tests <- function() { 82 | lapply(names(svglite_manual_tests), function(test) { 83 | utils::browseURL(svglite_manual_tests[[test]]) 84 | }) 85 | } 86 | 87 | invalid_filename <- function(filename) { 88 | 89 | if (!is.character(filename) || length(filename) != 1) 90 | return(TRUE) 91 | 92 | # strip double occurences of % 93 | stripped_file <- gsub("%{2}", "", filename) 94 | # filename is fine if there are no % left 95 | if (!grepl("%", stripped_file)) 96 | return(FALSE) 97 | # remove first allowed pattern, % followed by digits followed by [diouxX] 98 | stripped_file <- sub("%[#0 ,+-]*[0-9.]*[diouxX]", "", stripped_file) 99 | # matching leftover % indicates multiple patterns or a single incorrect pattern (e.g., %s) 100 | return(grepl("%", stripped_file)) 101 | 102 | } 103 | #' Convert an svg file to svgz, overwriting the old file 104 | #' @param file the path to the file to convert 105 | #' @keywords internal 106 | #' @export 107 | create_svgz <- function(file) { 108 | svg <- readLines(file) 109 | out <- gzfile(file, "w") 110 | writeLines(svg, out) 111 | close(out) 112 | invisible(NULL) 113 | } 114 | -------------------------------------------------------------------------------- /R/utils.R: -------------------------------------------------------------------------------- 1 | package_version <- function(pkg) { 2 | as.character(utils::packageVersion(pkg)) 3 | } 4 | 5 | cat_line <- function(..., trailing = TRUE, file = "") { 6 | cat(paste_line(..., trailing = trailing), file = file) 7 | } 8 | paste_line <- function(..., trailing = FALSE) { 9 | lines <- paste(chr(...), collapse = "\n") 10 | if (trailing) { 11 | lines <- paste0(lines, "\n") 12 | } 13 | lines 14 | } 15 | 16 | push_log <- function(name, old_path, new_path) { 17 | log_path <- Sys.getenv("VDIFFR_LOG_PATH") 18 | 19 | # If no envvar is set, check if we are running under R CMD check. In 20 | # that case, always push a log file. 21 | if (!nzchar(log_path)) { 22 | if (!is_checking_remotely()) { 23 | return(invisible(FALSE)) 24 | } 25 | log_path <- testthat::test_path("..", "vdiffr.Rout.fail") 26 | } 27 | 28 | log_exists <- file.exists(log_path) 29 | 30 | file <- file(log_path, "a") 31 | on.exit(close(file)) 32 | 33 | if (!log_exists) { 34 | cat_line( 35 | file = file, 36 | "Environment:", 37 | vdiffr_info(), 38 | "" 39 | ) 40 | } 41 | 42 | diff_lines <- diff_lines(name, old_path, new_path) 43 | cat_line(file = file, "", !!!diff_lines, "") 44 | } 45 | is_checking_remotely <- function() { 46 | nzchar(Sys.getenv("CI")) || !nzchar(Sys.getenv("NOT_CRAN")) 47 | } 48 | 49 | diff_lines <- function(name, 50 | before_path, 51 | after_path) { 52 | before <- readLines(before_path) 53 | after <- readLines(after_path) 54 | 55 | diff <- diffobj::diffChr( 56 | before, 57 | after, 58 | format = "raw", 59 | # For reproducibility 60 | disp.width = 80 61 | ) 62 | lines <- as.character(diff) 63 | 64 | paste_line( 65 | glue("Failed doppelganger: {name} ({before_path})"), 66 | "", 67 | !!!lines 68 | ) 69 | } 70 | 71 | vdiffr_info <- function() { 72 | glue( 73 | "- vdiffr-svg-engine: { SVG_ENGINE_VER } 74 | - vdiffr: { utils::packageVersion('vdiffr') }" 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /R/vdiffr-package.R: -------------------------------------------------------------------------------- 1 | #' @import rlang 2 | #' @importFrom glue glue 3 | #' @useDynLib vdiffr, .registration = TRUE 4 | #' @keywords internal 5 | "_PACKAGE" 6 | 7 | SVG_ENGINE_VER <- "2.0" 8 | 9 | svg_engine_ver <- function() { 10 | as.numeric_version(SVG_ENGINE_VER) 11 | } 12 | 13 | .onLoad <- function(lib, pkg) { 14 | set_engine_version(SVG_ENGINE_VER) 15 | } 16 | 17 | ## usethis namespace: start 18 | #' @importFrom lifecycle deprecated 19 | ## usethis namespace: end 20 | NULL 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vdiffr 2 | 3 | 4 | [![CRAN status](https://www.r-pkg.org/badges/version/vdiffr)](https://cran.r-project.org/package=vdiffr) 5 | [![R-CMD-check](https://github.com/r-lib/vdiffr/actions/workflows/R-CMD-check.yaml/badge.svg)](https://github.com/r-lib/vdiffr/actions/workflows/R-CMD-check.yaml) 6 | [![Codecov test coverage](https://codecov.io/gh/r-lib/vdiffr/graph/badge.svg)](https://app.codecov.io/gh/r-lib/vdiffr) 7 | 8 | 9 | vdiffr is a testthat extension for monitoring the appearance of R plots. It generates reproducible SVG files and registers them as [testthat snapshots](https://testthat.r-lib.org/articles/snapshotting.html). 10 | 11 | 12 | ## How to use vdiffr 13 | 14 | 1) Add graphical expectations by including `expect_doppelganger()` in your test files. 15 | 16 | 1) Run `devtools::test()`. 17 | 18 | 1) If `test()` detected new snapshots or changes to existing snapshots, run `testthat::snapshot_review()` to review them. 19 | 20 | There may be many reasons for a snapshot to fail. Upstream changes (e.g. to the R graphics engine or to ggplot2) may cause subtle differences in your plots that are not actual failures. For this reason, snapshots do not cause failures on CRAN by default. You will only see failures locally or on CI platforms such as Github Actions. 21 | 22 | 23 | ### Adding expectations 24 | 25 | vdiffr integrates with testthat through the `expect_doppelganger()` expectation. It takes as arguments: 26 | 27 | - A title. This title is used in two ways. First, the title is standardised (it is converted to lowercase and any character that is not alphanumeric or a space is turned into a dash) and used as filename for storing the figure. Secondly, with ggplot2 figures the title is automatically added to the plot with `ggtitle()` (only if no ggtitle has been set). 28 | 29 | - A figure. This can be a ggplot object, a recorded plot, a function to be called, or more generally any object with a `print` method. 30 | 31 | The snapshots are recorded in subfolders of the `_snaps/` directory. 32 | 33 | ```{r} 34 | disp_hist_base <- function() hist(mtcars$disp) 35 | disp_hist_ggplot <- ggplot(mtcars, aes(disp)) + geom_histogram() 36 | 37 | vdiffr::expect_doppelganger("Base graphics histogram", disp_hist_base) 38 | vdiffr::expect_doppelganger("ggplot2 histogram", disp_hist_ggplot) 39 | ``` 40 | 41 | Note that in addition to automatic ggtitles, ggplot2 figures are 42 | assigned the minimalistic theme `theme_test()` (unless they already 43 | have been assigned a theme). 44 | 45 | 46 | ### Debugging 47 | 48 | It is sometimes difficult to understand the cause of a doppelganger failure. A frequent cause of failure is undeterministic generation of plots. Potential culprits are: 49 | 50 | * Some of the plot components depend on random variation. Try setting a seed, for instance with [`withr::local_seed()`](https://withr.r-lib.org/reference/with_seed.html). 51 | 52 | * The plot depends on some system library. For instance sf plots depend on libraries like GEOS and GDAL. It might not be possible to test these plots with vdiffr (which can still be used for manual inspection, add a [testthat::skip()] before the `expect_doppelganger()` call in that case). 53 | 54 | To help you understand the causes of a failure, vdiffr automatically logs the SVG diff of all failures when run under R CMD check. The log is located in `tests/vdiffr.Rout.fail` and should be displayed in your CI logs. 55 | 56 | You can also set the `VDIFFR_LOG_PATH` environment variable with `Sys.setenv()` to unconditionally (also interactively) log failures in the file pointed by the variable. 57 | 58 | 59 | ### Skipping all vdiffr checks on some platforms 60 | 61 | See ggplot2's approach which wraps `expect_doppelganger()` with a version that that calls `testthat::skip()` on the relevant platforms (as determined by environment variables): https://github.com/tidyverse/ggplot2/blob/ddd207e926cc1c1847dc661d9a099b8ec19c4010/tests/testthat/helper-vdiffr.R#L1-L15. 62 | 63 | 64 | ## Building vdiffr 65 | 66 | _This section is only relevant for building vdiffr from scratch, as opposed to installing from a pre-built package on CRAN._ 67 | 68 | Building vdiffr requires the system dependency libpng. As vdiffr doesn't have any build-time configuration, your R configuration must point to libpng's `include` and `lib` folders. 69 | 70 | For instance on macOS, install libpng with: 71 | 72 | ```sh 73 | brew install libpng 74 | ``` 75 | 76 | And make sure your `~/.R/Makevars` knows about Homebrew's `include` and `lib` folders where libpng should now be installed. On arm64 hardware, this would be: 77 | 78 | ```mk 79 | CPPFLAGS += -I/opt/homebrew/include 80 | LDFLAGS += -L/opt/homebrew/lib 81 | ``` 82 | -------------------------------------------------------------------------------- /_pkgdown.yml: -------------------------------------------------------------------------------- 1 | url: https://vdiffr.r-lib.org 2 | 3 | template: 4 | package: tidytemplate 5 | bootstrap: 5 6 | 7 | includes: 8 | in_header: | 9 | 10 | 11 | news: 12 | releases: 13 | - text: "Version 1.0.0" 14 | href: https://www.tidyverse.org/blog/2021/06/vdiffr-1-0-0/ 15 | - text: "Version 0.3.0" 16 | href: https://www.tidyverse.org/articles/2019/01/vdiffr-0-3-0/ 17 | -------------------------------------------------------------------------------- /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 | This restores the C++11 specification because using uterior C++ versions causes issues, see https://github.com/r-lib/vdiffr/issues/137 2 | -------------------------------------------------------------------------------- /inst/create_glyph_dims.R: -------------------------------------------------------------------------------- 1 | # This code creates "src/glyph_dims.h" which inlines the glyph dimensions of 2 | # Liberation and Symbola for all unicode points up until 50000. 3 | # 4 | # Changes in the entries of "src/glyph_dims.h" will result in potential failures 5 | # of visual tests since it will change string dimension calculations. Only 6 | # update it for very good reasons! 7 | 8 | # Create vector of unicode characters 9 | char_num <- seq_len(50000) 10 | res <- 1e4 11 | chars <- eval( 12 | parse(text = paste0( 13 | 'c("', 14 | paste(sprintf("\\U%04X", char_num), collapse = '","'), 15 | '")') 16 | ) 17 | ) 18 | 19 | # Get font files from fontquiver (old vdiffr dependency) 20 | liberation <- fontquiver::font("Liberation", 'Sans', 'Regular')$ttf 21 | symbola <- fontquiver::font("Symbola", 'Symbols', 'Regular')$ttf 22 | 23 | # Extract glyph dimensions from Liberation 24 | liberation_glyphs <- systemfonts::glyph_info(chars, res = res, path = liberation) 25 | liberation_glyphs$num <- char_num 26 | liberation_glyphs <- liberation_glyphs[!duplicated(liberation_glyphs$index), ] 27 | liberation_glyphs <- data.frame( 28 | char = liberation_glyphs$num, 29 | width = liberation_glyphs$x_advance * 72 / res, 30 | ascent = vapply(liberation_glyphs$bbox, `[`, numeric(1), 4) * 72 / res, 31 | descent = -vapply(liberation_glyphs$bbox, `[`, numeric(1), 3) * 72 / res 32 | ) 33 | # Extract glyph dimensions from Symbola 34 | symbola_glyphs <- systemfonts::glyph_info(chars, res = res, path = symbola) 35 | symbola_glyphs$num <- char_num 36 | symbola_glyphs <- symbola_glyphs[!duplicated(symbola_glyphs$index), ] 37 | symbola_glyphs <- data.frame( 38 | char = symbola_glyphs$num, 39 | width = symbola_glyphs$x_advance * 72 / res, 40 | ascent = vapply(symbola_glyphs$bbox, `[`, numeric(1), 4) * 72 / res, 41 | descent = -vapply(symbola_glyphs$bbox, `[`, numeric(1), 3) * 72 / res 42 | ) 43 | 44 | # Write "src/glyph_dims.h" 45 | def <- c( 46 | "// Generated by inst/create_glyph_dims.R — Do not edit by hand", 47 | "#pragma once", 48 | "#include ", 49 | "struct Dim {", 50 | " double width;", 51 | " double ascent;", 52 | " double descent;", 53 | "};", 54 | "const std::unordered_map LIBERATION_DIM = {", 55 | paste(sprintf( 56 | " {%i, {%.4f, %.4f, %.4f}}", 57 | liberation_glyphs$char, 58 | liberation_glyphs$width, 59 | liberation_glyphs$ascent, 60 | liberation_glyphs$descent 61 | ), collapse = ",\n"), 62 | "};", 63 | "const std::unordered_map SYMBOLA_DIM = {", 64 | paste(sprintf( 65 | " {%i, {%.4f, %.4f, %.4f}}", 66 | symbola_glyphs$char, 67 | symbola_glyphs$width, 68 | symbola_glyphs$ascent, 69 | symbola_glyphs$descent 70 | ), collapse = ",\n"), 71 | "};" 72 | ) 73 | writeLines(def, "src/glyph_dims.h") 74 | -------------------------------------------------------------------------------- /man/expect_doppelganger.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/expect-doppelganger.R 3 | \name{expect_doppelganger} 4 | \alias{expect_doppelganger} 5 | \title{Does a figure look like its expected output?} 6 | \usage{ 7 | expect_doppelganger( 8 | title, 9 | fig, 10 | path = deprecated(), 11 | ..., 12 | writer = write_svg, 13 | cran = FALSE, 14 | variant = NULL 15 | ) 16 | } 17 | \arguments{ 18 | \item{title}{A brief description of what is being tested in the 19 | figure. For instance: "Points and lines overlap". 20 | 21 | If a ggplot2 figure doesn't have a title already, \code{title} is 22 | applied to the figure with \code{ggtitle()}. 23 | 24 | The title is also used as file name for storing SVG (in a 25 | sanitzed form, with special characters converted to \code{"-"}).} 26 | 27 | \item{fig}{A figure to test. This can be a ggplot object, a 28 | recordedplot, or more generally any object with a \code{print} method. 29 | 30 | If you need to test a plot with non-printable objects (e.g. base 31 | plots), \code{fig} can be a function that generates and prints the 32 | plot, e.g. \code{fig = function() plot(1:3)}.} 33 | 34 | \item{path, ...}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}}.} 35 | 36 | \item{writer}{A function that takes the plot, a target SVG file, 37 | and an optional plot title. It should transform the plot to SVG 38 | in a deterministic way and write it to the target file. See 39 | \code{\link[=write_svg]{write_svg()}} (the default) for an example.} 40 | 41 | \item{cran}{If \code{FALSE} (the default), mismatched snapshots only 42 | cause a failure when you run tests locally or in your CI (Github 43 | Actions or any platform that sets the \code{CI} environment variable). 44 | If \code{TRUE}, failures may also occur on CRAN machines. 45 | 46 | Failures are disabled on CRAN by default because testing the 47 | appearance of a figure is inherently fragile. Changes in the R 48 | graphics engine or in ggplot2 may cause subtle differences in the 49 | aspect of a plot, such as a slightly smaller or larger margin. 50 | These changes will cause spurious failures because you need to 51 | update your snapshots to reflect the upstream changes. 52 | 53 | It would be distracting for both you and the CRAN maintainers if 54 | such changes systematically caused failures on CRAN. This is why 55 | snapshot expectations do not fail on CRAN by default and should 56 | be treated as a monitoring tool that allows you to quickly check 57 | how the appearance of your figures changes over time, and to 58 | manually assess whether changes reflect actual problems in your 59 | package. 60 | 61 | Internally, this argument is passed to 62 | \code{\link[testthat:expect_snapshot_file]{testthat::expect_snapshot_file()}}.} 63 | 64 | \item{variant}{If not-\code{NULL}, results will be saved in 65 | \verb{_snaps/{variant}/{test}/{name}.{ext}}. This allows you to create 66 | different snapshots for different scenarios, like different operating 67 | systems or different R versions.} 68 | } 69 | \description{ 70 | \code{expect_doppelganger()} is a testthat expectation for graphical 71 | plots. It generates SVG snapshots that you can review graphically 72 | with \code{\link[testthat:snapshot_accept]{testthat::snapshot_review()}}. You will find more information 73 | about snapshotting in the \href{https://testthat.r-lib.org/articles/snapshotting.html}{testthat snapshots vignette}. 74 | 75 | Note that \code{expect_doppelgagner()} requires R version 4.1.0. If run 76 | on an earlier version of R, it emits a \code{testthat::skip()} so that you 77 | can still run other checks on old versions of R. 78 | } 79 | \section{Debugging}{ 80 | 81 | It is sometimes difficult to understand the cause of a failure. 82 | This usually indicates that the plot is not created 83 | deterministically. Potential culprits are: 84 | \itemize{ 85 | \item Some of the plot components depend on random variation. Try 86 | setting a seed. 87 | \item The plot depends on some system library. For instance sf plots 88 | depend on libraries like GEOS and GDAL. It might not be possible 89 | to test these plots with vdiffr. 90 | } 91 | 92 | To help you understand the causes of a failure, vdiffr 93 | automatically logs the SVG diff of all failures when run under R 94 | CMD check. The log is located in \code{tests/vdiffr.Rout.fail} and 95 | should be displayed on Travis. 96 | 97 | You can also set the \code{VDIFFR_LOG_PATH} environment variable with 98 | \code{Sys.setenv()} to unconditionally (also interactively) log failures 99 | in the file pointed by the variable. 100 | } 101 | 102 | \examples{ 103 | if (FALSE) { # Not run 104 | 105 | library("ggplot2") 106 | 107 | test_that("plots have known output", { 108 | disp_hist_base <- function() hist(mtcars$disp) 109 | expect_doppelganger("disp-histogram-base", disp_hist_base) 110 | 111 | disp_hist_ggplot <- ggplot(mtcars, aes(disp)) + geom_histogram() 112 | expect_doppelganger("disp-histogram-ggplot", disp_hist_ggplot) 113 | }) 114 | 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /man/figures/lifecycle-archived.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclearchivedarchived -------------------------------------------------------------------------------- /man/figures/lifecycle-defunct.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledefunctdefunct -------------------------------------------------------------------------------- /man/figures/lifecycle-deprecated.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycledeprecateddeprecated -------------------------------------------------------------------------------- /man/figures/lifecycle-experimental.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecycleexperimentalexperimental -------------------------------------------------------------------------------- /man/figures/lifecycle-maturing.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclematuringmaturing -------------------------------------------------------------------------------- /man/figures/lifecycle-questioning.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclequestioningquestioning -------------------------------------------------------------------------------- /man/figures/lifecycle-stable.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclestablestable -------------------------------------------------------------------------------- /man/figures/lifecycle-superseded.svg: -------------------------------------------------------------------------------- 1 | lifecyclelifecyclesupersededsuperseded -------------------------------------------------------------------------------- /man/vdiffr-package.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/vdiffr-package.R 3 | \docType{package} 4 | \name{vdiffr-package} 5 | \alias{vdiffr} 6 | \alias{vdiffr-package} 7 | \title{vdiffr: Visual Regression Testing and Graphical Diffing} 8 | \description{ 9 | An extension to the 'testthat' package that makes it easy to add graphical unit tests. It provides a Shiny application to manage the test cases. 10 | } 11 | \seealso{ 12 | Useful links: 13 | \itemize{ 14 | \item \url{https://vdiffr.r-lib.org/} 15 | \item \url{https://github.com/r-lib/vdiffr} 16 | \item Report bugs at \url{https://github.com/r-lib/vdiffr/issues} 17 | } 18 | 19 | } 20 | \author{ 21 | \strong{Maintainer}: Lionel Henry \email{lionel@posit.co} 22 | 23 | Authors: 24 | \itemize{ 25 | \item Thomas Lin Pedersen \email{thomas.pedersen@posit.co} (\href{https://orcid.org/0000-0002-5147-4711}{ORCID}) 26 | \item T Jake Luciani \email{jake@apache.org} (svglite) 27 | \item Matthieu Decorde \email{matthieu.decorde@ens-lyon.fr} (svglite) 28 | \item Vaudor Lise \email{lise.vaudor@ens-lyon.fr} (svglite) 29 | } 30 | 31 | Other contributors: 32 | \itemize{ 33 | \item Posit Software, PBC [copyright holder, funder] 34 | \item Tony Plate (svglite: Early line dashing code) [contributor] 35 | \item David Gohel (svglite: Line dashing code and raster code) [contributor] 36 | \item Yixuan Qiu (svglite: Improved styles; polypath implementation) [contributor] 37 | \item Håkon Malmedal (svglite: Opacity code) [contributor] 38 | } 39 | 40 | } 41 | \keyword{internal} 42 | -------------------------------------------------------------------------------- /man/write_svg.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/svg.R 3 | \name{write_svg} 4 | \alias{write_svg} 5 | \title{Default SVG writer} 6 | \usage{ 7 | write_svg(plot, file, title = "") 8 | } 9 | \arguments{ 10 | \item{plot}{A plot object to convert to SVG. Can be a ggplot2 object, 11 | a \link[grDevices:recordplot]{recorded plot}, or any object with a 12 | \link[base:print]{print()} method.} 13 | 14 | \item{file}{The file to write the SVG to.} 15 | 16 | \item{title}{An optional title for the test case.} 17 | } 18 | \description{ 19 | This is the default SVG writer for vdiffr test cases. It uses 20 | embedded versions of \href{https://svglite.r-lib.org}{svglite}, 21 | \href{https://harfbuzz.github.io/}{harfbuzz}, and the Liberation and 22 | Symbola fonts in order to create deterministic SVGs. 23 | } 24 | -------------------------------------------------------------------------------- /revdep/.gitignore: -------------------------------------------------------------------------------- 1 | checks/ 2 | checks 3 | library 4 | checks.noindex 5 | library.noindex 6 | cloud.noindex 7 | data.sqlite 8 | *.html 9 | -------------------------------------------------------------------------------- /revdep/README.md: -------------------------------------------------------------------------------- 1 | # Revdeps 2 | 3 | ## Failed to check (3) 4 | 5 | |package |version |error |warning |note | 6 | |:-------|:-------|:-----|:-------|:----| 7 | |NA |? | | | | 8 | |NA |? | | | | 9 | |NA |? | | | | 10 | 11 | -------------------------------------------------------------------------------- /revdep/cran.md: -------------------------------------------------------------------------------- 1 | ## revdepcheck results 2 | 3 | We checked 172 reverse dependencies (169 from CRAN + 3 from Bioconductor), comparing R CMD check results across CRAN and dev versions of this package. 4 | 5 | * We saw 0 new problems 6 | * We failed to check 0 packages 7 | 8 | -------------------------------------------------------------------------------- /revdep/email.yml: -------------------------------------------------------------------------------- 1 | release_date: ??? 2 | rel_release_date: ??? 3 | my_news_url: ??? 4 | release_version: ??? 5 | release_details: ??? 6 | -------------------------------------------------------------------------------- /revdep/failures.md: -------------------------------------------------------------------------------- 1 | # NA 2 | 3 |
4 | 5 | * Version: NA 6 | * GitHub: NA 7 | * Source code: https://github.com/cran/NA 8 | * Number of recursive dependencies: 0 9 | 10 | Run `cloud_details(, "NA")` for more info 11 | 12 |
13 | 14 | ## Error before installation 15 | 16 | ### Devel 17 | 18 | ``` 19 | 20 | 21 | 22 | 23 | 24 | 25 | ``` 26 | ### CRAN 27 | 28 | ``` 29 | 30 | 31 | 32 | 33 | 34 | 35 | ``` 36 | # NA 37 | 38 |
39 | 40 | * Version: NA 41 | * GitHub: NA 42 | * Source code: https://github.com/cran/NA 43 | * Number of recursive dependencies: 0 44 | 45 | Run `cloud_details(, "NA")` for more info 46 | 47 |
48 | 49 | ## Error before installation 50 | 51 | ### Devel 52 | 53 | ``` 54 | 55 | 56 | 57 | 58 | 59 | 60 | ``` 61 | ### CRAN 62 | 63 | ``` 64 | 65 | 66 | 67 | 68 | 69 | 70 | ``` 71 | # NA 72 | 73 |
74 | 75 | * Version: NA 76 | * GitHub: NA 77 | * Source code: https://github.com/cran/NA 78 | * Number of recursive dependencies: 0 79 | 80 | Run `cloud_details(, "NA")` for more info 81 | 82 |
83 | 84 | ## Error before installation 85 | 86 | ### Devel 87 | 88 | ``` 89 | 90 | 91 | 92 | 93 | 94 | 95 | ``` 96 | ### CRAN 97 | 98 | ``` 99 | 100 | 101 | 102 | 103 | 104 | 105 | ``` 106 | -------------------------------------------------------------------------------- /revdep/problems.md: -------------------------------------------------------------------------------- 1 | *Wow, no problems at all. :)* -------------------------------------------------------------------------------- /src/Makevars: -------------------------------------------------------------------------------- 1 | CXX_STD = CXX11 2 | 3 | PKG_LIBS = -lpng -lz 4 | -------------------------------------------------------------------------------- /src/Makevars.ucrt: -------------------------------------------------------------------------------- 1 | PKG_LIBS = -lpng -lz 2 | -------------------------------------------------------------------------------- /src/Makevars.win: -------------------------------------------------------------------------------- 1 | CXX_STD = CXX11 2 | 3 | VERSION = 2.7.4 4 | RWINLIB = ../windows/harfbuzz-${VERSION} 5 | 6 | PKG_CPPFLAGS = -I${RWINLIB}/include 7 | 8 | PKG_LIBS = -L${RWINLIB}/lib${R_ARCH}${CRT} -lpng -lz 9 | 10 | all: clean winlibs 11 | 12 | winlibs: 13 | "${R_HOME}/bin${R_ARCH_BIN}/Rscript.exe" "../tools/winlibs.R" ${VERSION} 14 | 15 | clean: 16 | rm -f $(OBJECTS) 17 | -------------------------------------------------------------------------------- /src/SvgStream.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include "utils.h" 13 | 14 | namespace svglite { namespace internal { 15 | 16 | template 17 | void write_double(T& stream, double data) { 18 | std::streamsize prec = stream.precision(); 19 | uint8_t newprec = std::fabs(data) >= 1 || data == 0. ? prec : std::ceil(-std::log10(std::fabs(data))) + 1; 20 | stream << std::setprecision(newprec) << data << std::setprecision(prec); 21 | } 22 | 23 | }} // namespace svglite::internal 24 | 25 | 26 | class SvgStream { 27 | std::unordered_set clip_ids; 28 | bool clipping = false; 29 | 30 | public: 31 | 32 | bool has_clip_id(std::string id) { 33 | return clip_ids.find(id) != clip_ids.end(); 34 | } 35 | void add_clip_id(std::string id) { 36 | clipping = true; 37 | clip_ids.insert(id); 38 | } 39 | void clear_clip_ids() { 40 | clipping = false; 41 | clip_ids.clear(); 42 | } 43 | bool is_clipping() {return clipping;} 44 | 45 | virtual ~SvgStream() {}; 46 | 47 | virtual void write(int data) = 0; 48 | virtual void write(double data) = 0; 49 | virtual void write(const char* data) = 0; 50 | virtual void write(const std::string& data) = 0; 51 | virtual void write(char data) = 0; 52 | virtual bool is_file_stream() = 0; 53 | 54 | void put(char data) { 55 | write(data); 56 | } 57 | 58 | virtual void flush() = 0; 59 | virtual void finish(bool close) = 0; 60 | }; 61 | 62 | template 63 | SvgStream& operator<<(SvgStream& object, const T& data) { 64 | object.write(data); 65 | return object; 66 | } 67 | template <> 68 | inline SvgStream& operator<<(SvgStream& object, const double& data) { 69 | // Make sure negative zeros are converted to positive zero for 70 | // reproducibility of SVGs 71 | object.write(dbl_format(data)); 72 | return object; 73 | } 74 | 75 | class SvgStreamFile : public SvgStream { 76 | std::ofstream stream_; 77 | std::string file = ""; 78 | bool always_valid = false; 79 | 80 | public: 81 | SvgStreamFile(const std::string& path, bool _always_valid = false) : always_valid(_always_valid) { 82 | std::string svgz_ext = path.size() > 5 ? path.substr(path.size() - 5) : ""; 83 | file = R_ExpandFileName(path.c_str()); 84 | 85 | stream_.open(file.c_str()); 86 | 87 | if (stream_.fail()) 88 | cpp11::stop("cannot open stream %s", path.c_str()); 89 | 90 | stream_ << std::fixed << std::setprecision(2); 91 | } 92 | 93 | SvgStreamFile(const std::string& path, int pageno, bool _always_valid = false) : always_valid(_always_valid) { 94 | std::string svgz_ext = path.size() > 5 ? path.substr(path.size() - 5) : ""; 95 | 96 | char buf[PATH_MAX+1]; 97 | snprintf(buf, PATH_MAX, path.c_str(), pageno); 98 | buf[PATH_MAX] = '\0'; 99 | file = R_ExpandFileName(buf); 100 | 101 | stream_.open(file.c_str()); 102 | if (stream_.fail()) 103 | cpp11::stop("cannot open stream %s", buf); 104 | 105 | stream_ << std::fixed << std::setprecision(2); 106 | } 107 | 108 | void write(int data) { stream_ << data; } 109 | void write(double data) { svglite::internal::write_double(stream_, data); } 110 | void write(const char* data) { stream_ << data; } 111 | void write(char data) { stream_ << data; } 112 | void write(const std::string& data) { stream_ << data; } 113 | bool is_file_stream() {return true; } 114 | 115 | // Adding a final newline here creates problems on Windows when 116 | // seeking back to original position. So we only write the newline 117 | // in finish() 118 | void flush() { 119 | if (!always_valid) { 120 | return; 121 | } 122 | 123 | stream_ << "\n"; 124 | #ifdef _WIN32 125 | stream_.seekp(-12, std::ios_base::cur); 126 | #else 127 | stream_.seekp(-11, std::ios_base::cur); 128 | #endif 129 | } 130 | 131 | void finish(bool close) { 132 | if (is_clipping()) { 133 | stream_ << "\n"; 134 | } 135 | stream_ << "\n"; 136 | stream_.flush(); 137 | clear_clip_ids(); 138 | } 139 | 140 | ~SvgStreamFile() { 141 | stream_.close(); 142 | } 143 | }; 144 | 145 | 146 | class SvgStreamString : public SvgStream { 147 | std::stringstream stream_; 148 | cpp11::environment env_; 149 | 150 | public: 151 | SvgStreamString(cpp11::environment env): env_(env) { 152 | stream_ << std::fixed << std::setprecision(2); 153 | env_["is_closed"] = false; 154 | } 155 | 156 | void write(int data) { stream_ << data; } 157 | void write(double data) { svglite::internal::write_double(stream_, data); } 158 | void write(const char* data) { stream_ << data; } 159 | void write(char data) { stream_ << data; } 160 | void write(const std::string& data) { stream_ << data; } 161 | bool is_file_stream() {return false; } 162 | 163 | void flush() { 164 | } 165 | 166 | void finish(bool close) { 167 | // When device is closed, stream_ will be destroyed, so we can no longer 168 | // get the svg string from stream_. In this case, we save the final string 169 | // to the environment env, so that R can read from env$svg_string even 170 | // after device is closed. 171 | env_["is_closed"] = close; 172 | 173 | stream_.flush(); 174 | std::string svgstr = stream_.str(); 175 | // If the current svg is empty, we also make the string empty 176 | // Otherwise append "" to make it a valid SVG 177 | if(!svgstr.empty()) { 178 | if (is_clipping()) { 179 | svgstr.append("\n"); 180 | } 181 | svgstr.append(""); 182 | } 183 | if (env_.exists("svg_string")) { 184 | cpp11::writable::strings str(env_["svg_string"]); 185 | str.push_back(svgstr.c_str()); 186 | env_["svg_string"] = str; 187 | } else { 188 | env_["svg_string"] = svgstr; 189 | } 190 | 191 | // clear the stream 192 | stream_.str(std::string()); 193 | stream_.clear(); 194 | clear_clip_ids(); 195 | } 196 | 197 | std::stringstream* string_src() { 198 | return &stream_; 199 | } 200 | }; 201 | -------------------------------------------------------------------------------- /src/algo-it.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | template 4 | InputIterator find_if_it(InputIterator it, InputIterator end, 5 | Predicate pred) { 6 | while (it != end && !pred(it)) ++it; 7 | return it; 8 | } 9 | 10 | template 11 | OutputIterator remove_copy_if_it(InputIterator it, InputIterator end, 12 | OutputIterator result, Predicate pred) { 13 | while (it != end) { 14 | if (!pred(it)) { 15 | *result = *it; 16 | ++result; 17 | } 18 | ++it; 19 | } 20 | return result; 21 | } 22 | 23 | template 24 | ForwardIterator remove_if_it(ForwardIterator it, ForwardIterator end, 25 | Predicate pred) { 26 | it = find_if_it(it, end, pred); 27 | ForwardIterator next = it; 28 | return it == end ? it : remove_copy_if_it(++next, end, it, pred); 29 | } 30 | -------------------------------------------------------------------------------- /src/compare.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include "algo-it.h" 7 | 8 | int compare_throw() { 9 | Rf_error("vdiffr error: unable to read svg files"); 10 | return 0; 11 | } 12 | 13 | struct is_cr { 14 | template 15 | bool operator()(InputIterator it) { 16 | return *it == 0x0D && *(it + 1) == 0x0A; 17 | } 18 | }; 19 | 20 | [[cpp11::register]] 21 | bool compare_files(std::string expected, std::string test) { 22 | std::ifstream file1(expected.c_str(), std::ifstream::ate | std::ifstream::binary); 23 | std::ifstream file2(test.c_str(), std::ifstream::ate | std::ifstream::binary); 24 | if (!file1 || !file2) 25 | compare_throw(); 26 | 27 | std::streamsize size1 = file1.tellg(); 28 | std::streamsize size2 = file2.tellg(); 29 | file1.seekg(0, std::ios::beg); 30 | file2.seekg(0, std::ios::beg); 31 | 32 | std::vector buffer1(size1); 33 | std::vector buffer2(size2); 34 | if (!file1.read(buffer1.data(), size1) || 35 | !file2.read(buffer2.data(), size2)) 36 | compare_throw(); 37 | 38 | // Remove carriage returns from SVGs generated on Windows 39 | buffer1.erase(remove_if_it(buffer1.begin(), buffer1.end() - 1, is_cr()), buffer1.end()); 40 | buffer2.erase(remove_if_it(buffer2.begin(), buffer2.end() - 1, is_cr()), buffer2.end()); 41 | 42 | return buffer1 == buffer2; 43 | } 44 | -------------------------------------------------------------------------------- /src/cpp11.cpp: -------------------------------------------------------------------------------- 1 | // Generated by cpp11: do not edit by hand 2 | // clang-format off 3 | 4 | #include "vdiffr_types.h" 5 | #include "cpp11/declarations.hpp" 6 | #include 7 | 8 | // compare.cpp 9 | bool compare_files(std::string expected, std::string test); 10 | extern "C" SEXP _vdiffr_compare_files(SEXP expected, SEXP test) { 11 | BEGIN_CPP11 12 | return cpp11::as_sexp(compare_files(cpp11::as_cpp>(expected), cpp11::as_cpp>(test))); 13 | END_CPP11 14 | } 15 | // devSVG.cpp 16 | bool svglite_(std::string file, std::string bg, double width, double height, double pointsize, bool standalone, bool always_valid); 17 | extern "C" SEXP _vdiffr_svglite_(SEXP file, SEXP bg, SEXP width, SEXP height, SEXP pointsize, SEXP standalone, SEXP always_valid) { 18 | BEGIN_CPP11 19 | return cpp11::as_sexp(svglite_(cpp11::as_cpp>(file), cpp11::as_cpp>(bg), cpp11::as_cpp>(width), cpp11::as_cpp>(height), cpp11::as_cpp>(pointsize), cpp11::as_cpp>(standalone), cpp11::as_cpp>(always_valid))); 20 | END_CPP11 21 | } 22 | // devSVG.cpp 23 | cpp11::external_pointer svgstring_(cpp11::environment env, std::string bg, double width, double height, double pointsize, bool standalone); 24 | extern "C" SEXP _vdiffr_svgstring_(SEXP env, SEXP bg, SEXP width, SEXP height, SEXP pointsize, SEXP standalone) { 25 | BEGIN_CPP11 26 | return cpp11::as_sexp(svgstring_(cpp11::as_cpp>(env), cpp11::as_cpp>(bg), cpp11::as_cpp>(width), cpp11::as_cpp>(height), cpp11::as_cpp>(pointsize), cpp11::as_cpp>(standalone))); 27 | END_CPP11 28 | } 29 | // devSVG.cpp 30 | std::string get_svg_content(cpp11::external_pointer p); 31 | extern "C" SEXP _vdiffr_get_svg_content(SEXP p) { 32 | BEGIN_CPP11 33 | return cpp11::as_sexp(get_svg_content(cpp11::as_cpp>>(p))); 34 | END_CPP11 35 | } 36 | // engine_version.cpp 37 | void set_engine_version(cpp11::strings version); 38 | extern "C" SEXP _vdiffr_set_engine_version(SEXP version) { 39 | BEGIN_CPP11 40 | set_engine_version(cpp11::as_cpp>(version)); 41 | return R_NilValue; 42 | END_CPP11 43 | } 44 | 45 | extern "C" { 46 | static const R_CallMethodDef CallEntries[] = { 47 | {"_vdiffr_compare_files", (DL_FUNC) &_vdiffr_compare_files, 2}, 48 | {"_vdiffr_get_svg_content", (DL_FUNC) &_vdiffr_get_svg_content, 1}, 49 | {"_vdiffr_set_engine_version", (DL_FUNC) &_vdiffr_set_engine_version, 1}, 50 | {"_vdiffr_svglite_", (DL_FUNC) &_vdiffr_svglite_, 7}, 51 | {"_vdiffr_svgstring_", (DL_FUNC) &_vdiffr_svgstring_, 6}, 52 | {NULL, NULL, 0} 53 | }; 54 | } 55 | 56 | extern "C" attribute_visible void R_init_vdiffr(DllInfo* dll){ 57 | R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); 58 | R_useDynamicSymbols(dll, FALSE); 59 | R_forceSymbols(dll, TRUE); 60 | } 61 | -------------------------------------------------------------------------------- /src/engine_version.cpp: -------------------------------------------------------------------------------- 1 | #include "engine_version.h" 2 | #include 3 | 4 | std::string ENGINE_VERSION = ""; 5 | 6 | std::string get_engine_version() { 7 | return ENGINE_VERSION; 8 | } 9 | 10 | [[cpp11::register]] 11 | void set_engine_version(cpp11::strings version) { 12 | ENGINE_VERSION = cpp11::as_cpp(version); 13 | } 14 | -------------------------------------------------------------------------------- /src/engine_version.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | std::string get_engine_version(); 6 | -------------------------------------------------------------------------------- /src/utils.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | inline static double dbl_format(double x) { 9 | if (std::abs(x) < std::numeric_limits::epsilon()) 10 | return 0.00; 11 | else 12 | return x; 13 | } 14 | 15 | inline bool iequals(const std::string& a, const std::string& b) { 16 | unsigned int sz = a.size(); 17 | if (b.size() != sz) { 18 | return false; 19 | } 20 | for (unsigned int i = 0; i < sz; ++i) { 21 | if (tolower(a[i]) != tolower(b[i])) { 22 | return false; 23 | } 24 | } 25 | return true; 26 | } 27 | 28 | /* 29 | Basic UTF-8 manipulation routines 30 | by Jeff Bezanson 31 | placed in the public domain Fall 2005 32 | 33 | This code is designed to provide the utilities you need to manipulate 34 | UTF-8 as an internal string encoding. These functions do not perform the 35 | error checking normally needed when handling UTF-8 data, so if you happen 36 | to be from the Unicode Consortium you will want to flay me alive. 37 | I do this because error checking can be performed at the boundaries (I/O), 38 | with these routines reserved for higher performance on data known to be 39 | valid. 40 | 41 | Source: https://www.cprogramming.com/tutorial/utf8.c 42 | 43 | Modified 2019 by Thomas Lin Pedersen to work with const char* 44 | */ 45 | 46 | static const uint32_t offsetsFromUTF8[6] = { 47 | 0x00000000UL, 0x00003080UL, 0x000E2080UL, 48 | 0x03C82080UL, 0xFA082080UL, 0x82082080UL 49 | }; 50 | 51 | static const char trailingBytesForUTF8[256] = { 52 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 53 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 54 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 55 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 56 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 57 | 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 58 | 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1, 59 | 2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2, 3,3,3,3,3,3,3,3,4,4,4,4,5,5,5,5 60 | }; 61 | 62 | /* conversions without error checking 63 | only works for valid UTF-8, i.e. no 5- or 6-byte sequences 64 | srcsz = source size in bytes, or -1 if 0-terminated 65 | sz = dest size in # of wide characters 66 | 67 | returns # characters converted 68 | dest will always be L'\0'-terminated, even if there isn't enough room 69 | for all the characters. 70 | if sz = srcsz+1 (i.e. 4*srcsz+4 bytes), there will always be enough space. 71 | */ 72 | static int u8_toucs(uint32_t *dest, int sz, const char *src, int srcsz) 73 | { 74 | uint32_t ch; 75 | const char *src_end = src + srcsz; 76 | int nb; 77 | int i=0; 78 | 79 | while (i < sz-1) { 80 | nb = trailingBytesForUTF8[(unsigned char)*src]; 81 | if (srcsz == -1) { 82 | if (*src == 0) 83 | goto done_toucs; 84 | } 85 | else { 86 | if (src + nb >= src_end) 87 | goto done_toucs; 88 | } 89 | ch = 0; 90 | switch (nb) { 91 | /* these fall through deliberately */ 92 | case 5: ch += (unsigned char)*src++; ch <<= 6; 93 | case 4: ch += (unsigned char)*src++; ch <<= 6; 94 | case 3: ch += (unsigned char)*src++; ch <<= 6; 95 | case 2: ch += (unsigned char)*src++; ch <<= 6; 96 | case 1: ch += (unsigned char)*src++; ch <<= 6; 97 | case 0: ch += (unsigned char)*src++; 98 | } 99 | ch -= offsetsFromUTF8[nb]; 100 | dest[i++] = ch; 101 | } 102 | done_toucs: 103 | dest[i] = 0; 104 | return i; 105 | } 106 | 107 | /* 108 | End of Basic UTF-8 manipulation routines 109 | by Jeff Bezanson 110 | */ 111 | 112 | class UTF_UCS { 113 | std::vector buffer; 114 | 115 | public: 116 | UTF_UCS() { 117 | // Allocate space in buffer 118 | buffer.resize(1024); 119 | } 120 | ~UTF_UCS() { 121 | } 122 | uint32_t * convert(const char * string, int &n_conv) { 123 | int n_bytes = strlen(string) + 1; 124 | unsigned int max_size = n_bytes * 4; 125 | if (buffer.size() < max_size) { 126 | buffer.resize(max_size); 127 | } 128 | 129 | n_conv = u8_toucs(buffer.data(), max_size, string, -1); 130 | 131 | return buffer.data(); 132 | } 133 | }; 134 | -------------------------------------------------------------------------------- /src/vdiffr_types.h: -------------------------------------------------------------------------------- 1 | #include 2 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library("testthat") 2 | library("vdiffr") 3 | test_check("vdiffr") 4 | 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/bar/expect-doppelganger/variant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | count 34 | count 35 | variant 36 | 37 | 38 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/base-doppelganger-with-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | x 29 | i 30 | + 31 | x 32 | y 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/grid-doppelganger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | foobar 22 | foobaz 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/grob.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/myplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 1 65 | 2 66 | 3 67 | 4 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 100 78 | 200 79 | 300 80 | 400 81 | disp 82 | count 83 | myplot 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/myplot2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/page-error1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/expect-doppelganger/page-error2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 1 26 | 2 27 | 3 28 | 29 | 30 | 31 | 32 | 1 33 | 2 34 | 3 35 | 36 | 37 | 38 | 39 | 1 40 | 2 41 | 3 42 | 43 | 1 44 | 2 45 | 3 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/foo/expect-doppelganger/variant.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | variant 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/ggplot/some-other-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/ggplot/some-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Some title 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/helper-mock.R: -------------------------------------------------------------------------------- 1 | vdiffr_skip_stale <- function() { 2 | skip_if_not_installed("base", "4.1.0") 3 | } 4 | 5 | create_mock_pkg <- function(pkg = "mock-pkg") { 6 | dir <- tempfile() 7 | 8 | dir.create(dir, recursive = TRUE) 9 | file.copy(pkg, dir, recursive = TRUE) 10 | 11 | # Enable `R CMD check` logging 12 | from <- file.path(dir, pkg) 13 | to <- paste0(from, ".Rcheck") 14 | file.rename(from, to) 15 | 16 | to 17 | } 18 | -------------------------------------------------------------------------------- /tests/testthat/helper-vdiffr.R: -------------------------------------------------------------------------------- 1 | regenerate_snapshots <- function() { 2 | FALSE 3 | } 4 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: mockpkg 2 | Title: title 3 | License: GPL-2 4 | Description: Package description. 5 | Author: author 6 | Maintainer: maintainer 7 | Version: 0.1 8 | Suggests: 9 | testthat, 10 | vdiffr 11 | Config/testthat/edition: 3 12 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/base-doppelganger-with-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | x 29 | i 30 | + 31 | x 32 | y 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/deps.txt: -------------------------------------------------------------------------------- 1 | - vdiffr-svg-engine: 2.0 2 | - vdiffr: 0.3.3.9000 3 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/ggplot/some-other-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/ggplot/some-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Some title 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/myplot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 1 65 | 2 66 | 3 67 | 4 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 100 78 | 200 79 | 300 80 | 400 81 | disp 82 | count 83 | myplot 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/myplot2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/figs/passed-plots/grid-doppelganger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | foobar 22 | foobaz 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat.R: -------------------------------------------------------------------------------- 1 | library("testthat") 2 | library("vdiffr") 3 | 4 | test_check("mockpkg") 5 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/ggplot/some-other-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/ggplot/some-title.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | Some title 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/new/alt1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/new/alt2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 1 65 | 2 66 | 3 67 | 4 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 100 78 | 200 79 | 300 80 | 400 81 | disp 82 | count 83 | alt2 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/new/context1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/new/new1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/new/new2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 0 64 | 1 65 | 2 66 | 3 67 | 4 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 100 78 | 200 79 | 300 80 | 400 81 | disp 82 | count 83 | new2 84 | 85 | 86 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/passed/base-doppelganger-with-symbol.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | x 29 | i 30 | + 31 | x 32 | y 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/passed/grid-doppelganger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | foobar 22 | foobaz 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/_snaps/passed/myplot2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 5 72 | 10 73 | 15 74 | 20 75 | 25 76 | 30 77 | 78 | 79 | 80 | 81 | 82 | 100 83 | 200 84 | 300 85 | 400 86 | 87 | Index 88 | mtcars$disp 89 | 90 | 91 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/test-failed.R: -------------------------------------------------------------------------------- 1 | library("vdiffr") 2 | 3 | p1_orig <- ggplot2::ggplot(mtcars, ggplot2::aes(disp)) + ggplot2::geom_histogram() 4 | p1_fail <- p1_orig + ggplot2::geom_vline(xintercept = 300) 5 | 6 | maintenance <- Sys.getenv("VDIFFR_REGENERATE_TESTS") == "yes" 7 | skip_if_maintenance <- function() { 8 | if (maintenance) { 9 | skip("maintenance") 10 | } 11 | } 12 | 13 | test_that("mismatches are hard failures when NOT_CRAN is set", { 14 | skip_if_maintenance() 15 | expect_doppelganger("myplot", p1_fail) 16 | }) 17 | 18 | test_that("Duplicated expectations issue a warning", { 19 | skip_if_maintenance() 20 | expect_doppelganger("myplot", p1_fail) 21 | }) 22 | 23 | test_that("mismatches are hard failures when CI is set", { 24 | skip_if_maintenance() 25 | withr::local_envvar(c(NOT_CRAN = "", CI = "true")) 26 | expect_doppelganger("myplot", p1_fail) 27 | }) 28 | 29 | test_that("mismatches are skipped when NOT_CRAN is unset", { 30 | skip_if_maintenance() 31 | withr::local_envvar(c(NOT_CRAN = "", CI = "")) 32 | expect_doppelganger("myplot", p1_fail) 33 | }) 34 | 35 | 36 | # Maintenance -------------------------------------------------------- 37 | 38 | test_that("(maintenance) Reset failing figure", { 39 | if (!maintenance) { 40 | skip("maintenance") 41 | } 42 | expect_doppelganger("myplot", p1_orig) 43 | }) 44 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/test-ggplot.R: -------------------------------------------------------------------------------- 1 | test_that("ggtitle is set correctly", { 2 | p <- ggplot2::ggplot() 3 | expect_doppelganger("Some title", p) 4 | expect_doppelganger("Some other title", p + ggplot2::ggtitle(NULL)) 5 | }) 6 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/test-new.R: -------------------------------------------------------------------------------- 1 | library("vdiffr") 2 | 3 | p1 <- function() plot(mtcars$disp) 4 | p2 <- ggplot2::ggplot(mtcars, ggplot2::aes(disp)) + 5 | ggplot2::geom_histogram() 6 | 7 | test_that("New plots are collected", { 8 | expect_doppelganger("new1", p1) 9 | expect_doppelganger("new2", p2) 10 | }) 11 | -------------------------------------------------------------------------------- /tests/testthat/mock-pkg/tests/testthat/test-passed.R: -------------------------------------------------------------------------------- 1 | library("vdiffr") 2 | 3 | test_that("ggplot doppelgangers pass", { 4 | p1_orig <- ggplot2::ggplot(mtcars, ggplot2::aes(disp)) + ggplot2::geom_histogram() 5 | expect_doppelganger("myplot", p1_orig) 6 | }) 7 | 8 | test_that("base doppelgangers pass", { 9 | p_base <- function() plot(mtcars$disp) 10 | expect_doppelganger("myplot2", p_base) 11 | 12 | p_base_symbol <- function() { 13 | plot.new() 14 | text(0.5, 0.8, expression(x[i] + over(x, y)), font = 5) 15 | } 16 | 17 | expect_doppelganger("Base doppelganger with symbol", p_base_symbol) 18 | }) 19 | 20 | library("grid") 21 | test_that("grid doppelgangers pass", { 22 | p_grid <- function() { 23 | grid.newpage() 24 | grid.text("foobar", gp = gpar(fontsize = 10.1)) 25 | grid.text("foobaz", 0.5, 0.1, gp = gpar(fontsize = 15.05)) 26 | } 27 | expect_doppelganger("Grid doppelganger", p_grid) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/testthat/test-expect-doppelganger.R: -------------------------------------------------------------------------------- 1 | vdiffr_skip_stale() 2 | 3 | test_that("ggplot doppelgangers pass", { 4 | skip_if_not_installed("ggplot2") 5 | p1_orig <- ggplot2::ggplot(mtcars, ggplot2::aes(disp)) + ggplot2::geom_histogram() 6 | expect_doppelganger("myplot", p1_orig) 7 | }) 8 | 9 | test_that("base doppelgangers pass", { 10 | p_base <- function() plot(mtcars$disp) 11 | expect_doppelganger("myplot2", p_base) 12 | 13 | p_base_symbol <- function() { 14 | plot.new() 15 | text(0.5, 0.8, expression(x[i] + over(x, y)), font = 5) 16 | } 17 | 18 | expect_doppelganger("Base doppelganger with symbol", p_base_symbol) 19 | }) 20 | 21 | test_that("grid doppelgangers pass", { 22 | p_grid <- function() { 23 | grid::grid.newpage() 24 | grid::grid.text("foobar", gp = grid::gpar(fontsize = 10.1)) 25 | grid::grid.text("foobaz", 0.5, 0.1, gp = grid::gpar(fontsize = 15.05)) 26 | } 27 | expect_doppelganger("Grid doppelganger", p_grid) 28 | }) 29 | 30 | test_that("no 'svglite supports one page' error (#85)", { 31 | test_draw_axis <- function(add_labels = FALSE) { 32 | theme <- theme_test() + theme(axis.line = element_line(size = 0.5)) 33 | positions <- c("top", "right", "bottom", "left") 34 | 35 | n_breaks <- 3 36 | break_positions <- seq_len(n_breaks) / (n_breaks + 1) 37 | labels <- if (add_labels) as.character(seq_along(break_positions)) 38 | 39 | # create the axes 40 | axes <- lapply(positions, function(position) { 41 | ggplot2:::draw_axis(break_positions, labels, axis_position = position, theme = theme) 42 | }) 43 | axes_grob <- gTree(children = do.call(gList, axes)) 44 | 45 | # arrange them so there's some padding on each side 46 | gt <- gtable( 47 | widths = unit(c(0.05, 0.9, 0.05), "npc"), 48 | heights = unit(c(0.05, 0.9, 0.05), "npc") 49 | ) 50 | gtable_add_grob(gt, list(axes_grob), 2, 2, clip = "off") 51 | } 52 | environment(test_draw_axis) <- env(ns_env("ggplot2")) 53 | 54 | expect_doppelganger("page-error1", test_draw_axis(FALSE)) 55 | expect_doppelganger("page-error2", test_draw_axis(TRUE)) 56 | }) 57 | 58 | test_that("supports `grob` objects (#36)", { 59 | circle <- grid::circleGrob( 60 | x = 0.5, 61 | y = 0.5, 62 | r = 0.5, 63 | gp = grid::gpar(col = "gray", lty = 3) 64 | ) 65 | expect_doppelganger("grob", circle) 66 | }) 67 | 68 | test_that("skips and unexpected errors reset snapshots (r-lib/testthat#1393)", { 69 | regenerate <- FALSE 70 | 71 | if (regenerate) { 72 | withr::local_envvar(c(VDIFFR_REGENERATE_SNAPS = "true")) 73 | } 74 | 75 | suppressMessages( 76 | test_file( 77 | test_path("test-snapshot", "test-snapshot.R"), 78 | reporter = NULL 79 | ) 80 | ) 81 | 82 | expect_true(file.exists("test-snapshot/_snaps/snapshot/error-resets-snapshots.svg")) 83 | expect_true(file.exists("test-snapshot/_snaps/snapshot/skip-resets-snapshots.svg")) 84 | }) 85 | 86 | test_that("`expect_doppelganger()` supports variants", { 87 | p1 <- ggplot2::ggplot() 88 | p2 <- ggplot2::ggplot() + ggplot2::geom_histogram() 89 | expect_doppelganger("variant", p1, variant = "foo") 90 | expect_doppelganger("variant", p2, variant = "bar") 91 | }) 92 | -------------------------------------------------------------------------------- /tests/testthat/test-ggplot.R: -------------------------------------------------------------------------------- 1 | vdiffr_skip_stale() 2 | skip_if_not_installed("ggplot2") 3 | 4 | test_that("ggtitle is set correctly", { 5 | p <- ggplot2::ggplot() 6 | expect_doppelganger("Some title", p) 7 | expect_doppelganger("Some other title", p + ggplot2::ggtitle(NULL)) 8 | }) 9 | -------------------------------------------------------------------------------- /tests/testthat/test-log.R: -------------------------------------------------------------------------------- 1 | vdiffr_skip_stale() 2 | 3 | test_that("Failures are logged", { 4 | skip_on_cran() 5 | 6 | reporter <- testthat::SilentReporter$new() 7 | 8 | path <- create_mock_pkg() 9 | vars <- c("CI" = "true") 10 | suppressMessages(withr::with_envvar(vars, 11 | testthat::test_dir( 12 | file.path(path, "tests", "testthat"), 13 | reporter = reporter, 14 | stop_on_failure = FALSE, 15 | stop_on_warning = FALSE 16 | ) 17 | )) 18 | 19 | log_path <- file.path(path, "tests", "vdiffr.Rout.fail") 20 | if (!file.exists(log_path)) { 21 | abort("Can't find log.") 22 | } 23 | 24 | on.exit(file.remove(log_path)) 25 | 26 | log <- readLines(log_path) 27 | 28 | results <- reporter$expectations() 29 | n_expected <- sum(vapply(results, inherits, NA, "expectation_failure")) 30 | 31 | n_logged <- length(grep("Failed doppelganger:", log)) 32 | expect_equal(n_logged, n_expected) 33 | }) 34 | -------------------------------------------------------------------------------- /tests/testthat/test-snapshot/_snaps/snapshot/error-resets-snapshots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 1.0 40 | 1.5 41 | 2.0 42 | 2.5 43 | 3.0 44 | 45 | 46 | 47 | 48 | 49 | 50 | 1.0 51 | 1.5 52 | 2.0 53 | 2.5 54 | 3.0 55 | 56 | Index 57 | 1:3 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/testthat/test-snapshot/_snaps/snapshot/skip-resets-snapshots.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 1.0 40 | 1.5 41 | 2.0 42 | 2.5 43 | 3.0 44 | 45 | 46 | 47 | 48 | 49 | 50 | 1.0 51 | 1.5 52 | 2.0 53 | 2.5 54 | 3.0 55 | 56 | Index 57 | 1:3 58 | 59 | 60 | -------------------------------------------------------------------------------- /tests/testthat/test-snapshot/test-snapshot.R: -------------------------------------------------------------------------------- 1 | test_that("errors reset snapshots", { 2 | if (nzchar(Sys.getenv("VDIFFR_REGENERATE_SNAPS"))) { 3 | expect_doppelganger("error resets snapshots", function() plot(1:3)) 4 | } else { 5 | expect_doppelganger("error resets snapshots", function() stop("failing")) 6 | } 7 | }) 8 | 9 | test_that("skips reset snapshots", { 10 | if (nzchar(Sys.getenv("VDIFFR_REGENERATE_SNAPS"))) { 11 | expect_doppelganger("skip resets snapshots", function() plot(1:3)) 12 | } else { 13 | expect_doppelganger("skip resets snapshots", function() skip("skipping")) 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /tools/winlibs.R: -------------------------------------------------------------------------------- 1 | VERSION <- commandArgs(TRUE) 2 | if(!file.exists(sprintf("../windows/harfbuzz-%s/include/png.h", VERSION))){ 3 | if(getRversion() < "3.3.0") setInternet2() 4 | download.file(sprintf("https://github.com/rwinlib/harfbuzz/archive/v%s.zip", VERSION), "lib.zip", quiet = TRUE) 5 | dir.create("../windows", showWarnings = FALSE) 6 | unzip("lib.zip", exdir = "../windows") 7 | unlink("lib.zip") 8 | } 9 | -------------------------------------------------------------------------------- /vdiffr.Rproj: -------------------------------------------------------------------------------- 1 | Version: 1.0 2 | 3 | RestoreWorkspace: Default 4 | SaveWorkspace: Default 5 | AlwaysSaveHistory: Default 6 | 7 | EnableCodeIndexing: Yes 8 | UseSpacesForTab: Yes 9 | NumSpacesForTab: 2 10 | Encoding: UTF-8 11 | 12 | RnwWeave: Sweave 13 | LaTeX: pdfLaTeX 14 | 15 | BuildType: Package 16 | PackageUseDevtools: Yes 17 | PackageInstallArgs: --no-multiarch --with-keep.source 18 | --------------------------------------------------------------------------------