Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extract code coverage from Android device #21

Open
wants to merge 23 commits into
base: feature/agoda-android-ci
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class ConfigFactory(val mapper: ObjectMapper) {
config.debug,
vendorConfiguration,
config.analyticsTracking
)
).also { println(it) }
}

private fun readConfigFile(configFile: File): FileConfiguration? {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.malinskiy.marathon.device.DeviceProvider
import com.malinskiy.marathon.exceptions.NoDevicesException
import com.malinskiy.marathon.execution.Configuration
import com.malinskiy.marathon.execution.Scheduler
import com.malinskiy.marathon.execution.TestPackageFilter
import com.malinskiy.marathon.execution.TestParser
import com.malinskiy.marathon.execution.TestShard
import com.malinskiy.marathon.execution.progress.ProgressReporter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.google.gson.Gson
import com.malinskiy.marathon.analytics.tracker.local.DeviceTracker
import com.malinskiy.marathon.analytics.tracker.local.JUnitTracker
import com.malinskiy.marathon.analytics.tracker.local.RawTestResultTracker
import com.malinskiy.marathon.analytics.tracker.local.TestCoverageTracker
import com.malinskiy.marathon.analytics.tracker.local.TestResultsTracker
import com.malinskiy.marathon.analytics.tracker.remote.influx.InfluxDbProvider
import com.malinskiy.marathon.analytics.tracker.remote.influx.InfluxDbTracker
Expand All @@ -12,6 +13,7 @@ import com.malinskiy.marathon.execution.Configuration
import com.malinskiy.marathon.io.FileManager
import com.malinskiy.marathon.report.allure.AllureTestListener
import com.malinskiy.marathon.report.internal.DeviceInfoReporter
import com.malinskiy.marathon.report.internal.TestCoverageReporter
import com.malinskiy.marathon.report.internal.TestResultReporter
import com.malinskiy.marathon.report.junit.JUnitReporter
import java.io.File
Expand All @@ -26,12 +28,13 @@ internal class TrackerFactory(private val configuration: Configuration,
val allureTracker = AllureTestListener(configuration, File(configuration.outputDir, "allure-results"))

fun create(): Tracker {
val defaultTrackers = listOf(
JUnitTracker(JUnitReporter(fileManager)),
DeviceTracker(deviceInfoReporter),
TestResultsTracker(testResultReporter),
rawTestResultTracker,
allureTracker
val defaultTrackers = listOfNotNull(
JUnitTracker(JUnitReporter(fileManager)),
DeviceTracker(deviceInfoReporter),
TestResultsTracker(testResultReporter),
rawTestResultTracker,
allureTracker,
if (configuration.isCodeCoverageEnabled) TestCoverageTracker(TestCoverageReporter(fileManager)) else null
)
return when {
configuration.analyticsConfiguration is InfluxDbConfiguration -> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.malinskiy.marathon.analytics.tracker.local

import com.malinskiy.marathon.analytics.tracker.NoOpTracker
import com.malinskiy.marathon.device.DeviceInfo
import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.execution.TestResult
import com.malinskiy.marathon.report.internal.TestCoverageReporter

internal class TestCoverageTracker(private val testCoverageReporter: TestCoverageReporter) :
NoOpTracker() {

override fun trackTestFinished(
poolId: DevicePoolId,
device: DeviceInfo,
testResult: TestResult
) {
testCoverageReporter.testFinished(poolId, device, testResult)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,35 @@ import com.malinskiy.marathon.device.DeviceInfo
import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.execution.TestResult
import com.malinskiy.marathon.execution.TestStatus
import com.malinskiy.marathon.log.MarathonLogging
import com.malinskiy.marathon.test.toSafeTestName
import org.influxdb.InfluxDB
import org.influxdb.dto.Point
import java.util.concurrent.TimeUnit

internal class InfluxDbTracker(private val influxDb: InfluxDB) : NoOpTracker() {

private val logger = MarathonLogging.logger("InfluxDB")

override fun trackRawTestRun(poolId: DevicePoolId, device: DeviceInfo, testResult: TestResult) {
influxDb.write(Point.measurement("tests")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.tag("testname", testResult.test.toSafeTestName())
.tag("package", testResult.test.pkg)
.tag("class", testResult.test.clazz)
.tag("method", testResult.test.method)
.tag("deviceSerial", device.serialNumber)
.addField("ignored", if (testResult.isIgnored) 1.0 else 0.0)
.addField("success", if (testResult.status == TestStatus.PASSED) 1.0 else 0.0)
.addField("duration", testResult.durationMillis())
.build())
val point = Point.measurement("tests")
.time(System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.tag("testname", testResult.test.toSafeTestName())
.tag("package", testResult.test.pkg)
.tag("class", testResult.test.clazz)
.tag("method", testResult.test.method)
.tag("deviceSerial", device.serialNumber)
.addField("ignored", if (testResult.isIgnored) 1.0 else 0.0)
.addField("success", if (testResult.status == TestStatus.PASSED) 1.0 else 0.0)
.addField("duration", testResult.durationMillis())
.build()
runCatching {
influxDb.write(point)
}.onSuccess {
logger.trace { "Tracked in influxDB $point" }
}.onFailure {
logger.error(it) { "Error writing to influxDB with $point" }
}
Comment on lines +19 to +36
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are having an issue writing to influxDB 400 Bad Request, just want to log the request marathon is sending.

}

override fun terminate() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@ data class Attachment(val file: File, val type: AttachmentType)
enum class AttachmentType {
SCREENSHOT,
VIDEO,
COVERAGE,
LOG;

fun toMimeType() = when(this) {
SCREENSHOT -> "image/gif"
VIDEO -> "video/mp4"
COVERAGE -> "application/ec"
LOG -> "text/txt"
}
}
4 changes: 2 additions & 2 deletions core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ enum class FileType(val dir: String, val suffix: String) {
LOG("logs", "log"),
DEVICE_INFO("devices", "json"),
VIDEO("video", "mp4"),
SCREENSHOT("screenshot", "gif")

SCREENSHOT("screenshot", "gif"),
COVERAGE("coverage", "ec")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.malinskiy.marathon.report.internal

import com.malinskiy.marathon.device.DeviceInfo
import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.execution.TestResult
import com.malinskiy.marathon.io.FileManager
import com.malinskiy.marathon.io.FileType
import com.malinskiy.marathon.log.MarathonLogging
import com.malinskiy.marathon.test.toTestName

class TestCoverageReporter(private val fileManager: FileManager) {

private val logger = MarathonLogging.logger("TestCoverageReporter")

fun testFinished(poolId: DevicePoolId, device: DeviceInfo, testResult: TestResult) {
testResult.attachments
.onEach { logger.debug { "Attachments on test ${testResult.test.toTestName()} ${it.file.name}" } }
.filter { it.file.name.endsWith(".ec") }
.forEach {
val file =
fileManager.createFile(FileType.COVERAGE, poolId, device, testResult.test)
file.setWritable(true)
logger.debug { "Coverage file is ${it.file.name}" }
fileCounter++
val bytes = it.file.readBytes()
file.appendBytes(bytes)
}
logger.debug { "Total coverage file generated so far $fileCounter" }
}

companion object {
private var fileCounter = 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import com.malinskiy.marathon.android.executor.listeners.DebugTestRunListener
import com.malinskiy.marathon.android.executor.listeners.LogCatListener
import com.malinskiy.marathon.android.executor.listeners.NoOpTestRunListener
import com.malinskiy.marathon.android.executor.listeners.ProgressTestRunListener
import com.malinskiy.marathon.android.executor.listeners.TestCoverageResultListener
import com.malinskiy.marathon.android.executor.listeners.TestRunResultsListener
import com.malinskiy.marathon.android.executor.listeners.screenshot.ScreenCapturerTestRunListener
import com.malinskiy.marathon.android.executor.listeners.video.ScreenRecorderTestRunListener
Expand All @@ -36,10 +37,13 @@ import kotlinx.coroutines.newFixedThreadPoolContext
import java.util.*
import kotlin.coroutines.CoroutineContext

class AndroidDevice(val ddmsDevice: IDevice,
private val serialStrategy: SerialStrategy = SerialStrategy.AUTOMATIC) : Device, CoroutineScope {
class AndroidDevice(
val ddmsDevice: IDevice,
private val serialStrategy: SerialStrategy = SerialStrategy.AUTOMATIC,
packageName: String = ""
) : Device, CoroutineScope {

val fileManager = RemoteFileManager(ddmsDevice)
val fileManager = RemoteFileManager(ddmsDevice, packageName)

private val dispatcher by lazy {
newFixedThreadPoolContext(1, "AndroidDevice - execution - ${ddmsDevice.serialNumber}")
Expand Down Expand Up @@ -164,14 +168,17 @@ class AndroidDevice(val ddmsDevice: IDevice,

val timer = SystemTimer()

val coverageListener = TestCoverageResultListener(fileManager, devicePoolId, this)

return CompositeTestRunListener(
listOf(
recorderListener,
logCatListener,
TestRunResultsListener(testBatch, this, deferred, timer, attachmentProviders),
DebugTestRunListener(this),
ProgressTestRunListener(this, devicePoolId, progressReporter)
)
listOfNotNull(
recorderListener,
logCatListener,
TestRunResultsListener(testBatch, this, deferred, timer, attachmentProviders),
DebugTestRunListener(this),
ProgressTestRunListener(this, devicePoolId, progressReporter),
if (configuration.isCodeCoverageEnabled) coverageListener else null
)
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,12 @@ class AndroidDeviceProvider : DeviceProvider, CoroutineScope {
if (vendorConfiguration !is AndroidConfiguration) {
throw IllegalStateException("Invalid configuration $vendorConfiguration passed")
}
val packageName = ApkParser().parseInstrumentationInfo(
vendorConfiguration.testApplicationOutput
).applicationPackage

logger.debug { "applicationPackage name is $packageName" }

DdmPreferences.setTimeOut(DEFAULT_DDM_LIB_TIMEOUT)
AndroidDebugBridge.initIfNeeded(false)

Expand All @@ -51,7 +57,11 @@ class AndroidDeviceProvider : DeviceProvider, CoroutineScope {
override fun deviceChanged(device: IDevice?, changeMask: Int) {
device?.let {
launch(context = bootWaitContext) {
val maybeNewAndroidDevice = AndroidDevice(it, vendorConfiguration.serialStrategy)
val maybeNewAndroidDevice = AndroidDevice(
it,
vendorConfiguration.serialStrategy,
packageName
)
val healthy = maybeNewAndroidDevice.healthy

logger.debug { "Device ${device.serialNumber} changed state. Healthy = $healthy" }
Expand All @@ -70,7 +80,7 @@ class AndroidDeviceProvider : DeviceProvider, CoroutineScope {
override fun deviceConnected(device: IDevice?) {
device?.let {
launch {
val maybeNewAndroidDevice = AndroidDevice(it, vendorConfiguration.serialStrategy)
val maybeNewAndroidDevice = AndroidDevice(it, vendorConfiguration.serialStrategy, packageName)
val healthy = maybeNewAndroidDevice.healthy
logger.debug { "Device ${maybeNewAndroidDevice.serialNumber} connected. Healthy = $healthy" }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@ import com.android.ddmlib.ShellCommandUnresponsiveException
import com.android.ddmlib.TimeoutException
import com.android.ddmlib.testrunner.TestIdentifier
import com.malinskiy.marathon.log.MarathonLogging

import java.io.File
import java.io.IOException

class RemoteFileManager(private val device: IDevice) {
class RemoteFileManager(
private val device: IDevice,
packageName: String = "com.agoda.mobile.consumer.debug"
) {

private val logger = MarathonLogging.logger("RemoteFileManager")

private val outputDir = device.getMountPoint(MNT_EXTERNAL_STORAGE)

private val filesDir = "/data/data/$packageName/files"

private val nullOutputReceiver = NullOutputReceiver()

fun removeRemotePath(remotePath: String) {
Expand Down Expand Up @@ -53,6 +58,32 @@ class RemoteFileManager(private val device: IDevice) {
return remoteFileForTest(videoFileName(test))
}

fun remoteCoverageForTest(): String {
logger.debug { "Directories accessible in Devices ---------------------->" }
logger.debug { "$outputDir" }
File(outputDir).listAllFilesInTheDirectory()
logger.debug { "$filesDir" }
File(filesDir).listAllFilesInTheDirectory()
logger.debug { "data folder" }
File("/data").listAllFilesInTheDirectory()
logger.debug { "storage folder" }
File("/storage").listAllFilesInTheDirectory()
logger.debug { "All files listed ------------------------------>" }
return "/sdcard/coverage.ec"
}

private fun File.listAllFilesInTheDirectory() {
if (isDirectory) {
listFiles().forEach {
it.listAllFilesInTheDirectory()
}
} else {
if (name.contains("coverage")) {
logger.debug { "$absolutePath" }
}
}
}

private fun remoteFileForTest(filename: String): String {
return "$outputDir/$filename"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,6 @@ class AndroidDeviceTestRunner(private val device: AndroidDevice) {
val androidConfiguration = configuration.vendorConfiguration as AndroidConfiguration
val info = ApkParser().parseInstrumentationInfo(androidConfiguration.testApplicationOutput)
val runner = prepareTestRunner(configuration, androidConfiguration, info, testBatch)


try {
clearData(androidConfiguration, info)
notifyIgnoredTest(ignoredTests, listener)
Expand Down Expand Up @@ -126,6 +124,9 @@ class AndroidDeviceTestRunner(private val device: AndroidDevice) {
runner.setMaxTimeout(batchTimeout, TimeUnit.MILLISECONDS)
runner.setClassNames(tests)

androidConfiguration.instrumentationArgs.toMutableMap().apply {
put("coverage", "true")
}
androidConfiguration.instrumentationArgs.forEach { (key, value) ->
runner.addInstrumentationArg(key, value)
}
Expand Down
Loading