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.DeviceHelpers.uiDevice 26 import android.platform.uiautomator_helpers.FailedEnsureException 27 import android.platform.uiautomator_helpers.WaitUtils.ensureThat 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 * Note that when this rule is set as: 59 * - `@ClassRule`, it will check only if the class has the [ScreenRecord] annotation. 60 * - `@Rule`, it will check each single test method. 61 */ 62 class ScreenRecordRule : TestRule { 63 64 private val automation: UiAutomation = getInstrumentation().uiAutomation 65 66 override fun apply(base: Statement, description: Description): Statement { 67 if (!shouldRecordScreen(description)) { 68 log("Not recording the screen.") 69 return base 70 } 71 return object : Statement() { 72 override fun evaluate() { 73 runWithRecording(description) { base.evaluate() } 74 } 75 } 76 } 77 78 private fun shouldRecordScreen(description: Description): Boolean { 79 return if (description.isTest) { 80 val screenRecordBinaryAvailable = File("/system/bin/screenrecord").exists() 81 log("screenRecordBinaryAvailable: $screenRecordBinaryAvailable") 82 screenRecordBinaryAvailable && 83 (description.getAnnotation(ScreenRecord::class.java) != null || 84 testLevelOverrideEnabled()) 85 } else { // class level 86 description.testClass.hasAnnotation(ScreenRecord::class.java) || 87 classLevelOverrideEnabled() 88 } 89 } 90 91 private fun classLevelOverrideEnabled() = 92 screenRecordOverrideEnabled(SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY) 93 private fun testLevelOverrideEnabled() = 94 screenRecordOverrideEnabled(SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY) 95 /** 96 * This is needed to enable screen recording when a parameter is passed to the instrumentation, 97 * avoid having to recompile the test. 98 */ 99 private fun screenRecordOverrideEnabled(key: String): Boolean { 100 val args = InstrumentationRegistry.getArguments() 101 val override = args.getString(key, "false").toBoolean() 102 if (override) { 103 log("Screen recording enabled due to $key param.") 104 } 105 return override 106 } 107 108 private fun runWithRecording(description: Description?, runnable: () -> Unit) { 109 val outputFile = ArtifactSaver.artifactFile(description, "ScreenRecord", "mp4") 110 log("Executing test with screen recording. Output file=$outputFile") 111 112 if (screenRecordingInProgress()) { 113 Log.w( 114 TAG, 115 "Multiple screen recording in progress (pids=\"$screenrecordPids\"). " + 116 "This might cause performance issues." 117 ) 118 } 119 // --bugreport adds the timestamp as overlay 120 val screenRecordingFileDescriptor = 121 automation.executeShellCommand("screenrecord --verbose --bugreport $outputFile") 122 // Getting latest PID as there might be multiple screenrecording in progress. 123 val screenRecordPid = screenrecordPids.max() 124 try { 125 runnable() 126 } finally { 127 // Doesn't crash if the file doesn't exist, as we want the command output to be logged. 128 outputFile.tryWaitingForFileToExists() 129 130 // temporary measure to see if b/266186795 is fixed 131 Thread.sleep(3000) 132 val killOutput = uiDevice.shell("kill -INT $screenRecordPid") 133 134 outputFile.tryWaitingForFileSizeToSettle() 135 136 val screenRecordOutput = screenRecordingFileDescriptor.readAllAndClose() 137 log( 138 """ 139 screenrecord killed (kill command output="$killOutput") 140 screenrecord command output: 141 142 """ 143 .trimIndent() + screenRecordOutput.prependIndent(" ") 144 ) 145 146 if (outputFile.exists()) { 147 val fileSizeKb = Files.size(outputFile.toPath()) / 1024 148 log("Screen recording captured at: $outputFile. File size: $fileSizeKb KB") 149 } else { 150 Log.e(TAG, "File not created successfully. Can't determine size of $outputFile") 151 } 152 } 153 154 if (screenRecordingInProgress()) { 155 Log.w( 156 TAG, 157 "Other screen recordings are in progress after this is done. " + 158 "(pids=\"$screenrecordPids\")." 159 ) 160 } 161 } 162 163 private fun File.tryWaitingForFileToExists() { 164 try { 165 ensureThat("Recording output created") { exists() } 166 } catch (e: FailedEnsureException) { 167 Log.e(TAG, "Recording not created successfully.", e) 168 } 169 } 170 171 private fun File.tryWaitingForFileSizeToSettle() { 172 try { 173 waitForValueToSettle( 174 "Screen recording output size", 175 minimumSettleTime = Duration.ofSeconds(5) 176 ) { 177 length() 178 } 179 } catch (e: FailedEnsureException) { 180 Log.e(TAG, "Recording size didn't settle.", e) 181 } 182 } 183 184 private fun screenRecordingInProgress() = screenrecordPids.isNotEmpty() 185 186 private val screenrecordPids: List<String> 187 get() = uiDevice.shell("pidof screenrecord").split(" ") 188 189 /** Interface to indicate that the test should capture screenrecord */ 190 @Retention(RetentionPolicy.RUNTIME) 191 @Target(FUNCTION, CLASS, PROPERTY_GETTER, PROPERTY_SETTER) 192 annotation class ScreenRecord 193 194 private fun log(s: String) = Log.d(TAG, s) 195 196 // Reads all from the stream and closes it. 197 private fun ParcelFileDescriptor.readAllAndClose(): String = 198 AutoCloseInputStream(this).use { inputStream -> 199 inputStream.bufferedReader().use { it.readText() } 200 } 201 202 companion object { 203 private const val TAG = "ScreenRecordRule" 204 private const val SCREEN_RECORDING_TEST_LEVEL_OVERRIDE_KEY = 205 "screen-recording-always-enabled-test-level" 206 private const val SCREEN_RECORDING_CLASS_LEVEL_OVERRIDE_KEY = 207 "screen-recording-always-enabled-class-level" 208 } 209 } 210