1 /* <lambda>null2 * 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 package android.platform.test.rule 17 18 import android.app.UiAutomation 19 import android.os.ParcelFileDescriptor 20 import android.os.ParcelFileDescriptor.AutoCloseInputStream 21 import android.platform.test.rule.ScreenRecordRule.Companion.SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY 22 import android.platform.test.rule.ScreenRecordRule.Companion.SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY 23 import android.platform.test.rule.ScreenRecordRule.ScreenRecord 24 import android.platform.uiautomator_helpers.DeviceHelpers.shell 25 import android.platform.uiautomator_helpers.FailedEnsureException 26 import android.platform.uiautomator_helpers.WaitUtils.ensureThat 27 import android.platform.uiautomator_helpers.WaitUtils.waitFor 28 import android.platform.uiautomator_helpers.WaitUtils.waitForValueToSettle 29 import android.util.Log 30 import androidx.test.InstrumentationRegistry.getInstrumentation 31 import androidx.test.platform.app.InstrumentationRegistry 32 import java.io.File 33 import java.lang.annotation.Retention 34 import java.lang.annotation.RetentionPolicy 35 import java.nio.file.Files 36 import java.time.Duration 37 import kotlin.annotation.AnnotationTarget.CLASS 38 import kotlin.annotation.AnnotationTarget.FUNCTION 39 import kotlin.annotation.AnnotationTarget.PROPERTY_GETTER 40 import kotlin.annotation.AnnotationTarget.PROPERTY_SETTER 41 import org.junit.rules.TestRule 42 import org.junit.runner.Description 43 import org.junit.runners.model.Statement 44 45 /** 46 * Rule which captures a screen record for a test. 47 * 48 * After adding this rule to the test class either: 49 * - apply the annotation [ScreenRecord] to individual tests or classes 50 * - pass the [SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY] or 51 * [SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY] instrumentation argument. e.g. `adb shell am 52 * instrument -w -e <key> true <test>`). 53 * 54 * Important note: After the file is created, in order to see it in artifacts, it needs to be pulled 55 * from the device. Typically, this is done using `FilePullerLogCollector` at module level, changing 56 * AndroidTest.xml. 57 * 58 * This rule requires that the device is rooted to be able to write data to the test app's data 59 * directory. You can use `RootTargetPreparer` in your test's AndroidTest.xml to ensure root is 60 * available. 61 * 62 * Note that when this rule is set as: 63 * - `@ClassRule`, it will check only if the class has the [ScreenRecord] annotation, and will 64 * record one video for the entire test class 65 * - `@Rule`, it will check each single test method, and record one video for each test annotated. 66 * If the class is annotated, then it will record a separate video for every test, regardless of 67 * if the test is annotated. 68 * 69 * @param keepTestLevelRecordingOnSuccess: Keep a recording of a single test, if the test passes. If 70 * false, the recording will be deleted. Does not apply to whole-class recordings 71 * @param waitExtraAfterEnd: Sometimes, recordings are cut off by ~3 seconds (b/266186795). If true, 72 * then all recordings will wait 3 seconds after the test ends before stopping recording 73 */ 74 class ScreenRecordRule 75 @JvmOverloads 76 constructor( 77 private val keepTestLevelRecordingOnSuccess: Boolean = true, 78 private val waitExtraAfterEnd: Boolean = true, 79 ) : TestRule { 80 81 private val automation: UiAutomation = getInstrumentation().uiAutomation 82 83 override fun apply(base: Statement, description: Description): Statement { 84 if (!shouldRecordScreen(description)) { 85 log("Not recording the screen.") 86 return base 87 } 88 return object : Statement() { 89 override fun evaluate() { 90 runWithRecording(description) { base.evaluate() } 91 } 92 } 93 } 94 95 private fun shouldRecordScreen(description: Description): Boolean { 96 if (!isRooted()) { 97 Log.w(TAG, "Device is not rooted. Skipping screen recording.") 98 return false 99 } 100 val screenRecordBinaryAvailable = File("/system/bin/screenrecord").exists() 101 log("screenRecordBinaryAvailable: $screenRecordBinaryAvailable") 102 if (!screenRecordBinaryAvailable) { 103 return false 104 } 105 return if (description.isTest) { 106 description.getAnnotation(ScreenRecord::class.java) != null || 107 description.testClass.hasAnnotation(ScreenRecord::class.java) || 108 testLevelOverrideEnabled() 109 } else { // class level annotation is set 110 description.testClass.hasAnnotation(ScreenRecord::class.java) || 111 classLevelOverrideEnabled() 112 } 113 } 114 115 private fun isRooted(): Boolean { 116 return "root".equals(shell("whoami").trim()) 117 } 118 119 private fun classLevelOverrideEnabled() = 120 screenRecordOverrideEnabled(SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY) 121 private fun testLevelOverrideEnabled() = 122 screenRecordOverrideEnabled(SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY) 123 /** 124 * This is needed to enable screen recording when a parameter is passed to the instrumentation, 125 * avoid having to recompile the test. 126 */ 127 private fun screenRecordOverrideEnabled(key: String): Boolean { 128 val args = InstrumentationRegistry.getArguments() 129 val override = args.getString(key, "false").toBoolean() 130 if (override) { 131 log("Screen recording enabled due to $key param.") 132 } 133 return override 134 } 135 136 private fun runWithRecording(description: Description?, runnable: () -> Unit) { 137 val outputFile = ArtifactSaver.artifactFile(description, "ScreenRecord", "mp4") 138 log("Executing test with screen recording. Output file=$outputFile") 139 140 if (screenRecordingInProgress()) { 141 Log.w( 142 TAG, 143 "Multiple screen recording in progress (pids=\"$screenrecordPids\"). " + 144 "This might cause performance issues." 145 ) 146 } 147 // --bugreport adds the timestamp as overlay 148 val screenRecordingFileDescriptor = 149 automation.executeShellCommand("screenrecord --verbose --bugreport $outputFile") 150 // Getting latest PID as there might be multiple screenrecording in progress. 151 val screenRecordPid = waitFor("screenrecording pid") { screenrecordPids.maxOrNull() } 152 var success = false 153 try { 154 runnable() 155 success = true 156 } finally { 157 // Doesn't crash if the file doesn't exist, as we want the command output to be logged. 158 outputFile.tryWaitingForFileToExists() 159 160 if (waitExtraAfterEnd) { 161 // temporary measure to see if b/266186795 is fixed 162 Thread.sleep(3000) 163 } 164 val killOutput = shell("kill -INT $screenRecordPid") 165 166 outputFile.tryWaitingForFileSizeToSettle() 167 168 val screenRecordOutput = screenRecordingFileDescriptor.readAllAndClose() 169 log( 170 """ 171 screenrecord killed (kill command output="$killOutput") 172 screenrecord command output: 173 174 """ 175 .trimIndent() + screenRecordOutput.prependIndent(" ") 176 ) 177 178 val shouldDeleteRecording = !keepTestLevelRecordingOnSuccess && success 179 if (shouldDeleteRecording) { 180 shell("rm $outputFile") 181 log("$outputFile deleted, because test passed") 182 } 183 184 if (outputFile.exists()) { 185 val fileSizeKb = Files.size(outputFile.toPath()) / 1024 186 log("Screen recording captured at: $outputFile. File size: $fileSizeKb KB") 187 } else if (!shouldDeleteRecording) { 188 Log.e(TAG, "File not created successfully. Can't determine size of $outputFile") 189 } 190 } 191 192 if (screenRecordingInProgress()) { 193 Log.w( 194 TAG, 195 "Other screen recordings are in progress after this is done. " + 196 "(pids=\"$screenrecordPids\")." 197 ) 198 } 199 } 200 201 private fun File.tryWaitingForFileToExists() { 202 try { 203 ensureThat("Recording output created") { exists() } 204 } catch (e: FailedEnsureException) { 205 Log.e(TAG, "Recording not created successfully.", e) 206 } 207 } 208 209 private fun File.tryWaitingForFileSizeToSettle() { 210 try { 211 waitForValueToSettle( 212 "Screen recording output size", 213 minimumSettleTime = Duration.ofSeconds(5) 214 ) { 215 length() 216 } 217 } catch (e: FailedEnsureException) { 218 Log.e(TAG, "Recording size didn't settle.", e) 219 } 220 } 221 222 private fun screenRecordingInProgress() = screenrecordPids.isNotEmpty() 223 224 private val screenrecordPids: List<String> 225 get() = shell("pidof screenrecord").split(" ").filter { it != "" } 226 227 /** Interface to indicate that the test should capture screenrecord */ 228 @Retention(RetentionPolicy.RUNTIME) 229 @Target(FUNCTION, CLASS, PROPERTY_GETTER, PROPERTY_SETTER) 230 annotation class ScreenRecord 231 232 private fun log(s: String) = Log.d(TAG, s) 233 234 // Reads all from the stream and closes it. 235 private fun ParcelFileDescriptor.readAllAndClose(): String = 236 AutoCloseInputStream(this).use { inputStream -> 237 inputStream.bufferedReader().use { it.readText() } 238 } 239 240 companion object { 241 private const val TAG = "ScreenRecordRule" 242 private const val SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY = 243 "screen-recording-always-enabled-test-level" 244 private const val SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY = 245 "screen-recording-always-enabled-class-level" 246 } 247 } 248