Produce a test per flicker assertion

Currently flicker runs all assertions for a transition and checks if the CUJ fails or passes.

For easier debugging, split the assertions into individual tests. Use a parameterized test runner to execute such tests until JUnit5 becomes available in the platform

Bug: 162923992
Test: atest FlickerTests
Change-Id: I21b062f60da745515a3f6280601d68f5afa17eb5
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
index ad251e3..7116492 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/Flicker.kt
@@ -20,16 +20,12 @@
 import android.support.test.launcherhelper.ILauncherStrategy
 import androidx.annotation.VisibleForTesting
 import androidx.test.uiautomator.UiDevice
-import com.android.server.wm.flicker.assertions.FlickerAssertionError
-import com.android.server.wm.flicker.dsl.AssertionTag
-import com.android.server.wm.flicker.dsl.AssertionTarget
-import com.android.server.wm.flicker.dsl.TestCommands
+import com.android.server.wm.flicker.assertions.AssertionData
 import com.android.server.wm.flicker.monitor.ITransitionMonitor
 import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor
-import com.android.server.wm.traces.parser.getCurrentState
-import com.google.common.truth.Truth
-import java.io.IOException
-import java.nio.file.Files
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import java.nio.file.Path
 
 @DslMarker
@@ -41,189 +37,120 @@
  * [WindowManagerTrace] and [LayersTrace]
  */
 @FlickerDslMarker
-data class Flicker(
+class Flicker(
     /**
      * Instrumentation to run the tests
      */
-    val instrumentation: Instrumentation,
+    @JvmField val instrumentation: Instrumentation,
     /**
      * Test automation component used to interact with the device
      */
-    val device: UiDevice,
+    @JvmField val device: UiDevice,
     /**
      * Strategy used to interact with the launcher
      */
-    val launcherStrategy: ILauncherStrategy,
+    @JvmField val launcherStrategy: ILauncherStrategy,
     /**
      * Output directory for test results
      */
-    val outputDir: Path,
+    @JvmField val outputDir: Path,
     /**
      * Test name used to store the test results
      */
-    private val testName: String,
+    @JvmField val testName: String,
     /**
      * Number of times the test should be executed
      */
-    private var repetitions: Int,
+    @JvmField var repetitions: Int,
     /**
      * Monitor for janky frames, when filtering out janky runs
      */
-    private val frameStatsMonitor: WindowAnimationFrameStatsMonitor?,
+    @JvmField val frameStatsMonitor: WindowAnimationFrameStatsMonitor?,
     /**
      * Enabled tracing monitors
      */
-    private val traceMonitors: List<ITransitionMonitor>,
+    @JvmField val traceMonitors: List<ITransitionMonitor>,
+    /**
+     * Commands to be executed before each run
+     */
+    @JvmField val testSetup: List<Flicker.() -> Any>,
     /**
      * Commands to be executed before the test
      */
-    private val setup: TestCommands,
+    @JvmField val runSetup: List<Flicker.() -> Any>,
     /**
      * Commands to be executed after the test
      */
-    private val teardown: TestCommands,
+    @JvmField val testTeardown: List<Flicker.() -> Any>,
+    /**
+     * Commands to be executed after the run
+     */
+    @JvmField val runTeardown: List<Flicker.() -> Any>,
     /**
      * Test commands
      */
-    private val transitions: List<Flicker.() -> Any>,
+    @JvmField val transitions: List<Flicker.() -> Any>,
     /**
      * Custom set of assertions
      */
-    private val assertions: AssertionTarget
-) {
-    private val results = mutableListOf<FlickerRunResult>()
-    private val tags = AssertionTag.DEFAULT.map { it.tag }.toMutableSet()
     @VisibleForTesting
-    var error: Throwable? = null
-        private set
-
+    @JvmField val assertions: List<AssertionData>,
     /**
-     * Iteration identifier during test run
+     * Runner to execute the test transitions
      */
-    private var iteration = 0
+    @JvmField val runner: TransitionRunner,
+    /**
+     * Helper object for WM Synchronization
+     */
+    @JvmField val wmHelper: WindowManagerStateHelper
+) {
+    var result = FlickerResult()
+        private set
 
     /**
      * Executes the test.
      *
-     * The commands are executed in the following order:
-     * 1) [setup] ([TestCommands.testCommands])
-     * 2) [setup] ([TestCommands.runCommands])
-     * 3) Start monitors
-     * 4) [transitions]
-     * 5) Stop monitors
-     * 6) [teardown] ([TestCommands.runCommands])
-     * 7) [teardown] ([TestCommands.testCommands])
-     *
-     * If the tests were already executed, reuse the previous results
-     *
-     * @throws IllegalArgumentException If the transitions
+     * @throws IllegalStateException If cannot execute the transition
      */
-    fun execute() = apply {
-        require(transitions.isNotEmpty()) { "A flicker test must include transitions to run" }
-        if (results.isNotEmpty()) {
-            Log.w(FLICKER_TAG, "Flicker test already executed. Reusing results.")
-            return this
-        }
-        try {
-            try {
-                error = null
-                setup.testCommands.forEach { it.invoke(this) }
-                for (iteration in 0 until repetitions) {
-                    this.iteration = iteration
-                    try {
-                        setup.runCommands.forEach { it.invoke(this) }
-                        traceMonitors.forEach { it.start() }
-                        frameStatsMonitor?.run { start() }
-                        transitions.forEach { it.invoke(this) }
-                    } finally {
-                        traceMonitors.forEach { it.tryStop() }
-                        frameStatsMonitor?.run { tryStop() }
-                        teardown.runCommands.forEach { it.invoke(this) }
-                    }
-                    if (frameStatsMonitor?.jankyFramesDetected() == true) {
-                        Log.e(FLICKER_TAG, "Skipping iteration $iteration/${repetitions - 1} " +
-                                "for test $testName due to jank. $frameStatsMonitor")
-                        continue
-                    }
-                    saveResult(this.iteration)
-                }
-            } finally {
-                teardown.testCommands.forEach { it.invoke(this) }
-            }
-        } catch (e: Throwable) {
-            error = e
-            throw RuntimeException(e)
+    fun execute(): Flicker = apply {
+        result = runner.execute(this)
+        val error = result.error
+        if (error != null) {
+            throw IllegalStateException("Unable to execute transition", error)
         }
     }
 
-    private fun cleanUp(failures: List<FlickerAssertionError>) {
-        results.forEach {
-            if (it.canDelete(failures)) {
-                it.cleanUp()
-            }
-        }
-    }
-
-    @Deprecated("Prefer checkAssertions", replaceWith = ReplaceWith("checkAssertions"))
-    fun makeAssertions() = checkAssertions(includeFlakyAssertions = false)
+    /**
+     * Asserts if the transition of this flicker test has ben executed
+     */
+    fun checkIsExecuted() = result.checkIsExecuted()
 
     /**
      * Run the assertions on the trace
      *
-     * @param includeFlakyAssertions If true, checks the flaky assertion
+     * @param onlyFlaky Runs only the flaky assertions
      * @throws AssertionError If the assertions fail or the transition crashed
      */
     @JvmOverloads
-    fun checkAssertions(includeFlakyAssertions: Boolean = false) {
-        Truth.assertWithMessage(error?.message).that(error).isNull()
-        Truth.assertWithMessage("Transition was not executed").that(results).isNotEmpty()
-        val failures = results.flatMap { assertions.checkAssertions(it, includeFlakyAssertions) }
-        this.cleanUp(failures)
+    fun checkAssertions(onlyFlaky: Boolean = false) {
+        if (result.isEmpty()) {
+            execute()
+        }
+        val failures = result.checkAssertions(assertions, onlyFlaky)
         val failureMessage = failures.joinToString("\n") { it.message }
-        Truth.assertWithMessage(failureMessage).that(failureMessage.isEmpty()).isTrue()
+
+        if (failureMessage.isNotEmpty()) {
+            throw AssertionError(failureMessage)
+        }
     }
 
-    private fun getTaggedFilePath(tag: String, file: String) =
-            "${this.testName}_${this.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
-     *
-     * @throws IllegalArgumentException If [tag] contains invalid characters
+     * Deletes the traces files for successful assertions and clears the cached runner results
      */
-    fun createTag(tag: String) {
-        if (tag in tags) {
-            throw IllegalArgumentException("Tag $tag has already been used")
-        }
-        tags.add(tag)
-        val assertionTag = AssertionTag(tag)
-
-        val deviceState = getCurrentState(instrumentation.uiAutomation)
-        try {
-            val wmTraceFile = outputDir.resolve(getTaggedFilePath(tag, "wm_trace"))
-            Files.write(wmTraceFile, deviceState.wmTraceData)
-
-            val layersTraceFile = outputDir.resolve(getTaggedFilePath(tag, "layers_trace"))
-            Files.write(layersTraceFile, deviceState.layersTraceData)
-
-            val result = FlickerRunResult(
-                    assertionTag,
-                    iteration = this.iteration,
-                    wmTraceFile = wmTraceFile,
-                    layersTraceFile = layersTraceFile,
-                    wmTrace = deviceState.wmTrace,
-                    layersTrace = deviceState.layersTrace
-            )
-            results.add(result)
-        } catch (e: IOException) {
-            throw RuntimeException("Unable to create trace file: ${e.message}", e)
-        }
+    fun cleanUp() {
+        runner.cleanUp()
+        result.cleanUp()
+        result = FlickerResult()
     }
 
     /**
@@ -235,26 +162,25 @@
      */
     fun withTag(tag: String, commands: Flicker.() -> Any) {
         commands()
-        createTag(tag)
+        runner.createTag(this, tag)
     }
 
-    private fun saveResult(iteration: Int) {
-        val resultBuilder = FlickerRunResult.Builder()
-        traceMonitors.forEach { it.save(testName, iteration, resultBuilder) }
-
-        AssertionTag.DEFAULT.forEach { location ->
-            results.add(resultBuilder.build(location))
-        }
+    fun createTag(tag: String) {
+        withTag(tag) {}
     }
 
-    private fun ITransitionMonitor.tryStop() {
-        this.run {
-            try {
-                stop()
-            } catch (e: Exception) {
-                Log.e(FLICKER_TAG, "Unable to stop $this")
-            }
+    @JvmOverloads
+    fun copy(newAssertion: AssertionData?, newName: String = ""): Flicker {
+        val name = if (newName.isNotEmpty()) {
+            newName
+        } else {
+            testName
         }
+        val assertion = newAssertion?.let { listOf(it) } ?: emptyList()
+        return Flicker(instrumentation, device, launcherStrategy, outputDir, name,
+            repetitions, frameStatsMonitor, traceMonitors, testSetup, runSetup,
+            testTeardown, runTeardown, transitions, assertion, runner, wmHelper
+        )
     }
 
     override fun toString(): String {
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
new file mode 100644
index 0000000..e0bf86e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerResult.kt
@@ -0,0 +1,89 @@
+/*
+ * 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 com.android.server.wm.flicker.assertions.AssertionData
+import com.android.server.wm.flicker.assertions.FlickerAssertionError
+import com.google.common.truth.Truth
+
+/**
+ * Result of a flicker run, including transitions, errors and create tags
+ */
+data class FlickerResult(
+    /**
+     * Result of each transition run
+     */
+    @JvmField val runs: List<FlickerRunResult> = listOf(),
+    /**
+     * List of test created during the execution
+     */
+    @JvmField val tags: Set<String> = setOf(),
+    /**
+     * Error which happened during the transition
+     */
+    @JvmField val error: Throwable? = null
+) {
+    /**
+     * List of failures during assertion
+     */
+    private val failures: MutableList<FlickerAssertionError> = mutableListOf()
+
+    /**
+     * Asserts if the transition of this flicker test has ben executed
+     */
+    internal fun checkIsExecuted() {
+        Truth.assertWithMessage(error?.message).that(error).isNull()
+        Truth.assertWithMessage("Transition was not executed").that(runs).isNotEmpty()
+    }
+
+    /**
+     * Run the assertions on the trace
+     *
+     * @param onlyFlaky Runs only the flaky assertions
+     * @throws AssertionError If the assertions fail or the transition crashed
+     */
+    internal fun checkAssertions(
+        assertions: List<AssertionData>,
+        onlyFlaky: Boolean
+    ): List<FlickerAssertionError> {
+        checkIsExecuted()
+        val currFailures: List<FlickerAssertionError> = runs.flatMap { run ->
+            assertions.mapNotNull { assertion ->
+                try {
+                    assertion.checkAssertion(run, onlyFlaky)
+                    null
+                } catch (error: Throwable) {
+                    FlickerAssertionError(error, assertion, run)
+                }
+            }
+        }
+        failures.addAll(currFailures)
+        return currFailures
+    }
+
+    fun cleanUp() {
+        runs.forEach {
+            if (it.canDelete(failures)) {
+                it.cleanUp()
+            }
+        }
+    }
+
+    fun isEmpty(): Boolean = error == null && runs.isEmpty()
+
+    fun isNotEmpty(): Boolean = !isEmpty()
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
index 715262d..b17383f 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerRunResult.kt
@@ -17,11 +17,18 @@
 package com.android.server.wm.flicker
 
 import android.util.Log
+import androidx.annotation.VisibleForTesting
 import com.android.server.wm.flicker.assertions.FlickerAssertionError
-import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.flicker.assertions.FlickerSubject
 import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.traces.eventlog.EventLogSubject
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
 import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import com.android.server.wm.flicker.traces.layers.LayersTraceSubject
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
 import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
 import com.android.server.wm.traces.parser.layers.LayersTraceParser
 import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
 import java.io.IOException
@@ -33,95 +40,40 @@
  */
 class FlickerRunResult private constructor(
     /**
-     * Determines which assertions to run (e.g., start, end, all, or a custom tag)
-     */
-    var assertionTag: AssertionTag,
-    /**
      * Run identifier
      */
-    val iteration: Int,
+    @JvmField val iteration: Int,
     /**
-     * Path to the WindowManager trace file, if collected
+     * Path to the trace files associated with the result (incl. screen recording)
      */
-    @JvmField val wmTraceFile: Path?,
+    @JvmField val traceFiles: List<Path>,
     /**
-     * Path to the SurfaceFlinger trace file, if collected
+     * Determines which assertions to run (e.g., start, end, all, or a custom tag)
      */
-    @JvmField val layersTraceFile: Path?,
+    @JvmField var assertionTag: String,
     /**
-     * Path to screen recording of the run, if collected
+     * Truth subject that corresponds to a [WindowManagerTrace] or [WindowManagerState]
      */
-    @JvmField val screenRecording: Path?,
-
+    private val wmSubject: FlickerSubject?,
     /**
-     * List of focus events, if collected
+     * Truth subject that corresponds to a [LayersTrace] or [LayerTraceEntry]
      */
-    val eventLog: List<FocusEvent>,
+    private val layersSubject: FlickerSubject?,
     /**
-     * Parse a [WindowManagerTrace]
+     * Truth subject that corresponds to a list of [FocusEvent]
      */
-    val parseWmTrace: (() -> WindowManagerTrace?)?,
-    /**
-     * Parse a [WindowManagerTrace]
-     */
-    val parseLayersTrace: (() -> LayersTrace?)?
+    @VisibleForTesting
+    val eventLogSubject: EventLogSubject?
 ) {
-    /**
-     * [WindowManagerTrace] that corresponds to [wmTraceFile], or null if the
-     * path is invalid
-     */
-    val wmTrace: WindowManagerTrace? get() = parseWmTrace?.invoke()
-    /**
-     * [LayersTrace] that corresponds to [layersTrace], or null if the
-     * path is invalid
-     */
-    val layersTrace: LayersTrace? get() = parseLayersTrace?.invoke()
+    fun getSubjects(): List<FlickerSubject> {
+        val result = mutableListOf<FlickerSubject>()
 
-    constructor(
-        assertionTag: AssertionTag,
-        iteration: Int,
-        wmTraceFile: Path?,
-        layersTraceFile: Path?,
-        screenRecording: Path?,
-        eventLog: List<FocusEvent>
-    ) : this(
-        assertionTag,
-        iteration,
-        wmTraceFile,
-        layersTraceFile,
-        screenRecording,
-        eventLog,
-        parseWmTrace = {
-            wmTraceFile?.let {
-                val traceData = Files.readAllBytes(it)
-                WindowManagerTraceParser.parseFromTrace(traceData)
-            }
-        },
-        parseLayersTrace = {
-            layersTraceFile?.let {
-                val traceData = Files.readAllBytes(it)
-                LayersTraceParser.parseFromTrace(traceData)
-            }
-        }
-    )
+        wmSubject?.run { result.add(this) }
+        layersSubject?.run { result.add(this) }
+        eventLogSubject?.run { result.add(this) }
 
-    constructor(
-        assertionTag: AssertionTag,
-        iteration: Int,
-        wmTraceFile: Path?,
-        layersTraceFile: Path?,
-        wmTrace: WindowManagerTrace?,
-        layersTrace: LayersTrace?
-    ) : this(
-        assertionTag,
-        iteration,
-        wmTraceFile,
-        layersTraceFile,
-        screenRecording = null,
-        eventLog = emptyList(),
-        parseWmTrace = { wmTrace },
-        parseLayersTrace = { layersTrace }
-    )
+        return result
+    }
 
     private fun Path?.tryDelete() {
         try {
@@ -132,8 +84,8 @@
     }
 
     fun canDelete(failures: List<FlickerAssertionError>): Boolean {
-        return failures.map { it.trace }.none {
-            it == this.wmTraceFile || it == this.layersTraceFile
+        return failures.flatMap { it.traceFiles }.none { failureTrace ->
+            this.traceFiles.any { it == failureTrace }
         }
     }
 
@@ -141,9 +93,7 @@
      * Delete the trace files collected
      */
     fun cleanUp() {
-        wmTraceFile.tryDelete()
-        layersTraceFile.tryDelete()
-        screenRecording.tryDelete()
+        this.traceFiles.forEach { it.tryDelete() }
     }
 
     class Builder @JvmOverloads constructor(private val iteration: Int = 0) {
@@ -165,23 +115,94 @@
         /**
          * List of focus events, if collected
          */
-        var eventLog = listOf<FocusEvent>()
+        var eventLog: List<FocusEvent>? = null
 
-        /**
-         * Creates a new run result associated with an assertion tag
-         *
-         * By default assert all entries
-         */
-        @JvmOverloads
-        fun build(assertionTag: AssertionTag = AssertionTag.ALL): FlickerRunResult {
-            return FlickerRunResult(
-                    assertionTag,
-                    iteration,
-                    wmTraceFile,
-                    layersTraceFile,
-                    screenRecording,
-                    eventLog
+        private fun getTraceFiles() = listOfNotNull(wmTraceFile, layersTraceFile, screenRecording)
+
+        private fun buildResult(
+            assertionTag: String,
+            wmSubject: FlickerSubject?,
+            layersSubject: FlickerSubject?,
+            eventLogSubject: EventLogSubject? = null
+        ): FlickerRunResult {
+            return FlickerRunResult(iteration,
+                getTraceFiles(),
+                assertionTag,
+                wmSubject,
+                layersSubject,
+                eventLogSubject
             )
         }
+
+        /**
+         * Builds a new [FlickerRunResult] for a trace
+         *
+         * @param assertionTag Tag to associate with the result
+         * @param wmTrace WindowManager trace
+         * @param layersTrace Layers trace
+         */
+        fun buildStateResult(
+            assertionTag: String,
+            wmTrace: WindowManagerTrace?,
+            layersTrace: LayersTrace?
+        ): FlickerRunResult {
+            val wmSubject = wmTrace?.let { WindowManagerTraceSubject.assertThat(it).first() }
+            val layersSubject = layersTrace?.let { LayersTraceSubject.assertThat(it).first() }
+            return buildResult(assertionTag, wmSubject, layersSubject)
+        }
+
+        @VisibleForTesting
+        fun buildEventLogResult(): FlickerRunResult {
+            val events = eventLog ?: emptyList()
+            return buildResult(
+                AssertionTag.ALL,
+                wmSubject = null,
+                layersSubject = null,
+                eventLogSubject = EventLogSubject.assertThat(events)
+            )
+        }
+
+        @VisibleForTesting
+        fun buildTraceResults(): List<FlickerRunResult> {
+            var wmTrace: WindowManagerTrace? = null
+            var layersTrace: LayersTrace? = null
+
+            if (wmTrace == null && wmTraceFile != null) {
+                Log.v(FLICKER_TAG, "Parsing WM trace")
+                wmTrace = wmTraceFile?.let {
+                    val traceData = Files.readAllBytes(it)
+                    WindowManagerTraceParser.parseFromTrace(traceData)
+                }
+            }
+
+            if (layersTrace == null && layersTraceFile != null) {
+                Log.v(FLICKER_TAG, "Parsing Layers trace")
+                layersTrace = layersTraceFile?.let {
+                    val traceData = Files.readAllBytes(it)
+                    LayersTraceParser.parseFromTrace(traceData)
+                }
+            }
+
+            val wmSubject = wmTrace?.let { WindowManagerTraceSubject.assertThat(it) }
+            val layersSubject = layersTrace?.let { LayersTraceSubject.assertThat(it) }
+
+            val traceResult = buildResult(
+                AssertionTag.ALL, wmSubject, layersSubject)
+            val initialStateResult = buildResult(
+                AssertionTag.START, wmSubject?.first(), layersSubject?.first())
+            val finalStateResult = buildResult(
+                AssertionTag.END, wmSubject?.last(), layersSubject?.last())
+
+            return listOf(initialStateResult, finalStateResult, traceResult)
+        }
+
+        fun buildAll(): List<FlickerRunResult> {
+            val result = buildTraceResults().toMutableList()
+            if (eventLog != null) {
+                result.add(buildEventLogResult())
+            }
+
+            return result
+        }
     }
-}
\ No newline at end of file
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunner.kt
index e0eef46..4b7af23 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunner.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunner.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -16,8 +16,9 @@
 
 package com.android.server.wm.flicker
 
+import android.util.Log
 import androidx.test.filters.FlakyTest
-import com.google.common.truth.Truth
+import org.junit.Assume
 import org.junit.Rule
 import org.junit.Test
 
@@ -29,25 +30,45 @@
  *
  * All the enabled assertions are created in a single test and all flaky assertions are created on
  * a second test annotated with @FlakyTest
+ *
+ * @param testName Name of the test. Appears on log outputs and test dashboards
+ * @param flickerSpec Flicker test to execute
+ * @param cleanUp If this test should delete the traces and screen recording files if it passes
  */
-abstract class FlickerTestRunner(testName: String, private val flickerSpec: Flicker) {
+abstract class FlickerTestRunner(
+    testName: String,
+    private val flickerProvider: () -> Flicker,
+    private val cleanUp: Boolean
+) {
+    private val flickerSpec = flickerProvider.invoke()
+
     @get:Rule
     val flickerTestRule = FlickerTestRule(flickerSpec)
 
-    /**
-     * Tests if the transition executed successfully
-     */
-    @Test
-    fun checkTransition() {
-        Truth.assertWithMessage(flickerSpec.error?.message).that(flickerSpec.error).isNull()
+    private fun checkRequirements(onlyFlaky: Boolean) {
+        if (flickerSpec.assertions.size == 1) {
+            val isTestEnabled = flickerSpec.assertions.first().enabled
+            if (onlyFlaky) {
+                Assume.assumeFalse(isTestEnabled)
+            } else {
+                Assume.assumeTrue(isTestEnabled)
+            }
+        }
     }
 
     /**
      * Run only the enabled assertions on the recorded traces.
      */
     @Test
-    fun checkAssertions() {
-        flickerSpec.checkAssertions(includeFlakyAssertions = false)
+    fun test() {
+        checkRequirements(onlyFlaky = false)
+        flickerSpec.checkIsExecuted()
+        if (flickerSpec.hasAssertions()) {
+            flickerSpec.checkAssertions(includeFlakyAssertions = false)
+            if (cleanUp) {
+                flickerSpec.cleanUp()
+            }
+        }
     }
 
     /**
@@ -55,7 +76,14 @@
      */
     @FlakyTest
     @Test
-    fun checkFlakyAssertions() {
-        flickerSpec.checkAssertions(includeFlakyAssertions = true)
+    fun testFlaky() {
+        checkRequirements(onlyFlaky = true)
+        flickerSpec.checkIsExecuted()
+        if (flickerSpec.hasAssertions()) {
+            flickerSpec.checkAssertions(includeFlakyAssertions = true)
+            if (cleanUp) {
+                flickerSpec.cleanUp()
+            }
+        }
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunnerFactory.kt b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunnerFactory.kt
index 5b7754d..7e44b3b 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunnerFactory.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/FlickerTestRunnerFactory.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -22,6 +22,7 @@
 import android.support.test.launcherhelper.LauncherStrategyFactory
 import android.view.Surface
 import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
 /**
  * Factory for creating JUnit4 compatible tests based on the flicker DSL
@@ -33,7 +34,8 @@
     private val supportedRotations: List<Int> = listOf(Surface.ROTATION_0, Surface.ROTATION_90),
     private val repetitions: Int = 1,
     private val launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
-        .getInstance(instrumentation).launcherStrategy
+        .getInstance(instrumentation).launcherStrategy,
+    private val wmHelper: WindowManagerStateHelper = WindowManagerStateHelper()
 ) {
     /**
      * Creates multiple instances of the same test, running on different device orientations
@@ -49,12 +51,12 @@
         deviceConfigurations: List<Bundle> = getConfigNonRotationTests(),
         testSpecification: FlickerBuilder.(Bundle) -> Any
     ): List<Array<Any>> {
-        return deviceConfigurations.map {
-            val builder = FlickerBuilder(instrumentation, launcherStrategy)
-            val flickerTests = builder.apply { testSpecification(it) }.build()
+        return deviceConfigurations.flatMap {
+            val builder = FlickerBuilder(instrumentation, launcherStrategy, wmHelper = wmHelper)
+            val flickerTests = buildIndividualTests(builder.apply { testSpecification(it) })
 
             flickerTests
-        }.map { arrayOf(it.toString(), it) }
+        }.map { arrayOf(it.first, it.second, it.third) }
     }
 
     /**
@@ -72,12 +74,44 @@
         deviceConfigurations: List<Bundle> = getConfigRotationTests(),
         testSpecification: FlickerBuilder.(Bundle) -> Any
     ): List<Array<Any>> {
-        return deviceConfigurations.map {
-            val builder = FlickerBuilder(instrumentation, launcherStrategy)
-            val flickerTests = builder.apply { testSpecification(it) }.build()
+        return deviceConfigurations.flatMap {
+            val builder = FlickerBuilder(instrumentation, launcherStrategy, wmHelper = wmHelper)
+            val flickerTests = buildIndividualTests(builder.apply { testSpecification(it) })
 
             flickerTests
-        }.map { arrayOf(it.toString(), it) }
+        }.map { arrayOf(it.first, it.second, it.third) }
+    }
+
+    /**
+     * Creates multiple flicker tests.
+     *
+     * Each test contains a single assertion, but all tests share the same setup, transition
+     * and results
+     */
+    private fun buildIndividualTests(
+        builder: FlickerBuilder
+    ): List<Triple<String, () -> Flicker, Boolean>> {
+        val transitionRunner = TransitionRunnerCached()
+        val flicker = builder.build(transitionRunner)
+        val assertionsList = flicker.assertions
+        val lastAssertionIdx = assertionsList.lastIndex
+        val onlyTransition = {
+            flicker.copy(newAssertion = null)
+        }
+
+        val result = mutableListOf(Triple(flicker.testName, onlyTransition, false))
+        result.addAll(
+            assertionsList.mapIndexed { idx, assertion ->
+                val newTestName = "${flicker.testName}_$assertion"
+                val newTest = {
+                    flicker.copy(newAssertion = assertion, newName = newTestName)
+                }
+                val cleanUp = idx == lastAssertionIdx
+
+                Triple(newTestName, newTest, cleanUp)
+            }
+        )
+        return result
     }
 
     /**
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
new file mode 100644
index 0000000..861576e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunner.kt
@@ -0,0 +1,195 @@
+/*
+ * 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.dsl.AssertionTag
+import com.android.server.wm.flicker.dsl.TestCommandsBuilder
+import com.android.server.wm.flicker.monitor.ITransitionMonitor
+import com.android.server.wm.traces.parser.DeviceStateDump
+import com.android.server.wm.traces.parser.getCurrentState
+import java.io.IOException
+import java.nio.file.Files
+
+/**
+ * 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
+     */
+    private var iteration = 0
+    private val tags = mutableSetOf<String>()
+    private var tagsResults = mutableListOf<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
+     */
+    fun execute(flicker: Flicker): 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
+     */
+    private 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" }
+    }
+
+    private fun internalCleanUp() {
+        tags.clear()
+        tagsResults.clear()
+    }
+
+    open fun cleanUp() {
+        internalCleanUp()
+    }
+
+    /**
+     * Runs the actual setup, transitions and teardown defined in [flicker]
+     *
+     * @param flicker test specification
+     */
+    internal open fun run(flicker: Flicker): FlickerResult {
+        val runs = mutableListOf<FlickerRunResult>()
+        var executionError: Throwable? = null
+        try {
+            try {
+                flicker.testSetup.forEach { it.invoke(flicker) }
+                for (iteration in 0 until flicker.repetitions) {
+                    try {
+                        flicker.runSetup.forEach { it.invoke(flicker) }
+                        flicker.traceMonitors.forEach { it.start() }
+                        flicker.frameStatsMonitor?.run { start() }
+                        flicker.transitions.forEach { it.invoke(flicker) }
+                    } finally {
+                        flicker.traceMonitors.forEach { it.tryStop() }
+                        flicker.frameStatsMonitor?.run { tryStop() }
+                        flicker.runTeardown.forEach { it.invoke(flicker) }
+                    }
+                    if (flicker.frameStatsMonitor?.jankyFramesDetected() == true) {
+                        Log.e(FLICKER_TAG, "Skipping iteration " +
+                            "$iteration/${flicker.repetitions - 1} " +
+                            "for test ${flicker.testName} due to jank. $flicker.frameStatsMonitor")
+                        continue
+                    }
+                    val runResults = saveResult(flicker, iteration)
+                    runs.addAll(runResults)
+                }
+            } finally {
+                flicker.testTeardown.forEach { it.invoke(flicker) }
+            }
+        } catch (e: Throwable) {
+            executionError = e
+        }
+
+        runs.addAll(tagsResults)
+        val result = FlickerResult(runs.toList(), tags.toSet(), executionError)
+        cleanUp()
+        return result
+    }
+
+    private fun saveResult(flicker: Flicker, iteration: Int): List<FlickerRunResult> {
+        val resultBuilder = FlickerRunResult.Builder(iteration)
+        flicker.traceMonitors.forEach {
+            it.save(flicker.testName, iteration, resultBuilder)
+        }
+
+        return resultBuilder.buildAll()
+    }
+
+    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
+     *
+     * @throws IllegalArgumentException If [tag] contains invalid characters
+     */
+    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"
+        }
+        if (tag in tags) {
+            throw IllegalArgumentException("Tag $tag has already been used")
+        }
+        tags.add(tag)
+
+        val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation)
+        val deviceState = DeviceStateDump.fromDump(deviceStateBytes.first, deviceStateBytes.second)
+        try {
+            val wmTraceFile = flicker.outputDir.resolve(
+                getTaggedFilePath(flicker, tag, "wm_trace"))
+            Files.write(wmTraceFile, deviceStateBytes.first)
+
+            val layersTraceFile = flicker.outputDir.resolve(
+                getTaggedFilePath(flicker, tag, "layers_trace"))
+            Files.write(layersTraceFile, deviceStateBytes.second)
+
+            val builder = FlickerRunResult.Builder(iteration)
+            builder.wmTraceFile = wmTraceFile
+            builder.layersTraceFile = layersTraceFile
+
+            val result = builder.buildStateResult(
+                tag,
+                deviceState.wmTrace,
+                deviceState.layersTrace
+            )
+            tagsResults.add(result)
+        } catch (e: IOException) {
+            throw RuntimeException("Unable to create trace file: ${e.message}", e)
+        }
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt
new file mode 100644
index 0000000..1d5216b
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/TransitionRunnerCached.kt
@@ -0,0 +1,48 @@
+/*
+ * 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
+
+/**
+ * Execute the transitions of a flicker test and caches the results.
+ *
+ * Return cached results instead of re-executing the transitions if possible.
+ *
+ * @param runner Actual runner to execute the test
+ */
+class TransitionRunnerCached @JvmOverloads constructor(
+    private val runner: TransitionRunner = TransitionRunner()
+) : TransitionRunner() {
+    private var result = FlickerResult()
+
+    /**
+     * {@inheritDoc}
+     *
+     * @param flicker test specification
+     */
+    override fun run(flicker: Flicker): FlickerResult {
+        if (result.isEmpty()) {
+            result = runner.run(flicker)
+        }
+
+        return result
+    }
+
+    override fun cleanUp() {
+        result = FlickerResult()
+        runner.cleanUp()
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
index 9b483e8..07e6180 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerAssertionError.kt
@@ -17,15 +17,21 @@
 package com.android.server.wm.flicker.assertions
 
 import com.android.server.wm.flicker.FlickerRunResult
+import com.android.server.wm.flicker.dsl.AssertionTag
+import com.android.server.wm.flicker.traces.FlickerSubjectException
 import java.nio.file.Path
 import kotlin.AssertionError
 
 class FlickerAssertionError(
     cause: Throwable,
-    val assertion: AssertionData<*>,
-    val run: FlickerRunResult,
-    var trace: Path?
+    @JvmField val assertion: AssertionData,
+    @JvmField val iteration: Int,
+    @JvmField val assertionTag: String,
+    @JvmField val traceFiles: List<Path>
 ) : AssertionError(cause) {
+    constructor(cause: Throwable, assertion: AssertionData, run: FlickerRunResult)
+        : this(cause, assertion, run.iteration, run.assertionTag, run.traceFiles)
+
     override val message: String
         get() = buildString {
             append("\n")
@@ -33,11 +39,19 @@
             append(assertion.name)
             append("\n")
             append("Iteration: ")
-            append(run.iteration)
+            append(iteration)
             append("\n")
-            append("Trace: ")
-            append(trace)
+            append("Tag: ")
+            append(assertionTag)
             append("\n")
-            cause?.message?.let { append(it) }
+            // For subject exceptions, add the facts (layer/window/entry/etc)
+            // and the original cause of failure
+            if (cause is FlickerSubjectException) {
+                append(cause.facts)
+                append("\n")
+                cause.cause?.message?.let { append(it) }
+            } else {
+                cause?.message?.let { append(it) }
+            }
         }
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
index 6ffdbad..1928d38 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/assertions/FlickerSubject.kt
@@ -62,7 +62,7 @@
      * @param reason for the failure
      */
     fun fail(reason: String): FlickerSubject = apply {
-        fail(Fact.simpleFact(reason))
+        fail(Fact.fact("Reason", reason))
     }
 
     /**
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt
index cc3f287..c83f5a3 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilder.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -22,6 +22,7 @@
 import androidx.test.uiautomator.UiDevice
 import com.android.server.wm.flicker.Flicker
 import com.android.server.wm.flicker.FlickerDslMarker
+import com.android.server.wm.flicker.TransitionRunner
 import com.android.server.wm.flicker.monitor.EventLogMonitor
 import com.android.server.wm.flicker.getDefaultFlickerOutputDir
 import com.android.server.wm.flicker.monitor.ITransitionMonitor
@@ -30,6 +31,10 @@
 import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor
 import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor
 import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.layers.LayerTraceEntry
+import com.android.server.wm.traces.common.windowmanager.WindowManagerState
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 import java.nio.file.Path
 
 /**
@@ -41,12 +46,13 @@
     private val launcherStrategy: ILauncherStrategy,
     private val includeJankyRuns: Boolean,
     private val outputDir: Path,
+    private val wmHelper: WindowManagerStateHelper,
     private var testName: String,
     private var iterations: Int,
-    private val setupCommands: TestCommands,
-    private val teardownCommands: TestCommands,
+    private val setupCommands: TestCommandsBuilder,
+    private val teardownCommands: TestCommandsBuilder,
     private val transitionCommands: MutableList<Flicker.() -> Any>,
-    private val assertions: AssertionTarget,
+    internal val assertions: AssertionTargetBuilder,
     val device: UiDevice,
     private val traceMonitors: MutableList<ITransitionMonitor>
 ) {
@@ -77,24 +83,29 @@
         /**
          * Output directory for the test results
          */
-        outputDir: Path = getDefaultFlickerOutputDir()
+        outputDir: Path = getDefaultFlickerOutputDir(),
+        /**
+         * Helper object for WM Synchronization
+         */
+        wmHelper: WindowManagerStateHelper = WindowManagerStateHelper()
     ) : this(
         instrumentation,
         launcherStrategy,
         includeJankyRuns,
         outputDir,
+        wmHelper,
         testName = "",
         iterations = 1,
-        setupCommands = TestCommands(),
-        teardownCommands = TestCommands(),
+        setupCommands = TestCommandsBuilder(),
+        teardownCommands = TestCommandsBuilder(),
         transitionCommands = mutableListOf(),
-        assertions = AssertionTarget(),
+        assertions = AssertionTargetBuilder(),
         device = UiDevice.getInstance(instrumentation),
         traceMonitors = mutableListOf<ITransitionMonitor>()
             .also {
                 it.add(WindowManagerTraceMonitor(outputDir))
                 it.add(LayersTraceMonitor(outputDir))
-                it.add(ScreenRecorder(outputDir))
+                it.add(ScreenRecorder(outputDir, instrumentation.targetContext))
                 it.add(EventLogMonitor())
             }
     )
@@ -107,12 +118,13 @@
         otherBuilder.launcherStrategy,
         otherBuilder.includeJankyRuns,
         otherBuilder.outputDir.toAbsolutePath(),
+        otherBuilder.wmHelper,
         otherBuilder.testName,
         otherBuilder.iterations,
-        TestCommands(otherBuilder.setupCommands),
-        TestCommands(otherBuilder.teardownCommands),
+        TestCommandsBuilder(otherBuilder.setupCommands),
+        TestCommandsBuilder(otherBuilder.teardownCommands),
         otherBuilder.transitionCommands.toMutableList(),
-        AssertionTarget(otherBuilder.assertions),
+        AssertionTargetBuilder(otherBuilder.assertions),
         UiDevice.getInstance(otherBuilder.instrumentation),
         otherBuilder.traceMonitors.toMutableList()
     )
@@ -138,8 +150,8 @@
      *
      * By default the tracing is always active. To disable tracing return null
      *
-     * If this tracing is disabled, the assertions for [AssertionTarget.layerAssertions] will
-     * not be executed
+     * If this tracing is disabled, the assertions for [WindowManagerTrace] and
+     * [WindowManagerState] will not be executed
      */
     fun withWindowManagerTracing(traceMonitor: (Path) -> WindowManagerTraceMonitor?) {
         traceMonitors.removeIf { it is WindowManagerTraceMonitor }
@@ -155,8 +167,8 @@
      *
      * By default the tracing is always active. To disable tracing return null
      *
-     * If this tracing is disabled, the assertions for [AssertionTarget.layerAssertions] will
-     * not be executed
+     * If this tracing is disabled, the assertions for [LayersTrace] and [LayerTraceEntry]
+     * will not be executed
      */
     fun withLayerTracing(traceMonitor: (Path) -> LayersTraceMonitor?) {
         traceMonitors.removeIf { it is LayersTraceMonitor }
@@ -191,39 +203,40 @@
     }
 
     /**
-     * Defines the test ([TestCommands.testCommands]) and run ([TestCommands.runCommands])
+     * Defines the test ([TestCommandsBuilder.testCommands]) and run ([TestCommandsBuilder.runCommands])
      * commands executed before the [transitions] to test
      */
-    fun setup(commands: TestCommands.() -> Unit) {
+    fun setup(commands: TestCommandsBuilder.() -> Unit) {
         setupCommands.apply { commands() }
     }
 
     /**
-     * Defines the test ([TestCommands.testCommands]) and run ([TestCommands.runCommands])
+     * Defines the test ([TestCommandsBuilder.testCommands]) and run ([TestCommandsBuilder.runCommands])
      * commands executed after the [transitions] to test
      */
-    fun teardown(commands: TestCommands.() -> Unit) {
+    fun teardown(commands: TestCommandsBuilder.() -> Unit) {
         teardownCommands.apply { commands() }
     }
 
     /**
      * Defines the commands that trigger the behavior to test
      */
-    fun transitions(command: Flicker.() -> Any) {
+    fun transitions(command: Flicker.() -> Unit) {
         transitionCommands.add(command)
     }
 
     /**
      * Defines the assertions to check the recorded traces
      */
-    fun assertions(assertion: AssertionTarget.() -> Unit) {
+    fun assertions(assertion: AssertionTargetBuilder.() -> Unit) {
         assertions.apply { assertion() }
     }
 
     /**
      * Creates a new Flicker runner based on the current builder configuration
      */
-    fun build() = Flicker(
+    @JvmOverloads
+    fun build(runner: TransitionRunner = TransitionRunner()) = Flicker(
         instrumentation,
         device,
         launcherStrategy,
@@ -232,10 +245,14 @@
         iterations,
         frameStatsMonitor,
         traceMonitors,
-        setupCommands,
-        teardownCommands,
+        setupCommands.buildTestCommands(),
+        setupCommands.buildRunCommands(),
+        teardownCommands.buildTestCommands(),
+        teardownCommands.buildRunCommands(),
         transitionCommands,
-        assertions
+        assertions.build(),
+        runner,
+        wmHelper
     )
 
     /**
@@ -261,7 +278,7 @@
     launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
             .getInstance(instrumentation).launcherStrategy,
     configuration: FlickerBuilder.() -> Unit
-) = runFlicker(instrumentation, launcherStrategy, configuration)
+) = runFlicker(instrumentation, launcherStrategy, configuration = configuration)
 
 /**
  * Entry point for the Flicker DSL.
@@ -269,6 +286,8 @@
  * Configures a builder, build the test runs, executes them and checks the configured assertions
  *
  * @param instrumentation to run the test (used to interact with the device)
+ * @param launcherStrategy to interact with the device's launcher
+ * @param runner to execute the transitions
  * @param configuration Flicker DSL configuration
  */
 @JvmOverloads
@@ -276,10 +295,11 @@
     instrumentation: Instrumentation,
     launcherStrategy: ILauncherStrategy = LauncherStrategyFactory
             .getInstance(instrumentation).launcherStrategy,
+    runner: TransitionRunner = TransitionRunner(),
     configuration: FlickerBuilder.() -> Unit
 ) {
     val builder = FlickerBuilder(instrumentation, launcherStrategy)
-    runWithFlicker(builder, configuration)
+    runWithFlicker(builder, runner, configuration)
 }
 
 /**
@@ -291,12 +311,14 @@
  * The original builder object is not changed.
  *
  * @param builder to run the test (used to interact with the device
+ * @param runner to execute the transitions
  * @param configuration Flicker DSL configuration
  */
 @JvmOverloads
 fun runWithFlicker(
     builder: FlickerBuilder,
+    runner: TransitionRunner = TransitionRunner(),
     configuration: FlickerBuilder.() -> Unit = {}
 ) {
-    builder.copy(configuration).build().execute().checkAssertions()
+    builder.copy(configuration).build(runner).execute().checkAssertions()
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilderJava.java b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilderJava.java
index 5e3a40c..36b91d9 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilderJava.java
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/FlickerBuilderJava.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -25,13 +25,17 @@
 import androidx.test.uiautomator.UiDevice;
 
 import com.android.server.wm.flicker.Flicker;
+import com.android.server.wm.flicker.TransitionRunner;
 import com.android.server.wm.flicker.monitor.ITransitionMonitor;
 import com.android.server.wm.flicker.monitor.LayersTraceMonitor;
 import com.android.server.wm.flicker.monitor.ScreenRecorder;
 import com.android.server.wm.flicker.monitor.WindowAnimationFrameStatsMonitor;
 import com.android.server.wm.flicker.monitor.WindowManagerTraceMonitor;
+import com.android.server.wm.flicker.traces.layers.LayerTraceEntrySubject;
 import com.android.server.wm.flicker.traces.layers.LayersTraceSubject;
-import com.android.server.wm.flicker.traces.windowmanager.WmTraceSubject;
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerStateSubject;
+import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject;
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper;
 
 import org.junit.Assert;
 
@@ -127,10 +131,10 @@
 
     private String testTag = "";
     private Integer iterations = 1;
-    private TestCommands setupCommands = new TestCommands();
-    private TestCommands teardownCommands = new TestCommands();
+    private TestCommandsBuilder setupCommands = new TestCommandsBuilder();
+    private TestCommandsBuilder teardownCommands = new TestCommandsBuilder();
     private List<Function1<? super Flicker, ?>> transitionCommands = new ArrayList<>();
-    private AssertionTarget assertions = new AssertionTarget();
+    private AssertionTargetBuilder assertions = new AssertionTargetBuilder();
     private List<ITransitionMonitor> traceMonitors = new ArrayList<>();
     private WindowAnimationFrameStatsMonitor frameStatsMonitor;
 
@@ -264,7 +268,7 @@
 
     /** Defines the assertions for the initial entry of the layers trace */
     public FlickerBuilderJava addLayerTraceAssertionStart(
-            FlickerAssertionJava<LayersTraceSubject> assertion) {
+            FlickerAssertionJava<LayerTraceEntrySubject> assertion) {
         assertions.layersTrace(
                 assertionData -> {
                     assertionData.start(assertion::invoke);
@@ -275,7 +279,7 @@
 
     /** Defines the assertions for the final entry of the layers trace */
     public FlickerBuilderJava addLayerTraceAssertionEnd(
-            FlickerAssertionJava<LayersTraceSubject> assertion) {
+            FlickerAssertionJava<LayerTraceEntrySubject> assertion) {
         assertions.layersTrace(
                 assertionData -> {
                     assertionData.end(assertion::invoke);
@@ -297,7 +301,7 @@
 
     /** Defines the assertions for the initial entry of the layers trace */
     public FlickerBuilderJava addWindowManagerTraceAssertionStart(
-            FlickerAssertionJava<WmTraceSubject> assertion) {
+            FlickerAssertionJava<WindowManagerStateSubject> assertion) {
         assertions.windowManagerTrace(
                 assertionData -> {
                     assertionData.start(assertion::invoke);
@@ -308,7 +312,7 @@
 
     /** Defines the assertions for the final entry of the layers trace */
     public FlickerBuilderJava addWindowManagerTraceAssertionEnd(
-            FlickerAssertionJava<WmTraceSubject> assertion) {
+            FlickerAssertionJava<WindowManagerStateSubject> assertion) {
         assertions.windowManagerTrace(
                 assertionData -> {
                     assertionData.end(assertion::invoke);
@@ -319,7 +323,7 @@
 
     /** Defines the assertions for all entries of the layers trace */
     public FlickerBuilderJava addWindowManagerTraceAssertionAll(
-            FlickerAssertionJava<WmTraceSubject> assertion) {
+            FlickerAssertionJava<WindowManagerTraceSubject> assertion) {
         assertions.windowManagerTrace(
                 assertionData -> {
                     assertionData.all(assertion::invoke);
@@ -339,9 +343,13 @@
                 iterations,
                 frameStatsMonitor,
                 traceMonitors,
-                setupCommands,
-                teardownCommands,
+                setupCommands.buildTestCommands(),
+                setupCommands.buildRunCommands(),
+                teardownCommands.buildTestCommands(),
+                teardownCommands.buildRunCommands(),
                 transitionCommands,
-                assertions);
+                assertions.build(),
+                new TransitionRunner(),
+                new WindowManagerStateHelper());
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommands.kt b/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt
similarity index 83%
rename from libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommands.kt
rename to libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt
index c05d1b2..e12ab64 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommands.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/dsl/TestCommandsBuilder.kt
@@ -23,9 +23,9 @@
  * Placeholder for test [Flicker.setup] and [Flicker.teardown] commands on the Flicker DSL
  */
 @FlickerDslMarker
-class TestCommands private constructor(
-    internal val testCommands: MutableList<Flicker.() -> Any>,
-    internal val runCommands: MutableList<Flicker.() -> Any>
+class TestCommandsBuilder private constructor(
+    private val testCommands: MutableList<Flicker.() -> Any>,
+    private val runCommands: MutableList<Flicker.() -> Any>
 ) {
     constructor() : this(
         testCommands = mutableListOf<Flicker.() -> Any>(),
@@ -35,7 +35,7 @@
     /**
      * Copy constructor
      */
-    constructor(otherCommands: TestCommands) : this(
+    constructor(otherCommands: TestCommandsBuilder) : this(
         otherCommands.testCommands.toMutableList(),
         otherCommands.runCommands.toMutableList()
     )
@@ -51,7 +51,7 @@
      *
      * This command can be used multiple times, and the results are appended
      */
-    fun test(command: Flicker.() -> Any) {
+    fun test(command: Flicker.() -> Unit) {
         testCommands.add(command)
     }
 
@@ -68,7 +68,11 @@
      *
      * This command can be used multiple times, and the results are appended
      */
-    fun eachRun(command: Flicker.() -> Any) {
+    fun eachRun(command: Flicker.() -> Unit) {
         runCommands.add(command)
     }
+
+    fun buildTestCommands(): List<Flicker.() -> Any> = testCommands
+
+    fun buildRunCommands(): List<Flicker.() -> Any> = runCommands
 }
\ No newline at end of file
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
index 3dd26c7..4f1dc6c 100644
--- a/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
+++ b/libraries/flicker/src/com/android/server/wm/flicker/helpers/StandardAppHelper.kt
@@ -18,9 +18,19 @@
 
 import android.app.ActivityManager
 import android.app.Instrumentation
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
 import android.platform.helpers.AbstractStandardAppHelper
 import android.support.test.launcherhelper.ILauncherStrategy
 import android.support.test.launcherhelper.LauncherStrategyFactory
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.BySelector
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.android.server.wm.traces.parser.toActivityName
+import com.android.server.wm.traces.parser.toWindowName
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerStateHelper
 
 /**
  * Class to take advantage of {@code IAppHelper} interface so the same test can be run against first
@@ -28,28 +38,57 @@
  */
 open class StandardAppHelper @JvmOverloads constructor(
     instr: Instrumentation,
-    protected val packageName: String,
-    protected val appName: String,
+    @JvmField val appName: String,
+    @JvmField val component: ComponentName,
     protected val launcherStrategy: ILauncherStrategy =
-            LauncherStrategyFactory.getInstance(instr).launcherStrategy
+        LauncherStrategyFactory.getInstance(instr).launcherStrategy
 ) : AbstractStandardAppHelper(instr) {
     constructor(
         instr: Instrumentation,
         appName: String,
+        packageName: String,
+        activity: String,
         launcherStrategy: ILauncherStrategy =
-                LauncherStrategyFactory.getInstance(instr).launcherStrategy
-    ) : this(instr, sFlickerPackage, appName, launcherStrategy)
+            LauncherStrategyFactory.getInstance(instr).launcherStrategy
+    ): this(instr, appName,
+        ComponentName.createRelative(packageName, ".$activity"), launcherStrategy)
+
+    val windowName: String = component.toWindowName()
+    val activityName: String = component.toActivityName()
 
     private val activityManager: ActivityManager?
         get() = mInstrumentation.context.getSystemService(ActivityManager::class.java)
 
+    protected val context: Context
+        get() = mInstrumentation.context
+
+    protected val uiDevice: UiDevice = UiDevice.getInstance(mInstrumentation)
+
+    private fun getAppSelector(expectedPackageName: String): BySelector {
+        val expected = if (expectedPackageName.isNotEmpty()) {
+            expectedPackageName
+        } else {
+            component.packageName
+        }
+        return By.pkg(expected).depth(0)
+    }
+
     override fun open() {
-        launcherStrategy.launch(appName, packageName)
+        launcherStrategy.launch(appName, component.packageName)
     }
 
     /** {@inheritDoc}  */
     override fun getPackage(): String {
-        return packageName
+        return component.packageName
+    }
+
+    /** {@inheritDoc}  */
+    override fun getOpenAppIntent(): Intent {
+        val intent = Intent()
+        intent.addCategory(Intent.CATEGORY_LAUNCHER)
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+        intent.component = component
+        return intent
     }
 
     /** {@inheritDoc}  */
@@ -65,10 +104,63 @@
         super.exit()
 
         // Ensure all testing components end up being closed.
-        activityManager?.forceStopPackage(packageName)
+        activityManager?.forceStopPackage(component.packageName)
+    }
+
+    private fun launchAppViaIntent(
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        val intent = openAppIntent
+        intent.action = action
+        stringExtras.forEach {
+            intent.putExtra(it.key, it.value)
+        }
+        context.startActivity(intent)
+    }
+
+    /**
+     * Launches the app through an intent instead of interacting with the launcher.
+     *
+     * Uses UiAutomation to detect when the app is open
+     */
+    @JvmOverloads
+    fun launchViaIntent(
+        expectedPackageName: String = "",
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        launchAppViaIntent(action, stringExtras)
+        val appSelector = getAppSelector(expectedPackageName)
+        uiDevice.wait(Until.hasObject(appSelector), APP_LAUNCH_WAIT_TIME_MS)
+    }
+
+    /**
+     * Launches the app through an intent instead of interacting with the launcher and waits
+     * until the app window is visible
+     */
+    @JvmOverloads
+    fun launchViaIntent(
+        wmHelper: WindowManagerStateHelper,
+        expectedWindowName: String = "",
+        action: String? = null,
+        stringExtras: Map<String, String> = mapOf()
+    ) {
+        launchAppViaIntent(action, stringExtras)
+
+        val window = if (expectedWindowName.isNotEmpty()) {
+            expectedWindowName
+        } else {
+            windowName
+        }
+        wmHelper.waitFor("App is shown") {
+            it.wmState.isComplete() && it.wmState.isWindowVisible(window)
+        }
+        wmHelper.waitForNavBarStatusBarVisible()
+        wmHelper.waitForAppTransitionIdle()
     }
 
     companion object {
-        private val sFlickerPackage = "com.android.server.wm.flicker.testapp"
+        private const val APP_LAUNCH_WAIT_TIME_MS = 10000L
     }
 }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
index 338c604..6534b31 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/DeviceStateDump.kt
@@ -29,31 +29,14 @@
  */
 class DeviceStateDump(
     /**
-     * [WindowManagerTrace] content
-     */
-    val wmTraceData: ByteArray,
-    /**
-     * [LayersTrace] content
-     */
-    val layersTraceData: ByteArray,
-    /**
-     * Predicate to parse [wmTraceData] into a [WindowManagerTrace]
-     */
-    val wmTraceParser: (ByteArray) -> WindowManagerTrace?,
-    /**
-     * Predicate to parse [layersTraceData] into a [LayersTrace]
-     */
-    val layersTraceParser: (ByteArray) -> LayersTrace?
-) {
-    /**
      * Parsed [WindowManagerTrace]
      */
-    val wmTrace: WindowManagerTrace? by lazy { wmTraceParser(wmTraceData) }
+    val wmTrace: WindowManagerTrace?,
     /**
      * Parsed [LayersTrace]
      */
-    val layersTrace: LayersTrace? by lazy { layersTraceParser(layersTraceData) }
-
+    val layersTrace: LayersTrace?
+) {
     companion object {
         /**
          * Creates a device state dump containing the [WindowManagerTrace] and [LayersTrace]
@@ -66,21 +49,15 @@
         @JvmStatic
         fun fromDump(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
             return DeviceStateDump(
-                wmTraceData,
-                layersTraceData,
-                {
-                    if (wmTraceData.isNotEmpty()) {
-                        WindowManagerTraceParser.parseFromDump(wmTraceData)
-                    } else {
-                        null
-                    }
+                wmTrace = if (wmTraceData.isNotEmpty()) {
+                    WindowManagerTraceParser.parseFromDump(wmTraceData)
+                } else {
+                    null
                 },
-                {
-                    if (layersTraceData.isNotEmpty()) {
-                        LayersTraceParser.parseFromDump(layersTraceData)
-                    } else {
-                        null
-                    }
+                layersTrace = if (layersTraceData.isNotEmpty()) {
+                    LayersTraceParser.parseFromDump(layersTraceData)
+                } else {
+                    null
                 }
             )
         }
@@ -96,21 +73,15 @@
         @JvmStatic
         fun fromTrace(wmTraceData: ByteArray, layersTraceData: ByteArray): DeviceStateDump {
             return DeviceStateDump(
-                wmTraceData,
-                layersTraceData,
-                {
-                    if (wmTraceData.isNotEmpty()) {
-                        WindowManagerTraceParser.parseFromTrace(wmTraceData)
-                    } else {
-                        null
-                    }
+                wmTrace = if (wmTraceData.isNotEmpty()) {
+                    WindowManagerTraceParser.parseFromTrace(wmTraceData)
+                } else {
+                    null
                 },
-                {
-                    if (layersTraceData.isNotEmpty()) {
-                        LayersTraceParser.parseFromTrace(layersTraceData)
-                    } else {
-                        null
-                    }
+                layersTrace = if (layersTraceData.isNotEmpty()) {
+                    LayersTraceParser.parseFromTrace(layersTraceData)
+                } else {
+                    null
                 }
             )
         }
diff --git a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
index 629d13a..3da3afa 100644
--- a/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
+++ b/libraries/flicker/src/com/android/server/wm/traces/parser/Extensions.kt
@@ -56,7 +56,7 @@
 fun getCurrentState(
     uiAutomation: UiAutomation,
     @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
-): DeviceStateDump {
+): Pair<ByteArray, ByteArray> {
     if (dumpFlags == 0) {
         throw IllegalArgumentException("No dump specified")
     }
@@ -72,5 +72,17 @@
     } else {
         ByteArray(0)
     }
+
+    return Pair(wmTraceData, layersTraceData)
+}
+
+@JvmOverloads
+fun getCurrentStateDump(
+    uiAutomation: UiAutomation,
+    @WmStateDumpFlags dumpFlags: Int = FLAG_STATE_DUMP_FLAG_WM.or(FLAG_STATE_DUMP_FLAG_LAYERS)
+): DeviceStateDump {
+    val currentStateDump = getCurrentState(uiAutomation, dumpFlags)
+    val wmTraceData = currentStateDump.first
+    val layersTraceData = currentStateDump.second
     return DeviceStateDump.fromDump(wmTraceData, layersTraceData)
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
index 8590371..2db00d1 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerDSLTest.kt
@@ -17,10 +17,9 @@
 package com.android.server.wm.flicker
 
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.dsl.AssertionTargetBuilder
 import com.android.server.wm.flicker.dsl.FlickerBuilder
-import com.android.server.wm.flicker.dsl.WmAssertionBuilder
 import com.android.server.wm.flicker.dsl.runWithFlicker
-import com.android.server.wm.flicker.traces.windowmanager.WindowManagerTraceSubject
 import com.google.common.truth.Truth
 import org.junit.Assert
 import org.junit.FixMethodOrder
@@ -75,12 +74,6 @@
         }
     }
 
-    private fun defaultAssertion(trace: WindowManagerTraceSubject): WindowManagerTraceSubject {
-        return trace("Has dump") {
-            it.isNotEmpty()
-        }
-    }
-
     @Test
     fun assertCreatedTags() {
         val builder = FlickerBuilder(instrumentation)
@@ -93,11 +86,17 @@
             }
             assertions {
                 windowManagerTrace {
-                    tag(myTag) { defaultAssertion(this) }
+                    tag(myTag) {
+                        this.isNotEmpty()
+                    }
 
-                    start { defaultAssertion(this) }
+                    start {
+                        this.isNotEmpty()
+                    }
 
-                    end { defaultAssertion(this) }
+                    end {
+                        this.isNotEmpty()
+                    }
 
                     tag("invalid") {
                         fail("`Invalid` tag was not created, so it should not " +
@@ -115,7 +114,9 @@
             runWithFlicker(builder) {
                 assertions {
                     windowManagerTrace {
-                        tag("tag") { defaultAssertion(this) }
+                        tag("tag") {
+                            this.isNotEmpty()
+                        }
                     }
                 }
             }
@@ -146,7 +147,7 @@
         }
     }
 
-    private fun detectFailedAssertion(assertion: WmAssertionBuilder.() -> Any): Throwable {
+    private fun detectFailedAssertion(assertions: AssertionTargetBuilder.() -> Any): Throwable {
         val builder = FlickerBuilder(instrumentation)
         return assertThrows(AssertionError::class.java) {
             runWithFlicker(builder) {
@@ -154,23 +155,18 @@
                     device.pressHome()
                 }
                 assertions {
-                    windowManagerTrace {
-                        assertion()
-                    }
+                    assertions()
                 }
             }
         }
     }
 
     @Test
-    fun detectFailedAssertion_All() {
+    fun detectFailedWMAssertion_All() {
         val error = detectFailedAssertion {
-            all("fail") {
-                fail("Correct error")
-            }
-
-            all("ignored", enabled = false) {
-                fail("Ignored error")
+            windowManagerTrace {
+                all("fail") { fail("Correct error") }
+                all("ignored", enabled = false) { fail("Ignored error") }
             }
         }
         assertFailure(error).hasMessageThat().contains("Correct error")
@@ -178,14 +174,11 @@
     }
 
     @Test
-    fun detectFailedAssertion_Start() {
+    fun detectFailedWMAssertion_Start() {
         val error = detectFailedAssertion {
-            start("fail") {
-                fail("Correct error")
-            }
-
-            start("ignored", enabled = false) {
-                fail("Ignored error")
+            windowManagerTrace {
+                start("fail") { fail("Correct error") }
+                start("ignored", enabled = false) { fail("Ignored error") }
             }
         }
         assertFailure(error).hasMessageThat().contains("Correct error")
@@ -193,14 +186,59 @@
     }
 
     @Test
-    fun detectFailedAssertion_End() {
+    fun detectFailedWMAssertion_End() {
         val error = detectFailedAssertion {
-            end("fail") {
-                fail("Correct error")
+            windowManagerTrace {
+                end("fail") { fail("Correct error") }
+                end("ignored", enabled = false) { fail("Ignored error") }
             }
+        }
+        assertFailure(error).hasMessageThat().contains("Correct error")
+        assertFailure(error).hasMessageThat().doesNotContain("Ignored error")
+    }
 
-            end("ignored", enabled = false) {
-                fail("Ignored error")
+    @Test
+    fun detectFailedLayersAssertion_All() {
+        val error = detectFailedAssertion {
+            layersTrace {
+                all("fail") { fail("Correct error") }
+                all("ignored", enabled = false) { fail("Ignored error") }
+            }
+        }
+        assertFailure(error).hasMessageThat().contains("Correct error")
+        assertFailure(error).hasMessageThat().doesNotContain("Ignored error")
+    }
+
+    @Test
+    fun detectFailedLayersAssertion_Start() {
+        val error = detectFailedAssertion {
+            layersTrace {
+                start("fail") { fail("Correct error") }
+                start("ignored", enabled = false) { fail("Ignored error") }
+            }
+        }
+        assertFailure(error).hasMessageThat().contains("Correct error")
+        assertFailure(error).hasMessageThat().doesNotContain("Ignored error")
+    }
+
+    @Test
+    fun detectFailedLayersAssertion_End() {
+        val error = detectFailedAssertion {
+            layersTrace {
+                end("fail") { fail("Correct error") }
+                end("ignored", enabled = false) { fail("Ignored error") }
+            }
+        }
+        assertFailure(error).hasMessageThat().contains("Correct error")
+        assertFailure(error).hasMessageThat().doesNotContain("Ignored error")
+    }
+
+    @Test
+    fun detectFailedEventLogAssertion_All() {
+        val error = detectFailedAssertion {
+            eventLog {
+                all("fail") { fail("Correct error") }
+                all("ignored", enabled = false) { fail("Ignored error") }
             }
         }
         assertFailure(error).hasMessageThat().contains("Correct error")
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestFactoryRunnerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestFactoryRunnerTest.kt
index 38eb181..3684fee 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestFactoryRunnerTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/FlickerTestFactoryRunnerTest.kt
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2020 The Android Open Source Project
+ * 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.
@@ -19,6 +19,7 @@
 import android.os.Bundle
 import android.view.Surface
 import androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.dsl.FlickerBuilder
 import com.google.common.truth.Truth.assertWithMessage
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -27,13 +28,19 @@
 /**
  * Contains [FlickerTestRunnerFactory] tests.
  *
- * To run this test: `atest FlickerLibTest:FlickerTestFacroty`
+ * To run this test: `atest FlickerLibTest:FlickerTestFactoryRunnerTest`
  */
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 class FlickerTestFactoryRunnerTest {
     private val instrumentation = InstrumentationRegistry.getInstrumentation()
     private val defaultRotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90)
 
+    private fun FlickerBuilder.setDefaultTestCfg() = apply {
+        assertions {
+            layersTrace { all { fail("First assertion") } }
+        }
+    }
+
     private fun validateRotationTest(actual: Bundle, rotations: List<Int> = defaultRotations) {
         assertWithMessage("Rotation tests should not have the same start and end rotation")
             .that(actual.startRotation).isNotEqualTo(actual.endRotation)
@@ -53,17 +60,25 @@
     @Test
     fun checkBuildTest() {
         val factory = FlickerTestRunnerFactory(instrumentation)
-        val actual = factory.buildTest { cfg -> validateTest(cfg) }
+        val actual = factory.buildTest { cfg ->
+            this.setDefaultTestCfg()
+            validateTest(cfg)
+        }
+        // Should have 1 test for transition and 1 for the assertions in each orientation
         assertWithMessage("Flicker should create tests for 0 and 90 degrees")
-            .that(actual).hasSize(2)
+            .that(actual).hasSize(4)
     }
 
     @Test
     fun checkBuildRotationTest() {
         val factory = FlickerTestRunnerFactory(instrumentation)
-        val actual = factory.buildRotationTest { cfg -> validateRotationTest(cfg) }
+        val actual = factory.buildRotationTest { cfg ->
+            this.setDefaultTestCfg()
+            validateRotationTest(cfg)
+        }
+        // Should have 1 test for transition and 1 for the assertions in each orientation
         assertWithMessage("Flicker should create tests for 0 and 90 degrees")
-            .that(actual).hasSize(2)
+            .that(actual).hasSize(4)
     }
 
     @Test
@@ -71,9 +86,13 @@
         val rotations = listOf(Surface.ROTATION_0, Surface.ROTATION_90, Surface.ROTATION_180,
                 Surface.ROTATION_270)
         val factory = FlickerTestRunnerFactory(instrumentation, rotations)
-        val actual = factory.buildRotationTest { cfg -> validateRotationTest(cfg, rotations) }
+        val actual = factory.buildRotationTest { cfg ->
+            this.setDefaultTestCfg()
+            validateRotationTest(cfg, rotations)
+        }
+        // Should have 1 test for transition and 1 for the assertions in each rotation
         assertWithMessage("Flicker should create tests for 0/90/180/270 degrees")
-            .that(actual).hasSize(12)
+            .that(actual).hasSize(24)
     }
 
     @Test
@@ -81,11 +100,97 @@
         val factory = FlickerTestRunnerFactory(instrumentation)
         val actual = listOf(Bundle().also { it.putBoolean("test", true) })
         val tests = factory.buildTest(actual) { cfg ->
+            this.setDefaultTestCfg()
             validateTest(cfg)
             assertWithMessage("Could not find custom payload data")
                 .that(cfg.getBoolean("test", false)).isTrue()
         }
-        assertWithMessage("Flicker should create tests for 0 and 90 degrees")
-            .that(tests).hasSize(1)
+        // Should have 1 test for transition and 1 for the assertions in each orientation
+        assertWithMessage("Flicker should create 1 test for transition and 1 for assertion")
+            .that(tests).hasSize(2)
+    }
+
+    private fun assertIsEmpty(producer: () -> Flicker) {
+        val spec = producer.invoke()
+        assertWithMessage("Should not have assertions")
+            .that(spec.assertions)
+            .isEmpty()
+    }
+
+    private fun assertHasSingleAssertion(producer: () -> Flicker) {
+        val spec = producer.invoke()
+        assertWithMessage("Should have 1 assertion")
+            .that(spec.assertions)
+            .hasSize(1)
+    }
+
+    @Test
+    fun checkBuildOneTestPerAssertion() {
+        val factory = FlickerTestRunnerFactory(instrumentation,
+            supportedRotations = listOf(Surface.ROTATION_0))
+        val tests = factory.buildTest {
+            assertions {
+                layersTrace { all { fail("First assertion") } }
+                windowManagerTrace { all { fail("Second assertion") } }
+                eventLog { all { fail("This assertion") } }
+            }
+        }
+
+        assertWithMessage("Factory should have created 4 tests, one for transition and " +
+            "3 with a single assertion each")
+            .that(tests)
+            .hasSize(4)
+
+        assertIsEmpty(tests.first()[1] as () -> Flicker)
+        tests.drop(1).forEach { (_, producer, _) ->
+            assertHasSingleAssertion(producer as () -> Flicker)
+        }
+    }
+
+    @Test
+    fun checkCleanUp() {
+        val factory = FlickerTestRunnerFactory(instrumentation)
+        val actual = factory.buildTest { cfg ->
+            this.setDefaultTestCfg()
+            validateTest(cfg)
+        }
+
+        actual.forEachIndexed { index, entry ->
+            val expectedCleanUp = index % 2 > 0
+            val actualCleanUp = entry[2]
+            val specProducer = entry[1] as () -> Flicker
+            val spec = specProducer.invoke()
+
+            assertWithMessage("Entry $index should${if (expectedCleanUp) "" else " not"} cleanup")
+                .that(actualCleanUp)
+                .isEqualTo(expectedCleanUp)
+        }
+    }
+
+    @Test
+    fun checkTransitionRunner() {
+        val factory = FlickerTestRunnerFactory(instrumentation)
+        val actual = factory.buildTest { cfg ->
+            this.setDefaultTestCfg()
+            validateTest(cfg)
+        }
+
+        val first = (actual[0][1] as () -> Flicker).invoke()
+        val second = (actual[1][1] as () -> Flicker).invoke()
+        val third = (actual[2][1] as () -> Flicker).invoke()
+        val fourth = (actual[3][1] as () -> Flicker).invoke()
+
+        assertWithMessage("First and second tests should share a runner")
+            .that(first.runner)
+            .isEqualTo(second.runner)
+
+        assertWithMessage("Third and fourth tests should share a runner")
+            .that(third.runner)
+            .isEqualTo(fourth.runner)
+
+        assertWithMessage("First and third tests should not share a runner")
+            .that(first.runner)
+            .isNotEqualTo(third.runner)
+
     }
 }
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt
new file mode 100644
index 0000000..e6b5883
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/TransitionRunnerTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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 androidx.test.platform.app.InstrumentationRegistry
+import com.android.server.wm.flicker.dsl.FlickerBuilder
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/**
+ * Contains [TransitionRunnerTest] and [TransitionRunnerCached] tests.
+ *
+ * To run this test: `atest FlickerLibTest:TransitionRunnerTest`
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class TransitionRunnerTest {
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+
+    @Test
+    fun canRunTransition() {
+        val runner = TransitionRunner()
+        var executed = false
+        val flicker = FlickerBuilder(instrumentation)
+            .apply {
+                transitions {
+                    executed = true
+                }
+            }.build(runner)
+        Truth.assertThat(executed).isFalse()
+        val result = runner.execute(flicker)
+        Truth.assertThat(executed).isTrue()
+        Truth.assertThat(result.error).isNull()
+        Truth.assertThat(result.runs).hasSize(4)
+    }
+
+    @Test
+    fun canRunTransitionCached() {
+        val runner = TransitionRunnerCached()
+        var executed = false
+        val flicker = FlickerBuilder(instrumentation)
+            .apply {
+                transitions {
+                    executed = true
+                }
+            }.build(runner)
+        val result = runner.execute(flicker)
+        executed = false
+        val cachedResult = runner.execute(flicker)
+        Truth.assertThat(executed).isFalse()
+        Truth.assertThat(cachedResult).isEqualTo(result)
+    }
+}
\ No newline at end of file
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt
index 1fcf81d..c6debc5 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/EventLogMonitorTest.kt
@@ -45,17 +45,18 @@
         val result = FlickerRunResult.Builder()
         monitor.save("test", result)
 
-        assertEquals(2, result.eventLog.size)
+        assertEquals(2, result.eventLog?.size)
         assertEquals(
             "4749f88 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
-            result.eventLog[0].window)
-        assertEquals(FocusEvent.Focus.LOST, result.eventLog[0].focus)
+            result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
         assertEquals(
             "7c01447 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
-            result.eventLog[1].window)
-        assertEquals(FocusEvent.Focus.GAINED, result.eventLog[1].focus)
-        assertTrue(result.eventLog[0].timestamp <= result.eventLog[1].timestamp)
-        assertEquals(result.eventLog[0].reason, "test")
+            result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
+        assertEquals(result.eventLog?.get(0)?.reason, "test")
     }
 
     @Test
@@ -86,17 +87,18 @@
         val result = FlickerRunResult.Builder()
         monitor.save("test", result)
 
-        assertEquals(2, result.eventLog.size)
+        assertEquals(2, result.eventLog?.size)
         assertEquals("479f88 " +
             "com.android.phone/" +
             "com.android.phone.settings.fdn.FdnSetting (server)",
-            result.eventLog[0].window)
-        assertEquals(FocusEvent.Focus.LOST, result.eventLog[0].focus)
+            result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
         assertEquals("7c01447 com.android.phone/" +
             "com.android.phone.settings.fdn.FdnSetting (server)",
-            result.eventLog[1].window)
-        assertEquals(FocusEvent.Focus.GAINED, result.eventLog[1].focus)
-        assertTrue(result.eventLog[0].timestamp <= result.eventLog[1].timestamp)
+            result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
     }
 
     @Test
@@ -120,17 +122,18 @@
         val result = FlickerRunResult.Builder()
         monitor.save("test", result)
 
-        assertEquals(2, result.eventLog.size)
+        assertEquals(2, result.eventLog?.size)
         assertEquals(
                 "4749f88 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
-                result.eventLog[0].window)
-        assertEquals(FocusEvent.Focus.LOST, result.eventLog[0].focus)
+                result.eventLog?.get(0)?.window)
+        assertEquals(FocusEvent.Focus.LOST, result.eventLog?.get(0)?.focus)
         assertEquals(
                 "7c01447 com.android.phone/com.android.phone.settings.fdn.FdnSetting (server)",
-                result.eventLog[1].window)
-        assertEquals(FocusEvent.Focus.GAINED, result.eventLog[1].focus)
-        assertTrue(result.eventLog[0].timestamp <= result.eventLog[1].timestamp)
-        assertEquals(result.eventLog[0].reason, "test")
+                result.eventLog?.get(1)?.window)
+        assertEquals(FocusEvent.Focus.GAINED, result.eventLog?.get(1)?.focus)
+        assertTrue(result.eventLog?.get(0)?.timestamp ?: 0
+            <= result.eventLog?.get(1)?.timestamp ?: 0)
+        assertEquals(result.eventLog?.get(0)?.reason, "test")
     }
 
     private companion object {
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt
index b1513cf..8060511 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/LayersTraceMonitorTest.kt
@@ -42,7 +42,7 @@
     }
 
     override fun getTraceFile(result: FlickerRunResult): Path? {
-        return result.layersTraceFile
+        return result.traceFiles.firstOrNull { it.toString().contains("layers_trace") }
     }
 
     @Test
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
index addc50b..dce8e50 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/ScreenRecorderTest.kt
@@ -52,7 +52,9 @@
         SystemClock.sleep(100)
         mScreenRecorder.stop()
         val file = mScreenRecorder.outputPath.toFile()
-        Truth.assertThat(file.exists()).isTrue()
+        Truth.assertWithMessage("Screen recording file not found")
+            .that(file.exists())
+            .isTrue()
     }
 
     @Test
@@ -62,7 +64,16 @@
         mScreenRecorder.stop()
         val builder = FlickerRunResult.Builder()
         mScreenRecorder.save("test", builder)
-        val file = builder.build().screenRecording
-        Truth.assertThat(Files.exists(file)).isTrue()
+        val traces = builder.buildTraceResults().mapNotNull { result ->
+            result.traceFiles.firstOrNull {
+                it.toString().contains("transition")
+            }
+        }
+        traces.forEach {
+            Truth.assertWithMessage("Trace file $it not found").that(Files.exists(it)).isTrue()
+        }
+        traces.forEach {
+            Files.deleteIfExists(it)
+        }
     }
 }
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
index f9ed773..ec22368 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/TraceMonitorTest.kt
@@ -74,7 +74,10 @@
         traceMonitor.stop()
         val builder = FlickerRunResult.Builder()
         traceMonitor.save("capturedTrace", builder)
-        savedTrace = getTraceFile(builder.build()) ?: error("Could not find saved trace file")
+        val results = builder.buildAll()
+        Truth.assertWithMessage("Expected 3 results for the trace").that(results).hasSize(3)
+        val result = results.first()
+        savedTrace = getTraceFile(result) ?: error("Could not find saved trace file")
         val testFile = savedTrace.toFile()
         Truth.assertThat(testFile.exists()).isTrue()
         val calculatedChecksum = TraceMonitor.calculateChecksum(savedTrace)
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt
index d1ab6ba..1cdf679 100644
--- a/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/monitor/WindowManagerTraceMonitorTest.kt
@@ -17,8 +17,8 @@
 package com.android.server.wm.flicker.monitor
 
 import androidx.test.uiautomator.UiDevice
-import com.android.server.wm.flicker.FlickerRunResult
 import com.android.server.wm.nano.WindowManagerTraceFileProto
+import com.android.server.wm.flicker.FlickerRunResult
 import com.google.common.truth.Truth
 import org.junit.FixMethodOrder
 import org.junit.Test
@@ -38,7 +38,7 @@
     }
 
     override fun getTraceFile(result: FlickerRunResult): Path? {
-        return result.wmTraceFile
+        return result.traceFiles.firstOrNull { it.toString().contains("wm_trace") }
     }
 
     @Test