/*
* 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.flicker.assertions
import android.tools.flicker.subject.FlickerSubject
import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder
import android.tools.flicker.subject.exceptions.SubjectAssertionError
/**
* 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 trace entry type
*/
class AssertionsChecker {
private val assertions = mutableListOf>()
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) {
if (assertions.isEmpty() || entries.isEmpty()) {
return
}
var entryIndex = 0
var assertionIndex = 0
var lastPassedAssertionIndex = -1
val assertionTrace = mutableListOf()
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: AssertionError) {
// 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) {
throw e
}
assertionIndex++
if (assertionIndex == assertions.size) {
throw e
}
}
}
// Didn't pass any assertions
if (lastPassedAssertionIndex == -1 && assertions.isNotEmpty()) {
val errorMsg =
ExceptionMessageBuilder()
.forSubject(entries.first())
.setMessage("Assertion never passed ${assertions.first()}")
.addExtraDescription(
assertions.mapIndexed { idx, assertion ->
Fact("Assertion$idx", assertion.toString())
}
)
throw SubjectAssertionError(errorMsg)
}
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 errorMsg =
ExceptionMessageBuilder()
.forSubject(entries.last())
.setMessage(
"Assertion ${assertions[assertionIndex]} (block $assertionIndex) never " +
"became false. The assertions that came after it were never reached."
)
.addExtraDescription(passedAssertionsFacts)
.addExtraDescription(untestedAssertionsFacts)
throw SubjectAssertionError(errorMsg)
}
}
/**
* 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
}
}