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