mirror of https://github.com/iSoron/uhabits.git
parent
22dcd9f7ae
commit
0fc9bb57ae
@ -1,198 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.isoron.uhabits;
|
||||
|
||||
import android.graphics.*;
|
||||
import android.view.*;
|
||||
import android.widget.*;
|
||||
|
||||
import androidx.annotation.*;
|
||||
import androidx.test.platform.app.*;
|
||||
|
||||
import org.isoron.uhabits.utils.*;
|
||||
import org.isoron.uhabits.widgets.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
|
||||
import static android.view.View.MeasureSpec.*;
|
||||
|
||||
public class BaseViewTest extends BaseAndroidTest
|
||||
{
|
||||
public double similarityCutoff = 0.00018;
|
||||
|
||||
@Override
|
||||
public void setUp()
|
||||
{
|
||||
super.setUp();
|
||||
}
|
||||
|
||||
protected void assertRenders(View view, String expectedImagePath)
|
||||
throws IOException
|
||||
{
|
||||
Bitmap actual = renderView(view);
|
||||
if(actual == null) throw new IllegalStateException("actual is null");
|
||||
assertRenders(actual, expectedImagePath);
|
||||
}
|
||||
|
||||
protected void assertRenders(Bitmap actual, String expectedImagePath) throws IOException {
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
|
||||
expectedImagePath = "views/" + expectedImagePath;
|
||||
try
|
||||
{
|
||||
Bitmap expected = getBitmapFromAssets(expectedImagePath);
|
||||
double distance = distance(actual, expected);
|
||||
if (distance > similarityCutoff)
|
||||
{
|
||||
saveBitmap(expectedImagePath, ".expected", expected);
|
||||
String path = saveBitmap(expectedImagePath, "", actual);
|
||||
fail(String.format("Image differs from expected " +
|
||||
"(distance=%f). Actual rendered " +
|
||||
"image saved to %s", distance, path));
|
||||
}
|
||||
|
||||
expected.recycle();
|
||||
}
|
||||
catch (IOException e)
|
||||
{
|
||||
String path = saveBitmap(expectedImagePath, "", actual);
|
||||
fail(String.format("Could not open expected image. Actual " +
|
||||
"rendered image saved to %s", path));
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
protected FrameLayout convertToView(BaseWidget widget,
|
||||
int width,
|
||||
int height)
|
||||
{
|
||||
widget.setDimensions(
|
||||
new WidgetDimensions(width, height, width, height));
|
||||
FrameLayout view = new FrameLayout(targetContext);
|
||||
RemoteViews remoteViews = widget.getPortraitRemoteViews();
|
||||
view.addView(remoteViews.apply(targetContext, view));
|
||||
measureView(view, width, height);
|
||||
return view;
|
||||
}
|
||||
|
||||
protected float dpToPixels(int dp)
|
||||
{
|
||||
return InterfaceUtils.dpToPixels(targetContext, dp);
|
||||
}
|
||||
|
||||
protected void measureView(View view, float width, float height)
|
||||
{
|
||||
int specWidth = makeMeasureSpec((int) width, View.MeasureSpec.EXACTLY);
|
||||
int specHeight = makeMeasureSpec((int) height, View.MeasureSpec.EXACTLY);
|
||||
|
||||
view.setLayoutParams(new ViewGroup.LayoutParams((int) width, (int) height));
|
||||
view.measure(specWidth, specHeight);
|
||||
view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
|
||||
}
|
||||
|
||||
protected void skipAnimation(View view)
|
||||
{
|
||||
ViewPropertyAnimator animator = view.animate();
|
||||
animator.setDuration(0);
|
||||
animator.start();
|
||||
}
|
||||
|
||||
private int[] colorToArgb(int c1)
|
||||
{
|
||||
return new int[]{
|
||||
(c1 >> 24) & 0xff, //alpha
|
||||
(c1 >> 16) & 0xff, //red
|
||||
(c1 >> 8) & 0xff, //green
|
||||
(c1) & 0xff //blue
|
||||
};
|
||||
}
|
||||
|
||||
private double distance(Bitmap b1, Bitmap b2)
|
||||
{
|
||||
if (b1.getWidth() != b2.getWidth()) return 1.0;
|
||||
if (b1.getHeight() != b2.getHeight()) return 1.0;
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
double distance = 0.0;
|
||||
for (int x = 0; x < b1.getWidth(); x++)
|
||||
{
|
||||
for (int y = 0; y < b1.getHeight(); y++)
|
||||
{
|
||||
if (random.nextInt(4) != 0) continue;
|
||||
|
||||
int[] argb1 = colorToArgb(b1.getPixel(x, y));
|
||||
int[] argb2 = colorToArgb(b2.getPixel(x, y));
|
||||
distance += Math.abs(argb1[0] - argb2[0]);
|
||||
distance += Math.abs(argb1[1] - argb2[1]);
|
||||
distance += Math.abs(argb1[2] - argb2[2]);
|
||||
distance += Math.abs(argb1[3] - argb2[3]);
|
||||
}
|
||||
}
|
||||
|
||||
distance /= 255.0 * 16 * b1.getWidth() * b1.getHeight();
|
||||
return distance;
|
||||
}
|
||||
|
||||
private Bitmap getBitmapFromAssets(String path) throws IOException
|
||||
{
|
||||
InputStream stream = testContext.getAssets().open(path);
|
||||
return BitmapFactory.decodeStream(stream);
|
||||
}
|
||||
|
||||
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
|
||||
throws IOException
|
||||
{
|
||||
File dir = FileUtils.getSDCardDir("test-screenshots");
|
||||
if (dir == null)
|
||||
dir = new AndroidDirFinder(targetContext).getFilesDir("test-screenshots");
|
||||
if (dir == null) throw new RuntimeException(
|
||||
"Could not find suitable dir for screenshots");
|
||||
|
||||
filename = filename.replaceAll("\\.png$", suffix + ".png");
|
||||
String absolutePath =
|
||||
String.format("%s/%s", dir.getAbsolutePath(), filename);
|
||||
|
||||
File parent = new File(absolutePath).getParentFile();
|
||||
if (!parent.exists() && !parent.mkdirs()) throw new RuntimeException(
|
||||
String.format("Could not create dir: %s",
|
||||
parent.getAbsolutePath()));
|
||||
|
||||
FileOutputStream out = new FileOutputStream(absolutePath);
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
|
||||
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
public Bitmap renderView(View view)
|
||||
{
|
||||
int width = view.getMeasuredWidth();
|
||||
int height = view.getMeasuredHeight();
|
||||
if(view.isLayoutRequested())
|
||||
measureView(view, width, height);
|
||||
|
||||
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
view.invalidate();
|
||||
view.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
}
|
@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
|
||||
*
|
||||
* This file is part of Loop Habit Tracker.
|
||||
*
|
||||
* Loop Habit Tracker is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by the
|
||||
* Free Software Foundation, either version 3 of the License, or (at your
|
||||
* option) any later version.
|
||||
*
|
||||
* Loop Habit Tracker is distributed in the hope that it will be useful, but
|
||||
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
||||
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
* more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along
|
||||
* with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.isoron.uhabits
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
import android.view.View
|
||||
import android.view.View.MeasureSpec
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.isoron.uhabits.utils.FileUtils.getSDCardDir
|
||||
import org.isoron.uhabits.utils.InterfaceUtils.dpToPixels
|
||||
import org.isoron.uhabits.widgets.BaseWidget
|
||||
import org.isoron.uhabits.widgets.WidgetDimensions
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.Random
|
||||
|
||||
open class BaseViewTest : BaseAndroidTest() {
|
||||
var similarityCutoff = 0.00018
|
||||
override fun setUp() {
|
||||
super.setUp()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun assertRenders(view: View, expectedImagePath: String) {
|
||||
val actual = renderView(view)
|
||||
assertRenders(actual, expectedImagePath)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
protected fun assertRenders(actual: Bitmap, expectedImagePath: String) {
|
||||
var expectedImagePath = expectedImagePath
|
||||
InstrumentationRegistry.getInstrumentation().waitForIdleSync()
|
||||
expectedImagePath = "views/$expectedImagePath"
|
||||
try {
|
||||
val expected = getBitmapFromAssets(expectedImagePath)
|
||||
val distance = distance(actual, expected)
|
||||
if (distance > similarityCutoff) {
|
||||
saveBitmap(expectedImagePath, ".expected", expected)
|
||||
val path = saveBitmap(expectedImagePath, "", actual)
|
||||
fail(
|
||||
String.format(
|
||||
"Image differs from expected " +
|
||||
"(distance=%f). Actual rendered " +
|
||||
"image saved to %s",
|
||||
distance,
|
||||
path
|
||||
)
|
||||
)
|
||||
}
|
||||
expected.recycle()
|
||||
} catch (e: IOException) {
|
||||
val path = saveBitmap(expectedImagePath, "", actual)
|
||||
fail(
|
||||
String.format(
|
||||
"Could not open expected image. Actual " +
|
||||
"rendered image saved to %s",
|
||||
path
|
||||
)
|
||||
)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
protected fun convertToView(
|
||||
widget: BaseWidget,
|
||||
width: Int,
|
||||
height: Int
|
||||
): FrameLayout {
|
||||
widget.setDimensions(
|
||||
WidgetDimensions(width, height, width, height)
|
||||
)
|
||||
val view = FrameLayout(targetContext)
|
||||
val remoteViews = widget.portraitRemoteViews
|
||||
view.addView(remoteViews.apply(targetContext, view))
|
||||
measureView(view, width.toFloat(), height.toFloat())
|
||||
return view
|
||||
}
|
||||
|
||||
protected fun dpToPixels(dp: Int): Float {
|
||||
return dpToPixels(targetContext, dp.toFloat())
|
||||
}
|
||||
|
||||
protected fun measureView(view: View, width: Float, height: Float) {
|
||||
val specWidth = MeasureSpec.makeMeasureSpec(width.toInt(), MeasureSpec.EXACTLY)
|
||||
val specHeight = MeasureSpec.makeMeasureSpec(height.toInt(), MeasureSpec.EXACTLY)
|
||||
view.layoutParams = ViewGroup.LayoutParams(width.toInt(), height.toInt())
|
||||
view.measure(specWidth, specHeight)
|
||||
view.layout(0, 0, view.measuredWidth, view.measuredHeight)
|
||||
}
|
||||
|
||||
protected fun skipAnimation(view: View) {
|
||||
val animator = view.animate()
|
||||
animator.duration = 0
|
||||
animator.start()
|
||||
}
|
||||
|
||||
private fun colorToArgb(c1: Int): IntArray {
|
||||
return intArrayOf(
|
||||
c1 shr 24 and 0xff, // alpha
|
||||
c1 shr 16 and 0xff, // red
|
||||
c1 shr 8 and 0xff, // green
|
||||
c1 and 0xff // blue
|
||||
)
|
||||
}
|
||||
|
||||
private fun distance(b1: Bitmap, b2: Bitmap): Double {
|
||||
if (b1.width != b2.width) return 1.0
|
||||
if (b1.height != b2.height) return 1.0
|
||||
val random = Random()
|
||||
var distance = 0.0
|
||||
for (x in 0 until b1.width) {
|
||||
for (y in 0 until b1.height) {
|
||||
if (random.nextInt(4) != 0) continue
|
||||
val argb1 = colorToArgb(b1.getPixel(x, y))
|
||||
val argb2 = colorToArgb(b2.getPixel(x, y))
|
||||
distance += Math.abs(argb1[0] - argb2[0]).toDouble()
|
||||
distance += Math.abs(argb1[1] - argb2[1]).toDouble()
|
||||
distance += Math.abs(argb1[2] - argb2[2]).toDouble()
|
||||
distance += Math.abs(argb1[3] - argb2[3]).toDouble()
|
||||
}
|
||||
}
|
||||
distance /= 255.0 * 16 * b1.width * b1.height
|
||||
return distance
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun getBitmapFromAssets(path: String): Bitmap {
|
||||
val stream = testContext.assets.open(path)
|
||||
return BitmapFactory.decodeStream(stream)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun saveBitmap(filename: String, suffix: String, bitmap: Bitmap): String {
|
||||
var filename = filename
|
||||
var dir = getSDCardDir("test-screenshots")
|
||||
if (dir == null) dir = AndroidDirFinder(targetContext).getFilesDir("test-screenshots")
|
||||
if (dir == null) throw RuntimeException(
|
||||
"Could not find suitable dir for screenshots"
|
||||
)
|
||||
filename = filename.replace("\\.png$".toRegex(), "$suffix.png")
|
||||
val absolutePath = String.format("%s/%s", dir.absolutePath, filename)
|
||||
val parent = File(absolutePath).parentFile
|
||||
if (!parent.exists() && !parent.mkdirs()) throw RuntimeException(
|
||||
String.format(
|
||||
"Could not create dir: %s",
|
||||
parent.absolutePath
|
||||
)
|
||||
)
|
||||
val out = FileOutputStream(absolutePath)
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
|
||||
return absolutePath
|
||||
}
|
||||
|
||||
fun renderView(view: View): Bitmap {
|
||||
val width = view.measuredWidth
|
||||
val height = view.measuredHeight
|
||||
if (view.isLayoutRequested) measureView(view, width.toFloat(), height.toFloat())
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
view.invalidate()
|
||||
view.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
}
|
Loading…
Reference in new issue