Skip to content

Commit 2f01222

Browse files
authored
Improve tolerance for screenshot test comparisons (#4173)
1 parent a5fe0fe commit 2f01222

File tree

1 file changed

+64
-8
lines changed

1 file changed

+64
-8
lines changed

CodenameOne/src/com/codename1/testing/TestUtils.java

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -589,15 +589,11 @@ public static boolean screenshotTest(String screenshotName) {
589589
Display.getInstance().getCurrent().paintComponent(mute.getGraphics(), true);
590590
screenshotName = screenshotName + ".png";
591591
if (Storage.getInstance().exists(screenshotName)) {
592-
int[] rgba = mute.getRGBCached();
593592
Image orig = Image.createImage(Storage.getInstance().createInputStream(screenshotName));
594-
int[] origRgba = orig.getRGBCached();
595-
for (int iter = 0; iter < rgba.length; iter++) {
596-
if (rgba[iter] != origRgba[iter]) {
597-
log("screenshots do not match at offset " + iter + " saving additional image under " + screenshotName + ".fail");
598-
io.save(mute, Storage.getInstance().createOutputStream(screenshotName + ".fail"), ImageIO.FORMAT_PNG, 1);
599-
return false;
600-
}
593+
if (!imagesWithinTolerance(mute, orig)) {
594+
log("screenshots do not match within tolerance; saving additional image under " + screenshotName + ".fail");
595+
io.save(mute, Storage.getInstance().createOutputStream(screenshotName + ".fail"), ImageIO.FORMAT_PNG, 1);
596+
return false;
601597
}
602598
} else {
603599
io.save(mute, Storage.getInstance().createOutputStream(screenshotName), ImageIO.FORMAT_PNG, 1);
@@ -609,6 +605,66 @@ public static boolean screenshotTest(String screenshotName) {
609605
}
610606
}
611607

608+
private static boolean imagesWithinTolerance(Image candidate, Image baseline) {
609+
int[] candidateRgba = candidate.getRGBCached();
610+
int[] baselineRgba = baseline.getRGBCached();
611+
if (candidateRgba.length != baselineRgba.length) {
612+
log("Screenshot sizes differ: candidate has " + candidateRgba.length + " pixels while baseline has " + baselineRgba.length);
613+
return false;
614+
}
615+
616+
// Allow small, widespread differences and a tiny proportion of slightly larger
617+
// differences to accommodate minor rendering artifacts. These values are tuned
618+
// to keep comparisons strict while ignoring harmless noise we see across
619+
// devices/runs.
620+
final int toleratedChannelDelta = 3; // Difference that triggers "pixel differs" counting
621+
final double maxDifferentPixelsRatio = 0.01; // Up to 1% of pixels may exceed toleratedChannelDelta
622+
final double maxAverageChannelDelta = 2.5; // Average absolute delta per channel across whole image
623+
final double maxRmsChannelDelta = 5.0; // RMS delta per channel across whole image
624+
625+
int differingPixels = 0;
626+
long totalChannelDelta = 0;
627+
long totalChannelDeltaSquared = 0;
628+
629+
for (int iter = 0; iter < candidateRgba.length; iter++) {
630+
int candidatePixel = candidateRgba[iter];
631+
int baselinePixel = baselineRgba[iter];
632+
633+
int deltaR = Math.abs(((candidatePixel >> 16) & 0xff) - ((baselinePixel >> 16) & 0xff));
634+
int deltaG = Math.abs(((candidatePixel >> 8) & 0xff) - ((baselinePixel >> 8) & 0xff));
635+
int deltaB = Math.abs((candidatePixel & 0xff) - (baselinePixel & 0xff));
636+
int deltaA = Math.abs(((candidatePixel >> 24) & 0xff) - ((baselinePixel >> 24) & 0xff));
637+
638+
int maxChannelDelta = Math.max(Math.max(deltaR, deltaG), Math.max(deltaB, deltaA));
639+
if (maxChannelDelta > toleratedChannelDelta) {
640+
differingPixels++;
641+
}
642+
643+
totalChannelDelta += deltaR + deltaG + deltaB + deltaA;
644+
totalChannelDeltaSquared += deltaR * (long) deltaR
645+
+ deltaG * (long) deltaG
646+
+ deltaB * (long) deltaB
647+
+ deltaA * (long) deltaA;
648+
}
649+
650+
double differentPixelRatio = differingPixels / (double) candidateRgba.length;
651+
double averageChannelDelta = totalChannelDelta / (double) (candidateRgba.length * 4);
652+
double rmsChannelDelta = Math.sqrt(totalChannelDeltaSquared / (double) (candidateRgba.length * 4));
653+
654+
boolean withinThresholds = differentPixelRatio <= maxDifferentPixelsRatio
655+
&& averageChannelDelta <= maxAverageChannelDelta
656+
&& rmsChannelDelta <= maxRmsChannelDelta;
657+
658+
if (!withinThresholds) {
659+
log("Screenshot differs: "
660+
+ (differentPixelRatio * 100.0) + "% pixels exceed delta " + toleratedChannelDelta
661+
+ ", avg channel delta=" + averageChannelDelta
662+
+ ", rms channel delta=" + rmsChannelDelta);
663+
}
664+
665+
return withinThresholds;
666+
}
667+
612668
/**
613669
* Log to the test log
614670
*

0 commit comments

Comments
 (0)