1 /*
2  * Copyright (C) 2024 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.motion.truth
18 
19 import com.google.common.truth.Fact.fact
20 import com.google.common.truth.Fact.simpleFact
21 import com.google.common.truth.FailureMetadata
22 import com.google.common.truth.Subject
23 import org.json.JSONException
24 import platform.test.motion.GoldenNotFoundException
25 import platform.test.motion.MotionTestRule
26 import platform.test.motion.RecordedMotion
27 import platform.test.motion.TimeSeriesVerificationResult
28 import platform.test.motion.golden.createTypeRegistry
29 import platform.test.motion.truth.TimeSeriesSubject.Companion.timeSeries
30 import platform.test.screenshot.matchers.BitmapMatcher
31 import platform.test.screenshot.matchers.PixelPerfectMatcher
32 
33 /**
34  * Subject to verify a [RecordedMotion] against golden data.
35  *
36  * @see [MotionTestRule.motion]
37  */
38 class RecordedMotionSubject
39 internal constructor(
40     failureMetadata: FailureMetadata,
41     private val actual: RecordedMotion?,
42     private val motionTestRule: MotionTestRule<*>,
43 ) : Subject(failureMetadata, actual) {
44 
45     /**
46      * Verifies a time series matches a previously captured golden.
47      *
48      * @param goldenName the name for the golden. When `null`, the test method name is used.
49      */
timeSeriesMatchesGoldennull50     fun timeSeriesMatchesGolden(goldenName: String? = null) {
51         isNotNull()
52         val recordedMotion = checkNotNull(actual)
53 
54         val goldenIdentifier = getGoldenIdentifier(recordedMotion, goldenName)
55         val actualTimeSeries = recordedMotion.timeSeries
56 
57         var result = TimeSeriesVerificationResult.FAILED
58         try {
59             try {
60                 // when de-serializing goldens, only types that are in the actual data are relevant
61                 val typeRegistry = actualTimeSeries.createTypeRegistry()
62                 val goldenTimeSeries =
63                     motionTestRule.readGoldenTimeSeries(goldenIdentifier, typeRegistry)
64 
65                 check("Motion time-series $goldenIdentifier")
66                     .about(timeSeries())
67                     .that(actualTimeSeries)
68                     .isEqualTo(goldenTimeSeries)
69 
70                 result = TimeSeriesVerificationResult.PASSED
71             } catch (e: GoldenNotFoundException) {
72                 result = TimeSeriesVerificationResult.MISSING_REFERENCE
73                 failWithoutActual(simpleFact("Golden [${e.missingGoldenFile}] not found"))
74             } catch (e: JSONException) {
75                 result = TimeSeriesVerificationResult.MISSING_REFERENCE
76                 failWithoutActual(fact("Golden [$goldenIdentifier] file is invalid", e))
77             }
78         } finally {
79             // Export the actual values, so that they can later be downloaded to update the golden.
80             motionTestRule.writeGeneratedTimeSeries(goldenIdentifier, recordedMotion, result)
81         }
82     }
83 
84     /**
85      * Verifies that the filmstrip of the recorded motion matches a golden bitmap thereof.
86      *
87      * Prefer capturing explicit signals and asserting those (via [timeSeriesMatchesGolden]). A
88      * filmstrip can easily assert on many irrelevant details that should be tested elsewhere, and
89      * could cause the test to fail on many irrelevant changes.
90      *
91      * @param goldenName the name for the golden. When `null`, the test method name is used.
92      */
filmstripMatchesGoldennull93     fun filmstripMatchesGolden(
94         goldenName: String? = null,
95         bitmapMatcher: BitmapMatcher = PixelPerfectMatcher()
96     ) {
97         isNotNull()
98         val recordedMotion = checkNotNull(actual)
99         val bitmapDiffer =
100             checkNotNull(motionTestRule.bitmapDiffer) {
101                 "BitmapDiffer must be supplied to MotionTestRule for filmstrip golden support"
102             }
103 
104         val filmstrip =
105             checkNotNull(recordedMotion.filmstrip) {
106                 "non-null `visualCapture` must be provided to [MotionRecorder.record]"
107             }
108 
109         val goldenIdentifier = getGoldenIdentifier(recordedMotion, goldenName)
110         val filmstripBitmap = filmstrip.renderFilmstrip()
111         bitmapDiffer.assertBitmapAgainstGolden(filmstripBitmap, goldenIdentifier, bitmapMatcher)
112     }
113 
getGoldenIdentifiernull114     private fun getGoldenIdentifier(recordedMotion: RecordedMotion, goldenName: String?): String =
115         goldenName ?: recordedMotion.testMethodName
116 }
117