Мне интересно, как тестировать функции, которые производят графику. У меня есть простой
функция построения графика img
:
img <- function() {
plot(1:10)
}
В моем пакете мне нравится создавать unit test для этой функции, используя testthat
.
Поскольку plot
и ее друзья в базовой графике просто возвращают NULL
простую
expect_identical
не работает:
library("testthat")
## example for a successful test
expect_identical(plot(1:10), img()) ## equal (as expected)
## example for a test failure
expect_identical(plot(1:10, col="red"), img()) ## DOES NOT FAIL!
# (because both return NULL)
Сначала я подумал о том, чтобы занести в файл и сравнить контрольные суммы md5 с убедитесь, что выход функций равен:
md5plot <- function(expr) {
file <- tempfile(fileext=".pdf")
on.exit(unlink(file))
pdf(file)
expr
dev.off()
unname(tools::md5sum(file))
}
## example for a successful test
expect_identical(md5plot(img()),
md5plot(plot(1:10))) ## equal (as expected)
## example for a test failure
expect_identical(md5plot(img()),
md5plot(plot(1:10, col="red"))) ## not equal (as expected)
Это хорошо работает в Linux, но не в Windows. Как ни странно
md5plot(plot(1:10))
приводит к новому md5sum при каждом вызове.
Помимо этой проблемы мне нужно создать много временных файлов.
Далее я использовал recordPlot
(сначала создав нулевое устройство, вызвав график
функция и запись ее выхода). Это работает как ожидалось:
recPlot <- function(expr) {
pdf(NULL)
on.exit(dev.off())
dev.control(displaylist="enable")
expr
recordPlot()
}
## example for a successful test
expect_identical(recPlot(plot(1:10)),
recPlot(img())) ## equal (as expected)
## example for a test failure
expect_identical(recPlot(plot(1:10, col="red")),
recPlot(img())) ## not equal (as expected)
Кто-нибудь знает лучший способ проверить графический вывод функций?
РЕДАКТИРОВАТЬ: о точках @josilber спрашивает в своих комментариях.
В то время как подход recordPlot
работает хорошо, вам нужно переписать всю функцию построения графика в unit test. Это усложняется для сложных функций построения. Было бы неплохо иметь подход, который позволяет хранить файл (*.RData
или *.pdf
,...), который содержит изображение против вас, которое можно сравнить в будущих тестах. Подход md5sum
не работает, потому что md5sums отличаются на разных платформах. Через recordPlot
вы можете создать файл *.RData
, но не можете полагаться на его формат (с страницы руководства recordPlot
):
Формат записанных графиков может изменяться между версиями R. Записанные графики не могут использоваться в качестве формата постоянного хранения для R.
Возможно, будет возможно сохранить файл изображения (*.png
, *.bmp
и т.д.), импортировать его и сравнить его по пикселям...
EDIT2: Следующий код иллюстрирует нужный ссылочный файл, используя svg в качестве вывода. Сначала необходимы вспомогательные функции:
## plot to svg and return file contant as character
plot_image <- function(expr) {
file <- tempfile(fileext=".svg")
on.exit(unlink(file))
svg(file)
expr
dev.off()
readLines(file)
}
## the IDs differ at each `svg` call, that why we simple remove them
ignore_svg_id <- function(lines) {
gsub(pattern = "(xlink:href|id)=\"#?([a-z0-9]+)-?(?<![0-9])[0-9]+\"",
replacement = "\\1=\"\\2\"", x = lines, perl = TRUE)
}
## compare svg character vs reference
expect_image_equal <- function(object, expected, ...) {
stopifnot(is.character(expected) && file.exists(expected))
expect_equal(ignore_svg_id(plot_image(object)),
ignore_svg_id(readLines(expected)), ...)
}
## create reference image
create_reference_image <- function(expr, file) {
svg(file)
expr
dev.off()
}
Тест:
create_reference_image(img(), "reference.svg")
## create tests
library("testthat")
expect_image_equal(img(), "reference.svg") ## equal (as expected)
expect_image_equal(plot(1:10, col="red"), "reference.svg") ## not equal (as expected)
К сожалению, это не работает на разных платформах. Порядок (и имена) элементов svg полностью отличается от Linux и Windows.
Аналогичные проблемы существуют для png
, jpeg
и recordPlot
. Полученные файлы
отличаются на всех платформах.
В настоящее время единственным рабочим решением является подход recPlot
выше. Но поэтому
Мне нужно переписать все функции построения в моих модульных тестах.
P.S.: Я полностью запутался в разных md5sums в Windows. Кажется, они зависят от времени создания временных файлов:
# on Windows
table(sapply(1:100, function(x)md5plot(plot(1:10))))
#4693c8bcf6b6cb78ce1fc7ca41831353 51e8845fead596c86a3f0ca36495eacb
# 40 60