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