diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index 8da2ce7f4..b3fbeafb8 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -26,6 +26,8 @@ object Versions { val influxDbClient = "2.24" val influxDb2Client = "6.12.0" val clikt = "4.2.2" + val mordant = "2.6.0" + val mordantCoroutines = "2.6.0" val jacksonDatabind = "2.15.3" val jacksonKotlin = jacksonDatabind val jacksonYaml = jacksonDatabind @@ -76,6 +78,8 @@ object Libraries { val influxDbClient = "org.influxdb:influxdb-java:${Versions.influxDbClient}" val influxDb2Client = "com.influxdb:influxdb-client-java:${Versions.influxDb2Client}" val clikt = "com.github.ajalt.clikt:clikt:${Versions.clikt}" + val mordant = "com.github.ajalt.mordant:mordant:${Versions.mordant}" + val mordantCoroutines = "com.github.ajalt.mordant:mordant-coroutines:${Versions.mordantCoroutines}" val jacksonDatabind = "com.fasterxml.jackson.core:jackson-databind:${Versions.jacksonDatabind}" val jacksonAnnotations = "com.fasterxml.jackson.core:jackson-annotations:${Versions.jacksonAnnotations}" val jacksonKotlin = "com.fasterxml.jackson.module:jackson-module-kotlin:${Versions.jacksonKotlin}" diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/Configuration.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/Configuration.kt index a9db8075d..f55291a25 100644 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/Configuration.kt +++ b/configuration/src/main/kotlin/com/malinskiy/marathon/config/Configuration.kt @@ -42,6 +42,7 @@ data class Configuration private constructor( val testBatchTimeoutMillis: Long, val testOutputTimeoutMillis: Long, val debug: Boolean, + val prettyTerminalOutput: Boolean, val screenRecordingPolicy: ScreenRecordingPolicy, @@ -72,6 +73,7 @@ data class Configuration private constructor( "testBatchTimeoutMillis" to testBatchTimeoutMillis.toString(), "testOutputTimeoutMillis" to testOutputTimeoutMillis.toString(), "debug" to debug.toString(), + "prettyTerminalOutput" to prettyTerminalOutput.toString(), "screenRecordingPolicy" to screenRecordingPolicy.toString(), "vendorConfiguration" to vendorConfiguration.toString(), "deviceInitializationTimeoutMillis" to deviceInitializationTimeoutMillis.toString() @@ -103,6 +105,7 @@ data class Configuration private constructor( if (testBatchTimeoutMillis != other.testBatchTimeoutMillis) return false if (testOutputTimeoutMillis != other.testOutputTimeoutMillis) return false if (debug != other.debug) return false + if (prettyTerminalOutput != other.prettyTerminalOutput) return false if (screenRecordingPolicy != other.screenRecordingPolicy) return false if (vendorConfiguration != other.vendorConfiguration) return false if (analyticsTracking != other.analyticsTracking) return false @@ -133,6 +136,7 @@ data class Configuration private constructor( result = 31 * result + testBatchTimeoutMillis.hashCode() result = 31 * result + testOutputTimeoutMillis.hashCode() result = 31 * result + debug.hashCode() + result = 31 * result + prettyTerminalOutput.hashCode() result = 31 * result + screenRecordingPolicy.hashCode() result = 31 * result + vendorConfiguration.hashCode() result = 31 * result + analyticsTracking.hashCode() @@ -164,6 +168,7 @@ data class Configuration private constructor( var testBatchTimeoutMillis: Long = DEFAULT_BATCH_EXECUTION_TIMEOUT_MILLIS, var testOutputTimeoutMillis: Long = DEFAULT_OUTPUT_TIMEOUT_MILLIS, var debug: Boolean = true, + var prettyTerminalOutput: Boolean = false, var screenRecordingPolicy: ScreenRecordingPolicy = ScreenRecordingPolicy.ON_FAILURE, @@ -196,6 +201,7 @@ data class Configuration private constructor( testBatchTimeoutMillis = testBatchTimeoutMillis, testOutputTimeoutMillis = testOutputTimeoutMillis, debug = debug, + prettyTerminalOutput = prettyTerminalOutput, screenRecordingPolicy = screenRecordingPolicy, vendorConfiguration = vendorConfiguration, analyticsTracking = analyticsTracking, diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 75c68120e..977c0fc99 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -57,6 +57,8 @@ dependencies { implementation(Libraries.kotlinCoroutines) implementation(Libraries.kotlinLogging) implementation(Libraries.logbackClassic) + implementation(Libraries.mordant) + implementation(Libraries.mordantCoroutines) implementation(Libraries.influxDbClient) implementation(Libraries.influxDb2Client) implementation(Libraries.scalr) diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index cb54b159b..49485d564 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -63,6 +63,7 @@ class Marathon( private fun configureLogging() { MarathonLogging.debug = configuration.debug + MarathonLogging.prettyTerminalOutput = configuration.prettyTerminalOutput logConfigurator.configure() } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt index 6bf823235..f191022a5 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/Scheduler.kt @@ -14,6 +14,7 @@ import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier import com.malinskiy.marathon.execution.progress.PoolProgressAccumulator import com.malinskiy.marathon.extension.toPoolingStrategy import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.log.TerminalPrettyOutput import com.malinskiy.marathon.time.Timer import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -53,6 +54,7 @@ class Scheduler( */ suspend fun execute() : Boolean { subscribeOnDevices(job) + TerminalPrettyOutput.launchAnimation() try { withTimeout(configuration.deviceInitializationTimeoutMillis) { while (pools.isEmpty()) { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/execution/progress/PoolProgressAccumulator.kt b/core/src/main/kotlin/com/malinskiy/marathon/execution/progress/PoolProgressAccumulator.kt index 0f39d565d..0c1c0c0af 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/execution/progress/PoolProgressAccumulator.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/execution/progress/PoolProgressAccumulator.kt @@ -1,5 +1,6 @@ package com.malinskiy.marathon.execution.progress +import com.github.ajalt.mordant.rendering.TextColors import com.malinskiy.marathon.actor.StateMachine import com.malinskiy.marathon.analytics.internal.pub.Track import com.malinskiy.marathon.config.Configuration @@ -13,6 +14,8 @@ import com.malinskiy.marathon.execution.queue.TestAction import com.malinskiy.marathon.execution.queue.TestEvent import com.malinskiy.marathon.execution.queue.TestState import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.log.TerminalPrettyOutput +import com.malinskiy.marathon.log.TerminalPrettyOutput.terminal import com.malinskiy.marathon.test.Test import com.malinskiy.marathon.test.toTestName import kotlin.math.roundToInt @@ -261,6 +264,8 @@ class PoolProgressAccumulator( }.also { tests.putAll(it) } + TerminalPrettyOutput.addProgressBar(poolId) + TerminalPrettyOutput.updateProgressBar(poolId, tests.size.toLong()) } fun testStarted(device: DeviceInfo, test: Test) { @@ -274,28 +279,31 @@ class PoolProgressAccumulator( fun testEnded(device: DeviceInfo, testResult: TestResult, final: Boolean = false): TestAction? { return when (testResult.status) { TestStatus.FAILURE -> { - println("${toPercent(progress())} | [${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} failed") + terminal.println("[${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} ${TextColors.brightRed("failed")}") + TerminalPrettyOutput.advanceProgressBar(poolId) transition(testResult.test, TestEvent.Failed(device, testResult)).sideffect() } TestStatus.PASSED -> { - println("${toPercent(progress())} | [${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} passed") + terminal.println("[${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} ${TextColors.brightGreen("passed")}") + TerminalPrettyOutput.advanceProgressBar(poolId) transition(testResult.test, TestEvent.Passed(device, testResult)).sideffect() } TestStatus.IGNORED, TestStatus.ASSUMPTION_FAILURE -> { - println("${toPercent(progress())} | [${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} ignored") + terminal.println("[${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} ${TextColors.brightYellow("ignored")}") + TerminalPrettyOutput.advanceProgressBar(poolId) transition(testResult.test, TestEvent.Passed(device, testResult)).sideffect() } TestStatus.INCOMPLETE -> { - println("${toPercent(progress())} | [${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} incomplete") + terminal.println("[${poolId.name}]-[${device.serialNumber}] ${testResult.test.toTestName()} ${TextColors.brightBlue("incomplete")}") + TerminalPrettyOutput.advanceProgressBar(poolId) transition(testResult.test, TestEvent.Incomplete(device, testResult, final)).sideffect() } } } - /** * Should always be called before testEnded, otherwise the FSM might transition into a terminal state prematurely */ diff --git a/core/src/main/kotlin/com/malinskiy/marathon/log/MarathonLogging.kt b/core/src/main/kotlin/com/malinskiy/marathon/log/MarathonLogging.kt index 91995658e..ff1595d8c 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/log/MarathonLogging.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/log/MarathonLogging.kt @@ -7,14 +7,17 @@ import mu.KotlinLogging object MarathonLogging { var debug = true + var prettyTerminalOutput = false private var warningPrinted = false fun logger(func: () -> Unit): KLogger { - return logger(level = null, func = func) + return if (prettyTerminalOutput) { MordantLogger(KotlinLogging.logger(func)) } + else { logger(level = null, func = func) } } fun logger(name: String): KLogger { - return logger(level = null, name = name) + return if (prettyTerminalOutput) { MordantLogger(KotlinLogging.logger(name)) } + else { logger(level = null, name = name) } } fun logger(level: Level?, func: () -> Unit): KLogger { diff --git a/core/src/main/kotlin/com/malinskiy/marathon/log/MordantLogger.kt b/core/src/main/kotlin/com/malinskiy/marathon/log/MordantLogger.kt new file mode 100644 index 000000000..5fefa0395 --- /dev/null +++ b/core/src/main/kotlin/com/malinskiy/marathon/log/MordantLogger.kt @@ -0,0 +1,104 @@ +package com.malinskiy.marathon.log + +import com.github.ajalt.mordant.rendering.TextColors +import com.malinskiy.marathon.log.TerminalPrettyOutput.terminal +import mu.KLogger +import mu.Marker + +class MordantLogger(override val underlyingLogger: KLogger) : KLogger by underlyingLogger { + + override fun trace(msg: () -> Any?) { +// terminal.println(TextColors.blue(msg.toStringSafe())) + } + override fun trace(t: Throwable?, msg: () -> Any?) {} + override fun trace(marker: mu.Marker?, msg: () -> Any?) {} + override fun trace(marker: mu.Marker?, t: Throwable?, msg: () -> Any?) {} + override fun trace(p0: String?) {} + override fun trace(p0: String?, p1: Any?) {} + override fun trace(p0: String?, p1: Any?, p2: Any?) {} + override fun trace(p0: String?, vararg p1: Any?) {} + override fun trace(p0: String?, p1: Throwable?) {} + override fun trace(p0: Marker?, p1: String?) {} + override fun trace(p0: Marker?, p1: String?, p2: Any?) {} + override fun trace(p0: Marker?, p1: String?, p2: Any?, p3: Any?) {} + override fun trace(p0: Marker?, p1: String?, vararg p2: Any?) {} + override fun trace(p0: Marker?, p1: String?, p2: Throwable?) {} + + override fun debug(msg: () -> Any?) { +// terminal.println(TextColors.white(msg.toStringSafe())) + } + override fun debug(t: Throwable?, msg: () -> Any?) {} + override fun debug(marker: mu.Marker?, msg: () -> Any?) {} + override fun debug(marker: mu.Marker?, t: Throwable?, msg: () -> Any?) {} + override fun debug(format: String, vararg arguments: Any) {} + override fun debug(marker: Marker?, format: String?, vararg arguments: Any?) {} + override fun debug(marker: Marker?, format: String?, arg: Any?) {} + override fun debug(format: String?, arg: Any?) {} + override fun debug(marker: Marker?, msg: String?) {} + override fun debug(format: String?, arg1: Any?, arg2: Any?) {} + override fun debug(msg: String?, t: Throwable?) {} + override fun debug(marker: Marker?, msg: String?, t: Throwable?) {} + override fun entry(vararg argArray: Any?) {} + override fun debug(msg: String?) {} + override fun debug(marker: Marker?, format: String?, arg1: Any?, arg2: Any?) {} + + override fun info(msg: () -> Any?) { +// terminal.println(TextColors.green(msg.toStringSafe())) + } + override fun info(t: Throwable?, msg: () -> Any?) {} + override fun info(marker: mu.Marker?, msg: () -> Any?) {} + override fun info(marker: mu.Marker?, t: Throwable?, msg: () -> Any?) {} + override fun info(p0: String?) {} + override fun info(p0: String?, p1: Any?) {} + override fun info(p0: String?, p1: Any?, p2: Any?) {} + override fun info(p0: String?, vararg p1: Any?) {} + override fun info(p0: String?, p1: Throwable?) {} + override fun info(p0: Marker?, p1: String?) {} + override fun info(p0: Marker?, p1: String?, p2: Any?) {} + override fun info(p0: Marker?, p1: String?, p2: Any?, p3: Any?) {} + override fun info(p0: Marker?, p1: String?, vararg p2: Any?) {} + override fun info(p0: Marker?, p1: String?, p2: Throwable?) {} + + override fun warn(msg: () -> Any?) { + terminal.println(TextColors.yellow(msg.toStringSafe())) + } + override fun warn(t: Throwable?, msg: () -> Any?) {} + override fun warn(marker: mu.Marker?, msg: () -> Any?) {} + override fun warn(marker: mu.Marker?, t: Throwable?, msg: () -> Any?) {} + override fun warn(p0: String?) {} + override fun warn(p0: String?, p1: Any?) {} + override fun warn(p0: String?, vararg p1: Any?) {} + override fun warn(p0: String?, p1: Any?, p2: Any?) {} + override fun warn(p0: String?, p1: Throwable?) {} + override fun warn(p0: Marker?, p1: String?) {} + override fun warn(p0: Marker?, p1: String?, p2: Any?) {} + override fun warn(p0: Marker?, p1: String?, p2: Any?, p3: Any?) {} + override fun warn(p0: Marker?, p1: String?, vararg p2: Any?) {} + override fun warn(p0: Marker?, p1: String?, p2: Throwable?) {} + + override fun error(msg: () -> Any?) { + terminal.println(TextColors.brightRed(msg.toStringSafe())) + } + override fun error(t: Throwable?, msg: () -> Any?) {} + override fun error(marker: mu.Marker?, msg: () -> Any?) {} + override fun error(marker: mu.Marker?, t: Throwable?, msg: () -> Any?) {} + override fun error(p0: String?) {} + override fun error(p0: String?, p1: Any?) {} + override fun error(p0: String?, p1: Any?, p2: Any?) {} + override fun error(p0: String?, vararg p1: Any?) {} + override fun error(p0: String?, p1: Throwable?) {} + override fun error(p0: Marker?, p1: String?) {} + override fun error(p0: Marker?, p1: String?, p2: Any?) {} + override fun error(p0: Marker?, p1: String?, p2: Any?, p3: Any?) {} + override fun error(p0: Marker?, p1: String?, vararg p2: Any?) {} + override fun error(p0: Marker?, p1: String?, p2: Throwable?) {} + + @Suppress("NOTHING_TO_INLINE") + private inline fun (() -> Any?).toStringSafe(): String { + return try { + invoke().toString() + } catch (e: Exception) { + return "Log message invocation failed: $e" + } + } +} diff --git a/core/src/main/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutput.kt b/core/src/main/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutput.kt new file mode 100644 index 000000000..408ca2069 --- /dev/null +++ b/core/src/main/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutput.kt @@ -0,0 +1,75 @@ +package com.malinskiy.marathon.log + +import com.github.ajalt.mordant.animation.coroutines.animateInCoroutine +import com.github.ajalt.mordant.animation.progress.MultiProgressBarAnimation +import com.github.ajalt.mordant.animation.progress.ProgressTask +import com.github.ajalt.mordant.animation.progress.addTask +import com.github.ajalt.mordant.animation.progress.advance +import com.github.ajalt.mordant.rendering.TextAlign +import com.github.ajalt.mordant.rendering.TextStyles +import com.github.ajalt.mordant.terminal.Terminal +import com.github.ajalt.mordant.widgets.Spinner +import com.github.ajalt.mordant.widgets.progress.completed +import com.github.ajalt.mordant.widgets.progress.percentage +import com.github.ajalt.mordant.widgets.progress.progressBar +import com.github.ajalt.mordant.widgets.progress.progressBarContextLayout +import com.github.ajalt.mordant.widgets.progress.progressBarLayout +import com.github.ajalt.mordant.widgets.progress.spinner +import com.github.ajalt.mordant.widgets.progress.text +import com.github.ajalt.mordant.widgets.progress.timeElapsed +import com.malinskiy.marathon.device.DevicePoolId +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.ExperimentalTime + + +@OptIn(ExperimentalTime::class) +object TerminalPrettyOutput { + private val logger = MarathonLogging.logger("TerminalPrettyOutput") + + val terminal = Terminal() + private val animation = MultiProgressBarAnimation(terminal).animateInCoroutine() + private val overallLayout = progressBarLayout(alignColumns = false) { + text { (terminal.theme.success + TextStyles.bold)("Overall") } + completed(style = terminal.theme.success) + spinner(Spinner.Dots()) + timeElapsed(style = terminal.theme.warning, compact = false) + } + private val overall = animation.addTask(overallLayout, total = 0) + + private val poolLayout = progressBarContextLayout { + text(fps = animationFps, align = TextAlign.LEFT) { "Pool: $context" } + progressBar(width = 40) + percentage() + completed(style = terminal.theme.success) + spinner(Spinner.Dots()) + timeElapsed(style = terminal.theme.info) + } + private val progressBars = ConcurrentHashMap>() + + suspend fun launchAnimation() { + animation.execute() + } + + fun addProgressBar(poolId: DevicePoolId) { + progressBars.computeIfAbsent(poolId) { id -> + animation.addTask(poolLayout, id.name)} + overall.update { total = total?.plus(1) } + } + + fun updateProgressBar(poolId: DevicePoolId, t: Long) { + val pb = progressBars[poolId]?.update { total = t } + if (pb == null) logger.debug { "Progress bar ${poolId.name} not registered in animation" } + } + + fun advanceProgressBar(poolId: DevicePoolId) { + val pb = progressBars[poolId] + if (pb != null) { + pb.advance() + if (pb.total == pb.completed) { + overall.advance() + } + } else { + logger.debug { "Progress bar ${poolId.name} not registered in animation" } + } + } +} diff --git a/core/src/main/kotlin/com/malinskiy/marathon/report/stdout/StdoutReporter.kt b/core/src/main/kotlin/com/malinskiy/marathon/report/stdout/StdoutReporter.kt index 8fe673325..e97cb3245 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/report/stdout/StdoutReporter.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/report/stdout/StdoutReporter.kt @@ -1,5 +1,13 @@ package com.malinskiy.marathon.report.stdout +import com.github.ajalt.mordant.rendering.BorderType.Companion.SQUARE_DOUBLE_SECTION_SEPARATOR +import com.github.ajalt.mordant.rendering.TextAlign +import com.github.ajalt.mordant.rendering.TextColors +import com.github.ajalt.mordant.rendering.TextColors.Companion.rgb +import com.github.ajalt.mordant.rendering.TextStyles +import com.github.ajalt.mordant.table.Borders +import com.github.ajalt.mordant.table.table +import com.github.ajalt.mordant.terminal.Terminal import com.malinskiy.marathon.analytics.internal.sub.ExecutionReport import com.malinskiy.marathon.report.Reporter import com.malinskiy.marathon.time.Timer @@ -10,46 +18,85 @@ class StdoutReporter(private val timer: Timer) : Reporter { val summary = executionReport.summary if (summary.pools.isEmpty()) return - val cliReportBuilder = StringBuilder().appendLine("Marathon run finished:") + val terminal = Terminal() + terminal.println(TextColors.cyan("Marathon run finished:")) summary.pools.forEach { poolSummary -> - cliReportBuilder.appendLine("Device pool ${poolSummary.poolId.name}:") - cliReportBuilder.appendLine("\t${poolSummary.passed.size} passed, ${poolSummary.failed.size} failed, ${poolSummary.ignored.size} ignored tests") - - if(poolSummary.failed.isNotEmpty()){ - cliReportBuilder.appendLine("\tFailed tests:") - poolSummary.failed - .toSortedSet() - .forEach { testName -> cliReportBuilder.appendLine("\t\t$testName") } - } - - cliReportBuilder.appendLine("\tFlakiness overhead: ${formatDuration(poolSummary.rawDurationMillis - poolSummary.durationMillis)}") - cliReportBuilder.appendLine("\tRaw: ${poolSummary.rawPassed.size} passed, ${poolSummary.rawFailed.size} failed, ${poolSummary.rawIgnored.size} ignored, ${poolSummary.rawIncomplete.size} incomplete tests") - - if(poolSummary.rawFailed.isNotEmpty()){ - cliReportBuilder.appendLine("\tFailed tests:") - poolSummary.rawFailed - .groupBy { it } - .toSortedMap() - .mapValues { it.value.size } - .forEach { (testName, count) -> - cliReportBuilder.appendLine("\t\t$testName failed $count time(s)") + if (poolSummary.rawFailed.isNotEmpty()) { + terminal.println(table { + borderType = SQUARE_DOUBLE_SECTION_SEPARATOR + borderStyle = rgb("#4b25b9") + align = TextAlign.RIGHT + tableBorders = Borders.NONE + header { + cellBorders = Borders.NONE + style = TextColors.red + TextStyles.bold + align = TextAlign.CENTER + row { + cell("Failed tests (raw): ${TextColors.brightRed(poolSummary.rawFailed.size.toString())}") { + columnSpan = 2 + } + } + row ("Test name", "times") {} + } + body { + column(0) { align = TextAlign.LEFT } + column(1) { + align = TextAlign.RIGHT + style = TextColors.brightRed + } + poolSummary.rawFailed + .groupBy { it } + .toSortedMap() + .mapValues { it.value.size } + .forEach { (testName, count) -> row(testName, count) } } + footer { + style(italic = true) + column(1) { style = TextColors.red } + row { + cells("Flakiness overhead: ", formatDuration(poolSummary.rawDurationMillis - poolSummary.durationMillis)) + } + } + }) } + terminal.println() - if(poolSummary.rawIncomplete.isNotEmpty()){ - cliReportBuilder.appendLine("\tIncomplete tests:") - poolSummary.rawIncomplete - .groupBy { it } - .toSortedMap() - .mapValues { it.value.size } - .forEach { (testName, count) -> - cliReportBuilder.appendLine("\t\t$testName incomplete $count time(s)") + if (poolSummary.rawIncomplete.isNotEmpty()) { + terminal.println(table { + borderType = SQUARE_DOUBLE_SECTION_SEPARATOR + borderStyle = rgb("#4b25b9") + align = TextAlign.RIGHT + tableBorders = Borders.NONE + header { + cellBorders = Borders.NONE + style = TextColors.yellow + TextStyles.bold + align = TextAlign.CENTER + row { + cell("Incomplete tests (raw): ${TextColors.brightYellow(poolSummary.rawIncomplete.size.toString())}") { + columnSpan = 2 + } + } + row ("Test name", "times") {} + } + body { + column(0) { align = TextAlign.LEFT } + column(1) { + align = TextAlign.RIGHT + style = TextColors.brightYellow } + poolSummary.rawIncomplete + .groupBy { it } + .toSortedMap() + .mapValues { it.value.size } + .forEach { (testName, count) -> row(testName, count) } + } + }) } + terminal.println(TextColors.cyan("\nDevice pool ${poolSummary.poolId.name}:")) + terminal.println(TextColors.cyan("\t${poolSummary.passed.size} passed, ${poolSummary.failed.size} failed, ${poolSummary.ignored.size} ignored tests")) + terminal.println(TextColors.cyan(" Raw: ${poolSummary.rawPassed.size} passed, ${poolSummary.rawFailed.size} failed, ${poolSummary.rawIgnored.size} ignored, ${poolSummary.rawIncomplete.size} incomplete tests\n")) + terminal.println(TextColors.cyan("Total time: ${formatDuration(timer.elapsedTimeMillis)}\n\n")) } - cliReportBuilder.appendLine("Total time: ${formatDuration(timer.elapsedTimeMillis)}") - - println(cliReportBuilder) } private fun formatDuration(millis: Long) = if(millis > 0) DurationFormatUtils.formatDuration(millis, "H'H' mm'm' ss's'") else "0s" diff --git a/core/src/test/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutputTest.kt b/core/src/test/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutputTest.kt new file mode 100644 index 000000000..43d7eec43 --- /dev/null +++ b/core/src/test/kotlin/com/malinskiy/marathon/log/TerminalPrettyOutputTest.kt @@ -0,0 +1,24 @@ +package com.malinskiy.marathon.log + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.github.ajalt.mordant.animation.progress.ProgressTask +import com.malinskiy.marathon.device.DevicePoolId +import org.junit.jupiter.api.Test +import java.util.concurrent.ConcurrentHashMap + +class TerminalPrettyOutputTest { + + @Test + @Suppress("UNCHECKED_CAST") + fun updateProgressBar() { + val pool = DevicePoolId("PoolId") + TerminalPrettyOutput.addProgressBar(pool) + TerminalPrettyOutput.updateProgressBar(pool, 1) + val f = TerminalPrettyOutput.javaClass.getDeclaredField("progressBars") + if (f.trySetAccessible()) { + val pbs = f.get(TerminalPrettyOutput) as ConcurrentHashMap> + assertThat(pbs[pool]?.total).isEqualTo(1) + } + } +}