xref: /aosp_15_r20/platform_testing/libraries/flicker/src/android/tools/flicker/assertions/AssertionsChecker.kt (revision dd0948b35e70be4c0246aabd6c72554a5eb8b22a)
1 /*
<lambda>null2  * Copyright (C) 2023 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 android.tools.flicker.assertions
18 
19 import android.tools.flicker.subject.FlickerSubject
20 import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder
21 import android.tools.flicker.subject.exceptions.SubjectAssertionError
22 import android.tools.function.AssertionPredicate
23 
24 /**
25  * Runs sequences of assertions on sequences of subjects.
26  *
27  * Starting at the first assertion and first trace entry, executes the assertions iteratively on the
28  * trace until all assertions and trace entries succeed.
29  *
30  * @param <T> trace entry type </T>
31  */
32 class AssertionsChecker<T : FlickerSubject> {
33     private val assertions = mutableListOf<CompoundAssertion<T>>()
34     private var skipUntilFirstAssertion = false
35 
36     internal fun isEmpty() = assertions.isEmpty()
37 
38     /** Add [assertion] to a new [CompoundAssertion] block. */
39     fun add(name: String, isOptional: Boolean = false, assertion: AssertionPredicate<T>) {
40         assertions.add(CompoundAssertion(assertion, name, isOptional))
41     }
42 
43     /** Append [assertion] to the last existing set of assertions. */
44     fun append(name: String, isOptional: Boolean = false, assertion: AssertionPredicate<T>) {
45         assertions.last().add(assertion, name, isOptional)
46     }
47 
48     /**
49      * Steps through each trace entry checking if provided assertions are true in the order they are
50      * added. Each assertion must be true for at least a single trace entry.
51      *
52      * This can be used to check for asserting a change in property over a trace. Such as visibility
53      * for a window changes from true to false or top-most window changes from A to B and back to A
54      * again.
55      *
56      * It is also possible to ignore failures on initial elements, until the first assertion passes,
57      * this allows the trace to be recorded for longer periods, and the checks to happen only after
58      * some time.
59      *
60      * @param entries list of entries to perform assertions on
61      * @return list of failed assertion results
62      */
63     fun test(entries: List<T>) {
64         if (assertions.isEmpty() || entries.isEmpty()) {
65             return
66         }
67 
68         var entryIndex = 0
69         var assertionIndex = 0
70         var lastPassedAssertionIndex = -1
71         val assertionTrace = mutableListOf<String>()
72         while (assertionIndex < assertions.size && entryIndex < entries.size) {
73             val currentAssertion = assertions[assertionIndex]
74             val currEntry = entries[entryIndex]
75             try {
76                 val log =
77                     "${assertionIndex + 1}/${assertions.size}:[${currentAssertion.name}]\t" +
78                         "Entry: ${entryIndex + 1}/${entries.size} $currEntry"
79                 assertionTrace.add(log)
80                 currentAssertion.invoke(currEntry)
81                 lastPassedAssertionIndex = assertionIndex
82                 entryIndex++
83             } catch (e: AssertionError) {
84                 // ignore errors at the start of the trace
85                 val ignoreFailure = skipUntilFirstAssertion && lastPassedAssertionIndex == -1
86                 if (ignoreFailure) {
87                     entryIndex++
88                     continue
89                 }
90                 // failure is an optional assertion, just consider it passed skip it
91                 if (currentAssertion.isOptional) {
92                     lastPassedAssertionIndex = assertionIndex
93                     assertionIndex++
94                     continue
95                 }
96                 if (lastPassedAssertionIndex != assertionIndex) {
97                     throw e
98                 }
99                 assertionIndex++
100                 if (assertionIndex == assertions.size) {
101                     throw e
102                 }
103             }
104         }
105         // Didn't pass any assertions
106         if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty()) {
107             val errorMsg =
108                 ExceptionMessageBuilder()
109                     .forSubject(entries.first())
110                     .setMessage("Assertion never passed ${assertions.first()}")
111                     .addExtraDescription(
112                         assertions.mapIndexed { idx, assertion ->
113                             Fact("Assertion$idx", assertion.toString())
114                         }
115                     )
116             throw SubjectAssertionError(errorMsg)
117         }
118 
119         val untestedAssertions = assertions.drop(assertionIndex + 1)
120         if (untestedAssertions.any { !it.isOptional }) {
121             val passedAssertionsFacts = assertions.take(assertionIndex).map { Fact("Passed", it) }
122             val untestedAssertionsFacts = untestedAssertions.map { Fact("Untested", it) }
123 
124             val errorMsg =
125                 ExceptionMessageBuilder()
126                     .forSubject(entries.last())
127                     .setMessage(
128                         "Assertion ${assertions[assertionIndex]} (block $assertionIndex) never " +
129                             "became false. The assertions that came after it were never reached."
130                     )
131                     .addExtraDescription(passedAssertionsFacts)
132                     .addExtraDescription(untestedAssertionsFacts)
133 
134             throw SubjectAssertionError(errorMsg)
135         }
136     }
137 
138     /**
139      * Ignores the first entries in the trace, until the first assertion passes. If it reaches the
140      * end of the trace without passing any assertion, return a failure with the name/reason from
141      * the first assertion
142      */
143     fun skipUntilFirstAssertion() {
144         skipUntilFirstAssertion = true
145     }
146 
147     fun isEqual(other: Any?): Boolean {
148         if (
149             other !is AssertionsChecker<*> ||
150                 skipUntilFirstAssertion != other.skipUntilFirstAssertion
151         ) {
152             return false
153         }
154         assertions.forEachIndexed { index, assertion ->
155             if (assertion != other.assertions[index]) {
156                 return false
157             }
158         }
159         return true
160     }
161 }
162