Подтвердить что ты не робот

Как проверить графический вывод функций?

Мне интересно, как тестировать функции, которые производят графику. У меня есть простой функция построения графика 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
4b9b3361

Ответ 1

Mango Solutions опубликовали пакет с открытым исходным кодом visualTest, который выполняет нечеткое сопоставление графиков, чтобы решить этот прецедент.

Пакет находится на github, поэтому установите его с помощью:

devtools::install_github("MangoTheCat/visualTest")
library(visualTest)

Затем используйте функцию getFingerprint(), чтобы извлечь отпечаток пальца для каждого графика, и сравните с помощью функции isSimilar(), указав подходящий порог.

Сначала создайте несколько графиков в файле:

png(filename = "test1.png")
img()
dev.off()

png(filename = "test2.png")
plot(1:11, col="red")
dev.off()

Отпечаток пальца - это числовой вектор:

> getFingerprint(file = "test1.png")
 [1]  4  7  4  4 10  4  7  7  4  7  7  4  7  4  5  9  4  7  7  5  6  7  4  7  4  4 10
[28]  4  7  7  4  7  7  4  7  4  3  7  4  4  3  4  4  5  5  4  7  4  7  4  7  7  7  4
[55]  7  7  4  7  4  7  5  6  7  7  4  8  6  4  7  4  7  4  7  7  7  4  4 10  4  7  4

> getFingerprint(file = "test2.png")
 [1]  7  7  4  4 17  4  7  4  7  4  7  7  4  5  9  4  7  7  5  6  7  4  7  7 11  4  7
[28]  7  5  6  7  4  7  4 14  4  3  4  7 11  7  4  7  5  6  7  7  4  7 11  7  4  7  5
[55]  6  7  7  4  8  6  4  7  7  4  4  7  7  4 10 11  4  7  7

Сравните с помощью isSimilar():

> isSimilar(file = "test2.png",
+           fingerprint = getFingerprint(file = "test1.png"),
+           threshold = 0.1
+ )
[1] FALSE

Подробнее о пакете можно узнать в http://www.mango-solutions.com/wp/products-services/r-services/r-packages/visualtest/

Ответ 2

Стоит отметить, что пакет vdiffr также поддерживает сравнение графиков. Хорошей особенностью является то, что он интегрируется с пакетом testthat - он фактически используется для тестирования в ggplot2 - и у него есть надстройка для RStudio, которая помогает управлять вашим testuite.