From b9299381cf8e72c66712c410da7058cd91a9e6c0 Mon Sep 17 00:00:00 2001 From: Tarek Belkahia Date: Mon, 4 Jun 2018 01:32:16 +0200 Subject: [PATCH 1/3] Add coverage report --- .../main/kotlin/com/gojuno/composer/Args.kt | 13 +++++-- .../main/kotlin/com/gojuno/composer/Main.kt | 25 +++++++++++-- .../kotlin/com/gojuno/composer/TestRun.kt | 36 ++++++++++++++----- 3 files changed, 61 insertions(+), 13 deletions(-) diff --git a/composer/src/main/kotlin/com/gojuno/composer/Args.kt b/composer/src/main/kotlin/com/gojuno/composer/Args.kt index ffe0da2..8024e58 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/Args.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/Args.kt @@ -106,7 +106,16 @@ data class Args( description = "Either `true` or `false` to enable/disable error on empty test suite. True by default.", order = 11 ) - var failIfNoTests: Boolean = true + var failIfNoTests: Boolean = true, + + @Parameter( + names = arrayOf("--coverage"), + required = false, + arity = 1, + description = "Either `true` or `false` to enable/disable test coverage collection. `false` by default.", + order = 12 + ) + var coverage: Boolean = false ) // No way to share array both for runtime and annotation without reflection. @@ -133,4 +142,4 @@ fun parseArgs(rawArgs: Array) = Args().also { args -> private class InstrumentationArgumentsConverter : IStringConverter> { override fun convert(argument: String): List = listOf(argument) -} \ No newline at end of file +} diff --git a/composer/src/main/kotlin/com/gojuno/composer/Main.kt b/composer/src/main/kotlin/com/gojuno/composer/Main.kt index 296513d..aaa7688 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/Main.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/Main.kt @@ -1,9 +1,11 @@ package com.gojuno.composer +import com.gojuno.commander.android.adb import com.gojuno.commander.android.connectedAdbDevices import com.gojuno.commander.android.installApk import com.gojuno.commander.os.log import com.gojuno.commander.os.nanosToHumanReadableTime +import com.gojuno.commander.os.process import com.gojuno.composer.html.writeHtmlReport import com.google.gson.Gson import rx.Observable @@ -111,15 +113,25 @@ private fun runAllTests(args: Args, testPackage: TestPackage.Valid, testRunner: val installTimeout = Pair(args.installTimeoutSeconds, TimeUnit.SECONDS) val installAppApk = device.installApk(pathToApk = args.appApkPath, timeout = installTimeout) val installTestApk = device.installApk(pathToApk = args.testApkPath, timeout = installTimeout) + val coverageDir = "/storage/emulated/0/app_coverage/${testPackage.value}" + val makeCoverageDir = if (args.coverage) { + process(commandAndArgs = listOf(adb, "-s", device.id, "shell", "mkdir", "-p", coverageDir)) + .map { Unit } + } else { + Observable.just(Unit) + } Observable - .concat(installAppApk, installTestApk) + .concat(installAppApk, installTestApk, makeCoverageDir) // Work with each device in parallel, but install apks sequentially on a device. .subscribeOn(Schedulers.io()) .toList() .flatMap { val instrumentationArguments = - buildShardArguments( + buildCoverageArguments( + coverage = args.coverage, + coverageDir = coverageDir + ) + buildShardArguments( shardingOn = args.shard, shardIndex = index, devices = connectedAdbDevices.size @@ -132,7 +144,8 @@ private fun runAllTests(args: Args, testPackage: TestPackage.Valid, testRunner: instrumentationArguments = instrumentationArguments.formatInstrumentationArguments(), outputDir = File(args.outputDirectory), verboseOutput = args.verboseOutput, - keepOutput = args.keepOutputOnExit + keepOutput = args.keepOutputOnExit, + coverage = args.coverage ) .flatMap { adbDeviceTestRun -> writeJunit4Report( @@ -210,6 +223,12 @@ private fun buildShardArguments(shardingOn: Boolean, shardIndex: Int, devices: I else -> emptyList() } +private fun buildCoverageArguments(coverage: Boolean, coverageDir: String): List> = + if (coverage) listOf( + "coverage" to "true", + "coverageFile" to "$coverageDir/coverage.ec" + ) else emptyList() + private fun List>.formatInstrumentationArguments(): String = when (isEmpty()) { true -> "" false -> " " + joinToString(separator = " ") { "-e ${it.first} ${it.second}" } diff --git a/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt b/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt index 4a845cd..254aca2 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt @@ -18,7 +18,8 @@ data class AdbDeviceTestRun( val durationNanos: Long, val timestampMillis: Long, val logcat: File, - val instrumentationOutput: File + val instrumentationOutput: File, + val coverageReport: File ) data class AdbDeviceTest( @@ -44,12 +45,14 @@ fun AdbDevice.runTests( instrumentationArguments: String, outputDir: File, verboseOutput: Boolean, - keepOutput: Boolean + keepOutput: Boolean, + coverage: Boolean ): Single { val adbDevice = this val logsDir = File(File(outputDir, "logs"), adbDevice.id) val instrumentationOutputFile = File(logsDir, "instrumentation.output") + val coverageReportDir = File(File(outputDir, "coverage"), adbDevice.id) val runTests = process( commandAndArgs = listOf( @@ -88,13 +91,31 @@ fun AdbDevice.runTests( } .toList() + val testRunFinish = runTests.ofType(Notification.Exit::class.java).cache() + + val pullCoverage = testRunFinish.toSingle() + .flatMap { + val coverageReport = File(coverageReportDir, "coverage.ec") + if (coverage) { + coverageReportDir.mkdirs() + adbDevice.pullFolder( + folderOnDevice = "/storage/emulated/0/app_coverage/$testPackageName/coverage.ec", + folderOnHostMachine = coverageReport, + logErrors = verboseOutput + ).map { coverageReport } + } else { + Single.just(coverageReport) + } + }.toObservable() + val adbDeviceTestRun = Observable .zip( Observable.fromCallable { System.nanoTime() }, runningTests, - { time, tests -> time to tests } + pullCoverage, + { time, tests, coverageFile -> Triple(time, tests, coverageFile) } ) - .map { (startTimeNanos, testsWithPulledFiles) -> + .map { (startTimeNanos, testsWithPulledFiles, coverageFile) -> val tests = testsWithPulledFiles.map { it.first } AdbDeviceTestRun( @@ -121,12 +142,11 @@ fun AdbDevice.runTests( durationNanos = System.nanoTime() - startTimeNanos, timestampMillis = System.currentTimeMillis(), logcat = logcatFileForDevice(logsDir), - instrumentationOutput = instrumentationOutputFile + instrumentationOutput = instrumentationOutputFile, + coverageReport = coverageFile ) } - val testRunFinish = runTests.ofType(Notification.Exit::class.java).cache() - val saveLogcat = saveLogcat(adbDevice, logsDir) .map { Unit } // TODO: Stop when all expected tests were parsed from logcat and not when instrumentation finishes. @@ -135,7 +155,7 @@ fun AdbDevice.runTests( .startWith(Unit) // To allow zip finish normally even if no tests were run. return Observable - .zip(adbDeviceTestRun, saveLogcat, testRunFinish) { suite, _, _ -> suite } + .zip(adbDeviceTestRun, saveLogcat, pullCoverage, testRunFinish) { suite, _, _, _ -> suite } .doOnSubscribe { adbDevice.log("Starting tests...") } .doOnNext { testRun -> adbDevice.log( From fcb3edfd50c39caca7adc0869a546d2e547c9cdb Mon Sep 17 00:00:00 2001 From: Tarek Belkahia Date: Wed, 6 Jun 2018 14:06:45 +0200 Subject: [PATCH 2/3] Add note and update README --- README.md | 7 ++++++- composer/src/main/kotlin/com/gojuno/composer/Args.kt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b14200e..4a8c4ef 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,12 @@ Composer shipped as jar, to run it you need JVM 1.8+: `java -jar composer-latest * Default: `true`. * `False` may be applicable when you run tests conditionally(via annotation/package filters) and empty suite is a valid outcome. * Example: `--fail-if-no-tests false` - +* `--coverage` + * Either `true` or `false` to enable/disable test coverage reports collection. + * Default: `false`. + * For this to work, your test APK should be built with instrumentation from EMMA or JaCoCo. + * Example: `--coverage true` + ##### Example Simplest : diff --git a/composer/src/main/kotlin/com/gojuno/composer/Args.kt b/composer/src/main/kotlin/com/gojuno/composer/Args.kt index 8024e58..1517cae 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/Args.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/Args.kt @@ -112,7 +112,7 @@ data class Args( names = arrayOf("--coverage"), required = false, arity = 1, - description = "Either `true` or `false` to enable/disable test coverage collection. `false` by default.", + description = "Either `true` or `false` to enable/disable test coverage reports collection. `false` by default. For this to work, your test APK should be built with instrumentation from EMMA or JaCoCo.", order = 12 ) var coverage: Boolean = false From 75bc99e5d946f2754a2096a898a4c57d5931bfc3 Mon Sep 17 00:00:00 2001 From: Tarek Belkahia Date: Wed, 6 Jun 2018 18:39:12 +0200 Subject: [PATCH 3/3] Extract consts for dirs --- composer/src/main/kotlin/com/gojuno/composer/Consts.kt | 5 +++++ composer/src/main/kotlin/com/gojuno/composer/Main.kt | 2 +- composer/src/main/kotlin/com/gojuno/composer/TestRun.kt | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 composer/src/main/kotlin/com/gojuno/composer/Consts.kt diff --git a/composer/src/main/kotlin/com/gojuno/composer/Consts.kt b/composer/src/main/kotlin/com/gojuno/composer/Consts.kt new file mode 100644 index 0000000..60502f3 --- /dev/null +++ b/composer/src/main/kotlin/com/gojuno/composer/Consts.kt @@ -0,0 +1,5 @@ +package com.gojuno.composer + +val EXTERNAL_STORAGE = "/storage/emulated/0" +val COVERAGE_DIR = "$EXTERNAL_STORAGE/app_coverage" +val SCREENSHOTS_DIR = "$EXTERNAL_STORAGE/app_spoon-screenshots" diff --git a/composer/src/main/kotlin/com/gojuno/composer/Main.kt b/composer/src/main/kotlin/com/gojuno/composer/Main.kt index aaa7688..96b2c7b 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/Main.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/Main.kt @@ -113,7 +113,7 @@ private fun runAllTests(args: Args, testPackage: TestPackage.Valid, testRunner: val installTimeout = Pair(args.installTimeoutSeconds, TimeUnit.SECONDS) val installAppApk = device.installApk(pathToApk = args.appApkPath, timeout = installTimeout) val installTestApk = device.installApk(pathToApk = args.testApkPath, timeout = installTimeout) - val coverageDir = "/storage/emulated/0/app_coverage/${testPackage.value}" + val coverageDir = "$COVERAGE_DIR/${testPackage.value}" val makeCoverageDir = if (args.coverage) { process(commandAndArgs = listOf(adb, "-s", device.id, "shell", "mkdir", "-p", coverageDir)) .map { Unit } diff --git a/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt b/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt index 254aca2..c820d93 100644 --- a/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt +++ b/composer/src/main/kotlin/com/gojuno/composer/TestRun.kt @@ -99,7 +99,7 @@ fun AdbDevice.runTests( if (coverage) { coverageReportDir.mkdirs() adbDevice.pullFolder( - folderOnDevice = "/storage/emulated/0/app_coverage/$testPackageName/coverage.ec", + folderOnDevice = "$COVERAGE_DIR/$testPackageName/coverage.ec", folderOnHostMachine = coverageReport, logErrors = verboseOutput ).map { coverageReport } @@ -183,7 +183,7 @@ private fun pullTestFiles(adbDevice: AdbDevice, test: InstrumentationTest, outpu adbDevice .pullFolder( // TODO: Add support for internal storage and external storage strategies. - folderOnDevice = "/storage/emulated/0/app_spoon-screenshots/${test.className}/${test.testName}", + folderOnDevice = "$SCREENSHOTS_DIR/${test.className}/${test.testName}", folderOnHostMachine = screenshotsFolderOnHostMachine, logErrors = verboseOutput )