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