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