From 63444caa4b0393ce9ce15a98068ae177e73869f1 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Tue, 16 Jul 2024 21:07:08 +1000 Subject: [PATCH] feat(android): add filtered logcat to am instrument parser --- .../marathon/android/AndroidDevice.kt | 3 +- .../android/adam/AdamAndroidDevice.kt | 23 ++- .../android/adam/AmInstrumentTestParser.kt | 182 ++++++++++-------- .../marathon/android/adam/LogcatManager.kt | 4 +- .../adam/log/LogcatAccumulatingListener.kt | 42 ++++ .../marathon/android/logcat/LogcatListener.kt | 9 + .../marathon/android/logcat/LogcatProducer.kt | 6 + 7 files changed, 177 insertions(+), 92 deletions(-) create mode 100644 vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/log/LogcatAccumulatingListener.kt create mode 100644 vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatListener.kt create mode 100644 vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatProducer.kt diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt index 101718a07..be1e5f31b 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/AndroidDevice.kt @@ -1,6 +1,7 @@ package com.malinskiy.marathon.android import com.android.sdklib.AndroidVersion +import com.malinskiy.marathon.android.logcat.LogcatProducer import com.malinskiy.marathon.android.model.ShellCommandResult import com.malinskiy.marathon.device.screenshot.Rotation import com.malinskiy.marathon.config.vendor.android.VideoConfiguration @@ -8,7 +9,7 @@ import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.screenshot.Screenshottable import com.malinskiy.marathon.report.logs.LogProducer -interface AndroidDevice : Device, LogProducer, Screenshottable { +interface AndroidDevice : Device, LogProducer, LogcatProducer, Screenshottable { val apiLevel: Int val version: AndroidVersion val fileManager: RemoteFileManager diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamAndroidDevice.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamAndroidDevice.kt index 4eb5375c8..c1da886fd 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamAndroidDevice.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AdamAndroidDevice.kt @@ -39,11 +39,13 @@ import com.malinskiy.marathon.android.AndroidTestBundleIdentifier import com.malinskiy.marathon.android.BaseAndroidDevice import com.malinskiy.marathon.android.RemoteFileManager import com.malinskiy.marathon.android.adam.extension.toShellResult +import com.malinskiy.marathon.android.adam.log.LogCatMessage import com.malinskiy.marathon.android.exception.CommandRejectedException import com.malinskiy.marathon.android.exception.InstallException import com.malinskiy.marathon.exceptions.TransferException import com.malinskiy.marathon.execution.listener.LineListener import com.malinskiy.marathon.android.extension.toScreenRecorderCommand +import com.malinskiy.marathon.android.logcat.LogcatListener import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.config.vendor.android.SerialStrategy @@ -358,13 +360,22 @@ class AdamAndroidDevice( } } - private val logcatListeners = CopyOnWriteArrayList() + private val logListeners = CopyOnWriteArrayList() + private val logcatListeners = CopyOnWriteArrayList() override fun addLineListener(listener: LineListener) { - logcatListeners.add(listener) + logListeners.add(listener) } override fun removeLineListener(listener: LineListener) { + logListeners.remove(listener) + } + + override fun addLogcatListener(listener: LogcatListener) { + logcatListeners.add(listener) + } + + override fun removeLogcatListener(listener: LogcatListener) { logcatListeners.remove(listener) } @@ -471,8 +482,14 @@ class AdamAndroidDevice( return client.execute(runnerRequest, serial = adbSerial) } + suspend fun onLogcat(msg: LogCatMessage) { + //Bridge to regular string lines log + onLine("${msg.timestamp} ${msg.pid}-${msg.tid}/${msg.appName} ${msg.logLevel.priorityLetter}/${msg.tag}: ${msg.message}") + logcatListeners.forEach { it.onMessage(msg) } + } + override suspend fun onLine(line: String) { - logcatListeners.forEach { listener -> + logListeners.forEach { listener -> listener.onLine(line) } } diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AmInstrumentTestParser.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AmInstrumentTestParser.kt index 7a5b52fb5..fa1f6de36 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AmInstrumentTestParser.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/AmInstrumentTestParser.kt @@ -16,6 +16,7 @@ import com.malinskiy.adam.request.testrunner.TestStarted import com.malinskiy.marathon.android.AndroidAppInstaller import com.malinskiy.marathon.android.AndroidTestBundleIdentifier import com.malinskiy.marathon.android.adam.event.TestAnnotationParser +import com.malinskiy.marathon.android.adam.log.LogcatAccumulatingListener import com.malinskiy.marathon.android.extension.testBundlesCompat import com.malinskiy.marathon.android.model.AndroidTestBundle import com.malinskiy.marathon.android.model.TestIdentifier @@ -56,11 +57,12 @@ class AmInstrumentTestParser( throw e } catch (e: TestAnnotationProducerNotFoundException) { logger.warn { - """ - Previous parsing attempt failed for ${e.instrumentationPackage} - file: ${e.testApplication} - due to test parser misconfiguration: test annotation producer was not found. See https://docs.marathonlabs.io/runner/android/configure#test-parser - Next parsing attempt will remove overridden test run listener. + """Previous parsing attempt failed for ${e.instrumentationPackage} + file: ${e.testApplication} + due to test parser misconfiguration: test annotation producer was not found. See https://docs.marathonlabs.io/runner/android/configure#test-parser + Next parsing attempt will remove overridden test run listener. + Device log: + ${e.logcat} """.trimIndent() } blockListenerArgumentOverride = true @@ -82,101 +84,111 @@ class AmInstrumentTestParser( val androidAppInstaller = AndroidAppInstaller(configuration) androidAppInstaller.prepareInstallation(device) - return testBundles.flatMap { bundle -> - val androidTestBundle = - AndroidTestBundle(bundle.application, bundle.testApplication, bundle.extraApplications, bundle.splitApks) - val instrumentationInfo = androidTestBundle.instrumentationInfo - - val testParserConfiguration = vendorConfiguration.testParserConfiguration - val overrides: Map = when { - testParserConfiguration is TestParserConfiguration.RemoteTestParserConfiguration -> { - if (blockListenerArgumentOverride) testParserConfiguration.instrumentationArgs - .filterNot { it.key == LISTENER_ARGUMENT && it.value == TEST_ANNOTATION_PRODUCER } - else testParserConfiguration.instrumentationArgs - } - - else -> emptyMap() - } + val listener = LogcatAccumulatingListener(device) + listener.setup() + + try { + return testBundles.flatMap { bundle -> + val androidTestBundle = + AndroidTestBundle(bundle.application, bundle.testApplication, bundle.extraApplications, bundle.splitApks) + val instrumentationInfo = androidTestBundle.instrumentationInfo + + val testParserConfiguration = vendorConfiguration.testParserConfiguration + val overrides: Map = when { + testParserConfiguration is TestParserConfiguration.RemoteTestParserConfiguration -> { + if (blockListenerArgumentOverride) testParserConfiguration.instrumentationArgs + .filterNot { it.key == LISTENER_ARGUMENT && it.value == TEST_ANNOTATION_PRODUCER } + else testParserConfiguration.instrumentationArgs + } - val runnerRequest = TestRunnerRequest( - testPackage = instrumentationInfo.instrumentationPackage, - runnerClass = instrumentationInfo.testRunnerClass, - instrumentOptions = InstrumentOptions( - log = true, - overrides = overrides, - ), - supportedFeatures = device.supportedFeatures, - coroutineScope = device, - ) - val channel = device.executeTestRequest(runnerRequest) - var observedAnnotations = false - - val tests = mutableSetOf() - while (!channel.isClosedForReceive && isActive) { - val events: List? = withTimeoutOrNull(configuration.testOutputTimeoutMillis) { - channel.receiveCatching().getOrNull() ?: emptyList() + else -> emptyMap() } - if (events == null) { - throw TestParsingException("Unable to parse test list using ${device.serialNumber}") - } else { - throw TestAnnotationProducerNotFoundException( - instrumentationInfo.instrumentationPackage, - androidTestBundle.testApplication - ) - for (event in events) { - when (event) { - is TestRunStartedEvent -> Unit - is TestStarted -> Unit - is TestFailed -> Unit - is TestAssumptionFailed -> Unit - is TestIgnored -> Unit - is TestEnded -> { - val annotations = testAnnotationParser.extractAnnotations(event) - if (annotations.isNotEmpty()) { - observedAnnotations = true + + val runnerRequest = TestRunnerRequest( + testPackage = instrumentationInfo.instrumentationPackage, + runnerClass = instrumentationInfo.testRunnerClass, + instrumentOptions = InstrumentOptions( + log = true, + overrides = overrides, + ), + supportedFeatures = device.supportedFeatures, + coroutineScope = device, + ) + listener.start() + val channel = device.executeTestRequest(runnerRequest) + var observedAnnotations = false + + val tests = mutableSetOf() + while (!channel.isClosedForReceive && isActive) { + val events: List? = withTimeoutOrNull(configuration.testOutputTimeoutMillis) { + channel.receiveCatching().getOrNull() ?: emptyList() + } + if (events == null) { + throw TestParsingException("Unable to parse test list using ${device.serialNumber}") + } else { + for (event in events) { + when (event) { + is TestRunStartedEvent -> Unit + is TestStarted -> Unit + is TestFailed -> Unit + is TestAssumptionFailed -> Unit + is TestIgnored -> Unit + is TestEnded -> { + val annotations = testAnnotationParser.extractAnnotations(event) + if (annotations.isNotEmpty()) { + observedAnnotations = true + } + val test = TestIdentifier(event.id.className, event.id.testName).toTest(annotations) + tests.add(test) + testBundleIdentifier.put(test, androidTestBundle) } - val test = TestIdentifier(event.id.className, event.id.testName).toTest(annotations) - tests.add(test) - testBundleIdentifier.put(test, androidTestBundle) - } - is TestRunFailing -> { - // Error message is stable, see https://github.com/android/android-test/blame/1ae53b93e02cc363311f6564a35edeea1b075103/runner/android_junit_runner/java/androidx/test/internal/runner/RunnerArgs.java#L624 - if (event.error.contains("Could not find extra class $TEST_ANNOTATION_PRODUCER")) { - throw TestAnnotationProducerNotFoundException( - instrumentationInfo.instrumentationPackage, - androidTestBundle.testApplication - ) + is TestRunFailing -> { + // Error message is stable, see https://github.com/android/android-test/blame/1ae53b93e02cc363311f6564a35edeea1b075103/runner/android_junit_runner/java/androidx/test/internal/runner/RunnerArgs.java#L624 + val logcat = listener.stop() + if (event.error.contains("Could not find extra class $TEST_ANNOTATION_PRODUCER")) { + throw TestAnnotationProducerNotFoundException( + instrumentationInfo.instrumentationPackage, + androidTestBundle.testApplication, + logcat, + ) + } } - } - is TestRunFailed -> { - //Happens on Android Wear if classpath is misconfigured - if (event.error.contains("Process crashed")) { - throw TestAnnotationProducerNotFoundException( - instrumentationInfo.instrumentationPackage, - androidTestBundle.testApplication - ) + is TestRunFailed -> { + val logcat = listener.stop() + //Happens on Android Wear if classpath is misconfigured + if (event.error.contains("Process crashed")) { + throw TestAnnotationProducerNotFoundException( + instrumentationInfo.instrumentationPackage, + androidTestBundle.testApplication, + logcat, + ) + } } - } - is TestRunStopped -> Unit - is TestRunEnded -> Unit + is TestRunStopped -> Unit + is TestRunEnded -> Unit + } } } } - } + listener.finish() - if (!observedAnnotations) { - logger.warn { - "Bundle ${bundle.id} did not report any test annotations. If you need test annotations retrieval, remote test parser requires additional setup " + - "see https://docs.marathonlabs.io/runner/android/configure#test-parser" + if (!observedAnnotations) { + logger.warn { + "Bundle ${bundle.id} did not report any test annotations. If you need test annotations retrieval, remote test parser requires additional setup " + + "see https://docs.marathonlabs.io/runner/android/configure#test-parser" + } } - } - tests + tests + } + } finally { + listener.stop() } } } -private class TestAnnotationProducerNotFoundException(val instrumentationPackage: String, val testApplication: File) : RuntimeException() +private class TestAnnotationProducerNotFoundException(val instrumentationPackage: String, val testApplication: File, val logcat: String) : + RuntimeException() diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/LogcatManager.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/LogcatManager.kt index 7270a7055..4ba3a992e 100644 --- a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/LogcatManager.kt +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/LogcatManager.kt @@ -36,9 +36,7 @@ class LogcatManager : CoroutineScope { val parser = LogCatMessageParser() for (logPart in logcatChannel) { val messages = parser.processLogLines(logPart.lines(), device) - messages.forEach { msg -> - device.onLine("${msg.timestamp} ${msg.pid}-${msg.tid}/${msg.appName} ${msg.logLevel.priorityLetter}/${msg.tag}: ${msg.message}") - } + messages.forEach { msg -> device.onLogcat(msg) } } } } diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/log/LogcatAccumulatingListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/log/LogcatAccumulatingListener.kt new file mode 100644 index 000000000..be47e01ff --- /dev/null +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/adam/log/LogcatAccumulatingListener.kt @@ -0,0 +1,42 @@ +package com.malinskiy.marathon.android.adam.log + +import com.malinskiy.marathon.android.logcat.LogcatListener +import com.malinskiy.marathon.android.logcat.LogcatProducer + +class LogcatAccumulatingListener( + private val logcatProducer: LogcatProducer, +) : LogcatListener { + private val stringBuffer = StringBuffer(4096) + private val allowlist = setOf("AndroidJUnitRunner", "AndroidRuntime") + + override suspend fun onMessage(msg: LogCatMessage) { + if (allowlist.contains(msg.tag)) { + when (msg.logLevel) { + Log.LogLevel.ERROR, Log.LogLevel.WARN -> stringBuffer.appendLine("${msg.logLevel.priorityLetter}/${msg.tag}: ${msg.message}\"") + else -> Unit + } + } + } + + fun setup() { + logcatProducer.addLogcatListener(this) + } + + fun finish() { + logcatProducer.removeLogcatListener(this) + } + + fun start() { + stringBuffer.reset() + } + + fun stop(): String { + val log = stringBuffer.toString() + stringBuffer.reset() + return log + } + + private fun StringBuffer.reset() { + delete(0, length) + } +} diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatListener.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatListener.kt new file mode 100644 index 000000000..24bf9ac8d --- /dev/null +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatListener.kt @@ -0,0 +1,9 @@ +package com.malinskiy.marathon.android.logcat + +import com.malinskiy.marathon.android.adam.log.LogCatMessage + +interface LogcatListener : AutoCloseable { + suspend fun onMessage(message: LogCatMessage) + + override fun close() {} +} diff --git a/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatProducer.kt b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatProducer.kt new file mode 100644 index 000000000..730c8eb33 --- /dev/null +++ b/vendor/vendor-android/src/main/kotlin/com/malinskiy/marathon/android/logcat/LogcatProducer.kt @@ -0,0 +1,6 @@ +package com.malinskiy.marathon.android.logcat + +interface LogcatProducer { + fun addLogcatListener(listener: LogcatListener) + fun removeLogcatListener(listener: LogcatListener) +}