1/ Create Reader/Writer for flicker result

A later CL will replace the current flicker results by these reader/writer components

Also unify all android.support.annotation to androidx.annotation as per https://developer.android.com/jetpack/androidx/migrate guidelines

Incl. tests

Fixes: 255715397
Fixes: 259382394
Fixes: 259251690
Test: atest FlickerLibTest
Change-Id: I1047f1821e6d3706f79907f1d6adbc7a3454efeb
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/Consts.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/Consts.kt
new file mode 100644
index 0000000..e97dc27
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/Consts.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.FLICKER_TAG
+
+const val WINSCOPE_EXT = ".winscope"
+internal const val FLICKER_IO_TAG = "$FLICKER_TAG-IO"
+internal const val BUFFER_SIZE = 2048
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/ResultData.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultData.kt
new file mode 100644
index 0000000..a7c7263
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultData.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import java.nio.file.Path
+
+/** Contents of a flicker run (e.g. files, status, event log) */
+data class ResultData(
+    /** Path to the artifact file */
+    val artifactPath: Path,
+    /**
+     * Event log contents
+     *
+     * TODO: Move to a file in the future
+     */
+    val eventLog: List<FocusEvent>?,
+    /** Transition start and end time */
+    val transitionTimeRange: TransitionTimeRange,
+    /** Transition execution error (if any) */
+    val executionError: Throwable?,
+    val runStatus: RunStatus
+) {
+    override fun toString(): String = buildString {
+        append(artifactPath)
+        append(" (status=")
+        append(runStatus)
+        executionError?.let {
+            append(", error=")
+            append(it.message)
+        }
+        append(") ")
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/ResultFileDescriptor.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultFileDescriptor.kt
new file mode 100644
index 0000000..fe007e7
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultFileDescriptor.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import androidx.annotation.VisibleForTesting
+import com.android.server.wm.flicker.AssertionTag
+
+/** Descriptor for files inside flicker result artifacts */
+class ResultFileDescriptor
+internal constructor(
+    /** Trace or dump type */
+    @VisibleForTesting val traceType: TraceType,
+    /** If the trace/dump is associated with a tag */
+    @VisibleForTesting val tag: String = AssertionTag.ALL
+) {
+    private val isTagTrace: Boolean
+        get() = tag != AssertionTag.ALL
+
+    /** Name of the trace file in the result artifact (e.g. zip) */
+    val fileNameInArtifact: String = buildString {
+        if (isTagTrace) {
+            append(tag)
+            append("__")
+        }
+        append(traceType.fileName)
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (other !is ResultFileDescriptor) return false
+
+        if (traceType != other.traceType) return false
+        if (tag != other.tag) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        var result = traceType.hashCode()
+        result = 31 * result + tag.hashCode()
+        return result
+    }
+
+    override fun toString(): String = fileNameInArtifact
+
+    companion object {
+        /**
+         * Creates a [ResultFileDescriptor] based on the [fileNameInArtifact]
+         *
+         * @param fileNameInArtifact Name of the trace file in the result artifact (e.g. zip)
+         */
+        fun fromFileName(fileNameInArtifact: String): ResultFileDescriptor {
+            val tagSplit = fileNameInArtifact.split("__")
+            require(tagSplit.size <= 2) {
+                "File name format should match '{tag}__{filename}' but was $fileNameInArtifact"
+            }
+            val tag = if (tagSplit.size > 1) tagSplit.first() else AssertionTag.ALL
+            val fileName = tagSplit.last()
+            return ResultFileDescriptor(TraceType.fromFileName(fileName), tag)
+        }
+
+        @VisibleForTesting
+        fun newTestInstance(traceType: TraceType, tag: String = AssertionTag.ALL) =
+            ResultFileDescriptor(traceType, tag)
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/ResultReader.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultReader.kt
new file mode 100644
index 0000000..10f004e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultReader.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.server.wm.flicker.AssertionTag
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.TraceConfig
+import com.android.server.wm.flicker.TraceConfigs
+import com.android.server.wm.flicker.Utils
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import com.android.server.wm.traces.common.layers.LayersTrace
+import com.android.server.wm.traces.common.transactions.TransactionsTrace
+import com.android.server.wm.traces.common.transition.TransitionsTrace
+import com.android.server.wm.traces.common.windowmanager.WindowManagerTrace
+import com.android.server.wm.traces.parser.layers.LayersTraceParser
+import com.android.server.wm.traces.parser.transaction.TransactionsTraceParser
+import com.android.server.wm.traces.parser.transition.TransitionsTraceParser
+import com.android.server.wm.traces.parser.windowmanager.WindowManagerTraceParser
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.ByteArrayOutputStream
+import java.io.FileInputStream
+import java.io.IOException
+import java.util.zip.ZipEntry
+import java.util.zip.ZipInputStream
+
+/**
+ * Helper class to read results from a flicker artifact
+ *
+ * @param result to read from
+ * @param traceConfig
+ */
+open class ResultReader(protected var result: ResultData, private val traceConfig: TraceConfigs) {
+    @VisibleForTesting
+    val artifactPath
+        get() = result.artifactPath
+    @VisibleForTesting
+    val runStatus
+        get() = result.runStatus
+    private val transitionTimeRange
+        get() = result.transitionTimeRange
+    internal val isFailure
+        get() = runStatus.isFailure
+    internal val executionError
+        get() = result.executionError
+
+    private fun withZipFile(predicate: (ZipInputStream) -> Unit) {
+        val zipInputStream =
+            ZipInputStream(
+                BufferedInputStream(FileInputStream(result.artifactPath.toFile()), BUFFER_SIZE)
+            )
+        try {
+            predicate(zipInputStream)
+        } finally {
+            zipInputStream.closeEntry()
+            zipInputStream.close()
+        }
+    }
+
+    private fun forEachFileInZip(predicate: (ZipEntry) -> Unit) {
+        withZipFile {
+            var zipEntry: ZipEntry? = it.nextEntry
+            while (zipEntry != null) {
+                predicate(zipEntry)
+                zipEntry = it.nextEntry
+            }
+        }
+    }
+
+    @Throws(IOException::class)
+    private fun readFromZip(descriptor: ResultFileDescriptor): ByteArray? {
+        Log.d(FLICKER_IO_TAG, "Reading descriptor=$descriptor from $result")
+
+        var foundFile = false
+        val outByteArray = ByteArrayOutputStream()
+        val tmpBuffer = ByteArray(BUFFER_SIZE)
+        withZipFile {
+            var zipEntry: ZipEntry? = it.nextEntry
+            while (zipEntry != null) {
+                if (zipEntry.name == descriptor.fileNameInArtifact) {
+                    val outputStream = BufferedOutputStream(outByteArray, BUFFER_SIZE)
+                    try {
+                        var size = it.read(tmpBuffer, 0, BUFFER_SIZE)
+                        while (size > 0) {
+                            outputStream.write(tmpBuffer, 0, size)
+                            size = it.read(tmpBuffer, 0, BUFFER_SIZE)
+                        }
+                        it.closeEntry()
+                    } finally {
+                        outputStream.flush()
+                        outputStream.close()
+                    }
+                    foundFile = true
+                    break
+                }
+                zipEntry = it.nextEntry
+            }
+        }
+
+        return if (foundFile) outByteArray.toByteArray() else null
+    }
+
+    /**
+     * @return a [WindowManagerTrace] from the dump associated to [tag]
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class)
+    internal fun readWmState(tag: String): WindowManagerTrace? = doReadWmState(tag)
+
+    protected open fun doReadWmState(tag: String): WindowManagerTrace? {
+        val descriptor = ResultFileDescriptor(TraceType.WM_DUMP, tag)
+        Log.d(FLICKER_IO_TAG, "Reading WM trace descriptor=$descriptor from $result")
+        val traceData = readFromZip(descriptor)
+        return traceData?.let {
+            WindowManagerTraceParser.parseFromDump(it, clearCacheAfterParsing = true)
+        }
+    }
+
+    /**
+     * @return a [WindowManagerTrace] for the part of the trace we want to run the assertions on
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class) internal fun readWmTrace(): WindowManagerTrace? = doReadWmTrace()
+
+    protected open fun doReadWmTrace(): WindowManagerTrace? {
+        val descriptor = ResultFileDescriptor(TraceType.WM)
+        val traceData = readFromZip(descriptor)
+        return traceData?.let {
+            val fullTrace =
+                WindowManagerTraceParser.parseFromTrace(it, clearCacheAfterParsing = true)
+            require(!traceConfig.wmTrace.required || fullTrace.entries.isNotEmpty()) {
+                "Full WM trace is empty..."
+            }
+            val trace =
+                fullTrace.slice(
+                    transitionTimeRange.start.elapsedRealtimeNanos,
+                    transitionTimeRange.end.elapsedRealtimeNanos,
+                    addInitialEntry = true
+                )
+            val minimumEntries = minimumTraceEntriesForConfig(traceConfig.wmTrace)
+            require(trace.entries.size >= minimumEntries) {
+                "WM trace contained ${trace.entries.size} entries, " +
+                    "expected at least $minimumEntries... :: " +
+                    "transition starts at ${transitionTimeRange.start.elapsedRealtimeNanos} and " +
+                    "ends at ${transitionTimeRange.end.elapsedRealtimeNanos}."
+            }
+            trace
+        }
+    }
+
+    /**
+     * @return a [LayersTrace] for the part of the trace we want to run the assertions on
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class) internal fun readLayersTrace(): LayersTrace? = doReadLayersTrace()
+
+    protected open fun doReadLayersTrace(): LayersTrace? {
+        val descriptor = ResultFileDescriptor(TraceType.SF)
+        val traceData = readFromZip(descriptor)
+        return traceData?.let {
+            val fullTrace = LayersTraceParser.parseFromTrace(it, clearCacheAfterParsing = true)
+            require(!traceConfig.layersTrace.required || fullTrace.entries.isNotEmpty()) {
+                "Full layers trace cannot be empty"
+            }
+            val trace =
+                fullTrace.slice(
+                    transitionTimeRange.start.systemTime,
+                    transitionTimeRange.end.systemTime,
+                    addInitialEntry = true
+                )
+            val minimumEntries = minimumTraceEntriesForConfig(traceConfig.layersTrace)
+            require(trace.entries.size >= minimumEntries) {
+                "Layers trace contained ${trace.entries.size} entries, " +
+                    "expected at least $minimumEntries... :: " +
+                    "transition starts at ${transitionTimeRange.start.systemTime} and " +
+                    "ends at ${transitionTimeRange.end.systemTime}."
+            }
+            trace
+        }
+    }
+
+    /**
+     * @return a [LayersTrace] from the dump associated to [tag]
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class)
+    internal fun readLayersDump(tag: String): LayersTrace? = doReadLayersDump(tag)
+
+    protected open fun doReadLayersDump(tag: String): LayersTrace? {
+        val descriptor = ResultFileDescriptor(TraceType.SF_DUMP, tag)
+        val traceData = readFromZip(descriptor)
+        return traceData?.let {
+            LayersTraceParser.parseFromTrace(it, clearCacheAfterParsing = true)
+        }
+    }
+
+    @Throws(IOException::class)
+    private fun readFullTransactionsTrace(): TransactionsTrace? {
+        val traceData = readFromZip(ResultFileDescriptor(TraceType.TRANSACTION))
+        return traceData?.let {
+            val fullTrace = TransactionsTraceParser.parseFromTrace(it)
+            require(fullTrace.entries.isNotEmpty()) { "Transactions trace cannot be empty" }
+            fullTrace
+        }
+    }
+
+    /**
+     * @return a [TransactionsTrace] for the part of the trace we want to run the assertions on
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class)
+    internal fun readTransactionsTrace(): TransactionsTrace? = doReadTransactionsTrace()
+
+    protected open fun doReadTransactionsTrace(): TransactionsTrace? {
+        val fullTrace = readFullTransactionsTrace() ?: return null
+        val trace =
+            fullTrace.slice(
+                transitionTimeRange.start.systemTime,
+                transitionTimeRange.end.systemTime
+            )
+        require(trace.entries.isNotEmpty()) { "Trimmed transactions trace cannot be empty" }
+        return trace
+    }
+
+    /**
+     * @return a [TransitionsTrace] for the part of the trace we want to run the assertions on
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    @Throws(IOException::class)
+    internal fun readTransitionsTrace(): TransitionsTrace? = doReadTransitionsTrace()
+
+    protected open fun doReadTransitionsTrace(): TransitionsTrace? {
+        val transactionsTrace = readFullTransactionsTrace()
+        val traceData = readFromZip(ResultFileDescriptor(TraceType.TRANSITION))
+        if (transactionsTrace == null || traceData == null) {
+            return null
+        }
+
+        val fullTrace = TransitionsTraceParser.parseFromTrace(traceData, transactionsTrace)
+        val trace =
+            fullTrace.slice(
+                transitionTimeRange.start.elapsedRealtimeNanos,
+                transitionTimeRange.end.elapsedRealtimeNanos
+            )
+        if (!traceConfig.transitionsTrace.allowNoChange) {
+            require(trace.entries.isNotEmpty()) { "Transitions trace cannot be empty" }
+        }
+        return trace
+    }
+
+    private fun minimumTraceEntriesForConfig(config: TraceConfig): Int {
+        return if (config.allowNoChange) 1 else 2
+    }
+
+    /**
+     * @return a List<[FocusEvent]> for the part of the trace we want to run the assertions on
+     * @throws IOException if the artifact file doesn't exist or can't be read
+     */
+    internal fun readEventLogTrace(): List<FocusEvent>? = doReadEventLogTrace()
+
+    protected open fun doReadEventLogTrace(): List<FocusEvent>? {
+        return result.eventLog?.slice(
+            transitionTimeRange.start.unixTimeNanos,
+            transitionTimeRange.end.unixTimeNanos
+        )
+    }
+
+    private fun List<FocusEvent>.slice(from: Long, to: Long): List<FocusEvent> {
+        return dropWhile { it.timestamp < from }.dropLastWhile { it.timestamp > to }
+    }
+
+    override fun toString(): String = "$result"
+
+    /** @return the number of files in the artifact */
+    @VisibleForTesting
+    fun countFiles(): Int {
+        var count = 0
+        forEachFileInZip { count++ }
+        return count
+    }
+
+    /** @return if a file with type [traceType] linked to a [tag] exists in the artifact */
+    @VisibleForTesting
+    fun hasTraceFile(traceType: TraceType, tag: String = AssertionTag.ALL): Boolean {
+        val descriptor = ResultFileDescriptor(traceType, tag)
+        var found = false
+        forEachFileInZip { found = found || (it.name == descriptor.fileNameInArtifact) }
+        return found
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/ResultWriter.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultWriter.kt
new file mode 100644
index 0000000..4cd5a6c
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/ResultWriter.kt
@@ -0,0 +1,145 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import android.util.Log
+import com.android.server.wm.flicker.AssertionTag
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.ScenarioBuilder
+import com.android.server.wm.flicker.traces.eventlog.FocusEvent
+import com.android.server.wm.traces.common.IScenario
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.File
+import java.io.FileInputStream
+import java.io.FileOutputStream
+import java.nio.file.Path
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+/** Helper class to create run result artifact files */
+open class ResultWriter {
+    protected var scenario: IScenario = ScenarioBuilder().createEmptyScenario()
+    private var runStatus: RunStatus = RunStatus.UNDEFINED
+    private val files = mutableMapOf<ResultFileDescriptor, File>()
+    private var eventLog: List<FocusEvent>? = null
+    private var transitionStartTime = TraceTime.MIN
+    private var transitionEndTime = TraceTime.MAX
+    private var executionError: Throwable? = null
+    private var outputDir: Path? = null
+
+    /** Sets the artifact scenario to [_scenario] */
+    fun forScenario(_scenario: IScenario) = apply { scenario = _scenario }
+
+    /** Sets the artifact transition start time to [time] */
+    fun setTransitionStartTime(time: TraceTime = TraceTime.now()) = apply {
+        transitionStartTime = time
+    }
+
+    /** Sets the artifact transition end time to [time] */
+    fun setTransitionEndTime(time: TraceTime = TraceTime.now()) = apply { transitionEndTime = time }
+
+    /** Sets the artifact status as successfully executed transition ([RunStatus.RUN_EXECUTED]) */
+    fun setRunComplete() = apply { runStatus = RunStatus.RUN_EXECUTED }
+
+    /** Sets the dir where the artifact file will be stored to [path] */
+    fun withOutputDir(path: Path) = apply { outputDir = path }
+
+    /**
+     * Sets the artifact status as failed executed transition ([RunStatus.RUN_FAILED])
+     *
+     * @param error that caused the transition to fail
+     */
+    fun setRunFailed(error: Throwable) = apply {
+        runStatus = RunStatus.RUN_FAILED
+        executionError = error
+    }
+
+    /** Adds [_eventLog] to the result artifact */
+    fun addEventLogResult(_eventLog: List<FocusEvent>) = apply {
+        Log.d(FLICKER_IO_TAG, "Adding event log to $scenario")
+        eventLog = _eventLog
+    }
+
+    /**
+     * Adds [file] to the result artifact
+     *
+     * @param traceType used when adding [file] to the result artifact
+     * @param tag used when adding [file] to the result artifact
+     */
+    fun addTraceResult(traceType: TraceType, file: File, tag: String = AssertionTag.ALL) = apply {
+        Log.d(
+            FLICKER_IO_TAG,
+            "Add trace result file=$file type=$traceType tag=$tag scenario=$scenario"
+        )
+        val fileDescriptor = ResultFileDescriptor(traceType, tag)
+        files[fileDescriptor] = file
+    }
+
+    private fun addFile(zipOutputStream: ZipOutputStream, file: File, nameInArchive: String) {
+        Log.v(FLICKER_IO_TAG, "Adding $file with name $nameInArchive to zip")
+        val fi = FileInputStream(file)
+        val inputStream = BufferedInputStream(fi, BUFFER_SIZE)
+        inputStream.use {
+            val entry = ZipEntry(nameInArchive)
+            zipOutputStream.putNextEntry(entry)
+            val data = ByteArray(BUFFER_SIZE)
+            var count: Int = it.read(data, 0, BUFFER_SIZE)
+            while (count != -1) {
+                zipOutputStream.write(data, 0, count)
+                count = it.read(data, 0, BUFFER_SIZE)
+            }
+        }
+        zipOutputStream.closeEntry()
+        file.delete()
+    }
+
+    private fun createZipFile(file: File): ZipOutputStream {
+        return ZipOutputStream(BufferedOutputStream(FileOutputStream(file), BUFFER_SIZE))
+    }
+
+    /** @return writes the result artifact to disk and returns it */
+    open fun write(): ResultData {
+        val outputDir = outputDir
+        requireNotNull(outputDir) { "Output dir not configured" }
+        require(!scenario.isEmpty) { "Scenario shouldn't be empty" }
+
+        // Ensure output directory exists
+        outputDir.toFile().mkdirs()
+
+        if (runStatus == RunStatus.UNDEFINED) {
+            Log.w(FLICKER_IO_TAG, "Writing result with $runStatus run status")
+        }
+
+        val newFileName = "${runStatus.prefix}_$scenario.zip"
+        val dstFile = outputDir.resolve(newFileName)
+        Log.d(FLICKER_IO_TAG, "Writing artifact file $dstFile")
+        createZipFile(dstFile.toFile()).use { zipOutputStream ->
+            files.forEach { (descriptor, file) ->
+                addFile(zipOutputStream, file, nameInArchive = descriptor.fileNameInArtifact)
+            }
+        }
+
+        return ResultData(
+            dstFile,
+            eventLog,
+            TransitionTimeRange(transitionStartTime, transitionEndTime),
+            executionError,
+            runStatus
+        )
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/TraceTime.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/TraceTime.kt
new file mode 100644
index 0000000..686cc24
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/TraceTime.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import android.os.SystemClock
+import java.util.concurrent.TimeUnit
+
+data class TraceTime(
+    val elapsedRealtimeNanos: Long,
+    val systemTime: Long,
+    val unixTimeNanos: Long
+) {
+    companion object {
+        val MIN = TraceTime(0, 0, 0)
+        val MAX = TraceTime(Long.MAX_VALUE, Long.MAX_VALUE, Long.MAX_VALUE)
+
+        /** @return the current timestamp as [TraceTime] */
+        fun now() =
+            TraceTime(
+                elapsedRealtimeNanos = SystemClock.elapsedRealtimeNanos(),
+                systemTime = SystemClock.uptimeNanos(),
+                unixTimeNanos =
+                    TimeUnit.NANOSECONDS.convert(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
+            )
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/TraceType.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/TraceType.kt
new file mode 100644
index 0000000..8c8828e
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/TraceType.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2022 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.io
+
+/** Types of traces/dumps that cna be in a flicker result */
+enum class TraceType(val fileName: String) {
+    SF("layers_trace$WINSCOPE_EXT"),
+    WM("wm_trace$WINSCOPE_EXT"),
+    TRANSACTION("transactions_trace$WINSCOPE_EXT"),
+    TRANSITION("transition_trace$WINSCOPE_EXT"),
+    SCREEN_RECORDING("transition.mp4"),
+    SF_DUMP("sf_dump$WINSCOPE_EXT"),
+    WM_DUMP("wm_dump$WINSCOPE_EXT");
+
+    companion object {
+        fun fromFileName(fileName: String): TraceType {
+            return when {
+                fileName == SF.fileName -> SF
+                fileName == WM.fileName -> WM
+                fileName == TRANSACTION.fileName -> TRANSACTION
+                fileName == TRANSITION.fileName -> TRANSITION
+                fileName == SCREEN_RECORDING.fileName -> SCREEN_RECORDING
+                fileName.endsWith(SF_DUMP.fileName) -> SF_DUMP
+                fileName.endsWith(WM_DUMP.fileName) -> WM_DUMP
+                else -> error("Unknown trace type for fileName=$fileName")
+            }
+        }
+    }
+}
diff --git a/libraries/flicker/src/com/android/server/wm/flicker/io/TransitionTimeRange.kt b/libraries/flicker/src/com/android/server/wm/flicker/io/TransitionTimeRange.kt
new file mode 100644
index 0000000..10385bd
--- /dev/null
+++ b/libraries/flicker/src/com/android/server/wm/flicker/io/TransitionTimeRange.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2022 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.io
+
+data class TransitionTimeRange(val start: TraceTime, val end: TraceTime) {
+    companion object {
+        val EMPTY = TransitionTimeRange(TraceTime.MIN, TraceTime.MAX)
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/BaseResultReaderTestParseTrace.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/BaseResultReaderTestParseTrace.kt
new file mode 100644
index 0000000..19f5dbf
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/BaseResultReaderTestParseTrace.kt
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.DEFAULT_TRACE_CONFIG
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.flicker.assertExceptionMessage
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.newTestResultWriter
+import com.android.server.wm.flicker.outputFileName
+import com.android.server.wm.traces.common.ITrace
+import com.google.common.truth.Truth
+import java.io.File
+import java.nio.file.Files
+import org.junit.Before
+import org.junit.Test
+
+/** Base class for [ResultReader] tests parsing traces */
+abstract class BaseResultReaderTestParseTrace {
+    protected abstract val assetFile: File
+    protected abstract val traceName: String
+    protected abstract val startTimeTrace: TraceTime
+    protected abstract val endTimeTrace: TraceTime
+    protected abstract val validSliceTime: TraceTime
+    protected abstract val invalidSliceTime: TraceTime
+    protected abstract val traceType: TraceType
+    protected abstract val expectedSlicedTraceSize: Int
+    protected open val invalidSizeMessage: String
+        get() = "$traceName contained 1 entries, expected at least 2"
+
+    protected abstract fun doParse(reader: ResultReader): ITrace<*>?
+    protected abstract fun getTime(traceTime: TraceTime): Long
+
+    protected open fun writeTrace(writer: ResultWriter): ResultWriter {
+        writer.addTraceResult(traceType, assetFile)
+        return writer
+    }
+
+    @Before
+    fun setup() {
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+    }
+
+    @Test
+    fun readTrace() {
+        val writer = writeTrace(newTestResultWriter())
+        val result = writer.write()
+
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        val trace = doParse(reader) ?: error("$traceName not built")
+
+        Truth.assertWithMessage(traceName).that(trace.entries).asList().isNotEmpty()
+        Truth.assertWithMessage("$traceName start")
+            .that(trace.entries.first().timestamp)
+            .isEqualTo(getTime(startTimeTrace))
+        Truth.assertWithMessage("$traceName end")
+            .that(trace.entries.last().timestamp)
+            .isEqualTo(getTime(endTimeTrace))
+    }
+
+    @Test
+    fun readTraceNullWhenDoesNotExist() {
+        val writer = newTestResultWriter()
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        val trace = doParse(reader)
+
+        Truth.assertWithMessage(traceName).that(trace).isNull()
+    }
+
+    @Test
+    fun readTraceAndSliceTraceByTimestamp() {
+        val result = doWriteTraceWithTransitionTime(validSliceTime)
+        val reader = ResultReader(result, TestTraces.TEST_TRACE_CONFIG)
+        val trace = doParse(reader) ?: error("$traceName not built")
+
+        Truth.assertWithMessage(traceName)
+            .that(trace.entries)
+            .asList()
+            .hasSize(expectedSlicedTraceSize)
+        Truth.assertWithMessage("$traceName start")
+            .that(trace.entries.first().timestamp)
+            .isEqualTo(getTime(startTimeTrace))
+    }
+
+    @Test
+    fun readTraceAndSliceTraceByTimestampAndFailInvalidSize() {
+        val result = doWriteTraceWithTransitionTime(invalidSliceTime)
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        val exception =
+            assertThrows(IllegalArgumentException::class.java) {
+                doParse(reader) ?: error("$traceName not built")
+            }
+        assertExceptionMessage(exception, invalidSizeMessage)
+    }
+
+    private fun doWriteTraceWithTransitionTime(endTime: TraceTime): ResultData {
+        return writeTrace(newTestResultWriter())
+            .setTransitionStartTime(startTimeTrace)
+            .setTransitionEndTime(endTime)
+            .write()
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultFileDescriptorTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultFileDescriptorTest.kt
new file mode 100644
index 0000000..b55ecd7
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultFileDescriptorTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import android.annotation.SuppressLint
+import com.android.server.wm.flicker.AssertionTag
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/** Tests for [ResultFileDescriptor] */
+@SuppressLint("VisibleForTests")
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ResultFileDescriptorTest {
+    @Test
+    fun generateDescriptorFromTrace() {
+        createDescriptorAndValidateFileName(TraceType.SF)
+        createDescriptorAndValidateFileName(TraceType.WM)
+        createDescriptorAndValidateFileName(TraceType.TRANSACTION)
+        createDescriptorAndValidateFileName(TraceType.TRANSACTION)
+        createDescriptorAndValidateFileName(TraceType.SCREEN_RECORDING)
+        createDescriptorAndValidateFileName(TraceType.WM_DUMP)
+        createDescriptorAndValidateFileName(TraceType.SF_DUMP)
+    }
+
+    @Test
+    fun generateDescriptorFromTraceWithTags() {
+        createDescriptorAndValidateFileNameWithTag(TraceType.SF)
+        createDescriptorAndValidateFileNameWithTag(TraceType.WM)
+        createDescriptorAndValidateFileNameWithTag(TraceType.TRANSACTION)
+        createDescriptorAndValidateFileNameWithTag(TraceType.TRANSACTION)
+        createDescriptorAndValidateFileNameWithTag(TraceType.SCREEN_RECORDING)
+        createDescriptorAndValidateFileNameWithTag(TraceType.WM_DUMP)
+        createDescriptorAndValidateFileNameWithTag(TraceType.SF_DUMP)
+    }
+
+    @Test
+    fun parseDescriptorFromFileName() {
+        parseDescriptorAndValidateType(TraceType.SF.fileName, TraceType.SF)
+        parseDescriptorAndValidateType(TraceType.WM.fileName, TraceType.WM)
+        parseDescriptorAndValidateType(TraceType.TRANSACTION.fileName, TraceType.TRANSACTION)
+        parseDescriptorAndValidateType(TraceType.TRANSACTION.fileName, TraceType.TRANSACTION)
+        parseDescriptorAndValidateType(
+            TraceType.SCREEN_RECORDING.fileName,
+            TraceType.SCREEN_RECORDING
+        )
+        parseDescriptorAndValidateType(TraceType.WM_DUMP.fileName, TraceType.WM_DUMP)
+        parseDescriptorAndValidateType(TraceType.SF_DUMP.fileName, TraceType.SF_DUMP)
+    }
+
+    @Test
+    fun parseDescriptorFromFileNameWithTags() {
+        parseDescriptorAndValidateType(buildTaggedName(TraceType.SF), TraceType.SF, TEST_TAG)
+        parseDescriptorAndValidateType(buildTaggedName(TraceType.WM), TraceType.WM, TEST_TAG)
+        parseDescriptorAndValidateType(
+            buildTaggedName(TraceType.TRANSACTION),
+            TraceType.TRANSACTION,
+            TEST_TAG
+        )
+        parseDescriptorAndValidateType(
+            buildTaggedName(TraceType.TRANSACTION),
+            TraceType.TRANSACTION,
+            TEST_TAG
+        )
+        parseDescriptorAndValidateType(
+            buildTaggedName(TraceType.SCREEN_RECORDING),
+            TraceType.SCREEN_RECORDING,
+            TEST_TAG
+        )
+        parseDescriptorAndValidateType(
+            buildTaggedName(TraceType.WM_DUMP),
+            TraceType.WM_DUMP,
+            TEST_TAG
+        )
+        parseDescriptorAndValidateType(
+            buildTaggedName(TraceType.SF_DUMP),
+            TraceType.SF_DUMP,
+            TEST_TAG
+        )
+    }
+
+    private fun buildTaggedName(traceType: TraceType): String =
+        ResultFileDescriptor.newTestInstance(traceType, TEST_TAG).fileNameInArtifact
+
+    private fun parseDescriptorAndValidateType(
+        fileNameInArtifact: String,
+        expectedTraceType: TraceType,
+        expectedTag: String = AssertionTag.ALL
+    ): ResultFileDescriptor {
+        val descriptor = ResultFileDescriptor.fromFileName(fileNameInArtifact)
+        Truth.assertWithMessage("Descriptor type")
+            .that(descriptor.traceType)
+            .isEqualTo(expectedTraceType)
+        Truth.assertWithMessage("Descriptor tag").that(descriptor.tag).isEqualTo(expectedTag)
+        return descriptor
+    }
+
+    private fun createDescriptorAndValidateFileName(traceType: TraceType) {
+        val descriptor = ResultFileDescriptor.newTestInstance(traceType)
+        Truth.assertWithMessage("Result file name")
+            .that(descriptor.fileNameInArtifact)
+            .isEqualTo(traceType.fileName)
+    }
+
+    private fun createDescriptorAndValidateFileNameWithTag(traceType: TraceType) {
+        val tag = "testTag"
+        val descriptor = ResultFileDescriptor.newTestInstance(traceType, TEST_TAG)
+        val subject =
+            Truth.assertWithMessage("Result file name").that(descriptor.fileNameInArtifact)
+        subject.startsWith(tag)
+        subject.endsWith(traceType.fileName)
+    }
+
+    companion object {
+        private const val TEST_TAG = "testTag"
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTest.kt
new file mode 100644
index 0000000..73eab96
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTest.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.DEFAULT_TRACE_CONFIG
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.assertExceptionMessage
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.newTestResultWriter
+import com.android.server.wm.flicker.outputFileName
+import java.io.IOException
+import java.nio.file.Files
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/** Tests for [ResultReader] */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ResultReaderTest {
+    @Before
+    fun setup() {
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+    }
+
+    @Test
+    fun failFileNotFound() {
+        val data = newTestResultWriter().write()
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+        val reader = ResultReader(data, DEFAULT_TRACE_CONFIG)
+        val exception =
+            assertThrows(IOException::class.java) {
+                reader.readTransitionsTrace() ?: error("Should have failed")
+            }
+
+        assertExceptionMessage(exception, "No such file or directory")
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseEventLog.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseEventLog.kt
new file mode 100644
index 0000000..f689d81
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseEventLog.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.DEFAULT_TRACE_CONFIG
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.flicker.newTestResultWriter
+import com.android.server.wm.flicker.outputFileName
+import com.google.common.truth.Truth
+import java.nio.file.Files
+import org.junit.Before
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/** Tests for [ResultReader] parsing event log */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class ResultReaderTestParseEventLog {
+    @Before
+    fun setup() {
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+    }
+
+    @Test
+    fun readEventLog() {
+        val writer = newTestResultWriter().addEventLogResult(TestTraces.TEST_EVENT_LOG)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        val actual = reader.readEventLogTrace()
+        Truth.assertWithMessage("Event log size").that(actual).hasSize(5)
+        Truth.assertWithMessage("Event log")
+            .that(actual)
+            .containsExactlyElementsIn(TestTraces.TEST_EVENT_LOG)
+    }
+
+    @Test
+    fun readEventLogAndSliceTraceByTimestamp() {
+        val writer =
+            newTestResultWriter()
+                .setTransitionStartTime(TestTraces.TIME_5)
+                .setTransitionEndTime(TestTraces.TIME_10)
+                .addEventLogResult(TestTraces.TEST_EVENT_LOG)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        val expected = TestTraces.TEST_EVENT_LOG.drop(1).dropLast(1)
+        val actual = reader.readEventLogTrace()
+        Truth.assertWithMessage("Event log size").that(actual).hasSize(3)
+        Truth.assertWithMessage("Event log").that(actual).containsExactlyElementsIn(expected)
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseLayers.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseLayers.kt
new file mode 100644
index 0000000..ea3748c
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseLayers.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.traces.common.ITrace
+import java.io.File
+
+/** Tests for [ResultReader] parsing [TraceType.SF] */
+class ResultReaderTestParseLayers : BaseResultReaderTestParseTrace() {
+    override val assetFile: File
+        get() = TestTraces.LayerTrace.FILE
+    override val traceName: String
+        get() = "Layers trace"
+    override val startTimeTrace: TraceTime
+        get() = TraceTime(0, TestTraces.LayerTrace.START_TIME, 0)
+    override val endTimeTrace: TraceTime
+        get() = TraceTime(0, TestTraces.LayerTrace.END_TIME, 0)
+    override val validSliceTime: TraceTime
+        get() = TraceTime(0, TestTraces.LayerTrace.SLICE_TIME, 0)
+    override val invalidSliceTime: TraceTime
+        get() = startTimeTrace
+    override val traceType: TraceType
+        get() = TraceType.SF
+    override val expectedSlicedTraceSize: Int
+        get() = 2
+
+    override fun doParse(reader: ResultReader): ITrace<*>? {
+        return reader.readLayersTrace()
+    }
+
+    override fun getTime(traceTime: TraceTime): Long {
+        return traceTime.systemTime
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransactions.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransactions.kt
new file mode 100644
index 0000000..e4574fb
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransactions.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.traces.common.ITrace
+import java.io.File
+
+/** Tests for [ResultReader] parsing [TraceType.TRANSACTION] */
+class ResultReaderTestParseTransactions : BaseResultReaderTestParseTrace() {
+    override val assetFile: File
+        get() = TestTraces.TransactionTrace.FILE
+    override val traceName: String
+        get() = "Transactions trace"
+    override val startTimeTrace: TraceTime
+        get() = TraceTime(0, TestTraces.TransactionTrace.START_TIME, 0)
+    override val endTimeTrace: TraceTime
+        get() = TraceTime(0, TestTraces.TransactionTrace.END_TIME, 0)
+    override val validSliceTime: TraceTime
+        get() = TraceTime(0, TestTraces.TransactionTrace.VALID_SLICE_TIME, 0)
+    override val invalidSliceTime: TraceTime
+        get() = TraceTime(0, TestTraces.TransactionTrace.INVALID_SLICE_TIME, 0)
+    override val traceType: TraceType
+        get() = TraceType.TRANSACTION
+    override val invalidSizeMessage: String
+        get() = "Trimmed transactions trace cannot be empty"
+    override val expectedSlicedTraceSize: Int
+        get() = 2
+
+    override fun doParse(reader: ResultReader): ITrace<*>? {
+        return reader.readTransactionsTrace()
+    }
+
+    override fun getTime(traceTime: TraceTime): Long {
+        return traceTime.systemTime
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransitions.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransitions.kt
new file mode 100644
index 0000000..a611d4b
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseTransitions.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.flicker.readAssetAsFile
+import com.android.server.wm.traces.common.ITrace
+import java.io.File
+
+/** Tests for [ResultReader] parsing [TraceType.TRANSITION] */
+class ResultReaderTestParseTransitions : BaseResultReaderTestParseTrace() {
+    override val assetFile: File
+        get() = TestTraces.TransitionTrace.FILE
+    override val traceName: String
+        get() = "Transitions trace"
+    override val startTimeTrace: TraceTime
+        get() =
+            TraceTime(
+                TestTraces.TransitionTrace.START_TIME,
+                TestTraces.TransactionTrace.START_TIME,
+                0
+            )
+    override val endTimeTrace: TraceTime
+        get() =
+            TraceTime(TestTraces.TransitionTrace.END_TIME, TestTraces.TransactionTrace.END_TIME, 0)
+    override val validSliceTime: TraceTime
+        get() =
+            TraceTime(
+                TestTraces.TransitionTrace.VALID_SLICE_TIME,
+                TestTraces.TransactionTrace.VALID_SLICE_TIME,
+                0
+            )
+    override val invalidSliceTime: TraceTime
+        get() =
+            TraceTime(
+                TestTraces.TransitionTrace.INVALID_SLICE_TIME,
+                TestTraces.TransactionTrace.INVALID_SLICE_TIME,
+                0
+            )
+    override val traceType: TraceType
+        get() = TraceType.TRANSITION
+    override val invalidSizeMessage: String
+        get() = "Transitions trace cannot be empty"
+    override val expectedSlicedTraceSize: Int
+        get() = 1
+
+    override fun writeTrace(writer: ResultWriter): ResultWriter {
+        return super.writeTrace(writer).also {
+            val trace = readAssetAsFile("transactions_trace.winscope")
+            it.addTraceResult(TraceType.TRANSACTION, trace)
+        }
+    }
+
+    override fun doParse(reader: ResultReader): ITrace<*>? {
+        return reader.readTransitionsTrace()
+    }
+
+    override fun getTime(traceTime: TraceTime): Long {
+        return traceTime.elapsedRealtimeNanos
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseWM.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseWM.kt
new file mode 100644
index 0000000..d75d21f
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultReaderTestParseWM.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.traces.common.ITrace
+import java.io.File
+
+/** Tests for [ResultReader] parsing [TraceType.WM] */
+class ResultReaderTestParseWM : BaseResultReaderTestParseTrace() {
+    override val assetFile: File
+        get() = TestTraces.WMTrace.FILE
+    override val traceName: String
+        get() = "WM trace"
+    override val startTimeTrace: TraceTime
+        get() = TraceTime(TestTraces.WMTrace.START_TIME, 0, 0)
+    override val endTimeTrace: TraceTime
+        get() = TraceTime(TestTraces.WMTrace.END_TIME, 0, 0)
+    override val validSliceTime: TraceTime
+        get() = TraceTime(TestTraces.WMTrace.SLICE_TIME, 0, 0)
+    override val invalidSliceTime: TraceTime
+        get() = startTimeTrace
+    override val traceType: TraceType
+        get() = TraceType.WM
+    override val expectedSlicedTraceSize: Int
+        get() = 2
+
+    override fun doParse(reader: ResultReader): ITrace<*>? {
+        return reader.readWmTrace()
+    }
+
+    override fun getTime(traceTime: TraceTime): Long {
+        return traceTime.elapsedRealtimeNanos
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultWriterTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultWriterTest.kt
new file mode 100644
index 0000000..43b07d8
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/ResultWriterTest.kt
@@ -0,0 +1,221 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import android.annotation.SuppressLint
+import com.android.server.wm.flicker.DEFAULT_TRACE_CONFIG
+import com.android.server.wm.flicker.RunStatus
+import com.android.server.wm.flicker.ScenarioBuilder
+import com.android.server.wm.flicker.TestTraces
+import com.android.server.wm.flicker.assertExceptionMessage
+import com.android.server.wm.flicker.assertThrows
+import com.android.server.wm.flicker.newTestResultWriter
+import com.android.server.wm.flicker.outputFileName
+import com.google.common.truth.Truth
+import java.nio.file.Files
+import java.nio.file.Path
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/** Tests for [ResultWriter] */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@SuppressLint("VisibleForTests")
+class ResultWriterTest {
+    @Test
+    fun cannotWriteFileWithoutScenario() {
+        val exception =
+            assertThrows(IllegalArgumentException::class.java) {
+                val writer =
+                    newTestResultWriter().forScenario(ScenarioBuilder().createEmptyScenario())
+                writer.write()
+            }
+
+        assertExceptionMessage(exception, "Scenario shouldn't be empty")
+    }
+
+    @Test
+    fun writesEmptyFile() {
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+        val writer = newTestResultWriter()
+        val result = writer.write()
+        val path = result.artifactPath
+        Truth.assertWithMessage("File exists").that(Files.exists(path)).isTrue()
+        Truth.assertWithMessage("Transition start time")
+            .that(result.transitionTimeRange.start)
+            .isEqualTo(TraceTime.MIN)
+        Truth.assertWithMessage("Transition end time")
+            .that(result.transitionTimeRange.end)
+            .isEqualTo(TraceTime.MAX)
+        Truth.assertWithMessage("Event log").that(result.eventLog).isNull()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(0)
+    }
+
+    @Test
+    fun writesUndefinedFile() {
+        Files.deleteIfExists(outputFileName(RunStatus.UNDEFINED))
+        val writer = newTestResultWriter()
+        val result = writer.write()
+        val path = result.artifactPath
+        validateFileName(path, RunStatus.UNDEFINED)
+    }
+
+    @Test
+    fun writesRunCompleteFile() {
+        Files.deleteIfExists(outputFileName(RunStatus.RUN_EXECUTED))
+        val writer = newTestResultWriter().setRunComplete()
+        val result = writer.write()
+        val path = result.artifactPath
+        validateFileName(path, RunStatus.RUN_EXECUTED)
+    }
+
+    @Test
+    fun writesRunFailureFile() {
+        Files.deleteIfExists(outputFileName(RunStatus.RUN_FAILED))
+        val writer = newTestResultWriter().setRunFailed(EXPECTED_FAILURE)
+        val result = writer.write()
+        val path = result.artifactPath
+        validateFileName(path, RunStatus.RUN_FAILED)
+        Truth.assertWithMessage("Expected assertion")
+            .that(result.executionError)
+            .isEqualTo(EXPECTED_FAILURE)
+    }
+
+    @Test
+    fun writesEventLog() {
+        val writer = newTestResultWriter().addEventLogResult(TestTraces.TEST_EVENT_LOG)
+        val result = writer.write()
+        Truth.assertWithMessage("Event log size").that(result.eventLog).hasSize(5)
+        Truth.assertWithMessage("Event log")
+            .that(result.eventLog)
+            .containsExactlyElementsIn(TestTraces.TEST_EVENT_LOG)
+    }
+
+    @Test
+    fun writesEventLogOutsideTransitionInterval() {
+        val writer =
+            newTestResultWriter()
+                .setTransitionStartTime(TestTraces.TIME_5)
+                .setTransitionEndTime(TestTraces.TIME_10)
+                .addEventLogResult(TestTraces.TEST_EVENT_LOG)
+        val result = writer.write()
+        Truth.assertWithMessage("Event log size").that(result.eventLog).hasSize(5)
+        Truth.assertWithMessage("Event log")
+            .that(result.eventLog)
+            .containsExactlyElementsIn(TestTraces.TEST_EVENT_LOG)
+    }
+
+    @Test
+    fun writesTransitionTime() {
+        val writer =
+            newTestResultWriter()
+                .setTransitionStartTime(TestTraces.TIME_5)
+                .setTransitionEndTime(TestTraces.TIME_10)
+
+        val result = writer.write()
+        Truth.assertWithMessage("Transition start time")
+            .that(result.transitionTimeRange.start)
+            .isEqualTo(TestTraces.TIME_5)
+        Truth.assertWithMessage("Transition end time")
+            .that(result.transitionTimeRange.end)
+            .isEqualTo(TestTraces.TIME_10)
+    }
+
+    @Test
+    fun writeWMTrace() {
+        val writer = newTestResultWriter().addTraceResult(TraceType.WM, TestTraces.WMTrace.FILE)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(1)
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.WM))
+            .isTrue()
+    }
+
+    @Test
+    fun writeLayersTrace() {
+        val writer = newTestResultWriter().addTraceResult(TraceType.SF, TestTraces.LayerTrace.FILE)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(1)
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.SF))
+            .isTrue()
+    }
+
+    @Test
+    fun writeTransactionTrace() {
+        val writer =
+            newTestResultWriter()
+                .addTraceResult(TraceType.TRANSACTION, TestTraces.TransactionTrace.FILE)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(1)
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.TRANSACTION))
+            .isTrue()
+    }
+
+    @Test
+    fun writeTransitionTrace() {
+        val writer =
+            newTestResultWriter()
+                .addTraceResult(TraceType.TRANSITION, TestTraces.TransitionTrace.FILE)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(1)
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.TRANSITION))
+            .isTrue()
+    }
+
+    @Test
+    fun writeAllTraces() {
+        val writer =
+            newTestResultWriter()
+                .addTraceResult(TraceType.WM, TestTraces.WMTrace.FILE)
+                .addTraceResult(TraceType.SF, TestTraces.LayerTrace.FILE)
+                .addTraceResult(TraceType.TRANSITION, TestTraces.TransactionTrace.FILE)
+                .addTraceResult(TraceType.TRANSACTION, TestTraces.TransitionTrace.FILE)
+        val result = writer.write()
+        val reader = ResultReader(result, DEFAULT_TRACE_CONFIG)
+        Truth.assertWithMessage("File count").that(reader.countFiles()).isEqualTo(4)
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.WM))
+            .isTrue()
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.WM))
+            .isTrue()
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.TRANSITION))
+            .isTrue()
+        Truth.assertWithMessage("Has file with type")
+            .that(reader.hasTraceFile(TraceType.TRANSACTION))
+            .isTrue()
+    }
+
+    companion object {
+        private val EXPECTED_FAILURE = IllegalArgumentException("Expected test exception")
+
+        private fun validateFileName(filePath: Path, status: RunStatus) {
+            Truth.assertWithMessage("File name contains run status")
+                .that(filePath.fileName.toString())
+                .contains(status.prefix)
+        }
+    }
+}
diff --git a/libraries/flicker/test/src/com/android/server/wm/flicker/io/TraceTypeTest.kt b/libraries/flicker/test/src/com/android/server/wm/flicker/io/TraceTypeTest.kt
new file mode 100644
index 0000000..8223ecd
--- /dev/null
+++ b/libraries/flicker/test/src/com/android/server/wm/flicker/io/TraceTypeTest.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2022 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.io
+
+import com.android.server.wm.flicker.assertThrows
+import com.google.common.truth.Truth
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runners.MethodSorters
+
+/** Tests for [TraceType] */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class TraceTypeTest {
+    @Test
+    fun canParseTraceTypes() {
+        assertFileName(TraceType.SF)
+        assertFileName(TraceType.WM)
+        assertFileName(TraceType.TRANSACTION)
+        assertFileName(TraceType.TRANSITION)
+        assertFileName(TraceType.SCREEN_RECORDING)
+    }
+
+    @Test
+    fun canParseDumpTypes() {
+        assertFileName(TraceType.SF_DUMP)
+        assertFileName(TraceType.WM_DUMP)
+        assertFileName(
+            TraceType.SF_DUMP,
+            TraceType.fromFileName("prefix${TraceType.SF_DUMP.fileName}")
+        )
+        assertFileName(
+            TraceType.WM_DUMP,
+            TraceType.fromFileName("prefix${TraceType.WM_DUMP.fileName}")
+        )
+    }
+
+    @Test
+    fun failParseInvalidTypes() {
+        assertFailure("prefix${TraceType.SF.fileName}")
+        assertFailure("prefix${TraceType.WM.fileName}")
+        assertFailure("prefix${TraceType.TRANSACTION.fileName}")
+        assertFailure("prefix${TraceType.TRANSITION.fileName}")
+        assertFailure("prefix${TraceType.SCREEN_RECORDING.fileName}")
+        assertFailure("${TraceType.SF_DUMP.fileName}suffix")
+        assertFailure("${TraceType.WM_DUMP.fileName}suffix")
+    }
+
+    private fun assertFailure(fileName: String) {
+        assertThrows(IllegalStateException::class.java) { TraceType.fromFileName(fileName) }
+    }
+
+    private fun assertFileName(
+        type: TraceType,
+        newInstance: TraceType = TraceType.fromFileName(type.fileName)
+    ) {
+        Truth.assertWithMessage("Trace type matches file name").that(newInstance).isEqualTo(type)
+    }
+}