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