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