From 0fc9bb57aedaf03893efabc5857a7257a3a14cb6 Mon Sep 17 00:00:00 2001 From: Quentin Hibon Date: Thu, 21 Jan 2021 23:07:42 +0100 Subject: [PATCH] Convert BaseViewTest --- .../java/org/isoron/uhabits/BaseViewTest.java | 198 ------------------ .../java/org/isoron/uhabits/BaseViewTest.kt | 185 ++++++++++++++++ 2 files changed, 185 insertions(+), 198 deletions(-) delete mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java create mode 100644 uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java deleted file mode 100644 index 66c07ffcc..000000000 --- a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (C) 2016-2021 Álinson Santos Xavier - * - * 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 . - */ - -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; - } -} diff --git a/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt new file mode 100644 index 000000000..c5cd6c94c --- /dev/null +++ b/uhabits-android/src/androidTest/java/org/isoron/uhabits/BaseViewTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2016-2021 Álinson Santos Xavier + * + * 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 . + */ +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 + } +}