• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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