1 /* 2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package platform.test.screenshot 18 19 import android.content.Context 20 import android.graphics.Bitmap 21 import android.graphics.Rect 22 import android.platform.uiautomator_helpers.DeviceHelpers.shell 23 import android.provider.Settings.System 24 import androidx.test.ext.junit.runners.AndroidJUnit4 25 import androidx.test.filters.MediumTest 26 import androidx.test.platform.app.InstrumentationRegistry 27 import com.google.common.truth.Truth.assertThat 28 import java.io.File 29 import java.lang.AssertionError 30 import java.util.ArrayList 31 import org.junit.After 32 import org.junit.AfterClass 33 import org.junit.BeforeClass 34 import org.junit.Rule 35 import org.junit.Test 36 import org.junit.runner.RunWith 37 import platform.test.screenshot.matchers.MSSIMMatcher 38 import platform.test.screenshot.matchers.PixelPerfectMatcher 39 import platform.test.screenshot.proto.ScreenshotResultProto.DiffResult 40 import platform.test.screenshot.report.DiffResultExportStrategy 41 import platform.test.screenshot.utils.loadBitmap 42 43 class CustomGoldenPathManager(appcontext: Context, assetsPath: String = "assets") : 44 GoldenPathManager(appcontext, assetsPath) { goldenIdentifierResolvernull45 override fun goldenIdentifierResolver(testName: String, extension: String): String = 46 "$testName.$extension" 47 } 48 49 @RunWith(AndroidJUnit4::class) 50 @MediumTest 51 class ScreenshotTestRuleTest { 52 53 private val fakeDiffEscrow = FakeDiffResultExport() 54 55 @get:Rule 56 val rule = 57 ScreenshotTestRule( 58 CustomGoldenPathManager(InstrumentationRegistry.getInstrumentation().context), 59 diffEscrowStrategy = fakeDiffEscrow 60 ) 61 62 @Test 63 fun performDiff_sameBitmaps() { 64 val goldenIdentifier = "round_rect_gray" 65 val first = loadBitmap(goldenIdentifier) 66 67 first.assertAgainstGolden(rule, goldenIdentifier, matcher = PixelPerfectMatcher()) 68 69 assertThat(fakeDiffEscrow.reports).isEmpty() 70 } 71 72 @Test 73 fun performDiff_sameBitmaps_materialYouColors() { 74 val goldenIdentifier = "defaultClock_largeClock_regionSampledColor" 75 val first = 76 bitmapWithMaterialYouColorsSimulation( 77 loadBitmap("defaultClock_largeClock_regionSampledColor_original"), 78 /* isDarkTheme= */ true 79 ) 80 81 first.assertAgainstGolden(rule, goldenIdentifier, matcher = PixelPerfectMatcher()) 82 assertThat(fakeDiffEscrow.reports).isEmpty() 83 } 84 85 @Test 86 fun performDiff_noPixelCompared() { 87 val first = loadBitmap("round_rect_gray") 88 val regions = ArrayList<Rect>() 89 regions.add(Rect(/* left= */ 1, /* top= */ 1, /* right= */ 2, /* bottom=*/ 2)) 90 91 val goldenIdentifier = "round_rect_green" 92 first.assertAgainstGolden( 93 rule, 94 goldenIdentifier, 95 matcher = MSSIMMatcher(), 96 regions = regions 97 ) 98 99 assertThat(fakeDiffEscrow.reports).isEmpty() 100 } 101 102 @Test 103 fun performDiff_sameRegion() { 104 val first = loadBitmap("qmc-folder1") 105 val startHeight = 18 * first.height / 20 106 val endHeight = 37 * first.height / 40 107 val startWidth = 10 * first.width / 20 108 val endWidth = 11 * first.width / 20 109 val matcher = MSSIMMatcher() 110 val regions = ArrayList<Rect>() 111 regions.add(Rect(startWidth, startHeight, endWidth, endHeight)) 112 regions.add(Rect()) 113 114 val goldenIdentifier = "qmc-folder2" 115 first.assertAgainstGolden(rule, goldenIdentifier, matcher, regions) 116 117 assertThat(fakeDiffEscrow.reports).isEmpty() 118 } 119 120 @Test 121 fun performDiff_sameSizes_default_noMatch() { 122 val imageExtension = ".png" 123 val first = loadBitmap("round_rect_gray") 124 val compStatistics = 125 DiffResult.ComparisonStatistics.newBuilder() 126 .setNumberPixelsCompared(1504) 127 .setNumberPixelsDifferent(74) 128 .setNumberPixelsIgnored(800) 129 .setNumberPixelsSimilar(1430) 130 .build() 131 132 val goldenIdentifier = "round_rect_green" 133 expectErrorMessage("Image mismatch! Comparison stats: '$compStatistics'") { 134 first.assertAgainstGolden(rule, goldenIdentifier) 135 } 136 137 assertThat(fakeDiffEscrow.reports) 138 .containsExactly( 139 ReportedDiffResult( 140 goldenIdentifier, 141 DiffResult.Status.FAILED, 142 hasExpected = true, 143 hasDiff = true, 144 comparisonStatistics = compStatistics 145 ) 146 ) 147 } 148 149 @Test 150 fun performDiff_sameSizes_pixelPerfect_noMatch() { 151 val first = loadBitmap("round_rect_gray") 152 val compStatistics = 153 DiffResult.ComparisonStatistics.newBuilder() 154 .setNumberPixelsCompared(2304) 155 .setNumberPixelsDifferent(556) 156 .setNumberPixelsIdentical(1748) 157 .build() 158 159 val goldenIdentifier = "round_rect_green" 160 expectErrorMessage("Image mismatch! Comparison stats: '$compStatistics'") { 161 first.assertAgainstGolden(rule, goldenIdentifier, matcher = PixelPerfectMatcher()) 162 } 163 164 assertThat(fakeDiffEscrow.reports) 165 .containsExactly( 166 ReportedDiffResult( 167 goldenIdentifier, 168 DiffResult.Status.FAILED, 169 hasExpected = true, 170 hasDiff = true, 171 comparisonStatistics = compStatistics 172 ) 173 ) 174 } 175 176 @Test 177 fun performDiff_differentSizes() { 178 val first = loadBitmap("fullscreen_rect_gray") 179 val goldenIdentifier = "round_rect_gray" 180 val compStatistics = 181 DiffResult.ComparisonStatistics.newBuilder() 182 .setNumberPixelsCompared(2304) 183 .setNumberPixelsDifferent(568) 184 .setNumberPixelsIdentical(1736) 185 .build() 186 187 expectErrorMessage( 188 "Sizes are different! Expected: [48, 48], Actual: [720, 1184]. Force aligned " 189 + "at (0, 0). Comparison stats: '${compStatistics}'") { 190 first.assertAgainstGolden(rule, goldenIdentifier) 191 } 192 193 assertThat(fakeDiffEscrow.reports) 194 .containsExactly( 195 ReportedDiffResult( 196 goldenIdentifier, 197 DiffResult.Status.FAILED, 198 hasExpected = true, 199 hasDiff = true, 200 comparisonStatistics = compStatistics 201 ) 202 ) 203 } 204 205 @Test(expected = IllegalArgumentException::class) 206 fun performDiff_incorrectGoldenName() { 207 val first = loadBitmap("fullscreen_rect_gray") 208 209 first.assertAgainstGolden(rule, "round_rect_gray #") 210 } 211 212 @Test 213 fun performDiff_missingGolden() { 214 val first = loadBitmap("round_rect_gray") 215 216 val goldenIdentifier = "does_not_exist" 217 218 expectErrorMessage( 219 "Missing golden image 'does_not_exist.png'. Did you mean to check in a new image?" 220 ) { 221 first.assertAgainstGolden(rule, goldenIdentifier) 222 } 223 224 assertThat(fakeDiffEscrow.reports) 225 .containsExactly( 226 ReportedDiffResult( 227 goldenIdentifier, 228 DiffResult.Status.MISSING_REFERENCE, 229 hasExpected = false, 230 hasDiff = false, 231 comparisonStatistics = null 232 ), 233 ) 234 } 235 236 @Test 237 fun screenshotAsserterHooks_successfulRun() { 238 var preRan = false 239 var postRan = false 240 val bitmap = loadBitmap("round_rect_green") 241 val asserter = 242 ScreenshotRuleAsserter.Builder(rule) 243 .setOnBeforeScreenshot { preRan = true } 244 .setOnAfterScreenshot { postRan = true } 245 .setScreenshotProvider { bitmap } 246 .build() 247 asserter.assertGoldenImage("round_rect_green") 248 assertThat(preRan).isTrue() 249 assertThat(postRan).isTrue() 250 } 251 252 @Test 253 fun screenshotAsserterHooks_disablesVisibleDebugSettings() { 254 // Turn visual debug settings on 255 pointerLocationSetting = 1 256 showTouchesSetting = 1 257 258 var preRan = false 259 val bitmap = loadBitmap("round_rect_green") 260 val asserter = 261 ScreenshotRuleAsserter.Builder(rule) 262 .setOnBeforeScreenshot { 263 preRan = true 264 assertThat(pointerLocationSetting).isEqualTo(0) 265 assertThat(showTouchesSetting).isEqualTo(0) 266 } 267 .setScreenshotProvider { bitmap } 268 .build() 269 asserter.assertGoldenImage("round_rect_green") 270 assertThat(preRan).isTrue() 271 272 // Clear visual debug settings 273 pointerLocationSetting = 0 274 showTouchesSetting = 0 275 } 276 277 @Test 278 fun screenshotAsserterHooks_whenVisibleDebugSettingsOn_revertsSettings() { 279 // Turn visual debug settings on 280 pointerLocationSetting = 1 281 showTouchesSetting = 1 282 283 val bitmap = loadBitmap("round_rect_green") 284 val asserter = ScreenshotRuleAsserter.Builder(rule).setScreenshotProvider { bitmap }.build() 285 asserter.assertGoldenImage("round_rect_green") 286 assertThat(pointerLocationSetting).isEqualTo(1) 287 assertThat(showTouchesSetting).isEqualTo(1) 288 289 // Clear visual debug settings to pre-test values 290 pointerLocationSetting = 0 291 showTouchesSetting = 0 292 } 293 294 @Test 295 fun screenshotAsserterHooks_whenVisibleDebugSettingsOff_retainsSettings() { 296 // Turn visual debug settings off 297 pointerLocationSetting = 0 298 showTouchesSetting = 0 299 300 val bitmap = loadBitmap("round_rect_green") 301 val asserter = ScreenshotRuleAsserter.Builder(rule).setScreenshotProvider { bitmap }.build() 302 asserter.assertGoldenImage("round_rect_green") 303 assertThat(pointerLocationSetting).isEqualTo(0) 304 assertThat(showTouchesSetting).isEqualTo(0) 305 } 306 307 @Test 308 fun screenshotAsserterHooks_assertionException() { 309 var preRan = false 310 var postRan = false 311 val bitmap = loadBitmap("round_rect_green") 312 val asserter = 313 ScreenshotRuleAsserter.Builder(rule) 314 .setOnBeforeScreenshot { preRan = true } 315 .setOnAfterScreenshot { postRan = true } 316 .setScreenshotProvider { 317 throw RuntimeException() 318 bitmap 319 } 320 .build() 321 try { 322 asserter.assertGoldenImage("round_rect_green") 323 } catch (e: RuntimeException) {} 324 assertThat(preRan).isTrue() 325 assertThat(postRan).isTrue() 326 } 327 328 @After 329 fun after() { 330 // Clear all files we generated so we don't have dependencies between tests 331 File(rule.goldenPathManager.deviceLocalPath).deleteRecursively() 332 } 333 334 private fun expectErrorMessage(expectedErrorMessage: String, block: () -> Unit) { 335 try { 336 block() 337 } catch (e: AssertionError) { 338 val received = e.localizedMessage!! 339 assertThat(received).isEqualTo(expectedErrorMessage.trim()) 340 return 341 } 342 343 throw AssertionError("No AssertionError thrown!") 344 } 345 346 data class ReportedDiffResult( 347 val goldenIdentifier: String, 348 val status: DiffResult.Status, 349 val hasExpected: Boolean = false, 350 val hasDiff: Boolean = false, 351 val comparisonStatistics: DiffResult.ComparisonStatistics? = null, 352 ) 353 354 class FakeDiffResultExport : DiffResultExportStrategy { 355 val reports = mutableListOf<ReportedDiffResult>() 356 override fun reportResult( 357 testIdentifier: String, 358 goldenIdentifier: String, 359 actual: Bitmap, 360 status: DiffResult.Status, 361 comparisonStatistics: DiffResult.ComparisonStatistics?, 362 expected: Bitmap?, 363 diff: Bitmap? 364 ) { 365 reports.add( 366 ReportedDiffResult( 367 goldenIdentifier, 368 status, 369 hasExpected = expected != null, 370 hasDiff = diff != null, 371 comparisonStatistics 372 ) 373 ) 374 } 375 } 376 private companion object { 377 var prevPointerLocationSetting: Int = 0 378 var prevShowTouchesSetting: Int = 0 379 380 private var pointerLocationSetting: Int 381 get() = 382 shell("settings get system ${System.POINTER_LOCATION}").trim().toIntOrNull() ?: 0 383 set(value) { 384 shell("settings put system ${System.POINTER_LOCATION} $value") 385 } 386 387 private var showTouchesSetting 388 get() = shell("settings get system ${System.SHOW_TOUCHES}").trim().toIntOrNull() ?: 0 389 set(value) { 390 shell("settings put system ${System.SHOW_TOUCHES} $value") 391 } 392 393 @JvmStatic 394 @BeforeClass 395 fun setUpClass() { 396 prevPointerLocationSetting = pointerLocationSetting 397 prevShowTouchesSetting = showTouchesSetting 398 } 399 400 @JvmStatic 401 @AfterClass 402 fun tearDownClass() { 403 pointerLocationSetting = prevPointerLocationSetting 404 showTouchesSetting = prevShowTouchesSetting 405 } 406 } 407 } 408