Skip to content

Commit

Permalink
Fix no tests being run when flowsOrder specified all tests in the w…
Browse files Browse the repository at this point in the history
…orkspace (#2003)
  • Loading branch information
bartekpacia authored Sep 3, 2024
1 parent 9ad6e39 commit 3384806
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 78 deletions.
3 changes: 1 addition & 2 deletions e2e/workspaces/demo_app/fail.yaml
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
appId: com.example.example
name: Fail
tags:
- android
- failing
---
- launchApp:
clearState: true
- tapOn:
id: non-existent-id-to-fail-this-test
id: non-existent-id-to-fail-this-test
1 change: 0 additions & 1 deletion e2e/workspaces/demo_app/fill_form.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
appId: com.example.example
name: Fill out form
tags:
- android
- passing
Expand Down
23 changes: 23 additions & 0 deletions e2e/workspaces/demo_app/swipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
appId: com.example.example
tags:
- android
- passing
---
- launchApp:
clearState: true
- tapOn: Swipe Test
- swipe:
start: 50%, 15%
end: 15%, 50%
duration: 1000
- swipe:
start: 15%, 50%
end: 85%, 85%
duration: 1000
- swipe:
start: 85%, 85%
end: 85%, 50%
duration: 1000
- tapOn:
point: 85%, 50%
- assertVisible: All green
139 changes: 80 additions & 59 deletions maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ import maestro.orchestra.util.Env.withDefaultEnvVars

@CommandLine.Command(
name = "test",
description = [
"Test a Flow or set of Flows on a local iOS Simulator or Android Emulator"
]
description = ["Test a Flow or set of Flows on a local iOS Simulator or Android Emulator"],
)
class TestCommand : Callable<Int> {

Expand All @@ -90,13 +88,13 @@ class TestCommand : Callable<Int> {

@Option(
names = ["--shard-split"],
description = ["Splits the tests across N connected devices"],
description = ["Run the tests across N connected devices, splitting the tests evenly across them"],
)
private var shardSplit: Int? = null

@Option(
names = ["--shard-all"],
description = ["Replicates all the tests across N connected devices"],
description = ["Run all the tests across N connected devices"],
)
private var shardAll: Int? = null

Expand All @@ -123,7 +121,7 @@ class TestCommand : Callable<Int> {

@Option(
names = ["--debug-output"],
description = ["Configures the debug output in this path, instead of default"]
description = ["Configures the debug output in this path, instead of default"],
)
private var debugOutput: String? = null

Expand Down Expand Up @@ -165,6 +163,12 @@ class TestCommand : Callable<Int> {
}

override fun call(): Int {
TestDebugReporter.install(
debugOutputPathAsString = debugOutput,
flattenDebugOutput = flattenDebugOutput,
printToConsole = parent?.verbose == true,
)

if (shardSplit != null && shardAll != null) {
throw CliError("Options --shard-split and --shard-all are mutually exclusive.")
}
Expand All @@ -173,11 +177,12 @@ class TestCommand : Callable<Int> {
PrintUtils.warn("--shards option is deprecated and will be removed in the next Maestro version. Use --shard-split or --shard-all instead.")
shardSplit = legacyShardCount
}

val executionPlan = try {
WorkspaceExecutionPlanner.plan(
flowFile.toPath().toAbsolutePath(),
includeTags,
excludeTags
input = flowFile.toPath().toAbsolutePath(),
includeTags = includeTags,
excludeTags = excludeTags,
)
} catch (e: ValidationError) {
throw CliError(e.message)
Expand All @@ -187,17 +192,18 @@ class TestCommand : Callable<Int> {
.withInjectedShellEnvVars()
.withDefaultEnvVars(flowFile)

TestDebugReporter.install(
debugOutputPathAsString = debugOutput,
flattenDebugOutput = flattenDebugOutput,
printToConsole = parent?.verbose == true,
)
val debugOutputPath = TestDebugReporter.getDebugOutputPath()

return handleSessions(debugOutputPath, executionPlan)
}

private fun handleSessions(debugOutputPath: Path, plan: ExecutionPlan): Int = runBlocking(Dispatchers.IO) {
val requestedShards = shardSplit ?: shardAll ?: 1
if (requestedShards > 1 && plan.sequence.flows.isNotEmpty()) {
error("Cannot run sharded tests with sequential execution")
}

val onlySequenceFlows = plan.sequence.flows.isNotEmpty() && plan.flowsToRun.isEmpty() // An edge case

runCatching {
val deviceIds = (if (isWebFlow())
Expand All @@ -214,29 +220,57 @@ class TestCommand : Callable<Int> {
it.instanceId
}.toMutableSet())

val shards = shardSplit ?: shardAll ?: 1

val availableDevices = if (deviceIds.isNotEmpty()) deviceIds.size else initialActiveDevices.size
val effectiveShards = shards.coerceAtMost(plan.flowsToRun.size)
val sharded = effectiveShards > 1

val chunkPlans =
if (shardAll != null) (0 until effectiveShards).map { plan.copy() }
else plan.flowsToRun
.withIndex()
.groupBy { it.index % effectiveShards }
.map { (shardIndex, files) ->
ExecutionPlan(
files.map { it.value },
plan.sequence.also {
if (it?.flows?.isNotEmpty() == true && sharded)
error("Cannot run sharded tests with sequential execution.")
}
)
val effectiveShards = if (onlySequenceFlows) 1 else requestedShards.coerceAtMost(plan.flowsToRun.size)

if (requestedShards > plan.flowsToRun.size) {
PrintUtils.warn("Requested $requestedShards shards, but it cannot be higher than the number of flows (${plan.flowsToRun.size}). Will use $effectiveShards shards instead.")
}

val chunkPlans: List<ExecutionPlan> = if (onlySequenceFlows) {
// Handle an edge case
// We only want to run sequential flows in this case.
listOf(plan)
} else if (shardAll != null) {
(0 until effectiveShards).map { plan.copy() }
} else {
plan.flowsToRun
.withIndex()
.groupBy { it.index % effectiveShards }
.map { (shardIndex, files) ->
ExecutionPlan(
flowsToRun = files.map { it.value },
sequence = plan.sequence,
)
}
}

PrintUtils.info(buildString {
val flowCount = if (onlySequenceFlows) plan.sequence.flows.size else plan.flowsToRun.size

val message = when {
shardAll != null -> "Will run $effectiveShards shards, with all $flowCount flows in each shard"

shardSplit != null -> {
val flowsPerShard = flowCount / effectiveShards
val isApprox = flowCount % effectiveShards != 0
val prefix = if (isApprox) "approx. " else ""
"Will split $flowCount flows across $effectiveShards shards (${prefix}$flowsPerShard flows per shard)"
}

else -> "Will run $flowCount flows in a single shard"
}

appendLine(message)
})

// Collect device configurations for missing shards, if any
val missing = effectiveShards - availableDevices

if (missing > 0) {
PrintUtils.warn("$availableDevices device(s) connected, which is not enough to run $effectiveShards shards. Missing $missing device(s).")
}

val allDeviceConfigs = if (shardAll == null) (0 until missing).map { shardIndex ->
PrintUtils.message("------------------ Shard ${shardIndex + 1} ------------------")
// Collect device configurations here, one per shard
Expand All @@ -247,10 +281,13 @@ class TestCommand : Callable<Int> {

val results = (0 until effectiveShards).map { shardIndex ->
async(Dispatchers.IO) {
val driverHostPort = if (!sharded) parent?.port ?: 7001 else
val driverHostPort = if (effectiveShards == 1) {
parent?.port ?: 7001
} else {
(7001..7128).shuffled().find { port ->
usedPorts.putIfAbsent(port, true) == null
} ?: error("No available ports found")
}

// Acquire lock to execute device creation block
deviceCreationSemaphore.acquire()
Expand All @@ -262,18 +299,11 @@ class TestCommand : Callable<Int> {
val cfg = allDeviceConfigs.first()
allDeviceConfigs.remove(cfg)
val deviceCreated = DeviceCreateUtil.getOrCreateDevice(
cfg.platform,
cfg.osVersion,
null,
null,
true,
shardIndex
cfg.platform, cfg.osVersion, null, null, true, shardIndex
)

DeviceService.startDevice(
deviceCreated,
driverHostPort,
initialActiveDevices + currentActiveDevices
deviceCreated, driverHostPort, initialActiveDevices + currentActiveDevices
).instanceId.also {
currentActiveDevices.add(it)
delay(2.seconds)
Expand Down Expand Up @@ -335,12 +365,7 @@ class TestCommand : Callable<Int> {
if (DisableAnsiMixin.ansiEnabled && parent?.verbose == false) AnsiResultView()
else PlainTextResultView()
val resultSingle = TestRunner.runSingle(
maestro,
device,
flowFile,
env,
resultView,
debugOutputPath
maestro, device, flowFile, env, resultView, debugOutputPath
)
if (resultSingle == 1) {
printExitDebugMessage()
Expand All @@ -362,7 +387,7 @@ class TestCommand : Callable<Int> {

suites.mergeSummaries()?.saveReport()

if (sharded) printShardsMessage(passed, total, suites)
if (effectiveShards > 1) printShardsMessage(passed, total, suites)
if (passed == total) 0 else 1
}.onFailure {
PrintUtils.message("❌ Error: ${it.message}")
Expand All @@ -380,10 +405,9 @@ class TestCommand : Callable<Int> {

private fun printShardsMessage(passedTests: Int, totalTests: Int, shardResults: List<TestExecutionSummary>) {
val box = buildString {
val lines = listOf("Passed: $passedTests/$totalTests") +
shardResults.mapIndexed { index, result ->
"[ ${result.suites.first().deviceName} ] - ${result.passedCount ?: 0}/${result.totalTests ?: 0}"
}
val lines = listOf("Passed: $passedTests/$totalTests") + shardResults.mapIndexed { index, result ->
"[ ${result.suites.first().deviceName} ] - ${result.passedCount ?: 0}/${result.totalTests ?: 0}"
}

val lineWidth = lines.maxOf(String::length)
append("${"".repeat(lineWidth)}\n")
Expand All @@ -397,8 +421,7 @@ class TestCommand : Callable<Int> {
val reporter = ReporterFactory.buildReporter(format, testSuiteName)

format.fileExtension?.let { extension ->
(output ?: File("report$extension"))
.sink()
(output ?: File("report$extension")).sink()
}?.also { sink ->
reporter.report(
this,
Expand All @@ -408,11 +431,9 @@ class TestCommand : Callable<Int> {
}

private fun List<TestExecutionSummary>.mergeSummaries(): TestExecutionSummary? = reduceOrNull { acc, summary ->
TestExecutionSummary(
passed = acc.passed && summary.passed,
TestExecutionSummary(passed = acc.passed && summary.passed,
suites = acc.suites + summary.suites,
passedCount = sumOf { it.passedCount ?: 0 },
totalTests = sumOf { it.totalTests ?: 0 }
)
totalTests = sumOf { it.totalTests ?: 0 })
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ object TestDebugReporter {
logger.info("---------------------")
}

/**
* Calls to this method should be done as soon as possible, to make all
* loggers use our custom configuration rather than the defaults.
*/
fun install(debugOutputPathAsString: String?, flattenDebugOutput: Boolean = false, printToConsole: Boolean) {
this.debugOutputPathAsString = debugOutputPathAsString
this.flattenDebugOutput = flattenDebugOutput
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import kotlin.time.Duration.Companion.seconds
* Similar to [TestRunner], but:
* * can run many flows at once
* * does not support continuous mode
*
* Does not care about sharding. It only has to know the index of the shard it's running it, for logging purposes.
*/
class TestSuiteInteractor(
private val maestro: Maestro,
Expand Down
10 changes: 10 additions & 0 deletions maestro-cli/src/main/java/maestro/cli/util/PrintUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ import kotlin.system.exitProcess

object PrintUtils {

fun info(message: String, bold: Boolean = false, newline: Boolean = true) {
val function: (Any) -> Unit = if (newline) ::println else ::print
function(
Ansi.ansi()
.bold(apply = bold)
.render(message)
.boldOff()
)
}

fun message(message: String) {
println(Ansi.ansi().render("@|cyan \n$message|@"))
}
Expand Down
Loading

0 comments on commit 3384806

Please sign in to comment.