2015-05-14 13 views
31

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 
+0

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

Odpowiedz

12

Mango Solutions opublikowały pakiet open source, visualTest, że robi rozmyte dopasowanie działek, aby rozwiązać ten przypadek użycia.

Pakiet jest github, więc zainstalować przy użyciu:

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

Następnie użyj funkcji getFingerprint() wydobyć odcisk palca dla każdej działki i porównać przy użyciu funkcji isSimilar(), określając odpowiedni próg.

Najpierw utwórz kilka działek o pliku:

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

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

Odcisk palca jest wektorem numeryczna:

> 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 

Porównaj użyciu isSimilar():

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

Można przeczytaj więcej o pakiecie na stronie http://www.mango-solutions.com/wp/products-services/r-services/r-packages/visualtest/

+0

Świetnie! To jest dokładnie to, czego szukałem! Niestety pakiet nie jest jeszcze na CRAN, ale w większości przypadków jest to w porządku. – sgibb

0

Warto zauważyć, że pakiet vdiffr obsługuje także porównywanie działek. Ciekawą cechą jest to, że integruje się z pakietem testthat - w rzeczywistości jest używany do testowania w ggplot2 - i ma dodatek do RStudio, aby pomóc w zarządzaniu testsuite.

Powiązane problemy