Skip to content
Closed
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,49 @@
Cross-platform test runner written for Android and iOS projects

## Main focus
- **stability** of test execution adjusting for flakiness in the environment and in the tests.
- **stability** of test execution adjusting for flakiness in the environment and in the tests.
- **performance** using high parallelization (handling dozens of devices)

## Documentation

Please check the official [documentation](https://marathonlabs.github.io/marathon/) for installation, configuration and more

## [iOS Only] Added Support to consume tags for test functions under XCTestCase Subclasses
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can now tag to your iOS UITests just like you would add tag to your feature file (if you are using [Cucumberish](https://cocoapods.org/pods/Cucumberish) or [XCTGherkin](https://cocoapods.org/pods/XCTest-Gherkin))
_(Multiline tag support is not yet there. Pls make sure that tag is added just above the func signature)_
> How to tag your UITests. See Sample below.
```
// @Flowers @apple @mock-batch-1
func testButton() {
let button = app.buttons.firstMatch
XCTAssertTrue(button.waitForExistence())
XCTAssertTrue(button.isHittable)
button.tap()
let label = app.staticTexts.firstMatch
XCTAssertTrue(label.waitForExistence())
}
```

> Tag reference in marathon file. (See last line)
```
vendorConfiguration:
type: "iOS"
derivedDataDir: "derived-data"
sourceRoot: "sample-appUITests"
knownHostsPath: ${HOME}/.ssh/known_hosts
remoteUsername: ${USER}
remotePrivateKey: ${HOME}/.ssh/marathon
xcTestRunnerTag: "Flowers"
```

Refer Branch: [`origin/ios-uitest-runner-via-tags`](https://github.com/abhishekbedi1432/Cross-Platform-Test-Runner-Marathon/tree/ios-uitest-runner-via-tags)

> Pls Note:
> Tags are case-sensitive 😉
> xcTestRunnerTag supports single param at the moment
> Multiline tag support is not yet there. Pls make sure that tag is added just above the func signature
License
-------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,23 @@ class ConfigurationFactory(
is VendorConfiguration.IOSConfiguration -> {
// Any relative path specified in Marathonfile should be resolved against the directory Marathonfile is in
val resolvedDerivedDataDir = marathonfileDir.resolve(configuration.vendorConfiguration.derivedDataDir)
val finalXCTestRunPath = configuration.vendorConfiguration.xctestrunPath?.resolveAgainst(marathonfileDir)
?: fileListProvider
val resolvedResultBundlePath = marathonfileDir.resolve(configuration.vendorConfiguration.xcResultBundlePath)

// Adding support for Test Plan
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove the comment. It doesn't make sense after we merge this.

val testPlanName = configuration.vendorConfiguration.xcTestPlan

var finalXCTestRunPath = if(!testPlanName.isNullOrEmpty()) {
fileListProvider
.fileList(resolvedDerivedDataDir)
.firstOrNull { it.extension == "xctestrun" }
?: throw ConfigurationException("Unable to find an xctestrun file in derived data folder")
.firstOrNull { it.extension == "xctestrun" && it.name.contains("$testPlanName") } ?: throw ConfigurationException("Unable to find matching TestPlan. Please recheck if testplan is enabled")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can skip creating an additional string here:
it.name.contains("$testPlanName") -> it.name.contains(testPlanName)
It might also be a good idea to use startsWith or equals instead of contains. I'm not sure if there is a contract on the file name here

} else {
configuration.vendorConfiguration.xctestrunPath?.resolveAgainst(marathonfileDir)
?: fileListProvider
.fileList(resolvedDerivedDataDir)
.firstOrNull { it.extension == "xctestrun" }
?: throw ConfigurationException("Unable to find an xctestrun file in derived data folder")
}

val optionalSourceRoot = configuration.vendorConfiguration.sourceRoot.resolveAgainst(marathonfileDir)
val optionalDevices = configuration.vendorConfiguration.devicesFile?.resolveAgainst(marathonfileDir)
?: marathonfileDir.resolve("Marathondevices")
Expand All @@ -71,6 +83,7 @@ class ConfigurationFactory(
sourceRoot = optionalSourceRoot,
devicesFile = optionalDevices,
knownHostsPath = optionalKnownHostsPath,
xcResultBundlePath = resolvedResultBundlePath
)
}
VendorConfiguration.StubVendorConfiguration -> configuration.vendorConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ class VendorConfigurationDeserializer(

// Any relative path specified in Marathonfile should be resolved against the directory Marathonfile is in
val resolvedDerivedDataDir = marathonfileDir.resolve(iosConfiguration.derivedDataDir)
val resolvedResultBundlePath = marathonfileDir.resolve(iosConfiguration.xcResultBundlePath)

val finalXCTestRunPath = iosConfiguration.xctestrunPath?.resolveAgainst(marathonfileDir)
?: fileListProvider
.fileList(resolvedDerivedDataDir)
Expand All @@ -50,7 +52,8 @@ class VendorConfigurationDeserializer(
sourceRoot = optionalSourceRoot,
devicesFile = optionalDevices,
knownHostsPath = optionalKnownHostsPath,
)
xcResultBundlePath = resolvedResultBundlePath,
)
}
TYPE_ANDROID -> {
(node as ObjectNode).remove("type")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ sealed class VendorConfiguration {
@JsonProperty("hideRunnerOutput") val hideRunnerOutput: Boolean = false,
@JsonProperty("compactOutput") val compactOutput: Boolean = false,
@JsonProperty("keepAliveIntervalMillis") val keepAliveIntervalMillis: Long = 0L,
@JsonProperty("xcResultBundlePath") val xcResultBundlePath: File,
@JsonProperty("xcTestRunnerTag") val xcTestRunnerTag: String? = null,
@JsonProperty("xcTestPlan") val xcTestPlan: String? = null,
@JsonProperty("devices") val devicesFile: File? = null,
) : VendorConfiguration() {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,7 @@ class ConfigurationFactoryTest {
keepAliveIntervalMillis = 300000L,
devicesFile = file.parentFile.resolve("Testdevices").canonicalFile,
sourceRoot = file.parentFile.resolve(".").canonicalFile,
xcResultBundlePath = file.parentFile.resolve("a/resultBundlePath").canonicalFile,
)
}

Expand Down
9 changes: 8 additions & 1 deletion core/src/main/kotlin/com/malinskiy/marathon/Marathon.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.malinskiy.marathon.analytics.internal.pub.Track
import com.malinskiy.marathon.analytics.internal.sub.TrackerInternal
import com.malinskiy.marathon.config.Configuration
import com.malinskiy.marathon.config.LogicalConfigurationValidator
import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.device.DeviceProvider
import com.malinskiy.marathon.exceptions.NoDevicesException
import com.malinskiy.marathon.exceptions.NoTestCasesFoundException
Expand Down Expand Up @@ -79,6 +80,10 @@ class Marathon(
val tests = applyTestFilters(parsedTests)
val shard = prepareTestShard(tests, analytics)

log.info("\n\n\n **** Marathon File Params **** \n")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a no-no: configuration might contain sensitive credentials, and we can't print them to the stdout

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really want to do this then we need to implement some kind of masking for sensitive fields

log.info("${configuration}")
log.info("\n\n\n")

log.info("Scheduling ${tests.size} tests")
log.debug(tests.joinToString(", ") { it.toTestName() })
val currentCoroutineContext = coroutineContext
Expand Down Expand Up @@ -146,7 +151,9 @@ class Marathon(
}
configuration.filteringConfiguration.allowlist.forEach { tests = it.toTestFilter().filter(tests) }
configuration.filteringConfiguration.blocklist.forEach { tests = it.toTestFilter().filterNot(tests) }
return tests
val iosConfig = configuration.vendorConfiguration as? VendorConfiguration.IOSConfiguration

return tests.filter { it.tags.contains(iosConfig?.xcTestRunnerTag) }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should just be an annotation filter, no vendor-specific logic in the core

}

private fun prepareTestShard(tests: List<Test>, analytics: Analytics): TestShard {
Expand Down
3 changes: 2 additions & 1 deletion core/src/main/kotlin/com/malinskiy/marathon/test/Test.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ data class Test(
val pkg: String,
val clazz: String,
val method: String,
val metaProperties: Collection<MetaProperty>
val metaProperties: Collection<MetaProperty>,
val tags: Collection<String> = emptyList()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MetaProprety is an abstraction over tags, annotations and so on. We should just reuse MetaProperty here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refer:
[iOS]Added Support for Cucumberish Tags / Function Annotations #702

) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand Down
13 changes: 8 additions & 5 deletions sample/ios-app/Marathonfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ poolingStrategy:
type: "omni"
batchingStrategy:
type: "fixed-size"
size: 5
size: 2
debug: true
filteringConfiguration:
allowlist:
- type: "simple-class-name"
regex: "StoryboardTests"
#filteringConfiguration:
# allowlist:
# - type: "simple-class-name"
# regex: "StoryboardTests"
vendorConfiguration:
type: "iOS"
derivedDataDir: "derived-data"
sourceRoot: "sample-appUITests"
knownHostsPath: ${HOME}/.ssh/known_hosts
remoteUsername: ${USER}
remotePrivateKey: ${HOME}/.ssh/marathon
xcResultBundlePath: "derived-data/resultBundlePath"
xcTestRunnerTag: "Flowers"
xcTestPlan: "UITesting"
34 changes: 34 additions & 0 deletions sample/ios-app/UITesting.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"configurations" : [
{
"id" : "F89CABE9-488A-423F-9C0B-ABBA5860F98F",
"name" : "Configuration 1",
"options" : {

}
}
],
"defaultOptions" : {
"codeCoverage" : false,
"targetForVariableExpansion" : {
"containerPath" : "container:sample-app.xcodeproj",
"identifier" : "1271DC9521351C6D002B8D3E",
"name" : "sample-app"
}
},
"testTargets" : [
{
"skippedTests" : [
"MoreTests\/testDismissModal()",
"SkippedSuite",
"StoryboardTests\/testDisabledButton()"
],
"target" : {
"containerPath" : "container:sample-app.xcodeproj",
"identifier" : "1271DCB421351C6D002B8D3E",
"name" : "sample-appUITests"
}
}
],
"version" : 1
}
24 changes: 24 additions & 0 deletions sample/ios-app/UnitTesting.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"configurations" : [
{
"id" : "BD75184A-5BEE-4C42-A2E2-3B453AFFCC77",
"name" : "Configuration 1",
"options" : {

}
}
],
"defaultOptions" : {

},
"testTargets" : [
{
"target" : {
"containerPath" : "container:sample-app.xcodeproj",
"identifier" : "686DC94628E1DAA800A49EC9",
"name" : "sample-appTests"
}
}
],
"version" : 1
}
Loading