Skip to content

Commit f2162d5

Browse files
committed
feat(apple): use provided xcuitest runner app as testApplication
this preserves original app signature
1 parent 4b8c0a1 commit f2162d5

File tree

11 files changed

+200
-86
lines changed

11 files changed

+200
-86
lines changed

configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

+34-14
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@ data class AppleTestBundleConfiguration(
2222
File(file.parentFile, file.nameWithoutExtension).apply { deleteRecursively(); mkdirs() }
2323
}
2424
) {
25-
@JsonIgnore var app: File? = null
26-
@JsonIgnore lateinit var xctest: File
25+
@JsonIgnore
26+
var app: File? = null
27+
@JsonIgnore
28+
var testApp: File? = null
29+
@JsonIgnore
30+
lateinit var xctest: File
2731

2832
fun validate() {
2933
when {
3034
application != null && testApplication != null -> {
3135
app = when {
3236
application.isFile && setOf("ipa", "zip").contains(application.extension) -> {
33-
extractAndValidateContainsDirectory(application, "app")
37+
extractAndFindDirectory(application, "app", validate = true)
3438
}
3539

3640
application.isDirectory && (application.extension == "app") -> {
@@ -39,45 +43,61 @@ data class AppleTestBundleConfiguration(
3943

4044
else -> throw ConfigurationException("application should be .ipa/.zip archive or a .app folder")
4145
}
42-
xctest = when {
46+
when {
4347
testApplication.isFile && setOf("ipa", "zip").contains(testApplication.extension) -> {
44-
extractAndValidateContainsDirectory(testApplication, "xctest")
48+
val extracted = extract(testApplication)
49+
val possibleTestApp = findDirectoryInDirectory(extracted, "app", validate = false)
50+
if (possibleTestApp != null) {
51+
testApp = possibleTestApp
52+
xctest = findDirectoryInDirectory(possibleTestApp, "xctest", validate = true)
53+
?: throw ConfigurationException("Unable to find xctest bundle")
54+
} else {
55+
xctest = findDirectoryInDirectory(extracted, "xctest", validate = true)
56+
?: throw ConfigurationException("Unable to find xctest bundle")
57+
}
58+
}
59+
60+
testApplication.isDirectory && testApplication.extension == "app" -> {
61+
testApp = testApplication
62+
xctest = findDirectoryInDirectory(testApplication, "xctest", validate = true)
63+
?: throw ConfigurationException("Unable to find xctest bundle")
4564
}
4665

4766
testApplication.isDirectory && testApplication.extension == "xctest" -> {
48-
testApplication
67+
xctest = testApplication
4968
}
5069

51-
else -> throw ConfigurationException("test application should be .ipa/.zip archive or a .xctest folder")
70+
else -> throw ConfigurationException("test application should be .ipa/.zip archive or a .app/.xctest folder")
5271
}
5372
}
5473

5574
derivedDataDir != null -> {
56-
xctest = findDirectoryInDirectory(derivedDataDir, "xctest")
57-
app = findDirectoryInDirectory(derivedDataDir, "app")
75+
xctest =
76+
findDirectoryInDirectory(derivedDataDir, "xctest", true) ?: throw ConfigurationException("Unable to find xctest bundle")
77+
app = findDirectoryInDirectory(derivedDataDir, "app", true)
5878
}
5979

6080
else -> throw ConfigurationException("please specify your application and test application either with files or provide derived data folder")
6181
}
6282
}
6383

64-
private fun findDirectoryInDirectory(directory: File, extension: String): File {
84+
private fun findDirectoryInDirectory(directory: File, extension: String, validate: Boolean): File? {
6585
var found = mutableListOf<File>()
6686
directory.walkTopDown().forEach {
6787
if (it.isDirectory && it.extension == extension) {
6888
found.add(it)
6989
}
7090
}
7191
when {
72-
found.isEmpty() -> throw ConfigurationException("Unable to find an $extension directory in ${directory.absolutePath}")
92+
found.isEmpty() && validate -> throw ConfigurationException("Unable to find an $extension directory in ${directory.absolutePath}")
7393
found.size > 1 -> throw ConfigurationException("Ambiguous $extension configuration in derived data folder [${found.joinToString { it.absolutePath }}]. Please specify parameters explicitly")
7494
}
75-
return found.first()
95+
return found.firstOrNull()
7696
}
7797

78-
private fun extractAndValidateContainsDirectory(file: File, extension: String): File {
98+
private fun extractAndFindDirectory(file: File, extension: String, validate: Boolean): File? {
7999
val extracted = extract(file)
80-
return findDirectoryInDirectory(extracted, extension)
100+
return findDirectoryInDirectory(extracted, extension, validate)
81101
}
82102

83103
private fun extract(file: File): File {

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleApplicationInstaller.kt

+31-12
Original file line numberDiff line numberDiff line change
@@ -11,34 +11,53 @@ import com.malinskiy.marathon.config.vendor.VendorConfiguration
1111
import com.malinskiy.marathon.config.vendor.apple.TestType
1212
import com.malinskiy.marathon.exceptions.DeviceSetupException
1313
import com.malinskiy.marathon.execution.withRetry
14+
import com.malinskiy.marathon.extension.relativePathTo
1415
import com.malinskiy.marathon.log.MarathonLogging
16+
import java.io.File
1517

16-
open class AppleApplicationInstaller<in T: AppleDevice>(
18+
open class AppleApplicationInstaller<in T : AppleDevice>(
1719
protected open val vendorConfiguration: VendorConfiguration,
1820
) {
1921
private val logger = MarathonLogging.logger {}
2022

2123
suspend fun prepareInstallation(device: AppleDevice, useXctestParser: Boolean = false) {
2224
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
2325
val xctestrunEnv = vendorConfiguration.xctestrunEnv() ?: throw IllegalArgumentException("No xctestrunEnv provided")
24-
val xcresultConfiguration = vendorConfiguration.xcresultConfiguration() ?: throw IllegalArgumentException("No xcresult configuration provided")
26+
val xcresultConfiguration =
27+
vendorConfiguration.xcresultConfiguration() ?: throw IllegalArgumentException("No xcresult configuration provided")
2528
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
2629
val app = bundleConfiguration.app
27-
val bundle = AppleTestBundle(app, xctest, device.sdk)
30+
val testApp = bundleConfiguration.testApp
31+
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
2832
val relativeTestBinaryPath = bundle.relativeBinaryPath
33+
val testBinary = bundle.testBinary
34+
var remoteXctest = ""
2935

30-
logger.debug { "Moving xctest to ${device.serialNumber}" }
31-
val remoteXctest = device.remoteFileManager.remoteXctestFile()
32-
withRetry(3, 1000L) {
33-
device.remoteFileManager.createRemoteDirectory()
34-
device.remoteFileManager.createRemoteSharedDirectory()
35-
if (!device.pushFolder(xctest, remoteXctest)) {
36-
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
36+
if (testApp != null) {
37+
logger.debug { "Moving xctest runner application to ${device.serialNumber}" }
38+
val remoteTestRunnerApplication = device.remoteFileManager.remoteTestRunnerApplication()
39+
val relativePath = xctest.relativePathTo(testApp).split(File.separator)
40+
remoteXctest = device.remoteFileManager.joinPath(remoteTestRunnerApplication, *relativePath.toTypedArray())
41+
withRetry(3, 1000L) {
42+
device.remoteFileManager.createRemoteDirectory()
43+
device.remoteFileManager.createRemoteSharedDirectory()
44+
if (!device.pushFolder(testApp, remoteTestRunnerApplication)) {
45+
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
46+
}
47+
}
48+
} else {
49+
logger.debug { "Moving xctest to ${device.serialNumber}" }
50+
remoteXctest = device.remoteFileManager.remoteXctestFile()
51+
withRetry(3, 1000L) {
52+
device.remoteFileManager.createRemoteDirectory()
53+
device.remoteFileManager.createRemoteSharedDirectory()
54+
if (!device.pushFolder(xctest, remoteXctest)) {
55+
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
56+
}
3757
}
3858
}
39-
logger.debug { "Generating test root for ${device.serialNumber}" }
4059

41-
val testBinary = bundle.testBinary
60+
logger.debug { "Generating test root for ${device.serialNumber}" }
4261
val remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, *relativeTestBinaryPath, testBinary.name)
4362
val testType = getTestTypeFor(device, device.sdk, remoteTestBinary)
4463
TestRootFactory(device, xctestrunEnv, xcresultConfiguration).generate(testType, bundle, useXctestParser)

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/NmTestParser.kt

+3-2
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ class NmTestParser(
3131
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
3232
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
3333
val app = bundleConfiguration.app
34-
val bundle = AppleTestBundle(app, xctest, device.sdk)
34+
val testApp = bundleConfiguration.testApp
35+
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
3536
return@withRetry parseTests(device, bundle)
3637
} catch (e: CancellationException) {
3738
throw e
@@ -48,7 +49,7 @@ class NmTestParser(
4849
): List<Test> {
4950
val testBinary = bundle.testBinary
5051
val relativeTestBinaryPath = bundle.relativeBinaryPath
51-
val xctest = bundle.testApplication
52+
val xctest = bundle.xctestBundle
5253

5354
logger.debug { "Found test binary $testBinary for xctest $xctest" }
5455

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/RemoteFileManager.kt

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ class RemoteFileManager(private val device: AppleDevice) {
4343
fun remoteXctestrunFile(): String = remoteFile(xctestrunFileName())
4444

4545
fun remoteXctestFile(): String = remoteSharedFile(xctestFileName())
46+
fun remoteTestRunnerApplication(): String = remoteSharedFile(testRunnerFileName())
4647
fun remoteXctestParserFile(): String = remoteSharedFile(`libXctestParserFileName`())
4748
fun remoteApplication(): String = remoteSharedFile(appUnderTestFileName())
4849
fun remoteExtraApplication(name: String) = remoteSharedFile(name)
@@ -58,6 +59,7 @@ class RemoteFileManager(private val device: AppleDevice) {
5859
private fun libXctestParserFileName(): String = "libxctest-parser.dylib"
5960

6061
fun appUnderTestFileName(): String = "appUnderTest.app"
62+
fun testRunnerFileName(): String = "xctestRunner.app"
6163

6264
private fun xcresultFileName(batch: TestBatch): String =
6365
"${device.udid}.${batch.id}.xcresult"

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/XCTestParser.kt

+2-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ class XCTestParser<T: AppleDevice>(
3636
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
3737
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
3838
val app = bundleConfiguration.app
39-
val bundle = AppleTestBundle(app, xctest, device.sdk)
39+
val testApp = bundleConfiguration.testApp
40+
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
4041
return@withRetry parseTests(device, bundle, applicationInstaller)
4142
} catch (e: CancellationException) {
4243
throw e

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/ConfigurationExtensions.kt

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.malinskiy.marathon.apple.extensions
22

3-
import com.malinskiy.marathon.apple.model.AppleTestBundle
43
import com.malinskiy.marathon.config.vendor.VendorConfiguration
54
import com.malinskiy.marathon.config.vendor.apple.AppleTestBundleConfiguration
65
import com.malinskiy.marathon.config.vendor.apple.ios.XcresultConfiguration

vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/AppleTestBundle.kt

+51-15
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,23 @@ import java.nio.file.Paths
1111

1212
class AppleTestBundle(
1313
val application: File?,
14-
val testApplication: File,
14+
val testApplication: File?,
15+
val xctestBundle: File,
1516
val sdk: Sdk,
1617
) : TestBundle() {
1718
private val logger = MarathonLogging.logger {}
1819
override val id: String
19-
get() = testApplication.absolutePath
20+
get() = xctestBundle.absolutePath
2021

2122
val applicationBundleInfo: BundleInfo? by lazy {
2223
application?.let {
2324
PropertyList.from<NSDictionary, BundleInfo>(
2425
when (sdk) {
25-
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> File(it, "Info.plist")
26+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> File(
27+
it,
28+
"Info.plist"
29+
)
30+
2631
Sdk.MACOS -> Paths.get(it.absolutePath, "Contents", "Info.plist").toFile()
2732
}
2833
)
@@ -34,36 +39,67 @@ class AppleTestBundle(
3439

3540
val testBundleInfo: BundleInfo by lazy {
3641
val file = when (sdk) {
37-
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> File(testApplication, "Info.plist")
38-
Sdk.MACOS -> Paths.get(testApplication.absolutePath, "Contents", "Info.plist").toFile()
42+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> File(
43+
xctestBundle,
44+
"Info.plist"
45+
)
46+
47+
Sdk.MACOS -> Paths.get(xctestBundle.absolutePath, "Contents", "Info.plist").toFile()
3948
}
4049
PropertyList.from(file)
4150
}
42-
val testBundleId = (testBundleInfo.naming.bundleName ?: testApplication.nameWithoutExtension).replace("[- ]".toRegex(), "_")
51+
val testBundleId = (testBundleInfo.naming.bundleName ?: xctestBundle.nameWithoutExtension).replace("[- ]".toRegex(), "_")
4352

4453
val testBinary: File by lazy {
4554
val possibleTestBinaries = when (sdk) {
46-
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> testApplication.listFiles()?.filter { it.isFile && it.extension == "" }
47-
?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
55+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> xctestBundle.listFiles()
56+
?.filter { it.isFile && it.extension == "" }
57+
?: throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")
4858

49-
Sdk.MACOS -> Paths.get(testApplication.absolutePath, *relativeBinaryPath).toFile().listFiles()
59+
Sdk.MACOS -> Paths.get(xctestBundle.absolutePath, *relativeBinaryPath).toFile().listFiles()
5060
?.filter { it.isFile && it.extension == "" }
51-
?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
61+
?: throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")
5262
}
5363
when (possibleTestBinaries.size) {
54-
0 -> throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
64+
0 -> throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")
5565
1 -> possibleTestBinaries[0]
5666
else -> {
57-
logger.warn { "Multiple test binaries present in xctest folder" }
58-
possibleTestBinaries.find { it.name == testApplication.nameWithoutExtension } ?: possibleTestBinaries.first()
67+
logger.warn { "Multiple test binaries present [${possibleTestBinaries.joinToString(",") { it.name }}] in xctest folder" }
68+
possibleTestBinaries.find { it.name == xctestBundle.nameWithoutExtension } ?: possibleTestBinaries.first()
69+
}
70+
}
71+
}
72+
73+
val testRunnerBinary: File by lazy {
74+
if (testApplication == null) {
75+
throw ConfigurationException("no test application provided")
76+
}
77+
78+
val possibleTestRunnerBinaries = when (sdk) {
79+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> testApplication.listFiles()
80+
?.filter { it.isFile && it.extension == "" }
81+
?: throw ConfigurationException("missing test binaries in test runner folder at $testApplication")
82+
83+
Sdk.MACOS -> Paths.get(testApplication.absolutePath, *relativeBinaryPath).toFile().listFiles()
84+
?.filter { it.isFile && it.extension == "" }
85+
?: throw ConfigurationException("missing test binaries in test runner folder at $testApplication")
86+
}
87+
when (possibleTestRunnerBinaries.size) {
88+
0 -> throw ConfigurationException("missing test binaries in test runner folder at $testApplication")
89+
1 -> possibleTestRunnerBinaries[0]
90+
else -> {
91+
logger.warn { "Multiple test binaries present [${possibleTestRunnerBinaries.joinToString(",") { it.name }}] in test runner folder" }
92+
possibleTestRunnerBinaries.find { it.name == testApplication.nameWithoutExtension } ?: possibleTestRunnerBinaries.first()
5993
}
6094
}
6195
}
6296

6397
val applicationBinary: File? by lazy {
6498
application?.let { application ->
6599
when (sdk) {
66-
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> application.listFiles()?.filter { it.isFile && it.extension == "" }
100+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> application.listFiles()
101+
?.filter { it.isFile && it.extension == "" }
102+
67103
Sdk.MACOS -> Paths.get(application.absolutePath, *relativeBinaryPath).toFile().listFiles()
68104
?.filter { it.isFile && it.extension == "" }
69105
}?.let { possibleBinaries ->
@@ -84,7 +120,7 @@ class AppleTestBundle(
84120
*/
85121
val relativeBinaryPath: Array<String> by lazy {
86122
when (sdk) {
87-
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> emptyArray()
123+
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> emptyArray()
88124
Sdk.MACOS -> arrayOf("Contents", "MacOS")
89125
}
90126
}

0 commit comments

Comments
 (0)