blob: e3922ca6bf5fdc1c6d1d762f465ab0e66ce52fa7 [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.subject.layers
import android.tools.common.datatypes.component.IComponentMatcher
import android.tools.common.flicker.assertions.Fact
import android.tools.common.flicker.subject.FlickerSubject
import android.tools.common.flicker.subject.region.RegionSubject
import android.tools.common.traces.surfaceflinger.Layer
import android.tools.common.traces.surfaceflinger.LayerTraceEntry
import android.tools.common.traces.surfaceflinger.LayersTrace
/**
* Subject for [LayerTraceEntry] objects, used to make assertions over behaviors that occur on a
* single SurfaceFlinger state.
*
* To make assertions over a specific state from a trace it is recommended to create a subject using
* [LayersTraceSubject](myTrace) and select the specific state using:
* ```
* [LayersTraceSubject.first]
* [LayersTraceSubject.last]
* [LayersTraceSubject.entry]
* ```
* Alternatively, it is also possible to use [LayerTraceEntrySubject](myState).
*
* Example:
* ```
* val trace = LayersTraceParser().parse(myTraceFile)
* val subject = LayersTraceSubject(trace).first()
* .contains("ValidLayer")
* .notContains("ImaginaryLayer")
* .coversExactly(DISPLAY_AREA)
* .invoke { myCustomAssertion(this) }
* ```
*/
class LayerTraceEntrySubject(
val entry: LayerTraceEntry,
val trace: LayersTrace? = null,
override val parent: FlickerSubject? = null
) : FlickerSubject(), ILayerSubject<LayerTraceEntrySubject, RegionSubject> {
override val timestamp = entry.timestamp
override val selfFacts = listOf(Fact("SF State", entry))
val subjects by lazy { entry.flattenedLayers.map { LayerSubject(this, timestamp, it) } }
/** Executes a custom [assertion] on the current subject */
operator fun invoke(assertion: (LayerTraceEntry) -> Unit): LayerTraceEntrySubject = apply {
assertion(this.entry)
}
/** {@inheritDoc} */
override fun isEmpty(): LayerTraceEntrySubject = apply {
check(entry.flattenedLayers.isEmpty()) { "SF state is empty" }
}
/** {@inheritDoc} */
override fun isNotEmpty(): LayerTraceEntrySubject = apply {
check(entry.flattenedLayers.isNotEmpty()) { "SF state is not empty" }
}
/** See [visibleRegion] */
fun visibleRegion(): RegionSubject =
visibleRegion(componentMatcher = null, useCompositionEngineRegionOnly = true)
/** See [visibleRegion] */
fun visibleRegion(componentMatcher: IComponentMatcher): RegionSubject =
visibleRegion(componentMatcher, useCompositionEngineRegionOnly = true)
/** {@inheritDoc} */
override fun visibleRegion(
componentMatcher: IComponentMatcher?,
useCompositionEngineRegionOnly: Boolean
): RegionSubject {
val selectedLayers =
if (componentMatcher == null) {
// No filters so use all subjects
subjects
} else {
subjects.filter { componentMatcher.layerMatchesAnyOf(it.layer) }
}
if (selectedLayers.isEmpty()) {
val str = componentMatcher?.toLayerIdentifier() ?: "<any>"
fail(
listOf(
Fact(ASSERTION_TAG, "visibleRegion($str)"),
Fact("Use composition engine region", useCompositionEngineRegionOnly),
Fact("Could not find layers", str)
)
)
}
val visibleLayers = selectedLayers.filter { it.isVisible }
return if (useCompositionEngineRegionOnly) {
val visibleAreas = visibleLayers.mapNotNull { it.layer.visibleRegion }.toTypedArray()
RegionSubject(visibleAreas, this, timestamp)
} else {
val visibleAreas = visibleLayers.map { it.layer.screenBounds }.toTypedArray()
RegionSubject(visibleAreas, this, timestamp)
}
}
/** {@inheritDoc} */
override fun contains(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
val found = componentMatcher.layerMatchesAnyOf(entry.flattenedLayers)
if (!found) {
fail(
Fact(ASSERTION_TAG, "contains(${componentMatcher.toLayerIdentifier()})"),
Fact("Could not find", componentMatcher.toLayerIdentifier())
)
}
}
/** {@inheritDoc} */
override fun notContains(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
val layers = subjects.map { it.layer }
val notContainsComponent =
componentMatcher.check(layers) { matchedLayers -> matchedLayers.isEmpty() }
if (notContainsComponent) {
return@apply
}
val failedEntries = subjects.filter { componentMatcher.layerMatchesAnyOf(it.layer) }
val failureFacts =
mutableListOf(
Fact(ASSERTION_TAG, "notContains(${componentMatcher.toLayerIdentifier()})")
)
failedEntries.forEach { entry -> failureFacts.add(Fact("Found", entry)) }
failedEntries.firstOrNull()?.fail(failureFacts)
}
/** {@inheritDoc} */
override fun isVisible(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
contains(componentMatcher)
val layers = subjects.map { it.layer }
val hasVisibleSubject =
componentMatcher.check(layers) { matchedLayers ->
matchedLayers.any { layer -> layer.isVisible }
}
if (hasVisibleSubject) {
return@apply
}
val matchingSubjects = subjects.filter { componentMatcher.layerMatchesAnyOf(it.layer) }
val failedEntries = matchingSubjects.filter { it.isInvisible }
val failureFacts =
mutableListOf(Fact(ASSERTION_TAG, "isVisible(${componentMatcher.toLayerIdentifier()})"))
failedEntries.forEach { entry ->
failureFacts.add(Fact("Is Invisible", entry))
failureFacts.addAll(entry.visibilityReason.map { Fact("Invisibility reason", it) })
}
failedEntries.firstOrNull()?.fail(failureFacts)
}
/** {@inheritDoc} */
override fun isInvisible(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
val layers = subjects.map { it.layer }
val hasInvisibleComponent =
componentMatcher.check(layers) { componentLayers ->
componentLayers.all { layer ->
subjects.first { subject -> subject.layer == layer }.isInvisible
}
}
if (hasInvisibleComponent) {
return@apply
}
val matchingSubjects = subjects.filter { componentMatcher.layerMatchesAnyOf(it.layer) }
val failedEntries = matchingSubjects.filter { it.isVisible }
val failureFacts =
mutableListOf(
Fact(ASSERTION_TAG, "isInvisible(${componentMatcher.toLayerIdentifier()})")
)
failureFacts.addAll(failedEntries.map { Fact("Is Visible", it) })
failedEntries.firstOrNull()?.fail(failureFacts)
}
/** {@inheritDoc} */
override fun isSplashScreenVisibleFor(
componentMatcher: IComponentMatcher
): LayerTraceEntrySubject = apply {
var target: FlickerSubject? = null
var reason: Fact? = null
val matchingLayer =
subjects.map { it.layer }.filter { componentMatcher.layerMatchesAnyOf(it) }
val matchingActivityRecords = matchingLayer.mapNotNull { getActivityRecordFor(it) }
if (matchingActivityRecords.isEmpty()) {
fail(
Fact(
ASSERTION_TAG,
"isSplashScreenVisibleFor(${componentMatcher.toLayerIdentifier()})"
),
Fact("Could not find Activity Record layer", componentMatcher.toLayerIdentifier())
)
return this
}
// Check the matched activity record layers for containing splash screens
for (layer in matchingActivityRecords) {
val splashScreenContainers = layer.children.filter { it.name.contains("Splash Screen") }
val splashScreenLayers =
splashScreenContainers.flatMap {
it.children.filter { childLayer -> childLayer.name.contains("Splash Screen") }
}
if (splashScreenLayers.all { it.isHiddenByParent || !it.isVisible }) {
reason = Fact("No splash screen visible for", layer.name)
target = subjects.first { it.layer == layer }
continue
}
reason = null
target = null
break
}
reason?.run {
target?.fail(
Fact(
ASSERTION_TAG,
"isSplashScreenVisibleFor(${componentMatcher.toLayerIdentifier()})"
),
reason
)
}
}
/** {@inheritDoc} */
override fun hasColor(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
contains(componentMatcher)
val hasColorLayer =
componentMatcher.check(subjects.map { it.layer }) {
it.any { layer -> layer.color.isNotEmpty }
}
if (!hasColorLayer) {
fail(Fact(ASSERTION_TAG, "hasColor(${componentMatcher.toLayerIdentifier()})"))
}
}
/** {@inheritDoc} */
override fun hasNoColor(componentMatcher: IComponentMatcher): LayerTraceEntrySubject = apply {
val hasNoColorLayer =
componentMatcher.check(subjects.map { it.layer }) {
it.all { layer -> layer.color.isEmpty }
}
if (!hasNoColorLayer) {
fail(Fact(ASSERTION_TAG, "hasNoColor(${componentMatcher.toLayerIdentifier()})"))
}
}
/** See [layer] */
fun layer(componentMatcher: IComponentMatcher): LayerSubject? {
return layer { componentMatcher.layerMatchesAnyOf(it) }
}
/** {@inheritDoc} */
override fun layer(name: String, frameNumber: Long): LayerSubject? {
return layer { it.name.contains(name) && it.currFrame == frameNumber }
}
/**
* Obtains a [LayerSubject] for the first occurrence of a [Layer] matching [predicate] or throws
* and error if the layer doesn't exist
*
* @param predicate to search for a layer
*
* @return [LayerSubject] that can be used to make assertions
*/
fun layer(predicate: (Layer) -> Boolean): LayerSubject? =
subjects.firstOrNull { predicate(it.layer) }
private fun getActivityRecordFor(layer: Layer): Layer? {
if (layer.name.startsWith("ActivityRecord{")) {
return layer
}
val parent = layer.parent ?: return null
return getActivityRecordFor(parent)
}
override fun toString(): String {
return "LayerTraceEntrySubject($entry)"
}
}