1 /*
<lambda>null2 * Copyright (C) 2023 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package android.tools.testutils
18
19 import android.content.Context
20 import android.tools.Scenario
21 import android.tools.ScenarioBuilder
22 import android.tools.ScenarioImpl
23 import android.tools.Timestamp
24 import android.tools.Timestamps
25 import android.tools.io.Reader
26 import android.tools.io.ResultArtifactDescriptor
27 import android.tools.io.RunStatus
28 import android.tools.rules.StopAllTracesRule
29 import android.tools.testrules.CacheCleanupRule
30 import android.tools.testrules.InitializeCrossPlatformRule
31 import android.tools.traces.io.ArtifactBuilder
32 import android.tools.traces.io.ResultWriter
33 import android.tools.traces.parsers.perfetto.LayersTraceParser
34 import android.tools.traces.parsers.perfetto.TraceProcessorSession
35 import android.tools.traces.parsers.perfetto.WindowManagerTraceParser
36 import android.tools.traces.parsers.wm.LegacyWindowManagerTraceParser
37 import android.tools.traces.parsers.wm.WindowManagerDumpParser
38 import android.tools.traces.wm.ConfigurationContainerImpl
39 import android.tools.traces.wm.RootWindowContainer
40 import android.tools.traces.wm.WindowContainerImpl
41 import android.tools.traces.wm.WindowManagerTrace
42 import androidx.test.platform.app.InstrumentationRegistry
43 import androidx.test.uiautomator.UiDevice
44 import com.google.common.io.ByteStreams
45 import com.google.common.truth.Truth
46 import java.io.File
47 import java.io.FileInputStream
48 import java.io.IOException
49 import java.util.zip.ZipInputStream
50 import kotlin.io.path.createTempDirectory
51 import kotlin.io.path.name
52 import org.junit.rules.RuleChain
53
54 /** Factory function to create cleanup test rule */
55 fun CleanFlickerEnvironmentRule(): RuleChain =
56 RuleChain.outerRule(InitializeCrossPlatformRule())
57 .around(StopAllTracesRule())
58 .around(CacheCleanupRule())
59
60 val TEST_SCENARIO = ScenarioBuilder().forClass("test").build() as ScenarioImpl
61
62 const val SYSTEMUI_PACKAGE = "com.android.systemui"
63
64 /**
65 * Runs `r` and asserts that an exception with type `expectedThrowable` is thrown.
66 *
67 * @param r the [Runnable] which is run and expected to throw.
68 * @throws AssertionError if `r` does not throw, or throws a runnable that is not an instance of
69 * `expectedThrowable`.
70 */
71 inline fun <reified ExceptionType> assertThrows(r: () -> Unit): ExceptionType {
72 try {
73 r()
74 } catch (t: Throwable) {
75 when {
76 ExceptionType::class.java.isInstance(t) -> return t as ExceptionType
77 t is Exception ->
78 throw AssertionError(
79 "Expected ${ExceptionType::class.java}, but got '${t.javaClass}'",
80 t,
81 )
82 // Re-throw Errors and other non-Exception throwables.
83 else -> throw t
84 }
85 }
86 error("Expected exception ${ExceptionType::class.java}, but nothing was thrown")
87 }
88
assertFailnull89 fun assertFail(expectedMessage: String, predicate: () -> Any) {
90 val error = assertThrows<AssertionError> { predicate() }
91 Truth.assertThat(error).hasMessageThat().contains(expectedMessage)
92 }
93
assertThatErrorContainsDebugInfonull94 fun assertThatErrorContainsDebugInfo(error: Throwable) {
95 Truth.assertThat(error).hasMessageThat().contains("What?")
96 Truth.assertThat(error).hasMessageThat().contains("Where?")
97 }
98
99 /**
100 * Method to check if the [archivePath] contains trace files from one of the expected files list as
101 * given in the [possibleExpectedFiles].
102 */
assertArchiveContainsFilesnull103 fun assertArchiveContainsFiles(archivePath: File, possibleExpectedFiles: List<List<String>>) {
104 Truth.assertWithMessage("Expected trace archive `$archivePath` to exist")
105 .that(archivePath.exists())
106 .isTrue()
107
108 val actualFiles = getActualTraceFilesFromArchive(archivePath)
109 var isActualTraceAsExpected = false
110
111 for (expectedFiles: List<String> in possibleExpectedFiles) {
112 if (actualFiles.equalsIgnoreOrder(expectedFiles)) {
113 isActualTraceAsExpected = true
114 break
115 }
116 }
117
118 val messageActualFiles = "[${actualFiles.joinToString(", ")}]"
119 val messageExpectedFiles =
120 "${possibleExpectedFiles.map { "[${it.joinToString(", ")}]"}.joinToString(", ")}"
121 Truth.assertWithMessage(
122 "Trace archive doesn't contain expected traces." +
123 "\n Actual: $messageActualFiles" +
124 "\n Expected: $messageExpectedFiles"
125 )
126 .that(isActualTraceAsExpected)
127 .isTrue()
128 }
129
getActualTraceFilesFromArchivenull130 fun getActualTraceFilesFromArchive(archivePath: File): List<String> {
131 val archiveStream = ZipInputStream(FileInputStream(archivePath))
132 return generateSequence { archiveStream.nextEntry }.map { it.name }.toList()
133 }
134
equalsIgnoreOrdernull135 fun <T> List<T>.equalsIgnoreOrder(other: List<T>) = this.toSet() == other.toSet()
136
137 fun getWmTraceReaderFromAsset(
138 relativePathWithoutExtension: String,
139 from: Long = Long.MIN_VALUE,
140 to: Long = Long.MAX_VALUE,
141 addInitialEntry: Boolean = true,
142 legacyTrace: Boolean = false,
143 ): Reader {
144 fun parseTrace(): WindowManagerTrace {
145 val traceData = readAsset("$relativePathWithoutExtension.perfetto-trace")
146 return TraceProcessorSession.loadPerfettoTrace(traceData) { session ->
147 WindowManagerTraceParser()
148 .parse(
149 session,
150 Timestamps.from(elapsedNanos = from),
151 Timestamps.from(elapsedNanos = to),
152 )
153 }
154 }
155
156 fun parseLegacyTrace(): WindowManagerTrace {
157 val traceData =
158 runCatching { readAsset("$relativePathWithoutExtension.pb") }.getOrNull()
159 ?: runCatching { readAsset("$relativePathWithoutExtension.winscope") }.getOrNull()
160 ?: error("Can't find legacy trace file $relativePathWithoutExtension")
161
162 return LegacyWindowManagerTraceParser(legacyTrace)
163 .parse(
164 traceData,
165 Timestamps.from(elapsedNanos = from),
166 Timestamps.from(elapsedNanos = to),
167 addInitialEntry,
168 clearCache = false,
169 )
170 }
171
172 val trace =
173 if (android.tracing.Flags.perfettoWmTracing()) {
174 parseTrace()
175 } else {
176 parseLegacyTrace()
177 }
178
179 return ParsedTracesReader(
180 artifact = TestArtifact(relativePathWithoutExtension),
181 wmTrace = trace,
182 )
183 }
184
getWmDumpReaderFromAssetnull185 fun getWmDumpReaderFromAsset(relativePathWithoutExtension: String): Reader {
186 fun parseDump(): WindowManagerTrace {
187 val traceData = readAsset("$relativePathWithoutExtension.perfetto-trace")
188 return TraceProcessorSession.loadPerfettoTrace(traceData) { session ->
189 WindowManagerTraceParser().parse(session)
190 }
191 }
192
193 fun parseLegacyDump(): WindowManagerTrace {
194 val traceData =
195 runCatching { readAsset("$relativePathWithoutExtension.pb") }.getOrNull()
196 ?: runCatching { readAsset("$relativePathWithoutExtension.winscope") }.getOrNull()
197 ?: error("Can't find legacy trace file $relativePathWithoutExtension")
198 return WindowManagerDumpParser().parse(traceData, clearCache = false)
199 }
200
201 val wmTrace =
202 if (android.tracing.Flags.perfettoWmDump()) {
203 parseDump()
204 } else {
205 parseLegacyDump()
206 }
207 return ParsedTracesReader(
208 artifact = TestArtifact(relativePathWithoutExtension),
209 wmTrace = wmTrace,
210 )
211 }
212
getLayerTraceReaderFromAssetnull213 fun getLayerTraceReaderFromAsset(
214 relativePath: String,
215 ignoreOrphanLayers: Boolean = true,
216 from: Timestamp = Timestamps.min(),
217 to: Timestamp = Timestamps.max(),
218 ): Reader {
219 val layersTrace =
220 TraceProcessorSession.loadPerfettoTrace(readAsset(relativePath)) { session ->
221 LayersTraceParser(
222 ignoreLayersStackMatchNoDisplay = false,
223 ignoreLayersInVirtualDisplay = false,
224 ) {
225 ignoreOrphanLayers
226 }
227 .parse(session, from, to)
228 }
229 return ParsedTracesReader(artifact = TestArtifact(relativePath), layersTrace = layersTrace)
230 }
231
232 @Throws(Exception::class)
readAssetnull233 fun readAsset(relativePath: String): ByteArray {
234 val context: Context = InstrumentationRegistry.getInstrumentation().context
235 val inputStream = context.resources.assets.open("testdata/$relativePath")
236 return ByteStreams.toByteArray(inputStream)
237 }
238
239 @Throws(IOException::class)
readAssetAsFilenull240 fun readAssetAsFile(relativePath: String): File {
241 val context: Context = InstrumentationRegistry.getInstrumentation().context
242 return File(context.cacheDir, relativePath).also {
243 if (!it.exists()) {
244 it.outputStream().use { cache ->
245 context.assets.open("testdata/$relativePath").use { inputStream ->
246 inputStream.copyTo(cache)
247 }
248 }
249 }
250 }
251 }
252
newTestResultWriternull253 fun newTestResultWriter(
254 scenario: Scenario = ScenarioBuilder().forClass(kotlin.io.path.createTempFile().name).build()
255 ) =
256 ResultWriter()
257 .forScenario(scenario)
258 .withOutputDir(createTempDirectory().toFile())
259 .setRunComplete()
260
261 fun assertExceptionMessage(error: Throwable?, expectedValue: String) {
262 Truth.assertWithMessage("Expected exception")
263 .that(error)
264 .hasMessageThat()
265 .contains(expectedValue)
266 }
267
outputFileNamenull268 fun outputFileName(status: RunStatus) =
269 File("/sdcard/flicker/${status.prefix}__test_ROTATION_0_GESTURAL_NAV.zip")
270
271 fun createDefaultArtifactBuilder(
272 status: RunStatus,
273 outputDir: File = createTempDirectory().toFile(),
274 files: Map<ResultArtifactDescriptor, File> = emptyMap(),
275 ) =
276 ArtifactBuilder()
277 .withScenario(TEST_SCENARIO)
278 .withOutputDir(outputDir)
279 .withStatus(status)
280 .withFiles(files)
281
282 fun getLauncherPackageName() =
283 UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()).launcherPackageName
284
285 fun getSystemUiUidName(): String {
286 val packageManager = InstrumentationRegistry.getInstrumentation().context.getPackageManager()
287 val uid = packageManager.getApplicationInfo(SYSTEMUI_PACKAGE, 0).uid
288 return requireNotNull(packageManager.getNameForUid(uid))
289 }
290
newEmptyRootContainernull291 fun newEmptyRootContainer(orientation: Int = 0, layerId: Int = 0) =
292 RootWindowContainer(
293 WindowContainerImpl(
294 title = "root",
295 token = "",
296 orientation = orientation,
297 layerId = layerId,
298 _isVisible = true,
299 _children = emptyList(),
300 configurationContainer = ConfigurationContainerImpl.EMPTY,
301 computedZ = 0,
302 )
303 )
304