blob: 6ffa4af7cd0da50b3daa0d88cfad1601e6db3754 [file] [log] [blame]
/*
* Copyright (C) 2021 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 com.android.server.wm.flicker
import android.util.Log
import com.android.server.wm.flicker.FlickerRunResult.Companion.RunStatus
import com.android.server.wm.flicker.monitor.IFileGeneratingMonitor
import com.android.server.wm.flicker.monitor.ITransitionMonitor
import com.android.server.wm.traces.common.ConditionList
import com.android.server.wm.traces.common.WindowManagerConditionsFactory
import com.android.server.wm.traces.parser.getCurrentState
import java.io.IOException
import java.nio.file.Files
import org.junit.runner.Description
/**
* Runner to execute the transitions of a flicker test
*
* The commands are executed in the following order:
* 1) [Flicker.testSetup]
* 2) [Flicker.runSetup
* 3) Start monitors
* 4) [Flicker.transitions]
* 5) Stop monitors
* 6) [Flicker.runTeardown]
* 7) [Flicker.testTeardown]
*
* If the tests were already executed, reuse the previous results
*
*/
open class TransitionRunner {
/**
* Iteration identifier during test run
*/
internal var iteration = -1
private set
private val tags = mutableSetOf<String>()
// Iteration to resultBuilder
private var results = mutableMapOf<Int, FlickerRunResult>()
/**
* Executes the setup, transitions and teardown defined in [flicker]
*
* @param flicker test specification
* @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0
*/
open fun execute(flicker: Flicker, useCacheIfAvailable: Boolean = true): FlickerResult {
check(flicker)
return run(flicker)
}
/**
* Validate the [flicker] test specification before executing the transitions
*
* @param flicker test specification
* @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0
*/
protected fun check(flicker: Flicker) {
require(flicker.transitions.isNotEmpty()) {
"A flicker test must include transitions to run"
}
require(flicker.repetitions > 0) {
"Number of repetitions must be greater than 0"
}
}
open fun cleanUp() {
tags.clear()
results.clear()
}
/**
* Runs the actual setup, transitions and teardown defined in [flicker]
*
* @param flicker test specification
*/
internal open fun run(flicker: Flicker): FlickerResult {
val executionErrors = mutableListOf<ExecutionError>()
safeExecution(flicker, executionErrors) {
runTestSetup(flicker)
for (x in 0 until flicker.repetitions) {
iteration = x
results[iteration] = FlickerRunResult(flicker.testName, iteration)
val description = Description.createSuiteDescription(flicker.testName)
if (flicker.faasEnabled) {
flicker.faas.setCriticalUserJourneyName(flicker.testName)
flicker.faas.testStarted(description)
}
runTransitionSetup(flicker)
runTransition(flicker)
runTransitionTeardown(flicker)
processRunTraces(flicker, RunStatus.ASSERTION_SUCCESS)
if (flicker.faasEnabled) {
flicker.faas.testFinished(description)
if (flicker.faas.executionErrors.isNotEmpty()) {
executionErrors.addAll(flicker.faas.executionErrors)
}
}
}
runTestTeardown(flicker)
}
val result = FlickerResult(
results.values.toList(), // toList ensures we clone the list before cleanUp
tags.toSet(),
executionErrors
)
cleanUp()
return result
}
private fun safeExecution(
flicker: Flicker,
executionErrors: MutableList<ExecutionError>,
execution: () -> Unit
) {
try {
execution()
} catch (e: TestSetupFailure) {
// If we fail on the test setup we can't run any of the transitions
executionErrors.add(e)
} catch (e: TransitionSetupFailure) {
// If we fail on the transition run setup then we don't want to run any further
// transitions nor save any results for this run. We simply want to run the test
// teardown.
executionErrors.add(e)
getCurrentRunResult().setStatus(RunStatus.RUN_FAILED)
safeExecution(flicker, executionErrors) {
runTestTeardown(flicker)
}
} catch (e: TransitionExecutionFailure) {
// If a transition fails to run we don't want to run the following iterations as the
// device is likely in an unexpected state which would lead to further errors. We simply
// want to run the test teardown
executionErrors.add(e)
flicker.traceMonitors.forEach { it.tryStop() }
safeExecution(flicker, executionErrors) {
processRunTraces(flicker, RunStatus.RUN_FAILED)
runTestTeardown(flicker)
}
} catch (e: TransitionTeardownFailure) {
// If a transition teardown fails to run we don't want to run the following iterations
// as the device is likely in an unexpected state which would lead to further errors.
// But, we do want to run the test teardown.
executionErrors.add(e)
flicker.traceMonitors.forEach { it.tryStop() }
safeExecution(flicker, executionErrors) {
processRunTraces(flicker, RunStatus.RUN_FAILED)
runTestTeardown(flicker)
}
} catch (e: TraceProcessingFailure) {
// If we fail to process the run traces we still want to run the teardowns and report
// the execution error.
executionErrors.add(e)
safeExecution(flicker, executionErrors) {
runTransitionTeardown(flicker)
runTestTeardown(flicker)
}
} catch (e: TestTeardownFailure) {
// If we fail in the execution of the test teardown there is nothing else to do apart
// from reporting the execution error.
executionErrors.add(e)
getCurrentRunResult().setStatus(RunStatus.RUN_FAILED)
}
}
/**
* Parses the traces collected by the monitors to generate FlickerRunResults containing the
* parsed trace and information about the status of the run.
* The run results are added to the resultBuilders list which is then used to run Flicker
* assertions on.
*/
@Throws(TraceProcessingFailure::class)
private fun processRunTraces(
flicker: Flicker,
status: RunStatus
) {
try {
val result = getCurrentRunResult()
result.setStatus(status)
setMonitorResults(flicker, result)
result.lock()
if (flicker.faasEnabled && !status.isFailure) {
// Don't run FaaS on failed transitions
val wmTrace = result.buildWmTrace()
val layersTrace = result.buildLayersTrace()
val transitionsTrace = result.buildTransitionsTrace()
flicker.faasTracesCollector.wmTrace = wmTrace
flicker.faasTracesCollector.layersTrace = layersTrace
flicker.faasTracesCollector.transitionsTrace = transitionsTrace
}
} catch (e: Throwable) {
// We have failed to add the results to the runs, so we can effectively consider these
// results as "lost" as they won't be used from now forth. So we can safely rename
// to file to indicate the failure and make it easier to find in the archives.
flicker.traceMonitors.forEach {
// All monitors that generate files we want to keep in the archives should implement
// IFileGeneratingMonitor
if (it is IFileGeneratingMonitor) {
Utils.addStatusToFileName(it.outputFile, RunStatus.PARSING_FAILURE)
}
}
throw TraceProcessingFailure(e)
}
}
@Throws(TestSetupFailure::class)
private fun runTestSetup(flicker: Flicker) {
try {
flicker.testSetup.forEach { it.invoke(flicker) }
} catch (e: Throwable) {
throw TestSetupFailure(e)
}
}
@Throws(TestTeardownFailure::class)
private fun runTestTeardown(flicker: Flicker) {
try {
flicker.testTeardown.forEach { it.invoke(flicker) }
} catch (e: Throwable) {
throw TestTeardownFailure(e)
}
}
@Throws(TransitionSetupFailure::class)
private fun runTransitionSetup(flicker: Flicker) {
try {
flicker.runSetup.forEach { it.invoke(flicker) }
flicker.wmHelper.StateSyncBuilder()
.add(UI_STABLE_CONDITIONS)
.waitFor()
} catch (e: Throwable) {
throw TransitionSetupFailure(e)
}
}
@Throws(TransitionExecutionFailure::class)
private fun runTransition(flicker: Flicker) {
try {
flicker.traceMonitors.forEach { it.start() }
flicker.transitions.forEach { it.invoke(flicker) }
} catch (e: Throwable) {
throw TransitionExecutionFailure(e)
}
}
@Throws(TransitionTeardownFailure::class)
private fun runTransitionTeardown(flicker: Flicker) {
try {
flicker.wmHelper.StateSyncBuilder()
.add(UI_STABLE_CONDITIONS)
.waitFor()
flicker.traceMonitors.forEach { it.tryStop() }
flicker.runTeardown.forEach { it.invoke(flicker) }
} catch (e: Throwable) {
throw TransitionTeardownFailure(e)
}
}
private fun setMonitorResults(
flicker: Flicker,
result: FlickerRunResult,
): FlickerRunResult {
flicker.traceMonitors.forEach {
result.setResultsFromMonitor(it)
}
return result
}
private fun ITransitionMonitor.tryStop() {
this.run {
try {
stop()
} catch (e: Exception) {
Log.e(FLICKER_TAG, "Unable to stop $this", e)
}
}
}
private fun getTaggedFilePath(flicker: Flicker, tag: String, file: String) =
"${flicker.testName}_${iteration}_${tag}_$file"
/**
* Captures a snapshot of the device state and associates it with a new tag.
*
* This tag can be used to make assertions about the state of the device when the
* snapshot is collected.
*
* [tag] is used as part of the trace file name, thus, only valid letters and digits
* can be used
*
* @param flicker test specification
* @throws IllegalArgumentException If [tag] contains invalid characters
*/
open fun createTag(flicker: Flicker, tag: String) {
require(!tag.contains(" ")) {
"The test tag $tag can not contain spaces since it is a part of the file name"
}
tags.add(tag)
val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation)
try {
val wmDumpFile = flicker.outputDir.resolve(
getTaggedFilePath(flicker, tag, "wm_dump")
)
Files.write(wmDumpFile, deviceStateBytes.first)
val layersDumpFile = flicker.outputDir.resolve(
getTaggedFilePath(flicker, tag, "layers_dump")
)
Files.write(layersDumpFile, deviceStateBytes.second)
getCurrentRunResult().addTaggedState(
tag,
wmDumpFile.toFile(),
layersDumpFile.toFile(),
)
} catch (e: IOException) {
throw RuntimeException("Unable to create trace file: ${e.message}", e)
}
}
private fun getCurrentRunResult(): FlickerRunResult {
return results[iteration]!!
}
companion object {
/**
* Conditions that determine when the UI is in a stable stable and no windows or layers are
* animating or changing state.
*/
private val UI_STABLE_CONDITIONS = ConditionList(
listOf(
WindowManagerConditionsFactory.isWMStateComplete(),
WindowManagerConditionsFactory.hasLayersAnimating().negate()
)
)
open class ExecutionError(private val inner: Throwable) : Throwable(inner) {
init {
super.setStackTrace(inner.stackTrace)
}
override val message: String?
get() = inner.toString()
}
class TestSetupFailure(val e: Throwable) : ExecutionError(e)
class TransitionSetupFailure(val e: Throwable) : ExecutionError(e)
class TransitionExecutionFailure(val e: Throwable) : ExecutionError(e)
class TraceProcessingFailure(val e: Throwable) : ExecutionError(e)
class TransitionTeardownFailure(val e: Throwable) : ExecutionError(e)
class TestTeardownFailure(val e: Throwable) : ExecutionError(e)
}
}