blob: 365b8de8b421f8bc73d1aca93ec8980060267bcd [file] [log] [blame]
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.tools.common.flicker.assertions
import android.tools.common.flicker.subject.FlickerSubject
import kotlin.math.max
/**
* Runs sequences of assertions on sequences of subjects.
*
* Starting at the first assertion and first trace entry, executes the assertions iteratively on the
* trace until all assertions and trace entries succeed.
*
* @param <T> trace entry type </T>
*/
class AssertionsChecker<T : FlickerSubject> {
private val assertions = mutableListOf<CompoundAssertion<T>>()
private var skipUntilFirstAssertion = false
internal fun isEmpty() = assertions.isEmpty()
/** Add [assertion] to a new [CompoundAssertion] block. */
fun add(name: String, isOptional: Boolean = false, assertion: (T) -> Unit) {
assertions.add(CompoundAssertion(assertion, name, isOptional))
}
/** Append [assertion] to the last existing set of assertions. */
fun append(name: String, isOptional: Boolean = false, assertion: (T) -> Unit) {
assertions.last().add(assertion, name, isOptional)
}
/**
* Steps through each trace entry checking if provided assertions are true in the order they are
* added. Each assertion must be true for at least a single trace entry.
*
* This can be used to check for asserting a change in property over a trace. Such as visibility
* for a window changes from true to false or top-most window changes from A to B and back to A
* again.
*
* It is also possible to ignore failures on initial elements, until the first assertion passes,
* this allows the trace to be recorded for longer periods, and the checks to happen only after
* some time.
*
* @param entries list of entries to perform assertions on
* @return list of failed assertion results
*/
fun test(entries: List<T>) {
if (assertions.isEmpty() || entries.isEmpty()) {
return
}
var entryIndex = 0
var assertionIndex = 0
var lastPassedAssertionIndex = -1
val assertionTrace = mutableListOf<String>()
while (assertionIndex < assertions.size && entryIndex < entries.size) {
val currentAssertion = assertions[assertionIndex]
val currEntry = entries[entryIndex]
try {
val log =
"${assertionIndex + 1}/${assertions.size}:[${currentAssertion.name}]\t" +
"Entry: ${entryIndex + 1}/${entries.size} $currEntry"
assertionTrace.add(log)
currentAssertion.invoke(currEntry)
lastPassedAssertionIndex = assertionIndex
entryIndex++
} catch (e: Throwable) {
// ignore errors at the start of the trace
val ignoreFailure = skipUntilFirstAssertion && lastPassedAssertionIndex == -1
if (ignoreFailure) {
entryIndex++
continue
}
// failure is an optional assertion, just consider it passed skip it
if (currentAssertion.isOptional) {
lastPassedAssertionIndex = assertionIndex
assertionIndex++
continue
}
if (lastPassedAssertionIndex != assertionIndex) {
val prevEntry = entries[max(entryIndex - 1, 0)]
prevEntry.fail(e)
}
assertionIndex++
if (assertionIndex == assertions.size) {
val prevEntry = entries[max(entryIndex - 1, 0)]
prevEntry.fail(e)
}
}
}
// Didn't pass any assertions
if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty()) {
entries.first().fail("Assertion never passed", assertions.first())
}
val untestedAssertions = assertions.drop(assertionIndex + 1)
if (untestedAssertions.any { !it.isOptional }) {
val passedAssertionsFacts = assertions.take(assertionIndex).map { Fact("Passed", it) }
val untestedAssertionsFacts = untestedAssertions.map { Fact("Untested", it) }
val trace = assertionTrace.map { Fact("Trace", it) }
val reason = mutableListOf<Fact>()
reason.addAll(passedAssertionsFacts)
reason.add(Fact("Assertion never failed", assertions[assertionIndex]))
reason.addAll(untestedAssertionsFacts)
reason.addAll(trace)
entries.first().fail(reason)
}
}
/**
* Ignores the first entries in the trace, until the first assertion passes. If it reaches the
* end of the trace without passing any assertion, return a failure with the name/reason from
* the first assertion
*/
fun skipUntilFirstAssertion() {
skipUntilFirstAssertion = true
}
fun isEqual(other: Any?): Boolean {
if (
other !is AssertionsChecker<*> ||
skipUntilFirstAssertion != other.skipUntilFirstAssertion
) {
return false
}
assertions.forEachIndexed { index, assertion ->
if (assertion != other.assertions[index]) {
return false
}
}
return true
}
}