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.uiautomatorhelpers.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 ) { 191 first.assertAgainstGolden(rule, goldenIdentifier) 192 } 193 194 assertThat(fakeDiffEscrow.reports) 195 .containsExactly( 196 ReportedDiffResult( 197 goldenIdentifier, 198 DiffResult.Status.FAILED, 199 hasExpected = true, 200 hasDiff = true, 201 comparisonStatistics = compStatistics, 202 ) 203 ) 204 } 205 206 @Test(expected = IllegalArgumentException::class) 207 fun performDiff_incorrectGoldenName() { 208 val first = loadBitmap("fullscreen_rect_gray") 209 210 first.assertAgainstGolden(rule, "round_rect_gray #") 211 } 212 213 @Test 214 fun performDiff_missingGolden() { 215 val first = loadBitmap("round_rect_gray") 216 217 val goldenIdentifier = "does_not_exist" 218 219 expectErrorMessage( 220 "Missing golden image 'does_not_exist.png'. Did you mean to check in a new image?" 221 ) { 222 first.assertAgainstGolden(rule, goldenIdentifier) 223 } 224 225 assertThat(fakeDiffEscrow.reports) 226 .containsExactly( 227 ReportedDiffResult( 228 goldenIdentifier, 229 DiffResult.Status.MISSING_REFERENCE, 230 hasExpected = false, 231 hasDiff = false, 232 comparisonStatistics = null, 233 ) 234 ) 235 } 236 237 @Test 238 fun screenshotAsserterHooks_successfulRun() { 239 var preRan = false 240 var postRan = false 241 val bitmap = loadBitmap("round_rect_green") 242 val asserter = 243 ScreenshotRuleAsserter.Builder(rule) 244 .setOnBeforeScreenshot { preRan = true } 245 .setOnAfterScreenshot { postRan = true } 246 .setScreenshotProvider { bitmap } 247 .build() 248 asserter.assertGoldenImage("round_rect_green") 249 assertThat(preRan).isTrue() 250 assertThat(postRan).isTrue() 251 } 252 253 @Test 254 fun screenshotAsserterHooks_disablesVisibleDebugSettings() { 255 // Turn visual debug settings on 256 pointerLocationSetting = 1 257 showTouchesSetting = 1 258 259 var preRan = false 260 val bitmap = loadBitmap("round_rect_green") 261 val asserter = 262 ScreenshotRuleAsserter.Builder(rule) 263 .setOnBeforeScreenshot { 264 preRan = true 265 assertThat(pointerLocationSetting).isEqualTo(0) 266 assertThat(showTouchesSetting).isEqualTo(0) 267 } 268 .setScreenshotProvider { bitmap } 269 .build() 270 asserter.assertGoldenImage("round_rect_green") 271 assertThat(preRan).isTrue() 272 273 // Clear visual debug settings 274 pointerLocationSetting = 0 275 showTouchesSetting = 0 276 } 277 278 @Test 279 fun screenshotAsserterHooks_whenVisibleDebugSettingsOn_revertsSettings() { 280 // Turn visual debug settings on 281 pointerLocationSetting = 1 282 showTouchesSetting = 1 283 284 val bitmap = loadBitmap("round_rect_green") 285 val asserter = ScreenshotRuleAsserter.Builder(rule).setScreenshotProvider { bitmap }.build() 286 asserter.assertGoldenImage("round_rect_green") 287 assertThat(pointerLocationSetting).isEqualTo(1) 288 assertThat(showTouchesSetting).isEqualTo(1) 289 290 // Clear visual debug settings to pre-test values 291 pointerLocationSetting = 0 292 showTouchesSetting = 0 293 } 294 295 @Test 296 fun screenshotAsserterHooks_whenVisibleDebugSettingsOff_retainsSettings() { 297 // Turn visual debug settings off 298 pointerLocationSetting = 0 299 showTouchesSetting = 0 300 301 val bitmap = loadBitmap("round_rect_green") 302 val asserter = ScreenshotRuleAsserter.Builder(rule).setScreenshotProvider { bitmap }.build() 303 asserter.assertGoldenImage("round_rect_green") 304 assertThat(pointerLocationSetting).isEqualTo(0) 305 assertThat(showTouchesSetting).isEqualTo(0) 306 } 307 308 @Test 309 fun screenshotAsserterHooks_assertionException() { 310 var preRan = false 311 var postRan = false 312 val bitmap = loadBitmap("round_rect_green") 313 val asserter = 314 ScreenshotRuleAsserter.Builder(rule) 315 .setOnBeforeScreenshot { preRan = true } 316 .setOnAfterScreenshot { postRan = true } 317 .setScreenshotProvider { 318 throw RuntimeException() 319 bitmap 320 } 321 .build() 322 try { 323 asserter.assertGoldenImage("round_rect_green") 324 } catch (e: RuntimeException) {} 325 assertThat(preRan).isTrue() 326 assertThat(postRan).isTrue() 327 } 328 329 @After 330 fun after() { 331 // Clear all files we generated so we don't have dependencies between tests 332 File(rule.goldenPathManager.deviceLocalPath).deleteRecursively() 333 } 334 335 private fun expectErrorMessage(expectedErrorMessage: String, block: () -> Unit) { 336 try { 337 block() 338 } catch (e: AssertionError) { 339 val received = e.localizedMessage!! 340 assertThat(received).isEqualTo(expectedErrorMessage.trim()) 341 return 342 } 343 344 throw AssertionError("No AssertionError thrown!") 345 } 346 347 data class ReportedDiffResult( 348 val goldenIdentifier: String, 349 val status: DiffResult.Status, 350 val hasExpected: Boolean = false, 351 val hasDiff: Boolean = false, 352 val comparisonStatistics: DiffResult.ComparisonStatistics? = null, 353 ) 354 355 class FakeDiffResultExport : DiffResultExportStrategy { 356 val reports = mutableListOf<ReportedDiffResult>() 357 358 override fun reportResult( 359 testIdentifier: String, 360 goldenIdentifier: String, 361 actual: Bitmap, 362 status: DiffResult.Status, 363 comparisonStatistics: DiffResult.ComparisonStatistics?, 364 expected: Bitmap?, 365 diff: Bitmap?, 366 ) { 367 reports.add( 368 ReportedDiffResult( 369 goldenIdentifier, 370 status, 371 hasExpected = expected != null, 372 hasDiff = diff != null, 373 comparisonStatistics, 374 ) 375 ) 376 } 377 } 378 379 private companion object { 380 var prevPointerLocationSetting: Int = 0 381 var prevShowTouchesSetting: Int = 0 382 383 private var pointerLocationSetting: Int 384 get() = 385 shell("settings get system ${System.POINTER_LOCATION}").trim().toIntOrNull() ?: 0 386 set(value) { 387 shell("settings put system ${System.POINTER_LOCATION} $value") 388 } 389 390 private var showTouchesSetting 391 get() = shell("settings get system ${System.SHOW_TOUCHES}").trim().toIntOrNull() ?: 0 392 set(value) { 393 shell("settings put system ${System.SHOW_TOUCHES} $value") 394 } 395 396 @JvmStatic 397 @BeforeClass 398 fun setUpClass() { 399 prevPointerLocationSetting = pointerLocationSetting 400 prevShowTouchesSetting = showTouchesSetting 401 } 402 403 @JvmStatic 404 @AfterClass 405 fun tearDownClass() { 406 pointerLocationSetting = prevPointerLocationSetting 407 showTouchesSetting = prevShowTouchesSetting 408 } 409 } 410 } 411