• 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.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