-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathexpect-doppelganger.R
More file actions
227 lines (205 loc) · 7.19 KB
/
Copy pathexpect-doppelganger.R
File metadata and controls
227 lines (205 loc) · 7.19 KB
1
2
3
4
5
6
7
8
9
10
11
12
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
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
#' Does a figure look like its expected output?
#'
#' @description
#'
#' `expect_doppelganger()` is a testthat expectation for graphical
#' plots. It generates SVG snapshots that you can review graphically
#' with [testthat::snapshot_review()]. You will find more information
#' about snapshotting in the [testthat snapshots
#' vignette](https://testthat.r-lib.org/articles/snapshotting.html).
#'
#' Note that `expect_doppelganger()` requires R version 4.1.0. If run
#' on an earlier version of R, it emits a `testthat::skip()` so that you
#' can still run other checks on old versions of R.
#'
#' @param title A brief description of what is being tested in the
#' figure. For instance: "Points and lines overlap".
#'
#' If a ggplot2 figure doesn't have a title already, `title` is
#' applied to the figure with `ggtitle()`.
#'
#' The title is also used as file name for storing SVG (in a
#' sanitized form, with special characters converted to `"-"`).
#' @param fig A figure to test. This can be a ggplot object, a
#' recordedplot, or more generally any object with a `print` method.
#'
#' If you need to test a plot with non-printable objects (e.g. base
#' plots), `fig` can be a function that generates and prints the
#' plot, e.g. `fig = function() plot(1:3)`.
#' @param path,... `r lifecycle::badge('deprecated')`.
#' @param writer A function that takes the plot, a target SVG file,
#' and an optional plot title. It should transform the plot to SVG
#' in a deterministic way and write it to the target file. See
#' [write_svg()] (the default) for an example.
#' @param cran If `FALSE` (the default), mismatched snapshots only
#' cause a failure when you run tests locally or in your CI (Github
#' Actions or any platform that sets the `CI` environment variable).
#' If `TRUE`, failures may also occur on CRAN machines.
#'
#' Failures are disabled on CRAN by default because testing the
#' appearance of a figure is inherently fragile. Changes in the R
#' graphics engine or in ggplot2 may cause subtle differences in the
#' aspect of a plot, such as a slightly smaller or larger margin.
#' These changes will cause spurious failures because you need to
#' update your snapshots to reflect the upstream changes.
#'
#' It would be distracting for both you and the CRAN maintainers if
#' such changes systematically caused failures on CRAN. This is why
#' snapshot expectations do not fail on CRAN by default and should
#' be treated as a monitoring tool that allows you to quickly check
#' how the appearance of your figures changes over time, and to
#' manually assess whether changes reflect actual problems in your
#' package.
#'
#' Internally, this argument is passed to
#' [testthat::expect_snapshot_file()].
#'
#' @inheritParams testthat::expect_snapshot_file
#'
#' @section Debugging:
#' It is sometimes difficult to understand the cause of a failure.
#' This usually indicates that the plot is not created
#' deterministically. Potential culprits are:
#'
#' * Some of the plot components depend on random variation. Try
#' setting a seed.
#'
#' * 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.
#'
#' 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 on Travis.
#'
#' 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.
#'
#' @examples
#' if (FALSE) { # Not run
#'
#' library("ggplot2")
#'
#' test_that("plots have known output", {
#' disp_hist_base <- function() hist(mtcars$disp)
#' expect_doppelganger("disp-histogram-base", disp_hist_base)
#'
#' disp_hist_ggplot <- ggplot(mtcars, aes(disp)) + geom_histogram()
#' expect_doppelganger("disp-histogram-ggplot", disp_hist_ggplot)
#' })
#'
#' }
#' @export
expect_doppelganger <- function(title,
fig,
path = deprecated(),
...,
writer = write_svg,
cran = FALSE,
variant = NULL) {
testthat::local_edition(3)
if (!is_string(title)) {
abort("`title` must be a string")
}
fig_name <- str_standardise(title)
file <- paste0(fig_name, ".svg")
# Announce snapshot file before touching `fig` in case evaluation
# causes an error. This allows testthat to restore the files
# (see r-lib/testthat#1393).
testthat::announce_snapshot_file(name = file)
testcase <- make_testcase_file(fig_name)
writer(fig, testcase, title)
if (!missing(...)) {
lifecycle::deprecate_soft(
"1.0.0",
"vdiffr::expect_doppelganger(... = )",
)
}
if (lifecycle::is_present(path)) {
lifecycle::deprecate_soft(
"1.0.0",
"vdiffr::expect_doppelganger(path = )",
)
}
if (is_graphics_engine_stale()) {
testthat::skip(paste_line(
"The R graphics engine is too old.",
"Please update to R 4.1.0 and regenerate the vdiffr snapshots."
))
}
withCallingHandlers(
testthat::expect_snapshot_file(
testcase,
name = file,
cran = cran,
variant = variant,
compare = testthat::compare_file_text
),
expectation_failure = function(cnd) {
if (is_snapshot_stale(title, testcase)) {
warn(paste_line(
"SVG snapshot generated under a different vdiffr version.",
"i" = "Please update your snapshots."
))
}
if (!is_null(snapshotter <- get_snapshotter())) {
path_old <- snapshot_path(snapshotter, file)
path_new <- snapshot_path(snapshotter, paste0(fig_name, ".new.svg"))
if (all(file.exists(path_old, path_new))) {
push_log(fig_name, path_old, path_new)
}
}
}
)
}
# From testthat
get_snapshotter <- function() {
x <- getOption("testthat.snapshotter")
if (is.null(x)) {
return()
}
if (!x$is_active()) {
return()
}
x
}
snapshot_path <- function(snapshotter, file) {
file.path(snapshotter$snap_dir, snapshotter$file, file)
}
is_graphics_engine_stale <- function() {
getRversion() < "4.1.0"
}
str_standardise <- function(s, sep = "-") {
stopifnot(is_scalar_character(s))
s <- gsub("[^a-z0-9]", sep, tolower(s))
s <- gsub(paste0(sep, sep, "+"), sep, s)
s <- gsub(paste0("^", sep, "|", sep, "$"), "", s)
s
}
is_snapshot_stale <- function(title, testcase) {
if (is_null(snapshotter <- get_snapshotter())) {
return(FALSE)
}
file <- paste0(str_standardise(title), ".svg")
path <- snapshot_path(snapshotter, file)
if (!file.exists(path)) {
return(FALSE)
}
lines <- readLines(path)
match <- regexec(
"data-engine-version='([0-9.]+)'",
lines
)
match <- Filter(length, regmatches(lines, match))
# Old vdiffr snapshot that doesn't embed a version
if (!length(match)) {
return(TRUE)
}
if (length(match) > 1) {
abort("Found multiple vdiffr engine versions in SVG snapshot.")
}
snapshot_version <- match[[1]][[2]]
svg_engine_ver() != snapshot_version
}