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.uiautomatorhelpers.DeviceHelpers.shell 25 import android.platform.uiautomatorhelpers.FailedEnsureException 26 import android.platform.uiautomatorhelpers.WaitUtils.ensureThat 27 import android.platform.uiautomatorhelpers.WaitUtils.waitFor 28 import android.platform.uiautomatorhelpers.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 122 private fun testLevelOverrideEnabled() = 123 screenRecordOverrideEnabled(SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY) 124 125 /** 126 * This is needed to enable screen recording when a parameter is passed to the instrumentation, 127 * avoid having to recompile the test. 128 */ 129 private fun screenRecordOverrideEnabled(key: String): Boolean { 130 val args = InstrumentationRegistry.getArguments() 131 val override = args.getString(key, "false").toBoolean() 132 if (override) { 133 log("Screen recording enabled due to $key param.") 134 } 135 return override 136 } 137 138 private fun runWithRecording(description: Description?, runnable: () -> Unit) { 139 val outputFile = ArtifactSaver.artifactFile(description, "ScreenRecord", "mp4") 140 log("Executing test with screen recording. Output file=$outputFile") 141 142 if (screenRecordingInProgress()) { 143 Log.w( 144 TAG, 145 "Multiple screen recording in progress (pids=\"$screenrecordPids\"). " + 146 "This might cause performance issues.", 147 ) 148 } 149 // --bugreport adds the timestamp as overlay 150 val screenRecordingFileDescriptor = 151 automation.executeShellCommand("screenrecord --verbose --bugreport $outputFile") 152 // Getting latest PID as there might be multiple screenrecording in progress. 153 val screenRecordPid = waitFor("screenrecording pid") { screenrecordPids.maxOrNull() } 154 var success = false 155 try { 156 runnable() 157 success = true 158 } finally { 159 // Doesn't crash if the file doesn't exist, as we want the command output to be logged. 160 outputFile.tryWaitingForFileToExists() 161 162 if (waitExtraAfterEnd) { 163 // temporary measure to see if b/266186795 is fixed 164 Thread.sleep(3000) 165 } 166 val killOutput = shell("kill -INT $screenRecordPid") 167 168 outputFile.tryWaitingForFileSizeToSettle() 169 170 val screenRecordOutput = screenRecordingFileDescriptor.readAllAndClose() 171 log( 172 """ 173 screenrecord killed (kill command output="$killOutput") 174 screenrecord command output: 175 176 """ 177 .trimIndent() + screenRecordOutput.prependIndent(" ") 178 ) 179 180 val shouldDeleteRecording = !keepTestLevelRecordingOnSuccess && success 181 if (shouldDeleteRecording) { 182 shell("rm $outputFile") 183 log("$outputFile deleted, because test passed") 184 } 185 186 if (outputFile.exists()) { 187 val fileSizeKb = Files.size(outputFile.toPath()) / 1024 188 log("Screen recording captured at: $outputFile. File size: $fileSizeKb KB") 189 } else if (!shouldDeleteRecording) { 190 Log.e(TAG, "File not created successfully. Can't determine size of $outputFile") 191 } 192 } 193 194 if (screenRecordingInProgress()) { 195 Log.w( 196 TAG, 197 "Other screen recordings are in progress after this is done. " + 198 "(pids=\"$screenrecordPids\").", 199 ) 200 } 201 } 202 203 private fun File.tryWaitingForFileToExists() { 204 try { 205 ensureThat("Recording output created") { exists() } 206 } catch (e: FailedEnsureException) { 207 Log.e(TAG, "Recording not created successfully.", e) 208 } 209 } 210 211 private fun File.tryWaitingForFileSizeToSettle() { 212 try { 213 waitForValueToSettle( 214 "Screen recording output size", 215 minimumSettleTime = Duration.ofSeconds(5), 216 ) { 217 length() 218 } 219 } catch (e: FailedEnsureException) { 220 Log.e(TAG, "Recording size didn't settle.", e) 221 } 222 } 223 224 private fun screenRecordingInProgress() = screenrecordPids.isNotEmpty() 225 226 private val screenrecordPids: List<String> 227 get() = shell("pidof screenrecord").split(" ").filter { it != "" } 228 229 /** Interface to indicate that the test should capture screenrecord */ 230 @Retention(RetentionPolicy.RUNTIME) 231 @Target(FUNCTION, CLASS, PROPERTY_GETTER, PROPERTY_SETTER) 232 annotation class ScreenRecord 233 234 private fun log(s: String) = Log.d(TAG, s) 235 236 // Reads all from the stream and closes it. 237 private fun ParcelFileDescriptor.readAllAndClose(): String = 238 AutoCloseInputStream(this).use { inputStream -> 239 inputStream.bufferedReader().use { it.readText() } 240 } 241 242 companion object { 243 private const val TAG = "ScreenRecordRule" 244 private const val SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY = 245 "screen-recording-always-enabled-test-level" 246 private const val SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY = 247 "screen-recording-always-enabled-class-level" 248 } 249 } 250