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