1 /* <lambda>null2 * 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 20 import com.google.common.truth.Fact.fact 21 import com.google.common.truth.Fact.simpleFact 22 import com.google.common.truth.FailureMetadata 23 import com.google.common.truth.Subject 24 import com.google.common.truth.Subject.Factory 25 import com.google.common.truth.Truth 26 import platform.test.motion.golden.TimeSeries 27 28 /** Subject on [TimeSeries] to produce meaningful failure diffs. */ 29 class TimeSeriesSubject 30 private constructor(failureMetadata: FailureMetadata, private val actual: TimeSeries?) : 31 Subject(failureMetadata, actual) { 32 33 override fun isEqualTo(expected: Any?) { 34 if (actual is TimeSeries && expected is TimeSeries) { 35 val facts = compareTimeSeries(expected, actual) 36 if (facts.isNotEmpty()) { 37 facts.add(simpleFact(MANAGE_GOLDEN_DOCUMENTATION)) 38 failWithoutActual(facts[0], *(facts.drop(1)).toTypedArray()) 39 } 40 } else { 41 super.isEqualTo(expected) 42 } 43 } 44 45 private fun compareTimeSeries(expected: TimeSeries, actual: TimeSeries) = 46 mutableListOf<Fact>().apply { 47 val actualToExpectedDataPointIndices: List<Pair<Int, Int>> 48 if (actual.frameIds != expected.frameIds) { 49 add(simpleFact("TimeSeries.frames does not match")) 50 add(fact("| expected", expected.frameIds.map { it.label })) 51 add(fact("| but got", actual.frameIds.map { it.label })) 52 53 val actualFrameIds = actual.frameIds.toSet() 54 val expectedFrameIds = expected.frameIds.toSet() 55 val framesToCompare = actualFrameIds.intersect(expectedFrameIds) 56 57 if (framesToCompare != actualFrameIds) { 58 val unexpected = actualFrameIds - framesToCompare 59 add(fact("| unexpected (${ unexpected.size})", unexpected.map { it.label })) 60 } 61 62 if (framesToCompare != expectedFrameIds) { 63 val missing = expectedFrameIds - framesToCompare 64 add(fact("| missing (${ missing.size})", missing.map { it.label })) 65 } 66 actualToExpectedDataPointIndices = 67 framesToCompare.map { 68 actual.frameIds.indexOf(it) to expected.frameIds.indexOf(it) 69 } 70 } else { 71 actualToExpectedDataPointIndices = List(actual.frameIds.size) { it to it } 72 } 73 74 val featuresToCompare: Set<String> 75 if (actual.features.keys != expected.features.keys) { 76 featuresToCompare = actual.features.keys.intersect(expected.features.keys) 77 add(simpleFact("TimeSeries.features does not match")) 78 79 if (featuresToCompare != actual.features.keys) { 80 val unexpected = actual.features.keys - featuresToCompare 81 add(fact("| unexpected (${ unexpected.size})", unexpected)) 82 } 83 84 if (featuresToCompare != expected.features.keys) { 85 val missing = expected.features.keys - featuresToCompare 86 add(fact("| missing (${ missing.size})", missing)) 87 } 88 } else { 89 featuresToCompare = actual.features.keys 90 } 91 92 featuresToCompare.forEach { featureKey -> 93 val actualFeature = checkNotNull(actual.features[featureKey]) 94 val expectedFeature = checkNotNull(expected.features[featureKey]) 95 96 val mismatchingDataPointIndices = 97 actualToExpectedDataPointIndices.filter { (actualIndex, expectedIndex) -> 98 actualFeature.dataPoints[actualIndex] != 99 expectedFeature.dataPoints[expectedIndex] 100 } 101 102 if (mismatchingDataPointIndices.isNotEmpty()) { 103 add(simpleFact("TimeSeries.features[$featureKey].dataPoints do not match")) 104 105 mismatchingDataPointIndices.forEach { (actualIndex, expectedIndex) -> 106 add(simpleFact("| @${actual.frameIds[actualIndex].label}")) 107 add(fact("| expected", expectedFeature.dataPoints[expectedIndex])) 108 add(fact("| but was", actualFeature.dataPoints[actualIndex])) 109 } 110 } 111 } 112 } 113 114 companion object { 115 /** Returns a factory to be used with [Truth.assertAbout]. */ 116 fun timeSeries(): Factory<TimeSeriesSubject, TimeSeries> { 117 return Factory { failureMetadata: FailureMetadata, subject: TimeSeries? -> 118 TimeSeriesSubject(failureMetadata, subject) 119 } 120 } 121 122 /** Shortcut for `Truth.assertAbout(timeSeries()).that(timeSeries)`. */ 123 fun assertThat(timeSeries: TimeSeries): TimeSeriesSubject = 124 Truth.assertAbout(timeSeries()).that(timeSeries) 125 126 const val MANAGE_GOLDEN_DOCUMENTATION = 127 "Documentation on how to verify the change visually and how to update the golden files can be found at http://go/motion-testing#managing-goldens" 128 } 129 } 130