From a21ee2463eb69afac7513f61eb1fa879af2ea520 Mon Sep 17 00:00:00 2001 From: pawelpasterz <32893017+pawelpasterz@users.noreply.github.com> Date: Thu, 15 Oct 2020 07:22:24 +0200 Subject: [PATCH 1/2] fix: Fix withClassName filter (#1233) --- .../SimilarNameTest1.kt | 21 +++++++++++ .../SimilarNameTest10.kt | 21 +++++++++++ .../src/main/kotlin/ftl/filter/TestFilters.kt | 23 +++++++++--- .../android/CreateAndroidTestContext.kt | 11 +++--- .../test/kotlin/ftl/filter/TestFiltersTest.kt | 36 +++++++++++++++++++ .../android/CreateAndroidTestContextKtTest.kt | 36 +++++++++++++++++++ 6 files changed, 139 insertions(+), 9 deletions(-) create mode 100644 test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest1.kt create mode 100644 test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest10.kt diff --git a/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest1.kt b/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest1.kt new file mode 100644 index 0000000000..1970401015 --- /dev/null +++ b/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest1.kt @@ -0,0 +1,21 @@ +package com.example.test_app.similar + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.test_app.BaseInstrumentedTest +import org.junit.Ignore +import androidx.test.filters.Suppress +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SimilarNameTest1 : BaseInstrumentedTest() { + + @Test + fun test1() = testMethod() + + @Test + fun test2() = testMethod() + + @Test + fun test19() = testMethod() +} diff --git a/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest10.kt b/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest10.kt new file mode 100644 index 0000000000..d7ffe9ceee --- /dev/null +++ b/test_projects/android/app/src/androidTestMultiple/java/com.example.test_app.similar/SimilarNameTest10.kt @@ -0,0 +1,21 @@ +package com.example.test_app.similar + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.example.test_app.BaseInstrumentedTest +import org.junit.Ignore +import androidx.test.filters.Suppress +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SimilarNameTest10 : BaseInstrumentedTest() { + + @Test + fun test1() = testMethod() + + @Test + fun test2() = testMethod() + + @Test + fun test19() = testMethod() +} diff --git a/test_runner/src/main/kotlin/ftl/filter/TestFilters.kt b/test_runner/src/main/kotlin/ftl/filter/TestFilters.kt index 772f316d70..381cf5fd76 100644 --- a/test_runner/src/main/kotlin/ftl/filter/TestFilters.kt +++ b/test_runner/src/main/kotlin/ftl/filter/TestFilters.kt @@ -150,12 +150,25 @@ object TestFilters { } ) - private fun withClassName(classNames: List): TestFilter = TestFilter( - describe = "withClassName (${classNames.joinToString(", ")})", - shouldRun = { testMethod -> - withPackageName(classNames).shouldRun(testMethod) + private fun withClassName(classNames: List): TestFilter { + // splits foo.bar.TestClass1#testMethod1 into [foo.bar.TestClass1, testMethod1] + fun String.extractClassAndTestNames() = split("#") + val classFilters = classNames.map { it.extractClassAndTestNames() } + return TestFilter( + describe = "withClassName (${classNames.joinToString(", ")})", + shouldRun = { testMethod -> testMethod.testName.extractClassAndTestNames().matchFilters(classFilters) } + ) + } + + private fun List.matchFilters(classFilters: List>): Boolean { + fun List.className() = first() + fun List.methodName() = last() + return classFilters.any { filter -> + // When filter.size == 1 all test methods from the class should run therefore we do not compare method names + // When filter.size != 1 only particular test from the class should be launched and we need to compare method names as well + className() == filter.className() && (filter.size == 1 || methodName() == filter.methodName()) } - ) + } private fun withAnnotation(annotations: List): TestFilter = TestFilter( describe = "withAnnotation (${annotations.joinToString(", ")})", diff --git a/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt b/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt index 03f7c36da4..70708822e8 100644 --- a/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt +++ b/test_runner/src/main/kotlin/ftl/run/platform/android/CreateAndroidTestContext.kt @@ -70,11 +70,15 @@ internal fun InstrumentationTestContext.getFlankTestMethods( .filter(testFilter.shouldRun) .filterNot(parameterizedClasses::belong) .map(TestMethod::toFlankTestMethod).toList() - .plus(parameterizedClasses.map(String::toFlankTestMethod)) + .plus(parameterizedClasses.onlyShouldRun(testFilter)) } private fun List.belong(method: TestMethod) = any { className -> method.testName.startsWith(className) } +private fun List.onlyShouldRun(filter: TestFilter) = this + .filter { filter.shouldRun(TestMethod(it, emptyList())) } + .map { FlankTestMethod("class $it", ignored = false, isParameterizedClass = true) } + private fun TestMethod.toFlankTestMethod() = FlankTestMethod( testName = "class $testName", ignored = annotations.any { it.name in ignoredAnnotations } @@ -86,9 +90,8 @@ private val ignoredAnnotations = listOf( "android.support.test.filters.Suppress" ) -private fun String.toFlankTestMethod() = FlankTestMethod("class $this", ignored = false, isParameterizedClass = true) - -private fun InstrumentationTestContext.getParametrizedClasses(): List = +@VisibleForTesting +internal fun InstrumentationTestContext.getParametrizedClasses(): List = DexParser.readDexFiles(test.local).fold(emptyList()) { accumulator, file: DexFile -> accumulator + file.classDefs .filter(file::isParametrizedClass) diff --git a/test_runner/src/test/kotlin/ftl/filter/TestFiltersTest.kt b/test_runner/src/test/kotlin/ftl/filter/TestFiltersTest.kt index 6173266a34..b5c382b594 100644 --- a/test_runner/src/test/kotlin/ftl/filter/TestFiltersTest.kt +++ b/test_runner/src/test/kotlin/ftl/filter/TestFiltersTest.kt @@ -358,6 +358,42 @@ class TestFiltersTest { assertThat(filter.shouldRun(BAR_PACKAGE)).isFalse() assertThat(filter.shouldRun(BAR_CLASSNAME)).isTrue() } + + @Test + fun `withClassName should correctly filter classes with similar name`() { + // test-targets: + // - class foo.bar.Class1 + // should filter foo.bar.Class11, foo.bar.Class101 ... + // the same is applicable for methods + + val filter = fromTestTargets( + listOf( + "class anyPackage_1.anyClass_1", + "class anyPackage_3.anyClass_3#anyMethod_3" + ) + ) + + val tests = listOf( + TargetsHelper(pack = "anyPackage_1", cl = "anyClass_1", m = "anyMethod_1", annotation = "Foo"), + TargetsHelper(pack = "anyPackage_1", cl = "anyClass_1", m = "anyMethod_2", annotation = "Foo"), + TargetsHelper(pack = "anyPackage_1", cl = "anyClass_12", m = "anyMethod_1", annotation = "Bar"), + TargetsHelper(pack = "anyPackage_1", cl = "anyClass_12", m = "anyMethod_12", annotation = "Bar"), + TargetsHelper(pack = "anyPackage_3", cl = "anyClass_3", m = "anyMethod_3", annotation = "Bar"), + TargetsHelper(pack = "anyPackage_3", cl = "anyClass_3", m = "anyMethod_32", annotation = "Bar"), + TargetsHelper(pack = "anyPackage_3", cl = "anyClass_32", m = "anyMethod_3", annotation = "Bar"), + TargetsHelper(pack = "anyPackage_3", cl = "anyClass_32", m = "anyMethod_32", annotation = "Bar") + ).map { getDefaultTestMethod(it.fullView, it.annotation) } + + val expected = listOf( + "anyPackage_1.anyClass_1#anyMethod_1", + "anyPackage_1.anyClass_1#anyMethod_2", + "anyPackage_3.anyClass_3#anyMethod_3" + ) + + val result = tests.withFilter(filter) + + assertThat(result).isEqualTo(expected) + } } private fun getDefaultTestMethod(testName: String, annotation: String) = diff --git a/test_runner/src/test/kotlin/ftl/run/platform/android/CreateAndroidTestContextKtTest.kt b/test_runner/src/test/kotlin/ftl/run/platform/android/CreateAndroidTestContextKtTest.kt index 0808500490..57d3f28d45 100644 --- a/test_runner/src/test/kotlin/ftl/run/platform/android/CreateAndroidTestContextKtTest.kt +++ b/test_runner/src/test/kotlin/ftl/run/platform/android/CreateAndroidTestContextKtTest.kt @@ -1,25 +1,34 @@ package ftl.run.platform.android +import com.google.common.truth.Truth.assertThat import com.linkedin.dex.parser.DexParser import com.linkedin.dex.parser.DexParser.Companion.findTestMethods import com.linkedin.dex.parser.TestMethod import ftl.args.AndroidArgs import ftl.filter.TestFilter +import ftl.filter.TestFilters import ftl.run.model.AndroidTestContext import ftl.run.model.InstrumentationTestContext import ftl.run.model.RoboTestContext import ftl.test.util.mixedConfigYaml import ftl.test.util.should import ftl.util.FileReference +import ftl.util.FlankTestMethod import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject +import io.mockk.mockkStatic +import io.mockk.unmockkAll import kotlinx.coroutines.runBlocking +import org.junit.After import org.junit.Assert.assertEquals import org.junit.Test class CreateAndroidTestContextKtTest { + @After + fun tearDown() = unmockkAll() + @Test fun `create AndroidTestConfig for mixed tests`() { // given @@ -79,4 +88,31 @@ class CreateAndroidTestContextKtTest { // given assertEquals(actual, 2) } + + @Test + fun `should not append all parameterized classes to list of test methods`() { + val testInstrumentationContext = InstrumentationTestContext( + FileReference("./src/test/kotlin/ftl/fixtures/tmp/apk/app-debug.apk", ""), + FileReference("./src/test/kotlin/ftl/fixtures/tmp/apk/app-single-success-debug-androidTest.apk", "") + ) + + mockkObject(DexParser) { + every { findTestMethods(any()) } returns listOf( + TestMethod("foo.bar.TestClass1#test1", emptyList()), + TestMethod("foo.bar.TestClass1#test2", emptyList()), + TestMethod("foo.bar.TestClass2#test1", emptyList()), + TestMethod("foo.bar.TestClass2#test2", emptyList()), + TestMethod("foo.bar.ParamClass#testParam", emptyList()), + ) + mockkStatic("ftl.run.platform.android.CreateAndroidTestContextKt") + every { testInstrumentationContext.getParametrizedClasses() } returns listOf("foo.bar.ParamClass") + + val actual = testInstrumentationContext.getFlankTestMethods(TestFilters.fromTestTargets(listOf("class foo.bar.TestClass1"))) + val expected = listOf( + FlankTestMethod("class foo.bar.TestClass1#test1"), + FlankTestMethod("class foo.bar.TestClass1#test2") + ) + assertThat(actual).isEqualTo(expected) + } + } } From feead91aae784ee0876b67dc52fa7aef02975b78 Mon Sep 17 00:00:00 2001 From: Michael Wright Date: Thu, 15 Oct 2020 10:03:03 +0200 Subject: [PATCH 2/2] feat: Add type (#1230) * Add type * Finialize the type parameter * Better unit tests * Added more assertiveness for the Type * detekt fixes * Added tests and fixed erronous ones * Nicer name outputs --- README.md | 3 + test_runner/flank.yml | 3 + .../src/main/kotlin/ftl/args/AndroidArgs.kt | 4 + .../main/kotlin/ftl/args/CreateAndroidArgs.kt | 4 +- .../kotlin/ftl/args/ValidateAndroidArgs.kt | 10 +++ .../src/main/kotlin/ftl/args/yml/Type.kt | 17 ++++ .../ftl/config/android/AndroidGcloudConfig.kt | 10 ++- .../test/kotlin/ftl/args/AndroidArgsTest.kt | 87 +++++++++++++++++++ 8 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 test_runner/src/main/kotlin/ftl/args/yml/Type.kt diff --git a/README.md b/README.md index 191eabfe6a..693cf4de56 100644 --- a/README.md +++ b/README.md @@ -328,6 +328,9 @@ gcloud: ## By default, all permissions are granted. PERMISSIONS must be one of: all, none # grant-permissions: all + ## The type of test to run. TYPE must be one of: instrumentation, robo, game-loop. + # type: instrumentation + ## A list of device-path: file-path pairs that indicate the device paths to push files to the device before starting tests, and the paths of files to push. ## Device paths must be under absolute, whitelisted paths (${EXTERNAL_STORAGE}, or ${ANDROID_DATA}/local/tmp). ## Source file paths may be in the local filesystem or in Google Cloud Storage (gs://…). diff --git a/test_runner/flank.yml b/test_runner/flank.yml index 8f95a63adb..a493e54154 100644 --- a/test_runner/flank.yml +++ b/test_runner/flank.yml @@ -82,6 +82,9 @@ gcloud: # directories-to-pull: # - /sdcard/ + ## The type of test to run. TYPE must be one of: instrumentation, robo, game-loop. + # type: instrumentation + ## Whether to grant runtime permissions on the device before the test begins. ## By default, all permissions are granted. PERMISSIONS must be one of: all, none # grant-permissions: all diff --git a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt index 1956d3916c..3ae8af0843 100644 --- a/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/AndroidArgs.kt @@ -1,6 +1,7 @@ package ftl.args import ftl.args.yml.AppTestPair +import ftl.args.yml.Type data class AndroidArgs( val commonArgs: CommonArgs, @@ -14,6 +15,7 @@ data class AndroidArgs( val environmentVariables: Map, // should not be printed, becuase could contains sensitive informations val directoriesToPull: List, val grantPermissions: String?, + val type: Type?, val otherFiles: Map, val performanceMetrics: Boolean, val numUniformShards: Int?, @@ -45,6 +47,7 @@ AndroidArgs use-orchestrator: $useOrchestrator directories-to-pull: ${ArgsToString.listToString(directoriesToPull)} grant-permissions: $grantPermissions + type: ${type?.ymlName} other-files: ${ArgsToString.mapToString(otherFiles)} performance-metrics: $performanceMetrics num-uniform-shards: $numUniformShards @@ -84,6 +87,7 @@ AndroidArgs val AndroidArgs.isDontAutograntPermissions get() = !(grantPermissions.isNotNull() && (grantPermissions.equals("all"))) + val AndroidArgs.isInstrumentationTest get() = appApk.isNotNull() && testApk.isNotNull() || diff --git a/test_runner/src/main/kotlin/ftl/args/CreateAndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/CreateAndroidArgs.kt index 90936d1a9a..67a1cdd7f7 100644 --- a/test_runner/src/main/kotlin/ftl/args/CreateAndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/CreateAndroidArgs.kt @@ -1,6 +1,7 @@ package ftl.args import ftl.args.yml.AppTestPair +import ftl.args.yml.Type import ftl.config.AndroidConfig import ftl.config.android.AndroidFlankConfig import ftl.config.android.AndroidGcloudConfig @@ -39,5 +40,6 @@ fun createAndroidArgs( } ?: emptyList(), useLegacyJUnitResult = flank.useLegacyJUnitResult!!, obfuscateDumpShards = obfuscate, - grantPermissions = gcloud.grantPermissions + grantPermissions = gcloud.grantPermissions, + type = gcloud.type?.let { Type.fromString(it) } ) diff --git a/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt b/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt index 1e22f27f9b..89cd10a671 100644 --- a/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt +++ b/test_runner/src/main/kotlin/ftl/args/ValidateAndroidArgs.kt @@ -5,6 +5,7 @@ import ftl.android.IncompatibleModelVersion import ftl.android.SupportedDeviceConfig import ftl.android.UnsupportedModelId import ftl.android.UnsupportedVersionId +import ftl.args.yml.Type import ftl.config.containsPhysicalDevices import ftl.config.containsVirtualDevices import ftl.run.exception.FlankConfigurationError @@ -23,12 +24,21 @@ fun AndroidArgs.validate() = apply { assertTestFiles() assertOtherFiles() assertGrantPermissions() + assertType() checkResultsDirUnique() checkEnvironmentVariables() checkFilesToDownload() checkNumUniformShards() } +private fun AndroidArgs.assertType() = type?.let { + if (appApk == null) throw FlankGeneralError("A valid AppApk must be defined if Type parameter is used.") + if (it == Type.INSTRUMENTATION) { + if (testApk == null) throw FlankGeneralError("Instrumentation tests require a valid testApk defined.") + if (testRunnerClass == null) throw FlankGeneralError("Instrumentation tests require a valid test-runner-class defined.") + } +} + private fun AndroidArgs.assertGrantPermissions() = grantPermissions?.let { if (it !in listOf("all", "none")) throw FlankGeneralError("Unsupported permission '$grantPermissions'\nOnly 'all' or 'none' supported.") } diff --git a/test_runner/src/main/kotlin/ftl/args/yml/Type.kt b/test_runner/src/main/kotlin/ftl/args/yml/Type.kt new file mode 100644 index 0000000000..7c7d163fa9 --- /dev/null +++ b/test_runner/src/main/kotlin/ftl/args/yml/Type.kt @@ -0,0 +1,17 @@ +package ftl.args.yml + +import ftl.run.exception.FlankGeneralError + +enum class Type(val ymlName: String) { + INSTRUMENTATION("instrumentation"), ROBO("robo"), GAMELOOP("game-loop"); + + companion object { + fun fromString(stringVal: String): Type { + val filtered = values().filter { it.ymlName == stringVal } + if (filtered.isEmpty()) { + throw FlankGeneralError("Unsupported Type given `$stringVal` only [${values().joinToString(","){it.ymlName}}] supported.") + } + return filtered.first() + } + } +} diff --git a/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt b/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt index 91394fda69..4359b1b03b 100644 --- a/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt +++ b/test_runner/src/main/kotlin/ftl/config/android/AndroidGcloudConfig.kt @@ -24,7 +24,7 @@ data class AndroidGcloudConfig @JsonIgnore constructor( @set:CommandLine.Option( names = ["--app"], description = ["The path to the application binary file. " + - "The path may be in the local filesystem or .setDontAutograntPermissions(true)in Google Cloud Storage using gs:// notation."] + "The path may be in the local filesystem or in Google Cloud Storage using gs:// notation."] ) @set:JsonProperty("app") var app: String? by data @@ -102,6 +102,13 @@ data class AndroidGcloudConfig @JsonIgnore constructor( @set:JsonProperty("grant-permissions") var grantPermissions: String? by data + @set:CommandLine.Option( + names = ["--type"], + description = ["The type of test to run. TYPE must be one of: instrumentation, robo, game-loop."] + ) + @set:JsonProperty("type") + var type: String? by data + @set:CommandLine.Option( names = ["--directories-to-pull"], split = ",", @@ -227,6 +234,7 @@ data class AndroidGcloudConfig @JsonIgnore constructor( testTargets = emptyList() roboDirectives = emptyMap() roboScript = null + type = null } } } diff --git a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt index 027875f6d5..249545ce28 100644 --- a/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt +++ b/test_runner/src/test/kotlin/ftl/args/AndroidArgsTest.kt @@ -5,6 +5,7 @@ import com.google.common.truth.Truth.assertThat import ftl.args.IArgs.Companion.AVAILABLE_PHYSICAL_SHARD_COUNT_RANGE import ftl.args.IArgs.Companion.AVAILABLE_VIRTUAL_SHARD_COUNT_RANGE import ftl.args.yml.AppTestPair +import ftl.args.yml.Type import ftl.cli.firebase.test.android.AndroidRunCommand import ftl.config.Device import ftl.config.FtlConstants.defaultAndroidModel @@ -98,6 +99,7 @@ class AndroidArgsTest { - /sdcard/screenshots - /sdcard/screenshots2 grant-permissions: all + type: instrumentation other-files: /sdcard/dir1/file1.txt: $appApk /sdcard/dir2/file2.jpg: $testApk @@ -238,6 +240,7 @@ class AndroidArgsTest { assert(useOrchestrator, false) assert(environmentVariables, linkedMapOf("clearPackageData" to "true", "randomEnvVar" to "false")) assert(directoriesToPull, listOf("/sdcard/screenshots", "/sdcard/screenshots2")) + assert(grantPermissions, "all") assert( otherFiles, mapOf( @@ -245,6 +248,7 @@ class AndroidArgsTest { "/sdcard/dir2/file2.jpg" to testApkAbsolutePath ) ) + assert(type, Type.INSTRUMENTATION) assert(performanceMetrics, false) assert(testRunnerClass, "com.foo.TestRunner") assert( @@ -312,6 +316,7 @@ AndroidArgs - /sdcard/screenshots - /sdcard/screenshots2 grant-permissions: all + type: instrumentation other-files: /sdcard/dir1/file1.txt: $appApkAbsolutePath /sdcard/dir2/file2.jpg: $testApkAbsolutePath @@ -390,6 +395,7 @@ AndroidArgs use-orchestrator: true directories-to-pull: grant-permissions: all + type: null other-files: performance-metrics: false num-uniform-shards: null @@ -1912,6 +1918,87 @@ AndroidArgs """.trimIndent() AndroidArgs.load(yaml).validate() } + @Test(expected = FlankGeneralError::class) + fun `should throw exception if incorrect type requested`() { + val yaml = """ + gcloud: + app: $appApk + test: $testApk + device: + - model: Nexus6 + version: 25 + locale: en + orientation: portrait + grant-permissions: error + """.trimIndent() + AndroidArgs.load(yaml).validate() + } + + @Test + fun `should Not throw exception if correct type requested game-loop`() { + val yaml = """ + gcloud: + app: $appApk + test: $testApk + device: + - model: Nexus6 + version: 25 + locale: en + orientation: portrait + type: game-loop + """.trimIndent() + AndroidArgs.load(yaml).validate() + } + + @Test + fun `should Not throw exception if correct type requested instrumental`() { + val yaml = """ + gcloud: + app: $appApk + test: $testApk + device: + - model: Nexus6 + version: 25 + locale: en + orientation: portrait + type: instrumentation + test-runner-class: com.foo.TestRunner + """.trimIndent() + AndroidArgs.load(yaml).validate() + } + + @Test(expected = FlankConfigurationError::class) + fun `should throw exception if correct type requested instrumental but no test runner set`() { + val yaml = """ + gcloud: + app: $appApk + test: $testApk + test + device: + - model: Nexus6 + version: 25 + locale: en + orientation: portrait + type: instrumentation + """.trimIndent() + AndroidArgs.load(yaml).validate() + } + + @Test + fun `should Not throw exception if correct type requested robo`() { + val yaml = """ + gcloud: + app: $appApk + test: $testApk + device: + - model: Nexus6 + version: 25 + locale: en + orientation: portrait + type: robo + """.trimIndent() + AndroidArgs.load(yaml).validate() + } } private fun AndroidArgs.Companion.load(yamlData: String, cli: AndroidRunCommand? = null): AndroidArgs =