Skip to content

Commit

Permalink
Handle multiple flow files in test command (#1995)
Browse files Browse the repository at this point in the history
Co-authored-by: Bartek Pacia <[email protected]>
  • Loading branch information
tokou and bartekpacia committed Sep 4, 2024
1 parent ec553d9 commit 8cd7387
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 95 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ class CloudCommand : Callable<Int> {
PrintUtils.message("Evaluating workspace...")
WorkspaceExecutionPlanner
.plan(
input = flowsFile.toPath().toAbsolutePath(),
input = setOf(flowsFile.toPath().toAbsolutePath()),
includeTags = includeTags,
excludeTags = excludeTags,
config = configFile?.toPath()?.toAbsolutePath(),
Expand Down
30 changes: 18 additions & 12 deletions maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import java.util.concurrent.CountDownLatch
import kotlin.io.path.absolutePathString
import kotlin.system.exitProcess
import kotlin.time.Duration.Companion.seconds
import maestro.utils.isSingleFile
import maestro.orchestra.util.Env.withDefaultEnvVars

@CommandLine.Command(
Expand All @@ -76,8 +77,8 @@ class TestCommand : Callable<Int> {
@CommandLine.ParentCommand
private val parent: App? = null

@CommandLine.Parameters
private lateinit var flowFile: File
@CommandLine.Parameters(description = ["One or more flow files or folders containing flow files"])
private lateinit var flowFiles: Set<File>

@Option(
names = ["--config"],
Expand Down Expand Up @@ -159,8 +160,8 @@ class TestCommand : Callable<Int> {
private val currentActiveDevices = ConcurrentSet<String>()

private fun isWebFlow(): Boolean {
if (!flowFile.isDirectory) {
val config = YamlCommandReader.readConfig(flowFile.toPath())
if (flowFiles.isSingleFile) {
val config = YamlCommandReader.readConfig(flowFiles.first().toPath())
return Regex("http(s?)://").containsMatchIn(config.appId)
}

Expand Down Expand Up @@ -189,7 +190,7 @@ class TestCommand : Callable<Int> {

val executionPlan = try {
WorkspaceExecutionPlanner.plan(
input = flowFile.toPath().toAbsolutePath(),
input = flowFiles.map { it.toPath().toAbsolutePath() }.toSet(),
includeTags = includeTags,
excludeTags = excludeTags,
config = configFile?.toPath()?.toAbsolutePath(),
Expand All @@ -198,10 +199,6 @@ class TestCommand : Callable<Int> {
throw CliError(e.message)
}

env = env
.withInjectedShellEnvVars()
.withDefaultEnvVars(flowFile)

val debugOutputPath = TestDebugReporter.getDebugOutputPath()

return handleSessions(debugOutputPath, executionPlan)
Expand Down Expand Up @@ -336,12 +333,12 @@ class TestCommand : Callable<Int> {
val maestro = session.maestro
val device = session.device

if (flowFile.isDirectory || format != ReportFormat.NOOP) {
if (flowFiles.isSingleFile.not() || format != ReportFormat.NOOP) {
// Run multiple flows
if (continuous) {
val error =
if (format != ReportFormat.NOOP) "Format can not be different from NOOP in continuous mode. Passed format is $format."
else "Continuous mode is not supported for directories. $flowFile is a directory"
else "Continuous mode is only supported in case of a single flow file. ${flowFiles.joinToString(", ") { it.absolutePath } } has more that a single flow file."
throw CommandLine.ParameterException(commandSpec.commandLine(), error)
}

Expand All @@ -363,6 +360,10 @@ class TestCommand : Callable<Int> {
return@newSession Triple(suiteResult.passedCount, suiteResult.totalTests, suiteResult)
} else {
// Run a single flow
val flowFile = flowFiles.first()
env = env
.withInjectedShellEnvVars()
.withDefaultEnvVars(flowFile)

if (continuous) {
if (!flattenDebugOutput) {
Expand All @@ -375,7 +376,12 @@ 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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import java.io.File
import java.nio.file.Path
import kotlin.system.measureTimeMillis
import kotlin.time.Duration.Companion.seconds
import maestro.orchestra.util.Env.withDefaultEnvVars
import maestro.orchestra.util.Env.withInjectedShellEnvVars

/**
* Similar to [TestRunner], but:
Expand Down Expand Up @@ -65,7 +67,11 @@ class TestSuiteInteractor(
// first run sequence of flows if present
val flowSequence = executionPlan.sequence
for (flow in flowSequence?.flows ?: emptyList()) {
val (result, aiOutput) = runFlow(flow.toFile(), env, maestro, debugOutputPath)
val flowFile = flow.toFile()
val updatedEnv = env
.withInjectedShellEnvVars()
.withDefaultEnvVars(flowFile)
val (result, aiOutput) = runFlow(flowFile, updatedEnv, maestro, debugOutputPath)
flowResults.add(result)
aiOutputs.add(aiOutput)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,86 +8,83 @@ import maestro.orchestra.yaml.YamlCommandReader
import org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.absolute
import kotlin.io.path.absolutePathString
import kotlin.io.path.exists
import kotlin.io.path.isRegularFile
import kotlin.io.path.name
import kotlin.io.path.notExists
import kotlin.io.path.pathString
import kotlin.io.path.*
import kotlin.streams.toList
import maestro.utils.isRegularFile

object WorkspaceExecutionPlanner {

private val logger = LoggerFactory.getLogger(WorkspaceExecutionPlanner::class.java)

fun plan(
input: Path,
input: Set<Path>,
includeTags: List<String>,
excludeTags: List<String>,
config: Path?,
): ExecutionPlan {
logger.info("start planning execution")

if (input.notExists()) {
throw ValidationError(
"""
Flow path does not exist: ${input.absolutePathString()}
""".trimIndent()
)
if (input.any { it.notExists() }) {
throw ValidationError("""
Flow path does not exist: ${input.find { it.notExists() }?.absolutePathString()}
""".trimIndent())
}

if (input.isRegularFile()) {
validateFlowFile(input)
if (input.isRegularFile) {
validateFlowFile(input.first())
return ExecutionPlan(
flowsToRun = listOf(input),
flowsToRun = input.toList(),
sequence = FlowSequence(emptyList()),
)
}

// retrieve all Flow files

val unfilteredFlowFiles = Files.walk(input).filter { isFlowFile(it) }.toList()
if (unfilteredFlowFiles.isEmpty()) {
throw ValidationError(
"""
Flow directory does not contain any Flow files: ${input.absolutePathString()}
""".trimIndent()
)
val (files, directories) = input.partition { it.isRegularFile() }

val flowFiles = files.filter { isFlowFile(it) }
val flowFilesInDirs: List<Path> = directories.flatMap { dir -> Files
.walk(dir)
.filter { isFlowFile(it) }
.toList()
}
if (flowFilesInDirs.isEmpty() && flowFiles.isEmpty()) {
throw ValidationError("""
Flow directories do not contain any Flow files: ${directories.joinToString(", ") { it.absolutePathString() }}
""".trimIndent())
}

// Filter flows based on flows config
val workspaceConfig = if (config != null) {
YamlCommandReader.readWorkspaceConfig(config.absolute())
} else {
findConfigFile(input)

val workspaceConfig =
if (config != null) YamlCommandReader.readWorkspaceConfig(config.absolute())
else directories.firstNotNullOfOrNull { findConfigFile(it) }
?.let { YamlCommandReader.readWorkspaceConfig(it) }
?: WorkspaceConfig()
}

val globs = workspaceConfig.flows ?: listOf("*")

val matchers = globs
.map {
input.fileSystem.getPathMatcher("glob:${input.pathString}/$it")
}
val matchers = globs.flatMap { glob ->
directories.map { it.fileSystem.getPathMatcher("glob:${it.pathString}/$glob") }
}

val unsortedFlowFiles = unfilteredFlowFiles
.filter { path -> matchers.any { matcher -> matcher.matches(path) } }
.toList()
val unsortedFlowFiles = flowFiles + flowFilesInDirs.filter { path ->
matchers.any { matcher -> matcher.matches(path) }
}.toList()

if (unsortedFlowFiles.isEmpty()) {
if ("*" == globs.singleOrNull()) {
throw ValidationError(
"""
Top-level directory does not contain any Flows: ${input.absolutePathString()}
val message = """
Top-level directories do not contain any Flows: ${directories.joinToString(", ") { it.absolutePathString() }}
To configure Maestro to run Flows in subdirectories, check out the following resources:
* https://maestro.mobile.dev/cli/test-suites-and-reports#inclusion-patterns
* https://blog.mobile.dev/maestro-best-practices-structuring-your-test-suite-54ec390c5c82
""".trimIndent()
)
throw ValidationError(message)
} else {
throw ValidationError("Flow inclusion pattern(s) did not match any Flow files:\n${toYamlListString(globs)}")
val message = """
|Flow inclusion pattern(s) did not match any Flow files:
|${toYamlListString(globs)}
""".trimMargin()
throw ValidationError(message)
}
}

Expand All @@ -105,17 +102,20 @@ object WorkspaceExecutionPlanner {
val tags = config?.tags ?: emptyList()

(allIncludeTags.isEmpty() || tags.any(allIncludeTags::contains))
&& (allExcludeTags.isEmpty() || !tags.any(allExcludeTags::contains))
&& (allExcludeTags.isEmpty() || !tags.any(allExcludeTags::contains))
}

if (allFlows.isEmpty()) {
throw ValidationError(
"Include / Exclude tags did not match any Flows:\n\nInclude Tags:\n${
toYamlListString(
allIncludeTags
)
}\n\nExclude Tags:\n${toYamlListString(allExcludeTags)}"
)
val message = """
|Include / Exclude tags did not match any Flows:
|
|Include Tags:
|${toYamlListString(allIncludeTags)}
|
|Exclude Tags:
|${toYamlListString(allExcludeTags)}
""".trimMargin()
throw ValidationError(message)
}

// Handle sequential execution
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import java.nio.file.Paths

object YamlCommandsPathValidator {

fun validatePathsExistInWorkspace(input: Path, flowFile: Path, pathStrings: List<String>) {
fun validatePathsExistInWorkspace(input: Set<Path>, flowFile: Path, pathStrings: List<String>) {
pathStrings.forEach {
val exists = validateInsideWorkspace(input, it)
if (!exists) {
Expand All @@ -16,12 +16,8 @@ object YamlCommandsPathValidator {
}
}

private fun validateInsideWorkspace(workspace: Path, pathString: String): Boolean {
val mediaPath = workspace.resolve(workspace.fileSystem.getPath(pathString))
val exists = Files.walk(workspace).anyMatch { path -> path.fileName == mediaPath.fileName }
if (!exists) {
return false
}
return true
private fun validateInsideWorkspace(workspace: Set<Path>, pathString: String): Boolean {
val mediaPath = workspace.firstNotNullOfOrNull { it.resolve(it.fileSystem.getPath(pathString)) }
return workspace.any { Files.walk(it).anyMatch { path -> path.fileName == mediaPath?.fileName } }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ internal class WorkspaceExecutionPlannerErrorsTest {
try {
val inputPath = singleFlowFilePath?.let { workspacePath.resolve(it) } ?: workspacePath
WorkspaceExecutionPlanner.plan(
input = inputPath,
input = setOf(inputPath),
includeTags = includeTags,
excludeTags = excludeTags,
config = null,
Expand Down
Loading

0 comments on commit 8cd7387

Please sign in to comment.