diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt index 68c0ecad1..7d49e3699 100644 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt +++ b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt @@ -83,7 +83,8 @@ sealed class VendorConfiguration { @JsonProperty("compactOutput") val compactOutput: Boolean = false, @JsonProperty("keepAliveIntervalMillis") val keepAliveIntervalMillis: Long = 0L, @JsonProperty("devices") val devicesFile: File? = null, - ) : VendorConfiguration() { + @JsonProperty("xcTestRunnerTag") val xcTestRunnerTag: String? = null, + ) : VendorConfiguration() { /** * Exception should not happen since it will be first thrown in deserializer */ diff --git a/configuration/src/test/kotlin/com/malinskiy/marathon/config/serialization/ConfigurationFactoryTest.kt b/configuration/src/test/kotlin/com/malinskiy/marathon/config/serialization/ConfigurationFactoryTest.kt index 0e4eb333a..1c65c96f0 100644 --- a/configuration/src/test/kotlin/com/malinskiy/marathon/config/serialization/ConfigurationFactoryTest.kt +++ b/configuration/src/test/kotlin/com/malinskiy/marathon/config/serialization/ConfigurationFactoryTest.kt @@ -274,6 +274,7 @@ class ConfigurationFactoryTest { keepAliveIntervalMillis = 300000L, devicesFile = file.parentFile.resolve("Testdevices").canonicalFile, sourceRoot = file.parentFile.resolve(".").canonicalFile, + xcTestRunnerTag = "demoTag" ) } diff --git a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt index a2bc6e49d..cd644c872 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt @@ -80,7 +80,7 @@ class Marathon( val shard = prepareTestShard(tests, analytics) log.info("Scheduling ${tests.size} tests") - log.debug(tests.joinToString(", ") { it.toTestName() }) + log.info(tests.joinToString(", ") { it.toTestName() }) val currentCoroutineContext = coroutineContext val scheduler = Scheduler( deviceProvider, diff --git a/core/src/main/kotlin/com/malinskiy/marathon/test/Test.kt b/core/src/main/kotlin/com/malinskiy/marathon/test/Test.kt index 19c0d6b5c..89422e3d7 100644 --- a/core/src/main/kotlin/com/malinskiy/marathon/test/Test.kt +++ b/core/src/main/kotlin/com/malinskiy/marathon/test/Test.kt @@ -43,3 +43,13 @@ fun Test.toClassName(separator: Char = '.'): String { fun Test.toSimpleSafeTestName(): String = "$clazz.$method" fun Test.toSafeTestName() = toTestName(methodSeparator = '.') + +fun Test.getCucumberishTags(): List{ + var tags: List = emptyList() + + if (metaProperties.isNotEmpty()) { + val mappedValues = metaProperties.filter { it.name == "cucumberishTags" }.first().values + tags = mappedValues.get("tags") as? List ?: emptyList() + } + return tags +} diff --git a/docs/_posts/2018-11-19-ios.md b/docs/_posts/2018-11-19-ios.md index 0f3166e33..ec794e396 100644 --- a/docs/_posts/2018-11-19-ios.md +++ b/docs/_posts/2018-11-19-ios.md @@ -96,3 +96,11 @@ this here ```yaml devicesFile: "/opt/Marathondevices" ``` + +## Annotating Test Function Using Cucumberish @Tags +You can annotate your test functions with cucumberish tags for ex: `@SmokeTest @RegressionTest` etc. +If your Marathon file contains the parameter `xcTestRunnerTag`, then only the selected test functions will be executed by marathon. + +```yaml +xcTestRunnerTag: "SmokeTest" +``` diff --git a/sample/ios-app/Marathonfile b/sample/ios-app/Marathonfile index a770a814a..898d5b67c 100644 --- a/sample/ios-app/Marathonfile +++ b/sample/ios-app/Marathonfile @@ -18,3 +18,4 @@ vendorConfiguration: knownHostsPath: ${HOME}/.ssh/known_hosts remoteUsername: ${USER} remotePrivateKey: ${HOME}/.ssh/marathon + xcTestRunnerTag: "SmokeTest" diff --git a/sample/ios-app/sample-appUITests/StoryboardTests.swift b/sample/ios-app/sample-appUITests/StoryboardTests.swift index 66b007ee2..ecf688165 100644 --- a/sample/ios-app/sample-appUITests/StoryboardTests.swift +++ b/sample/ios-app/sample-appUITests/StoryboardTests.swift @@ -61,6 +61,19 @@ class StoryboardTests: XCTestCase { let label = app.staticTexts.firstMatch XCTAssertEqual(label.label, "Label") } + + // @SmokeTest @RegressionTest + func testButtonBasedOnAnnotationOrTag() { + let button = app.buttons.firstMatch + XCTAssertTrue(button.waitForExistence()) + XCTAssertTrue(button.isHittable) + + button.tap() + + let label = app.staticTexts.firstMatch + XCTAssertTrue(label.waitForExistence()) + } + } private let standardTimeout: TimeInterval = 30.0 diff --git a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt index d51b1b00f..7132c4679 100644 --- a/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt +++ b/vendor/vendor-ios/src/main/kotlin/com/malinskiy/marathon/ios/IOSTestParser.kt @@ -4,14 +4,18 @@ import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.execution.TestParser import com.malinskiy.marathon.ios.xctestrun.Xctestrun import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.test.MetaProperty import com.malinskiy.marathon.test.Test +import com.malinskiy.marathon.test.getCucumberishTags import java.io.File class IOSTestParser(private val vendorConfiguration: VendorConfiguration.IOSConfiguration) : TestParser { private val swiftTestClassRegex = """class ([^:\s]+)\s*:\s*XCTestCase""".toRegex() private val swiftTestMethodRegex = """^.*func\s+(test[^(\s]*)\s*\(.*$""".toRegex() + private val swiftTagRegex = """@([^:\s]+)\w*""".toRegex() private val logger = MarathonLogging.logger(IOSTestParser::class.java.simpleName) + private val CUCUMBERISH_TAGS = "cucumberishTags" /** * Looks up test methods running a text search in swift files. Considers classes that explicitly inherit @@ -35,7 +39,7 @@ class IOSTestParser(private val vendorConfiguration: VendorConfiguration.IOSConf val implementedTests = mutableListOf() for (file in swiftFilesWithTests) { var testClassName: String? = null - for (line in file.readLines()) { + for ((index, line) in file.readLines().withIndex()) { val className = line.firstMatchOrNull(swiftTestClassRegex) val methodName = line.firstMatchOrNull(swiftTestMethodRegex) @@ -44,18 +48,47 @@ class IOSTestParser(private val vendorConfiguration: VendorConfiguration.IOSConf } if (testClassName != null && methodName != null) { - implementedTests.add(Test(targetName, testClassName, methodName, emptyList())) + // Find & Update Tags here + val tagLine = file.readLines()[index - 1] + val tags = swiftTagRegex.findAll(tagLine).map { it.groupValues[1] }.toList() + + if (tags.isEmpty()) { + implementedTests.add(Test(targetName, testClassName, methodName, emptyList())) + } + else { + val values = mapOf("tags" to tags) + val metaProperty = MetaProperty(name = CUCUMBERISH_TAGS, values = values) + implementedTests.add(Test(targetName, testClassName, methodName, arrayListOf(metaProperty))) + } } } } - val filteredTests = implementedTests.filter { !xctestrun.isSkipped(it) } + var filteredTests = implementedTests.filter { !xctestrun.isSkipped(it) } + + filteredTests = filterByXCTestRunnerTag(filteredTests) logger.trace { filteredTests.map { "${it.clazz}.${it.method}" }.joinToString() } logger.info { "Found ${filteredTests.size} tests in ${swiftFilesWithTests.count()} files" } return filteredTests } + + private fun filterByXCTestRunnerTag(tests: List): List { + if (vendorConfiguration.xcTestRunnerTag.isNullOrEmpty()) { + return tests + } + + var filteredTests = tests + val testRunnerTag = vendorConfiguration.xcTestRunnerTag ?: "" + + if (testRunnerTag.isNotEmpty()) { + filteredTests = filteredTests.filter { + it.getCucumberishTags().contains(testRunnerTag) + } + } + return filteredTests + } } private fun Sequence.filter(contentsRegex: Regex): Sequence {