• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.common.flicker.assertions
18 
19 import android.tools.common.flicker.subject.FlickerSubject
20 import android.tools.common.flicker.subject.exceptions.ExceptionMessageBuilder
21 import android.tools.common.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
112                             .mapIndexed { idx, assertion ->
113                                 Fact("Assertion$idx", assertion.toString())
114                             }
115                             .toTypedArray()
116                     )
117             throw SubjectAssertionError(errorMsg)
118         }
119 
120         val untestedAssertions = assertions.drop(assertionIndex + 1)
121         if (untestedAssertions.any { !it.isOptional }) {
122             val passedAssertionsFacts = assertions.take(assertionIndex).map { Fact("Passed", it) }
123             val untestedAssertionsFacts = untestedAssertions.map { Fact("Untested", it) }
124 
125             val errorMsg =
126                 ExceptionMessageBuilder()
127                     .forSubject(entries.last())
128                     .setMessage(
129                         "Assertion $assertionIndex never failed: ${assertions[assertionIndex]}"
130                     )
131                     .addExtraDescription(*passedAssertionsFacts.toTypedArray())
132                     .addExtraDescription(*untestedAssertionsFacts.toTypedArray())
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