Allow API version-specific view tests; use relative image distance
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
P=/sdcard/Android/data/org.isoron.uhabits/cache/Failed/
|
P=/sdcard/Android/data/org.isoron.uhabits/cache/Failed/
|
||||||
|
|
||||||
adb pull $P
|
adb pull $P Failed/
|
||||||
adb shell rm -rf $P
|
adb shell rm -r $P
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
BIN
app/src/androidTest/assets/views/CheckmarkView/checked.png
Normal file
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
BIN
app/src/androidTest/assets/views/CheckmarkView/large_size.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
app/src/androidTest/assets/views/CheckmarkView/unchecked.png
Normal file
|
After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
@@ -53,7 +53,7 @@ public class CheckmarkViewTest extends ViewTest
|
|||||||
@Test
|
@Test
|
||||||
public void render_checked() throws IOException
|
public void render_checked() throws IOException
|
||||||
{
|
{
|
||||||
assertRenders(view, "Views/CheckmarkView/checked.png");
|
assertRenders(view, "CheckmarkView/checked.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -62,7 +62,7 @@ public class CheckmarkViewTest extends ViewTest
|
|||||||
habit.repetitions.toggle(DateHelper.getStartOfToday());
|
habit.repetitions.toggle(DateHelper.getStartOfToday());
|
||||||
view.refreshData();
|
view.refreshData();
|
||||||
|
|
||||||
assertRenders(view, "Views/CheckmarkView/unchecked.png");
|
assertRenders(view, "CheckmarkView/unchecked.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -75,13 +75,13 @@ public class CheckmarkViewTest extends ViewTest
|
|||||||
habit.repetitions.toggle(today - 2 * day);
|
habit.repetitions.toggle(today - 2 * day);
|
||||||
view.refreshData();
|
view.refreshData();
|
||||||
|
|
||||||
assertRenders(view, "Views/CheckmarkView/implicitly_checked.png");
|
assertRenders(view, "CheckmarkView/implicitly_checked.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void render_largeSize() throws IOException
|
public void render_largeSize() throws IOException
|
||||||
{
|
{
|
||||||
measureView(dpToPixels(300), dpToPixels(300), view);
|
measureView(dpToPixels(300), dpToPixels(300), view);
|
||||||
assertRenders(view, "Views/CheckmarkView/large_size.png");
|
assertRenders(view, "CheckmarkView/large_size.png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ public class RingViewTest extends ViewTest
|
|||||||
public void render_base() throws IOException
|
public void render_base() throws IOException
|
||||||
{
|
{
|
||||||
measureView(dpToPixels(100), dpToPixels(100), view);
|
measureView(dpToPixels(100), dpToPixels(100), view);
|
||||||
assertRenders(view, "Views/RingView/render.png");
|
assertRenders(view, "RingView/render.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -61,7 +61,7 @@ public class RingViewTest extends ViewTest
|
|||||||
view.setLabel("The quick brown fox jumps over the lazy fox");
|
view.setLabel("The quick brown fox jumps over the lazy fox");
|
||||||
|
|
||||||
measureView(dpToPixels(100), dpToPixels(100), view);
|
measureView(dpToPixels(100), dpToPixels(100), view);
|
||||||
assertRenders(view, "Views/RingView/renderLongLabel.png");
|
assertRenders(view, "RingView/renderLongLabel.png");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -73,6 +73,6 @@ public class RingViewTest extends ViewTest
|
|||||||
view.setColor(ColorHelper.palette[5]);
|
view.setColor(ColorHelper.palette[5]);
|
||||||
|
|
||||||
measureView(dpToPixels(200), dpToPixels(200), view);
|
measureView(dpToPixels(200), dpToPixels(200), view);
|
||||||
assertRenders(view, "Views/RingView/renderDifferentParams.png");
|
assertRenders(view, "RingView/renderDifferentParams.png");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ import static junit.framework.Assert.fail;
|
|||||||
|
|
||||||
public class ViewTest
|
public class ViewTest
|
||||||
{
|
{
|
||||||
protected static final int SIMILARITY_CUTOFF = 6000;
|
protected static final double SIMILARITY_CUTOFF = 0.02;
|
||||||
|
public static final int HISTOGRAM_BIN_SIZE = 8;
|
||||||
|
|
||||||
protected Context testContext;
|
protected Context testContext;
|
||||||
protected Context targetContext;
|
protected Context targetContext;
|
||||||
@@ -61,37 +62,39 @@ public class ViewTest
|
|||||||
protected void assertRenders(View view, String expectedImagePath) throws IOException
|
protected void assertRenders(View view, String expectedImagePath) throws IOException
|
||||||
{
|
{
|
||||||
StringBuilder errorMessage = new StringBuilder();
|
StringBuilder errorMessage = new StringBuilder();
|
||||||
|
expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
|
||||||
|
|
||||||
view.setDrawingCacheEnabled(true);
|
view.setDrawingCacheEnabled(true);
|
||||||
view.buildDrawingCache();
|
view.buildDrawingCache();
|
||||||
|
Bitmap actual = view.getDrawingCache();
|
||||||
|
Bitmap expected = getBitmapFromAssets(expectedImagePath);
|
||||||
|
|
||||||
Bitmap actualBitmap = view.getDrawingCache();
|
int width = actual.getWidth();
|
||||||
Bitmap expectedBitmap = getBitmapFromAssets(expectedImagePath);
|
int height = actual.getHeight();
|
||||||
Bitmap scaledExpectedBitmap = Bitmap.createScaledBitmap(expectedBitmap,
|
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true);
|
||||||
actualBitmap.getWidth(), actualBitmap.getHeight(), false);
|
|
||||||
|
|
||||||
|
double distance;
|
||||||
boolean similarEnough = true;
|
boolean similarEnough = true;
|
||||||
long distance;
|
|
||||||
|
|
||||||
if ((distance = compareHistograms(getHistogram(actualBitmap), getHistogram(
|
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > SIMILARITY_CUTOFF)
|
||||||
scaledExpectedBitmap))) > SIMILARITY_CUTOFF)
|
|
||||||
{
|
{
|
||||||
similarEnough = false;
|
similarEnough = false;
|
||||||
errorMessage.append(String.format(
|
errorMessage.append(String.format(
|
||||||
"Rendered image has wrong histogram (distance=%d). ",
|
"Rendered image has wrong histogram (distance=%f). ",
|
||||||
distance));
|
distance));
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!similarEnough)
|
if(!similarEnough)
|
||||||
{
|
{
|
||||||
String path = saveBitmap(expectedImagePath, actualBitmap);
|
saveBitmap(expectedImagePath, ".scaledExpected", scaledExpected);
|
||||||
|
String path = saveBitmap(expectedImagePath, ".actual", actual);
|
||||||
errorMessage.append(String.format("Actual rendered image " + "saved to %s", path));
|
errorMessage.append(String.format("Actual rendered image " + "saved to %s", path));
|
||||||
fail(errorMessage.toString());
|
fail(errorMessage.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
actualBitmap.recycle();
|
actual.recycle();
|
||||||
expectedBitmap.recycle();
|
expected.recycle();
|
||||||
scaledExpectedBitmap.recycle();
|
scaledExpected.recycle();
|
||||||
}
|
}
|
||||||
|
|
||||||
private Bitmap getBitmapFromAssets(String path) throws IOException
|
private Bitmap getBitmapFromAssets(String path) throws IOException
|
||||||
@@ -100,11 +103,35 @@ public class ViewTest
|
|||||||
return BitmapFactory.decodeStream(stream);
|
return BitmapFactory.decodeStream(stream);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String saveBitmap(String filename, Bitmap bitmap)
|
private String getVersionedViewAssetPath(String path)
|
||||||
|
{
|
||||||
|
String result = null;
|
||||||
|
|
||||||
|
if (android.os.Build.VERSION.SDK_INT >= 21)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
String vpath = "views-v21/" + path;
|
||||||
|
testContext.getAssets().open(vpath);
|
||||||
|
result = vpath;
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
// ignored
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(result == null)
|
||||||
|
result = "views/" + path;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
String absolutePath = String.format("%s/Failed/%s", targetContext.getExternalCacheDir(),
|
String absolutePath = String.format("%s/Failed/%s", targetContext.getExternalCacheDir(),
|
||||||
filename.replaceAll("\\.png$", ".actual.png"));
|
filename.replaceAll("\\.png$", suffix + ".png"));
|
||||||
new File(absolutePath).getParentFile().mkdirs();
|
new File(absolutePath).getParentFile().mkdirs();
|
||||||
FileOutputStream out = new FileOutputStream(absolutePath);
|
FileOutputStream out = new FileOutputStream(absolutePath);
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||||
@@ -114,7 +141,7 @@ public class ViewTest
|
|||||||
|
|
||||||
private int[][] getHistogram(Bitmap bitmap)
|
private int[][] getHistogram(Bitmap bitmap)
|
||||||
{
|
{
|
||||||
int histogram[][] = new int[4][256];
|
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE];
|
||||||
|
|
||||||
for(int x = 0; x < bitmap.getWidth(); x++)
|
for(int x = 0; x < bitmap.getWidth(); x++)
|
||||||
{
|
{
|
||||||
@@ -128,29 +155,35 @@ public class ViewTest
|
|||||||
(color ) & 0xff //blue
|
(color ) & 0xff //blue
|
||||||
};
|
};
|
||||||
|
|
||||||
histogram[0][argb[0]]++;
|
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
|
||||||
histogram[1][argb[1]]++;
|
histogram[1][argb[1] / HISTOGRAM_BIN_SIZE]++;
|
||||||
histogram[2][argb[2]]++;
|
histogram[2][argb[2] / HISTOGRAM_BIN_SIZE]++;
|
||||||
histogram[3][argb[3]]++;
|
histogram[3][argb[3] / HISTOGRAM_BIN_SIZE]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return histogram;
|
return histogram;
|
||||||
}
|
}
|
||||||
|
|
||||||
private long compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
|
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram)
|
||||||
{
|
{
|
||||||
long distance = 0;
|
long diff = 0;
|
||||||
|
long total = 0;
|
||||||
|
|
||||||
for(int i = 0; i < 255; i ++)
|
for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++)
|
||||||
{
|
{
|
||||||
distance += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]);
|
diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]);
|
||||||
distance += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]);
|
diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]);
|
||||||
distance += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]);
|
diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]);
|
||||||
distance += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]);
|
diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]);
|
||||||
|
|
||||||
|
total += actualHistogram[0][i];
|
||||||
|
total += actualHistogram[1][i];
|
||||||
|
total += actualHistogram[2][i];
|
||||||
|
total += actualHistogram[3][i];
|
||||||
}
|
}
|
||||||
|
|
||||||
return distance;
|
return (double) diff / total / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected int dpToPixels(int dp)
|
protected int dpToPixels(int dp)
|
||||||
|
|||||||