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)
+ }
+}