Skip to content

Instantly share code, notes, and snippets.

@eakurnikov
Last active March 11, 2021 15:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eakurnikov/09bc15bb73e7f9273241b301166c3efb to your computer and use it in GitHub Desktop.
Save eakurnikov/09bc15bb73e7f9273241b301166c3efb to your computer and use it in GitHub Desktop.
Тест метод должен быть помечен аннотацией @RecordVideo.
class VideoTestInterceptor(
private val zenVideoRecorder: ZenVideoRecorder
) : TestRunWatcherInterceptor {
override fun onTestStarted(testInfo: TestInfo) {
zenVideoRecorder.start("Video_${testInfo.testName}")
}
override fun onTestFinished(testInfo: TestInfo, success: Boolean) {
zenVideoRecorder.stop()?.attachVideoToReport()
}
}
fun File.attachVideoToReport() {
AllureAndroidLifecycle.addAttachment(
name = name,
type = "video/mp4",
fileExtension = ".mp4",
file = this
)
}
/**
* For some reason TC build timeouts if a lot of video saved at once, so
* there is a set limit of [TC_VIDEOS_LIMIT] at max of videos to be saved.
*/
private const val TC_VIDEOS_LIMIT: Int = 12
private const val START_RECORDING_TIME_MS: Long = 3_000L
private const val STOP_RECORDING_TIME_MS: Long = 2_000L
class ZenVideoRecorder(
private val testResultsFilesProvider: TestResultsFilesProvider
) {
private var recordedVideosCount = 0
private var videoRecordingThread: VideoRecordingThread? = null
/**
* Starts video recording. It must be manually finished with [stop] afterward.
*/
@Synchronized
fun start(tag: String) {
if (!allowRecording()) {
return
}
val videoFile: File = testResultsFilesProvider.provideVideoFile(tag)
ZenTestLogger.i("Video recording started for test: $tag to path: ${videoFile.absolutePath}")
videoRecordingThread = VideoRecordingThread(videoFile).apply {
priority = Thread.MAX_PRIORITY
start()
}
waitForRecordingToStart()
}
@Synchronized
fun stop(): File? = stopRecording()?.also { recordedVideosCount++ }
@Synchronized
private fun stopRecording(): File? = videoRecordingThread
?.run {
ZenTestLogger.i("Killing video recording process")
killRecordingProcess()
waitForRecordingToStop()
interrupt()
file
}
?.apply {
videoRecordingThread = null
}
?: run {
ZenTestLogger.i("Video recording was not started")
null
}
private fun allowRecording(): Boolean = when {
ZenTestRunListener.currentTest?.getAnnotation(RecordVideo::class.java) == null -> {
false
}
videoRecordingThread != null -> {
ZenTestLogger.i("Video recording already started")
false
}
recordedVideosCount >= TC_VIDEOS_LIMIT -> {
ZenTestLogger.i("Exceeded video files quota of $TC_VIDEOS_LIMIT files at maximum")
false
}
else -> {
true
}
}
private fun waitForRecordingToStart() {
try {
ZenTestLogger.i("ZenVideoRecorder is waiting for recording to start")
Thread.sleep(START_RECORDING_TIME_MS)
} catch (e: InterruptedException) {
ZenTestLogger.e("Recording was interrupted:\n${Log.getStackTraceString(e)}")
stopRecording()
}
}
private fun waitForRecordingToStop() {
try {
ZenTestLogger.i("ZenVideoRecorder is waiting for recording to stop")
Thread.sleep(STOP_RECORDING_TIME_MS)
} catch (e: InterruptedException) {
ZenTestLogger.e("Recording was interrupted:\n${Log.getStackTraceString(e)}")
}
}
}
class VideoRecordingThread(
val file: File
) : Thread() {
override fun run() {
execShellCommand("screenrecord --bit-rate 100000 --bugreport ${file.absolutePath}")
}
fun killRecordingProcess() {
execShellCommand("pkill -l INT screenrecord")
}
private fun execShellCommand(command: String) {
runCatching {
UiDevice.getInstance(
InstrumentationRegistry.getInstrumentation()
).executeShellCommand(command)
}.onFailure { e: Throwable ->
ZenTestLogger.e("Adb shell command:\n${command}\nexecution failure: ${e.message}")
}
}
}
class ZenTestRunListener : InstrumentationRunListener() {
companion object {
@Volatile
var currentTest: Description? = null
}
override fun testStarted(description: Description) {
currentTest = description
}
override fun testRunFinished(result: Result?) {
currentTest = null
}
}
private const val TAG: String = "ZenTestRunner"
private const val WRITE_EXT_STRG_PERMISSION: String = "android.permission.WRITE_EXTERNAL_STORAGE"
private const val READ_EXT_STRG_PERMISSION: String = "android.permission.READ_EXTERNAL_STORAGE"
/**
* Instrumentation (espresso) test runner with customizations required for Zen tests.
* Customizations:
* - Support of Allure reports
* - Support of run by test runner based on grishberg test runner lib.
* - Close system dialogs before start.
*/
class ZenTestRunner : AndroidJUnitRunner() {
private val customInstrumentationListeners = listOf<String>(
AllureAndroidListener::class.java.name,
ZenTestRunListener::class.java.name
)
override fun onCreate(arguments: Bundle?) {
arguments?.apply {
val listeners: String =
(getCharSequence("listener")?.let { "$it," } ?: "") +
customInstrumentationListeners.joinToString(separator = ",")
putCharSequence("listener", listeners)
}
super.onCreate(arguments)
}
override fun newApplication(
cl: ClassLoader?,
className: String?,
context: Context?
): Application = Instrumentation.newApplication(TestZenApp::class.java, context)
override fun onStart() {
grantStoragePermissions()
targetContext.sendBroadcast(Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS))
super.onStart()
}
/**
* Ensure storage permissions are granted.
* <p>
* Trying to fight
* "FileNotFoundException: /storage/emulated/0/allure-results/".
* Probably AllureAndroidListener ignores silently when fails to create "allure-results" dir.
* <p>
* NOTE: to create directories, granting permission needed, so method must be called
* after Application was fully established.
*/
private fun grantStoragePermissions() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return
}
Log.d(TAG, "Grant read/write storage permissions")
try {
UiDevice.getInstance(this).apply {
grantPermission(context.packageName, WRITE_EXT_STRG_PERMISSION)
grantPermission(context.packageName, READ_EXT_STRG_PERMISSION)
grantPermission(targetContext.packageName, WRITE_EXT_STRG_PERMISSION)
grantPermission(targetContext.packageName, READ_EXT_STRG_PERMISSION)
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
@Throws(IOException::class)
private fun UiDevice.grantPermission(packageName: String, permission: String): String =
executeShellCommand("pm grant $packageName $permission")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment