Last active
March 11, 2021 15:23
-
-
Save eakurnikov/09bc15bb73e7f9273241b301166c3efb to your computer and use it in GitHub Desktop.
Тест метод должен быть помечен аннотацией @RecordVideo.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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