diff --git a/.github/workflows/test-e2e.yaml b/.github/workflows/test-e2e.yaml index 8b9a55e7b5..2a9cea8e7c 100644 --- a/.github/workflows/test-e2e.yaml +++ b/.github/workflows/test-e2e.yaml @@ -11,6 +11,7 @@ jobs: timeout-minutes: 20 strategy: + fail-fast: false matrix: java-version: [11, 17] diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 9155af7969..d43032ff95 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -10,6 +10,7 @@ jobs: runs-on: ubuntu-latest strategy: + fail-fast: false matrix: java-version: [11, 17] diff --git a/maestro-cli/src/main/java/maestro/cli/command/CloudCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/CloudCommand.kt index c736a096b9..30dc310617 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/CloudCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/CloudCommand.kt @@ -57,6 +57,9 @@ class CloudCommand : Callable { @CommandLine.Parameters(hidden = true, arity = "0..2", description = ["App file and/or Flow file i.e "]) private lateinit var files: List + @Option(names = ["--config"], description = ["Optional .yaml configuration file for Flows. If not provided, Maestro will look for a config.yaml file in the root directory."]) + private var configFile: File? = null + @Option(names = ["--app-file"], description = ["App binary to run your Flows against"]) private var appFile: File? = null @@ -217,6 +220,7 @@ class CloudCommand : Callable { input = flowsFile.toPath().toAbsolutePath(), includeTags = includeTags, excludeTags = excludeTags, + config = configFile?.toPath()?.toAbsolutePath(), ) } catch (e: Exception) { throw CliError("Upload aborted. Received error when evaluating workspace: ${e.message}") @@ -225,6 +229,10 @@ class CloudCommand : Callable { private fun validateFiles() { + if (configFile != null && configFile?.exists()?.not() == true) { + throw CliError("The config file ${configFile?.absolutePath} does not exist.") + } + // Maintains backwards compatibility for this syntax: maestro cloud // App file can be optional now if (this::files.isInitialized) { diff --git a/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt index abc5328adc..bb2ec5d95e 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/RecordCommand.kt @@ -57,6 +57,9 @@ class RecordCommand : Callable { @CommandLine.Parameters private lateinit var flowFile: File + @Option(names = ["--config"], description = ["Optional .yaml configuration file for Flows. If not provided, Maestro will look for a config.yaml file in the root directory."]) + private var configFile: File? = null + @Option(names = ["-e", "--env"]) private var env: Map = emptyMap() @@ -77,6 +80,9 @@ class RecordCommand : Callable { ) } + if (configFile != null && configFile?.exists()?.not() == true) { + throw CliError("The config file ${configFile?.absolutePath} does not exist.") + } TestDebugReporter.install(debugOutputPathAsString = debugOutput, printToConsole = parent?.verbose == true) val path = TestDebugReporter.getDebugOutputPath() diff --git a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt index cd094d95a6..c39edd6883 100644 --- a/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt +++ b/maestro-cli/src/main/java/maestro/cli/command/TestCommand.kt @@ -79,6 +79,11 @@ class TestCommand : Callable { @CommandLine.Parameters private lateinit var flowFile: File + @Option( + names = ["--config"], + description = ["Optional YAML configuration file for the workspace. If not provided, Maestro will look for a config.yaml file in the workspace's root directory."]) + private var configFile: File? = null + @Option( names = ["-s", "--shards"], description = ["Number of parallel shards to distribute tests across"], @@ -178,11 +183,16 @@ class TestCommand : Callable { shardSplit = legacyShardCount } + if (configFile != null && configFile?.exists()?.not() == true) { + throw CliError("The config file ${configFile?.absolutePath} does not exist.") + } + val executionPlan = try { WorkspaceExecutionPlanner.plan( input = flowFile.toPath().toAbsolutePath(), includeTags = includeTags, excludeTags = excludeTags, + config = configFile?.toPath()?.toAbsolutePath(), ) } catch (e: ValidationError) { throw CliError(e.message) diff --git a/maestro-orchestra/src/main/java/maestro/orchestra/workspace/WorkspaceExecutionPlanner.kt b/maestro-orchestra/src/main/java/maestro/orchestra/workspace/WorkspaceExecutionPlanner.kt index 958c6ea1a6..f3a74de730 100644 --- a/maestro-orchestra/src/main/java/maestro/orchestra/workspace/WorkspaceExecutionPlanner.kt +++ b/maestro-orchestra/src/main/java/maestro/orchestra/workspace/WorkspaceExecutionPlanner.kt @@ -8,6 +8,7 @@ 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 @@ -24,6 +25,7 @@ object WorkspaceExecutionPlanner { input: Path, includeTags: List, excludeTags: List, + config: Path?, ): ExecutionPlan { logger.info("start planning execution") @@ -55,10 +57,13 @@ object WorkspaceExecutionPlanner { } // Filter flows based on flows config - - val workspaceConfig = findConfigFile(input) - ?.let { YamlCommandReader.readWorkspaceConfig(it) } - ?: WorkspaceConfig() + val workspaceConfig = if (config != null) { + YamlCommandReader.readWorkspaceConfig(config.absolute()) + } else { + findConfigFile(input) + ?.let { YamlCommandReader.readWorkspaceConfig(it) } + ?: WorkspaceConfig() + } val globs = workspaceConfig.flows ?: listOf("*") diff --git a/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerErrorsTest.kt b/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerErrorsTest.kt index 49d76e0817..57cc9c1b68 100644 --- a/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerErrorsTest.kt +++ b/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerErrorsTest.kt @@ -48,7 +48,12 @@ internal class WorkspaceExecutionPlannerErrorsTest { val excludeTags = path.resolve("excludeTags.txt").takeIf { it.isRegularFile() }?.readLines() ?: emptyList() try { val inputPath = singleFlowFilePath?.let { workspacePath.resolve(it) } ?: workspacePath - WorkspaceExecutionPlanner.plan(inputPath, includeTags, excludeTags) + WorkspaceExecutionPlanner.plan( + input = inputPath, + includeTags = includeTags, + excludeTags = excludeTags, + config = null, + ) assertWithMessage("No exception was not thrown. Ensure this test case triggers a ValidationError.").fail() } catch (e: Exception) { if (e !is ValidationError) { diff --git a/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerTest.kt b/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerTest.kt index 0c342d208c..791611efee 100644 --- a/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerTest.kt +++ b/maestro-orchestra/src/test/java/maestro/orchestra/workspace/WorkspaceExecutionPlannerTest.kt @@ -14,6 +14,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/000_individual_file/flow.yaml"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -29,6 +30,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/001_simple"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -45,6 +47,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/002_subflows"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -61,6 +64,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/003_include_tags"), includeTags = listOf("included"), excludeTags = listOf(), + config = null, ) // Then @@ -76,6 +80,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/004_exclude_tags"), includeTags = listOf(), excludeTags = listOf("excluded"), + config = null, ) // Then @@ -92,6 +97,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/005_custom_include_pattern"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -108,6 +114,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/006_include_subfolders"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -126,6 +133,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/007_empty_config"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -142,6 +150,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/008_literal_pattern"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -157,6 +166,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/009_custom_config_fields"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -173,6 +183,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/010_global_include_tags"), includeTags = listOf("featureB"), excludeTags = listOf(), + config = null, ) // Then @@ -190,6 +201,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/011_global_exclude_tags"), includeTags = listOf(), excludeTags = listOf("featureA"), + config = null, ) // Then @@ -207,6 +219,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/012_local_deterministic_order"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -224,6 +237,7 @@ internal class WorkspaceExecutionPlannerTest { input = path("/workspaces/013_execution_order"), includeTags = listOf(), excludeTags = listOf(), + config = null, ) // Then @@ -240,6 +254,22 @@ internal class WorkspaceExecutionPlannerTest { ).inOrder() } + @Test + internal fun `014 - Config not null`() { + // When + val plan = WorkspaceExecutionPlanner.plan( + input = path("/workspaces/014_config_not_null"), + includeTags = listOf(), + excludeTags = listOf(), + config = path("/workspaces/014_config_not_null/config/another_config.yaml"), + ) + + // Then + assertThat(plan.flowsToRun).containsExactly( + path("/workspaces/014_config_not_null/flowA.yaml"), + ) + } + private fun path(pathStr: String): Path { return Paths.get(WorkspaceExecutionPlannerTest::class.java.getResource(pathStr).toURI()) } diff --git a/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config.yaml b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config.yaml new file mode 100644 index 0000000000..0493995a34 --- /dev/null +++ b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config.yaml @@ -0,0 +1,3 @@ +includeTags: + - included + - excluded diff --git a/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config/another_config.yaml b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config/another_config.yaml new file mode 100644 index 0000000000..1005e30d12 --- /dev/null +++ b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/config/another_config.yaml @@ -0,0 +1,2 @@ +includeTags: + - included diff --git a/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowA.yaml b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowA.yaml new file mode 100644 index 0000000000..f4c990f1d8 --- /dev/null +++ b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowA.yaml @@ -0,0 +1,5 @@ +appId: com.example.app +tags: + - included +--- +- launchApp diff --git a/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowB.yaml b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowB.yaml new file mode 100644 index 0000000000..8b16303c4c --- /dev/null +++ b/maestro-orchestra/src/test/resources/workspaces/014_config_not_null/flowB.yaml @@ -0,0 +1,5 @@ +appId: com.example.app +tags: + - excluded +--- +- launchApp