Zastanawiam się, jak przetestować funkcje, które produkują grafikę. Mam prostą funkcję kreślenia img
:Jak przetestować wyjście graficzne funkcji?
img <- function() {
plot(1:10)
}
W moim pakiecie Lubię tworzyć badanej jednostki dla tej funkcji przy użyciu testthat
. Ponieważ plot
i jego przyjaciół w grafik bazowych prostu wrócić NULL
prosty expect_identical
nie działa:
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)
Najpierw myślałem o spiskowanie w pliku i porównać sumy kontrolne MD5 do upewnić się, że wyjście z funkcje są równe:
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)
Działa to dobrze w systemie Linux, ale nie w systemie Windows. Zaskakująco md5plot(plot(1:10))
daje w wyniku nowe md5sum przy każdym połączeniu. Pomijając ten problem, muszę utworzyć wiele plików tymczasowych.
Następnie użyłem recordPlot
(najpierw tworząc urządzenie zerowe, wywołać funkcję plotowania i zapisać jej wynik). Działa to zgodnie z oczekiwaniami:
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)
Czy ktoś zna lepszy sposób sprawdzania wydajności graficznej funkcji?
EDIT: odnośnie do punktów @josilber pyta w swoich komentarzach.
Podczas gdy podejście recordPlot
działa dobrze, należy przepisać całą funkcję drukowania w teście jednostki. To staje się skomplikowane w przypadku złożonych funkcji kreślenia. Byłoby miło mieć podejście, które pozwala na przechowywanie pliku (*.RData
lub *.pdf
, ...), który zawiera obraz przeciw tobie mógł porównać w przyszłych testach. Podejście md5sum
nie działa, ponieważ sumy md5 różnią się na różnych platformach. Via recordPlot
można utworzyć plik *.RData
ale nie można polegać na jego formatu (od recordPlot
ręcznego strony):
Format zarejestrowanych działek może zmieniać się pomiędzy wersjami R. Zarejestrowane działki mogą nie być używane jako stały format przechowywania dla działek R.
Może byłoby to możliwe, aby zapisać plik obrazu (*.png
, *.bmp
itp), importować i porównać ją piksel po pikselu ...
EDIT2: Poniższy kod ilustruje żądaną odniesienie podejście do pliku przy użyciu svg jako wyjścia. Najpierw potrzebne funkcje pomocnicze:
## 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's 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()
}
Test będzie:
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)
Niestety to nie działa na różnych platformach.Kolejność (i nazwy) elementów svg różni się całkowicie w systemach Linux i Windows.
Podobne problemy istnieją dla png
, jpeg
i recordPlot
. Powstałe pliki różnią się na wszystkich platformach.
Obecnie jedynym działającym rozwiązaniem jest powyższe podejście recPlot
. Ale dlatego muszę przepisać wszystkie funkcje kreślenia w moich testach jednostkowych.
P.S .: Jestem kompletnie zdezorientowany na temat różnych sum Md5 w systemie Windows. Wydaje się, że w zależności od czasu tworzenia plików tymczasowych:
# on Windows
table(sapply(1:100, function(x)md5plot(plot(1:10))))
#4693c8bcf6b6cb78ce1fc7ca41831353 51e8845fead596c86a3f0ca36495eacb
# 40 60
Wygląda na to, że twoje rozwiązanie 'recordPlot' działa dobrze w twoim przypadku użycia, ale wtedy pytasz na końcu pytania, czy ktokolwiek zna lepszy sposób testowania. Czy mógłbyś rozwinąć to, czego szukasz, również dlatego, że twoje obecne podejście do 'recordPlot' nie jest wystarczające? – josliber