1 /* 2 * Copyright (C) 2021 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 com.android.server.wm.flicker 18 19 import android.util.Log 20 import com.android.server.wm.flicker.FlickerRunResult.Companion.RunStatus 21 import com.android.server.wm.flicker.monitor.IFileGeneratingMonitor 22 import com.android.server.wm.flicker.monitor.ITransitionMonitor 23 import com.android.server.wm.traces.common.ConditionList 24 import com.android.server.wm.traces.common.WindowManagerConditionsFactory 25 import com.android.server.wm.traces.parser.DeviceDumpParser 26 import com.android.server.wm.traces.parser.getCurrentState 27 import java.io.IOException 28 import java.nio.file.Files 29 30 /** 31 * Runner to execute the transitions of a flicker test 32 * 33 * The commands are executed in the following order: 34 * 1) [Flicker.testSetup] 35 * 2) [Flicker.runSetup 36 * 3) Start monitors 37 * 4) [Flicker.transitions] 38 * 5) Stop monitors 39 * 6) [Flicker.runTeardown] 40 * 7) [Flicker.testTeardown] 41 * 42 * If the tests were already executed, reuse the previous results 43 * 44 */ 45 open class TransitionRunner { 46 /** 47 * Iteration identifier during test run 48 */ 49 private var iteration = 0 50 private val tags = mutableSetOf<String>() 51 private var tagsResults = mutableListOf<FlickerRunResult>() 52 53 /** 54 * Executes the setup, transitions and teardown defined in [flicker] 55 * 56 * @param flicker test specification 57 * @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0 58 */ executenull59 open fun execute(flicker: Flicker): FlickerResult { 60 check(flicker) 61 return run(flicker) 62 } 63 64 /** 65 * Validate the [flicker] test specification before executing the transitions 66 * 67 * @param flicker test specification 68 * @throws IllegalArgumentException If the transitions are empty or repetitions is set to 0 69 */ checknull70 protected fun check(flicker: Flicker) { 71 require(flicker.transitions.isNotEmpty()) { 72 "A flicker test must include transitions to run" } 73 require(flicker.repetitions > 0) { 74 "Number of repetitions must be greater than 0" } 75 } 76 cleanUpnull77 open fun cleanUp() { 78 tags.clear() 79 tagsResults.clear() 80 } 81 82 /** 83 * Runs the actual setup, transitions and teardown defined in [flicker] 84 * 85 * @param flicker test specification 86 */ runnull87 internal open fun run(flicker: Flicker): FlickerResult { 88 val runs = mutableListOf<FlickerRunResult>() 89 val executionErrors = mutableListOf<Throwable>() 90 safeExecution(flicker, runs, executionErrors) { 91 runTestSetup(flicker) 92 93 for (x in 0 until flicker.repetitions) { 94 iteration = x 95 runTransitionSetup(flicker) 96 runTransition(flicker) 97 runTransitionTeardown(flicker) 98 processRunTraces(flicker, runs, RunStatus.ASSERTION_SUCCESS) 99 } 100 101 runTestTeardown(flicker) 102 } 103 104 runs.addAll(tagsResults) 105 val result = FlickerResult(runs.toList(), tags.toSet(), executionErrors) 106 cleanUp() 107 return result 108 } 109 safeExecutionnull110 private fun safeExecution( 111 flicker: Flicker, 112 runs: MutableList<FlickerRunResult>, 113 executionErrors: MutableList<Throwable>, 114 execution: () -> Unit 115 ) { 116 try { 117 execution() 118 } catch (e: TestSetupFailure) { 119 // If we failure on the test setup we can't run any of the transitions 120 executionErrors.add(e) 121 } catch (e: TransitionSetupFailure) { 122 // If we fail on the transition run setup then we don't want to run any further 123 // transitions nor save any results for this run. We simply want to run the test 124 // teardown. 125 executionErrors.add(e) 126 safeExecution(flicker, runs, executionErrors) { 127 runTestTeardown(flicker) 128 } 129 } catch (e: TransitionExecutionFailure) { 130 // If a transition fails to run we don't want to run the following iterations as the 131 // device is likely in an unexpected state which would lead to further errors. We simply 132 // want to run the test teardown 133 executionErrors.add(e) 134 flicker.traceMonitors.forEach { it.tryStop() } 135 safeExecution(flicker, runs, executionErrors) { 136 processRunTraces(flicker, runs, RunStatus.RUN_FAILED) 137 runTestTeardown(flicker) 138 } 139 } catch (e: TransitionTeardownFailure) { 140 // If a transition teardown fails to run we don't want to run the following iterations 141 // as the device is likely in an unexpected state which would lead to further errors. 142 // But, we do want to run the test teardown. 143 executionErrors.add(e) 144 flicker.traceMonitors.forEach { it.tryStop() } 145 safeExecution(flicker, runs, executionErrors) { 146 processRunTraces(flicker, runs, RunStatus.RUN_FAILED) 147 runTestTeardown(flicker) 148 } 149 } catch (e: TraceProcessingFailure) { 150 // If we fail to process the run traces we still want to run the teardowns and report 151 // the execution error. 152 executionErrors.add(e) 153 safeExecution(flicker, runs, executionErrors) { 154 runTransitionTeardown(flicker) 155 runTestTeardown(flicker) 156 } 157 } catch (e: TestTeardownFailure) { 158 // If we fail in the execution of the test teardown there is nothing else to do apart 159 // from reporting the execution error. 160 executionErrors.add(e) 161 for (run in runs) { 162 run.setRunFailed() 163 } 164 } 165 } 166 167 /** 168 * Parses the traces collected by the monitors to generate FlickerRunResults containing the 169 * parsed trace and information about the status of the run. 170 * The run results are added to the runs list which is then used to run Flicker assertions on. 171 */ 172 @Throws(TraceProcessingFailure::class) processRunTracesnull173 private fun processRunTraces( 174 flicker: Flicker, 175 runs: MutableList<FlickerRunResult>, 176 status: RunStatus 177 ) { 178 try { 179 val runResults = buildRunResults(flicker, iteration, status) 180 runs.addAll(runResults) 181 } catch (e: Throwable) { 182 // We have failed to add the results to the runs, so we can effectively consider these 183 // results as "lost" as they won't be used from now forth. So we can safely rename 184 // to file to indicate the failure and make it easier to find in the archives. 185 flicker.traceMonitors.forEach { 186 // All monitors that generate files we want to keep in the archives should implement 187 // IFileGeneratingMonitor 188 if (it is IFileGeneratingMonitor) { 189 Utils.addStatusToFileName(it.outputFile, RunStatus.PARSING_FAILURE) 190 } 191 } 192 throw TraceProcessingFailure(e) 193 } 194 195 // Update the status of all the tags created in this iteration and add them to runs 196 for (result in tagsResults) { 197 result.status = status 198 runs.add(result) 199 } 200 tagsResults.clear() 201 } 202 203 @Throws(TestSetupFailure::class) runTestSetupnull204 private fun runTestSetup(flicker: Flicker) { 205 try { 206 flicker.testSetup.forEach { it.invoke(flicker) } 207 } catch (e: Throwable) { 208 throw TestSetupFailure(e) 209 } 210 } 211 212 @Throws(TestTeardownFailure::class) runTestTeardownnull213 private fun runTestTeardown(flicker: Flicker) { 214 try { 215 flicker.testTeardown.forEach { it.invoke(flicker) } 216 } catch (e: Throwable) { 217 throw TestTeardownFailure(e) 218 } 219 } 220 221 @Throws(TransitionSetupFailure::class) runTransitionSetupnull222 private fun runTransitionSetup(flicker: Flicker) { 223 try { 224 flicker.runSetup.forEach { it.invoke(flicker) } 225 flicker.wmHelper.waitFor(UI_STABLE_CONDITIONS) 226 } catch (e: Throwable) { 227 throw TransitionSetupFailure(e) 228 } 229 } 230 231 @Throws(TransitionExecutionFailure::class) runTransitionnull232 private fun runTransition(flicker: Flicker) { 233 try { 234 flicker.traceMonitors.forEach { it.start() } 235 flicker.transitions.forEach { it.invoke(flicker) } 236 } catch (e: Throwable) { 237 throw TransitionExecutionFailure(e) 238 } 239 } 240 241 @Throws(TransitionTeardownFailure::class) runTransitionTeardownnull242 private fun runTransitionTeardown(flicker: Flicker) { 243 try { 244 flicker.wmHelper.waitFor(UI_STABLE_CONDITIONS) 245 flicker.traceMonitors.forEach { it.tryStop() } 246 flicker.runTeardown.forEach { it.invoke(flicker) } 247 } catch (e: Throwable) { 248 throw TransitionTeardownFailure(e) 249 } 250 } 251 buildRunResultsnull252 private fun buildRunResults( 253 flicker: Flicker, 254 iteration: Int, 255 status: RunStatus 256 ): List<FlickerRunResult> { 257 val resultBuilder = FlickerRunResult.Builder() 258 flicker.traceMonitors.forEach { 259 resultBuilder.setResultFrom(it) 260 } 261 262 return resultBuilder.buildAll(flicker.testName, iteration, status) 263 } 264 ITransitionMonitornull265 private fun ITransitionMonitor.tryStop() { 266 this.run { 267 try { 268 stop() 269 } catch (e: Exception) { 270 Log.e(FLICKER_TAG, "Unable to stop $this", e) 271 } 272 } 273 } 274 getTaggedFilePathnull275 private fun getTaggedFilePath(flicker: Flicker, tag: String, file: String) = 276 "${flicker.testName}_${iteration}_${tag}_$file" 277 278 /** 279 * Captures a snapshot of the device state and associates it with a new tag. 280 * 281 * This tag can be used to make assertions about the state of the device when the 282 * snapshot is collected. 283 * 284 * [tag] is used as part of the trace file name, thus, only valid letters and digits 285 * can be used 286 * 287 * @param flicker test specification 288 * @throws IllegalArgumentException If [tag] contains invalid characters 289 */ 290 open fun createTag(flicker: Flicker, tag: String) { 291 require(!tag.contains(" ")) { 292 "The test tag $tag can not contain spaces since it is a part of the file name" 293 } 294 tags.add(tag) 295 296 val deviceStateBytes = getCurrentState(flicker.instrumentation.uiAutomation) 297 val deviceState = DeviceDumpParser.fromDump(deviceStateBytes.first, deviceStateBytes.second) 298 try { 299 val wmTraceFile = flicker.outputDir.resolve( 300 getTaggedFilePath(flicker, tag, "wm_trace")) 301 Files.write(wmTraceFile, deviceStateBytes.first) 302 303 val layersTraceFile = flicker.outputDir.resolve( 304 getTaggedFilePath(flicker, tag, "layers_trace")) 305 Files.write(layersTraceFile, deviceStateBytes.second) 306 307 val builder = FlickerRunResult.Builder() 308 val result = builder.buildStateResult( 309 tag, 310 deviceState.wmState?.asTrace(), 311 deviceState.layerState?.asTrace(), 312 wmTraceFile, 313 layersTraceFile, 314 flicker.testName, 315 iteration, 316 // Undefined until it is updated in processRunTraces 317 RunStatus.UNDEFINED 318 ) 319 tagsResults.add(result) 320 } catch (e: IOException) { 321 throw RuntimeException("Unable to create trace file: ${e.message}", e) 322 } 323 } 324 325 companion object { 326 /** 327 * Conditions that determine when the UI is in a stable stable and no windows or layers are 328 * animating or changing state. 329 */ 330 private val UI_STABLE_CONDITIONS = ConditionList(listOf( 331 WindowManagerConditionsFactory.isWMStateComplete(), 332 WindowManagerConditionsFactory.hasLayersAnimating().negate() 333 )) 334 335 class TestSetupFailure(val e: Throwable) : Throwable(e) 336 class TransitionSetupFailure(val e: Throwable) : Throwable(e) 337 class TransitionExecutionFailure(val e: Throwable) : Throwable(e) 338 class TraceProcessingFailure(val e: Throwable) : Throwable(e) 339 class TransitionTeardownFailure(val e: Throwable) : Throwable(e) 340 class TestTeardownFailure(val e: Throwable) : Throwable(e) 341 } 342 } 343