Skip to main content
Glama
TestSuiteStatusView.kt7.68 kB
package maestro.cli.view import maestro.cli.api.UploadStatus import maestro.cli.model.FlowStatus import maestro.cli.util.PrintUtils import maestro.cli.view.TestSuiteStatusView.TestSuiteViewModel.FlowResult import maestro.cli.view.TestSuiteStatusView.uploadUrl import org.fusesource.jansi.Ansi import java.util.UUID import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds object TestSuiteStatusView { fun showFlowCompletion(result: FlowResult) { val shardPrefix = result.shardIndex?.let { "[shard ${it + 1}] " }.orEmpty() print(Ansi.ansi().fgCyan().render(shardPrefix).fgDefault()) printStatus(result.status, result.cancellationReason) val durationString = result.duration?.let { " ($it)" }.orEmpty() print(" ${result.name}$durationString") if (result.status == FlowStatus.ERROR && result.error != null) { val error = " (${result.error})" print(Ansi.ansi().fgRed().render(error).fgDefault()) } else if (result.status == FlowStatus.WARNING) { val warning = " (Warning)" print(Ansi.ansi().fgYellow().render(warning).fgDefault()) } println() } fun showSuiteResult( suite: TestSuiteViewModel, uploadUrl: String, ) { val hasError = suite.flows.find { it.status == FlowStatus.ERROR } != null val canceledFlows = suite.flows .filter { it.status == FlowStatus.CANCELED } val shardPrefix = suite.shardIndex?.let { "[shard ${it + 1}] " }.orEmpty() if (suite.status == FlowStatus.ERROR || hasError) { val failedFlows = suite.flows .filter { it.status == FlowStatus.ERROR } PrintUtils.err( "${shardPrefix}${failedFlows.size}/${suite.flows.size} ${flowWord(failedFlows.size)} Failed", bold = true, ) if (canceledFlows.isNotEmpty()) { PrintUtils.warn("${shardPrefix}${canceledFlows.size} ${flowWord(canceledFlows.size)} Canceled") } } else { val passedFlows = suite.flows .filter { it.status == FlowStatus.SUCCESS || it.status == FlowStatus.WARNING } if (passedFlows.isNotEmpty()) { val durationMessage = suite.duration?.let { " in $it" } ?: "" PrintUtils.success( "${shardPrefix}${passedFlows.size}/${suite.flows.size} ${flowWord(passedFlows.size)} Passed$durationMessage", bold = true, ) if (canceledFlows.isNotEmpty()) { PrintUtils.warn("${shardPrefix}${canceledFlows.size} ${flowWord(canceledFlows.size)} Canceled") } } else { println() PrintUtils.err("${shardPrefix}All flows were canceled") } } println() if (suite.uploadDetails != null) { PrintUtils.info("==== View Details on Maestro Cloud ====") PrintUtils.info(uploadUrl.cyan()) println() } } private fun printStatus(status: FlowStatus, cancellationReason: UploadStatus.CancellationReason?) { val color = when (status) { FlowStatus.SUCCESS, FlowStatus.WARNING -> Ansi.Color.GREEN FlowStatus.ERROR -> Ansi.Color.RED FlowStatus.STOPPED -> Ansi.Color.RED else -> Ansi.Color.DEFAULT } val title = when (status) { FlowStatus.SUCCESS, FlowStatus.WARNING -> "Passed" FlowStatus.ERROR -> "Failed" FlowStatus.PENDING -> "Pending" FlowStatus.RUNNING -> "Running" FlowStatus.STOPPED -> "Stopped" FlowStatus.PREPARING -> "Preparing Device" FlowStatus.INSTALLING -> "Installing App" FlowStatus.CANCELED -> when (cancellationReason) { UploadStatus.CancellationReason.TIMEOUT -> "Timeout" UploadStatus.CancellationReason.OVERLAPPING_BENCHMARK -> "Skipped" UploadStatus.CancellationReason.BENCHMARK_DEPENDENCY_FAILED -> "Skipped" UploadStatus.CancellationReason.CANCELED_BY_USER -> "Canceled by user" UploadStatus.CancellationReason.RUN_EXPIRED -> "Run expired" else -> "Canceled (unknown reason)" } } print( Ansi.ansi() .fgBright(color) .render("[$title]") .fgDefault() ) } fun uploadUrl( projectId: String, appId: String, uploadId: String, domain: String = "" ): String { return if (domain.contains("localhost")) { "http://localhost:3000/project/$projectId/maestro-test/app/$appId/upload/$uploadId" } else { "https://app.maestro.dev/project/$projectId/maestro-test/app/$appId/upload/$uploadId" } } private fun flowWord(count: Int) = if (count == 1) "Flow" else "Flows" data class TestSuiteViewModel( val status: FlowStatus, val flows: List<FlowResult>, val duration: Duration? = null, val shardIndex: Int? = null, val uploadDetails: UploadDetails? = null, ) { data class FlowResult( val name: String, val status: FlowStatus, val duration: Duration? = null, val error: String? = null, val shardIndex: Int? = null, val cancellationReason: UploadStatus.CancellationReason? = null ) data class UploadDetails( val uploadId: String, val appId: String, val domain: String, ) companion object { fun UploadStatus.toViewModel( uploadDetails: UploadDetails ) = TestSuiteViewModel( uploadDetails = uploadDetails, status = FlowStatus.from(status), flows = flows.map { it.toViewModel() } ) fun UploadStatus.FlowResult.toViewModel() = FlowResult( name = name, status = status, error = errors.firstOrNull(), cancellationReason = cancellationReason, duration = totalTime?.milliseconds ) } } } // Helper launcher to play around with presentation fun main() { val uploadDetails = TestSuiteStatusView.TestSuiteViewModel.UploadDetails( uploadId = UUID.randomUUID().toString(), appId = "appid", domain = "mobile.dev", ) val status = TestSuiteStatusView.TestSuiteViewModel( uploadDetails = uploadDetails, status = FlowStatus.CANCELED, flows = listOf( FlowResult( name = "A", status = FlowStatus.SUCCESS, duration = 42.seconds, ), FlowResult( name = "B", status = FlowStatus.SUCCESS, duration = 231.seconds, ), FlowResult( name = "C", status = FlowStatus.CANCELED, ) ), duration = 273.seconds, ) status.flows .forEach { TestSuiteStatusView.showFlowCompletion(it) } val uploadUrl = uploadUrl( uploadDetails.uploadId.toString(), "teamid", uploadDetails.appId, uploadDetails.domain, ) TestSuiteStatusView.showSuiteResult(status, uploadUrl) }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mobile-dev-inc/Maestro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server