Skip to content

Commit

Permalink
Add screen record for last fail. Also fix test file pulling logic. (#10)
Browse files Browse the repository at this point in the history
* Add screen record for last fail. Also fix test file pulling logic.

* Rename video file name. Add more clear null file name message. Remove unnecessary coroutine launch.

* Promote version to release.
  • Loading branch information
hujim authored Dec 17, 2019
1 parent 6061e2d commit 9d7c699
Show file tree
Hide file tree
Showing 11 changed files with 275 additions and 70 deletions.
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
VERSION_NAME=1.0.2
VERSION_NAME=1.0.3
GROUP=com.workday

POM_DESCRIPTION=A Reactive Android instrumentation test orchestrator with multi-library-modules-testing and test pooling/grouping support.
Expand Down
20 changes: 14 additions & 6 deletions torque-core/src/main/kotlin/com/workday/torque/Args.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const val DEFAULT_CHUNK_SIZE = 1
const val DEFAULT_PER_CHUNK_TIMEOUT_SECONDS = 120L
const val DEFAULT_MAX_RETRIES_PER_CHUNK = 1
const val DEFAULT_TORQUE_TIMEOUT_MINUTES = 60L
const val DEFAULT_FILES_PULL_DEVICE_DIR_PATH = "/sdcard/test-files"

data class Args(
@Parameter(
Expand Down Expand Up @@ -112,24 +113,31 @@ data class Args(

@Parameter(
names = ["--test-files-pull-device-directory"],
description = "Directory on device to pull test files from. Setting this directory and --file-pull-host-directory will enable recursive pulling of the folders." +
"This folder would have the structure of deviceDirectory\\TestClass\\TestMethod for all tests"
description = "Directory on device to pull test files from. Setting this directory and --file-pull-host-directory will enable recursive pulling of the folders."
)
var testFilesPullDeviceDirectory: String = "",
var testFilesPullDeviceDirectory: String = DEFAULT_FILES_PULL_DEVICE_DIR_PATH,

@Parameter(
names = ["--test-files-pull-host-directory"],
description = "Directory on the Torque run host machine to pull test files to. Setting this and --file-pull-device-directory will enable pulling of the folders." +
"This folder would have the structure of hostDirectory\\TestClass\\TestMethod for all tests"
description = "Directory on the Torque run host machine to pull test files into. Setting this and --file-pull-device-directory will enable pulling of the folders." +
"This folder would have the structure of hostDirectory\\deviceDirectory"
)
var testFilesPullHostDirectory: String = "",

@Parameter(
names = ["--uninstall-apk-after-test"],
arity = 1,
description = "Always only have one module's test apk and app apk installed per device. Uninstalls the current test modules and app modules when starting a different test module." +
"This is required when multiple apks contain the same intent filters due to AndroidManifest.xml merging."
)
var uninstallApkAfterTest: Boolean = false
var uninstallApkAfterTest: Boolean = false,

@Parameter(
names = ["--record-failed-tests"],
arity = 1,
description = "Screen record failed tests on last try, file will be under"
)
var recordFailedTests: Boolean = false
)

fun parseArgs(rawArgs: Array<String>): Args {
Expand Down
19 changes: 6 additions & 13 deletions torque-core/src/main/kotlin/com/workday/torque/FilePuller.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,22 @@ package com.workday.torque
import com.gojuno.commander.os.Notification
import io.reactivex.Completable
import java.io.File
import java.util.concurrent.TimeUnit

private const val MAX_RETRIES: Long = 3

class FilePuller(private val adbDevice: AdbDevice,
private val processRunner: ProcessRunner = ProcessRunner()) {

fun pullTestFolder(
hostDirectory: String,
deviceDirectory: String,
testDetails: TestDetails,
timeout: Timeout
): Completable {
val folderOnHostMachine = hostDirectory.setupFolderPathForTestDetails(testDetails)
fun pullFolder(args: Args, subFolder: String = ""): Completable {
val pullFileTimeout = Timeout(args.installTimeoutSeconds, TimeUnit.SECONDS)
val folderOnHostMachine = args.testFilesPullHostDirectory
val folderOnHostMachineFile = File(folderOnHostMachine)
folderOnHostMachineFile.mkdirs()
val folderOnDevice = deviceDirectory.setupFolderPathForTestDetails(testDetails)
val folderOnDevice = "${args.testFilesPullDeviceDirectory}/$subFolder"
val pullFiles = processRunner.runAdb(
commandAndArgs = listOf("-s", adbDevice.id, "pull", folderOnDevice, folderOnHostMachineFile.absolutePath),
timeout = timeout,
timeout = pullFileTimeout,
unbufferedOutput = true
)

Expand All @@ -34,8 +31,4 @@ class FilePuller(private val adbDevice: AdbDevice,
.ignoreElements()
.onErrorComplete()
}

private fun String.setupFolderPathForTestDetails(testDetails: TestDetails): String {
return "$this/${testDetails.testClass}/${testDetails.testName}"
}
}
69 changes: 69 additions & 0 deletions torque-core/src/main/kotlin/com/workday/torque/ScreenRecorder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.workday.torque

import com.gojuno.commander.os.Notification
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.await
import java.io.File

class ScreenRecorder(private val adbDevice: AdbDevice,
args: Args,
private val processRunner: ProcessRunner = ProcessRunner()) {
private val videosDir = File(File(args.testFilesPullDeviceDirectory, "videos"), adbDevice.id)
private val timeoutSeconds = args.chunkTimeoutSeconds
private var videoRecordJob: Job? = null
private var videoFileName: File? = null

suspend fun start(coroutineScope: CoroutineScope, testDetails: TestDetails) {
videoRecordJob = coroutineScope.launch { startRecordTestRun(testDetails) }
}

private suspend fun startRecordTestRun(testDetails: TestDetails) {
videoFileName = getVideoFile(testDetails)

processRunner.runAdb(commandAndArgs = listOf(
"-s", adbDevice.id,
"shell", "mkdir -p ${videoFileName!!.parentFile}"
),
destroyOnUnsubscribe = true)
.ofType(Notification.Exit::class.java)
.map { true }
.doOnError { error -> adbDevice.log("Failed to mkdir on ${adbDevice.tag}, filepath: ${videoFileName!!.parentFile}, failed: $error") }
.ignoreElements()
.await()

processRunner.runAdb(commandAndArgs = listOf(
"-s", adbDevice.id,
"shell", "screenrecord $videoFileName --time-limit $timeoutSeconds --size 720x1440"
),
destroyOnUnsubscribe = true)
.ofType(Notification.Exit::class.java)
.map { true }
.doOnSubscribe { adbDevice.log("Started recording on ${adbDevice.tag}, filename: $videoFileName") }
.doOnComplete { adbDevice.log("Ended recording on ${adbDevice.tag}, filename: $videoFileName") }
.doOnError { error -> adbDevice.log("Failed to record on ${adbDevice.tag}, filename: $videoFileName, failed: $error") }
.ignoreElements()
.await()
}

private fun getVideoFile(testDetails: TestDetails): File {
val testFolder = File(File(videosDir, testDetails.testClass), testDetails.testName)
return File(testFolder, "test_recording.mp4")
}

fun stop() = videoRecordJob?.cancel()

suspend fun removeLastFile() {
checkNotNull(videoFileName) { "Filename cannot be null, must call start() first" }
processRunner.runAdb(commandAndArgs = listOf(
"-s", adbDevice.id,
"shell", "rm $videoFileName"
),
destroyOnUnsubscribe = true)
.ofType(Notification.Exit::class.java)
.map { true }
.ignoreElements()
.await()
}
}
41 changes: 39 additions & 2 deletions torque-core/src/main/kotlin/com/workday/torque/TestChunkRetryer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.workday.torque

import com.linkedin.dex.parser.TestMethod
import com.workday.torque.pooling.TestChunk
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.withTimeout
import java.util.concurrent.TimeUnit
Expand All @@ -10,7 +11,8 @@ class TestChunkRetryer(private val adbDevice: AdbDevice,
private val args: Args,
private val logcatFileIO: LogcatFileIO,
private val testChunkRunner: TestChunkRunner,
private val installer: Installer
private val installer: Installer,
private val screenRecorder: ScreenRecorder = ScreenRecorder(adbDevice, args)
) {

suspend fun runTestChunkWithRetry(testChunk: TestChunk): List<AdbDeviceTestResult> {
Expand All @@ -21,7 +23,11 @@ class TestChunkRetryer(private val adbDevice: AdbDevice,
var resultsResponse = testChunkRunner.run(args, testChunk)
while (resultsResponse.needToRetry(testChunk, args.retriesPerChunk)) {
adbDevice.log("Chunk has failed tests, retry count: ${testChunk.retryCount++}/${args.retriesPerChunk}, retrying...")
resultsResponse = testChunkRunner.run(args, testChunk)
resultsResponse = if (args.recordFailedTests && isLastRun(testChunk.retryCount, args.retriesPerChunk)) {
runTestChunkInIndividualRecordedChunks(testChunk)
} else {
testChunkRunner.run(args, testChunk)
}
}
resultsResponse
}
Expand Down Expand Up @@ -56,6 +62,37 @@ class TestChunkRetryer(private val adbDevice: AdbDevice,

private fun isRetryable(retryCount: Int, maxRetryCount: Int) = retryCount < maxRetryCount

private fun isLastRun(retryCount: Int, maxRetryCount: Int) = retryCount == maxRetryCount

private suspend fun CoroutineScope.runTestChunkInIndividualRecordedChunks(testChunk: TestChunk): List<AdbDeviceTestResult> {
val resultsResponse = mutableListOf<AdbDeviceTestResult>()
testChunk.toSingleTestChunks().forEach { singleTestChunk ->
screenRecorder.start(this, singleTestChunk.getTestDetails())
val singleTestResultList = testChunkRunner.run(args, singleTestChunk)
resultsResponse.addAll(singleTestResultList)
screenRecorder.stop()
if (!singleTestResultList.hasFailedTests()) {
screenRecorder.removeLastFile()
}
}
return resultsResponse
}

private fun TestChunk.toSingleTestChunks(): List<TestChunk> {
return testMethods.map {
TestChunk(
index = index,
testModuleInfo = testModuleInfo,
testMethods = listOf(it),
retryCount = retryCount
)
}
}

private fun TestChunk.getTestDetails(): TestDetails {
return testMethods.single().toTestDetails()
}

private fun createTimedOutAdbDeviceTestResults(
testChunk: TestChunk,
timeoutMillis: Long,
Expand Down
26 changes: 3 additions & 23 deletions torque-core/src/main/kotlin/com/workday/torque/TestRunFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@ package com.workday.torque

import com.workday.torque.pooling.TestPool
import io.reactivex.Single
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.rx2.asSingle
import kotlinx.coroutines.rx2.await
import java.util.concurrent.TimeUnit

class TestRunFactory {

Expand Down Expand Up @@ -41,7 +38,6 @@ class TestRunFactory {
val chunk = testPool.getNextTestChunk()
if (chunk != null) {
val deviceTestsResults = testChunkRetryer.runTestChunkWithRetry(chunk)
pullChunkTestFiles(args, filePuller, deviceTestsResults)
testSession.apply {
testResults.addAll(deviceTestsResults)
passedCount += deviceTestsResults.count { it.status is AdbDeviceTestResult.Status.Passed }
Expand All @@ -61,34 +57,18 @@ class TestRunFactory {
"${testSession.failedCount} failed, took " +
"${testSession.durationMillis.millisToHoursMinutesSeconds()}."
)
pullDeviceFiles(args, filePuller)

testSession
}
).asSingle(Dispatchers.Default)
}

private fun CoroutineScope.pullChunkTestFiles(
args: Args, filePuller: FilePuller,
deviceTestsResults: List<AdbDeviceTestResult>
) {
private suspend fun pullDeviceFiles(args: Args, filePuller: FilePuller) {
if (args.testFilesPullDeviceDirectory.isEmpty() || args.testFilesPullHostDirectory.isEmpty()) {
return
}
val completedTestResults = deviceTestsResults.filter { it.status !is AdbDeviceTestResult.Status.Ignored }
if (completedTestResults.isEmpty()) {
return
}

launch {
val pullFileTimeout = Timeout(args.installTimeoutSeconds, TimeUnit.SECONDS)
completedTestResults
.map { TestDetails(it.className, it.testName) }
.forEach { testDetails: TestDetails ->
filePuller.pullTestFolder(args.testFilesPullDeviceDirectory,
args.testFilesPullHostDirectory,
testDetails,
pullFileTimeout).await()
}
}
filePuller.pullFolder(args).await()
}
}
34 changes: 34 additions & 0 deletions torque-core/src/test/kotlin/com/workday/torque/ArgsSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -194,4 +194,38 @@ class ArgsSpec : Spek(
assertThat(args.testFilesPullHostDirectory).isEqualTo("hostDir")
}
}

context("parse args with explicitly passed --uninstall-apk-after-test") {

listOf(true, false).forEach { uninstallApkAfterTest ->

context("--uninstall-apk-after-test $uninstallApkAfterTest") {

val args by memoized {
parseArgs(rawArgsWithOnlyRequiredFields + arrayOf("--uninstall-apk-after-test", "$uninstallApkAfterTest"))
}

it("parses --uninstall-apk-after-test correctly") {
assertThat(args.uninstallApkAfterTest).isEqualTo(uninstallApkAfterTest)
}
}
}
}

context("parse args with explicitly passed --record-failed-tests") {

listOf(true, false).forEach { recordFailedTests ->

context("--record-failed-tests $recordFailedTests") {

val args by memoized {
parseArgs(rawArgsWithOnlyRequiredFields + arrayOf("--record-failed-tests", "$recordFailedTests"))
}

it("parses --record-failed-tests correctly") {
assertThat(args.recordFailedTests).isEqualTo(recordFailedTests)
}
}
}
}
})
13 changes: 7 additions & 6 deletions torque-core/src/test/kotlin/com/workday/torque/FilePullerSpec.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ class FilePullerSpec : Spek(
}

given("A test detail") {
val hostDirectory = "someHostDir"
val deviceDirectory = "someDeviceDir"
val testDetails = TestDetails("com.some.package.class", "SomeTest")
val args = Args().apply {
testFilesPullDeviceDirectory = "someDeviceDir"
testFilesPullHostDirectory = "someHostDir"
}
it("Runs adb pull command for that test folder") {
filePuller.pullTestFolder(hostDirectory, deviceDirectory, testDetails, mockk())
filePuller.pullFolder(args, "subFolder")

verify {
val commandAndArgsMatcher = match<List<String>> {
it[0] == "-s" && it[1] == adbDevice.id && it[2] == "pull" &&
it[3] == "someDeviceDir/com.some.package.class/SomeTest" &&
it[4].endsWith("someHostDir/com.some.package.class/SomeTest")
it[3] == "someDeviceDir/subFolder" &&
it[4].endsWith("someHostDir")
}
processRunner.runAdb(commandAndArgsMatcher, any(), any(), any(), any(), any(), any())
}
Expand Down
Loading

0 comments on commit 9d7c699

Please sign in to comment.