• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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