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