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