Skip to content

Commit

Permalink
feat(apple): use provided xcuitest runner app as testApplication
Browse files Browse the repository at this point in the history
this preserves original app signature
  • Loading branch information
Malinskiy committed Feb 23, 2024
1 parent 4b8c0a1 commit f2162d5
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 86 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,19 @@ data class AppleTestBundleConfiguration(
File(file.parentFile, file.nameWithoutExtension).apply { deleteRecursively(); mkdirs() }
}
) {
@JsonIgnore var app: File? = null
@JsonIgnore lateinit var xctest: File
@JsonIgnore
var app: File? = null

Check warning on line 26 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L26 was not covered by tests
@JsonIgnore
var testApp: File? = null

Check warning on line 28 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L28 was not covered by tests
@JsonIgnore
lateinit var xctest: File

fun validate() {
when {
application != null && testApplication != null -> {
app = when {
application.isFile && setOf("ipa", "zip").contains(application.extension) -> {
extractAndValidateContainsDirectory(application, "app")
extractAndFindDirectory(application, "app", validate = true)
}

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

else -> throw ConfigurationException("application should be .ipa/.zip archive or a .app folder")
}
xctest = when {
when {
testApplication.isFile && setOf("ipa", "zip").contains(testApplication.extension) -> {
extractAndValidateContainsDirectory(testApplication, "xctest")
val extracted = extract(testApplication)
val possibleTestApp = findDirectoryInDirectory(extracted, "app", validate = false)
if (possibleTestApp != null) {
testApp = possibleTestApp

Check warning on line 51 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L51 was not covered by tests
xctest = findDirectoryInDirectory(possibleTestApp, "xctest", validate = true)
?: throw ConfigurationException("Unable to find xctest bundle")

Check warning on line 53 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L53 was not covered by tests
} else {
xctest = findDirectoryInDirectory(extracted, "xctest", validate = true)
?: throw ConfigurationException("Unable to find xctest bundle")

Check warning on line 56 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L56 was not covered by tests
}
}

testApplication.isDirectory && testApplication.extension == "app" -> {
testApp = testApplication

Check warning on line 61 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L61 was not covered by tests
xctest = findDirectoryInDirectory(testApplication, "xctest", validate = true)
?: throw ConfigurationException("Unable to find xctest bundle")

Check warning on line 63 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L63 was not covered by tests
}

testApplication.isDirectory && testApplication.extension == "xctest" -> {
testApplication
xctest = testApplication
}

else -> throw ConfigurationException("test application should be .ipa/.zip archive or a .xctest folder")
else -> throw ConfigurationException("test application should be .ipa/.zip archive or a .app/.xctest folder")

Check warning on line 70 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/AppleTestBundleConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L70 was not covered by tests
}
}

derivedDataDir != null -> {
xctest = findDirectoryInDirectory(derivedDataDir, "xctest")
app = findDirectoryInDirectory(derivedDataDir, "app")
xctest =
findDirectoryInDirectory(derivedDataDir, "xctest", true) ?: throw ConfigurationException("Unable to find xctest bundle")
app = findDirectoryInDirectory(derivedDataDir, "app", true)
}

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

private fun findDirectoryInDirectory(directory: File, extension: String): File {
private fun findDirectoryInDirectory(directory: File, extension: String, validate: Boolean): File? {
var found = mutableListOf<File>()
directory.walkTopDown().forEach {
if (it.isDirectory && it.extension == extension) {
found.add(it)
}
}
when {
found.isEmpty() -> throw ConfigurationException("Unable to find an $extension directory in ${directory.absolutePath}")
found.isEmpty() && validate -> throw ConfigurationException("Unable to find an $extension directory in ${directory.absolutePath}")
found.size > 1 -> throw ConfigurationException("Ambiguous $extension configuration in derived data folder [${found.joinToString { it.absolutePath }}]. Please specify parameters explicitly")
}
return found.first()
return found.firstOrNull()
}

private fun extractAndValidateContainsDirectory(file: File, extension: String): File {
private fun extractAndFindDirectory(file: File, extension: String, validate: Boolean): File? {
val extracted = extract(file)
return findDirectoryInDirectory(extracted, extension)
return findDirectoryInDirectory(extracted, extension, validate)
}

private fun extract(file: File): File {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,34 +11,53 @@ import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.config.vendor.apple.TestType
import com.malinskiy.marathon.exceptions.DeviceSetupException
import com.malinskiy.marathon.execution.withRetry
import com.malinskiy.marathon.extension.relativePathTo
import com.malinskiy.marathon.log.MarathonLogging
import java.io.File

open class AppleApplicationInstaller<in T: AppleDevice>(
open class AppleApplicationInstaller<in T : AppleDevice>(
protected open val vendorConfiguration: VendorConfiguration,
) {
private val logger = MarathonLogging.logger {}

suspend fun prepareInstallation(device: AppleDevice, useXctestParser: Boolean = false) {
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
val xctestrunEnv = vendorConfiguration.xctestrunEnv() ?: throw IllegalArgumentException("No xctestrunEnv provided")
val xcresultConfiguration = vendorConfiguration.xcresultConfiguration() ?: throw IllegalArgumentException("No xcresult configuration provided")
val xcresultConfiguration =
vendorConfiguration.xcresultConfiguration() ?: throw IllegalArgumentException("No xcresult configuration provided")
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
val app = bundleConfiguration.app
val bundle = AppleTestBundle(app, xctest, device.sdk)
val testApp = bundleConfiguration.testApp
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
val relativeTestBinaryPath = bundle.relativeBinaryPath
val testBinary = bundle.testBinary
var remoteXctest = ""

logger.debug { "Moving xctest to ${device.serialNumber}" }
val remoteXctest = device.remoteFileManager.remoteXctestFile()
withRetry(3, 1000L) {
device.remoteFileManager.createRemoteDirectory()
device.remoteFileManager.createRemoteSharedDirectory()
if (!device.pushFolder(xctest, remoteXctest)) {
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
if (testApp != null) {
logger.debug { "Moving xctest runner application to ${device.serialNumber}" }
val remoteTestRunnerApplication = device.remoteFileManager.remoteTestRunnerApplication()
val relativePath = xctest.relativePathTo(testApp).split(File.separator)
remoteXctest = device.remoteFileManager.joinPath(remoteTestRunnerApplication, *relativePath.toTypedArray())
withRetry(3, 1000L) {
device.remoteFileManager.createRemoteDirectory()
device.remoteFileManager.createRemoteSharedDirectory()
if (!device.pushFolder(testApp, remoteTestRunnerApplication)) {
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
}
}
} else {
logger.debug { "Moving xctest to ${device.serialNumber}" }
remoteXctest = device.remoteFileManager.remoteXctestFile()
withRetry(3, 1000L) {
device.remoteFileManager.createRemoteDirectory()
device.remoteFileManager.createRemoteSharedDirectory()
if (!device.pushFolder(xctest, remoteXctest)) {
throw DeviceSetupException("Error transferring $xctest to ${device.serialNumber}")
}
}
}
logger.debug { "Generating test root for ${device.serialNumber}" }

val testBinary = bundle.testBinary
logger.debug { "Generating test root for ${device.serialNumber}" }
val remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, *relativeTestBinaryPath, testBinary.name)
val testType = getTestTypeFor(device, device.sdk, remoteTestBinary)
TestRootFactory(device, xctestrunEnv, xcresultConfiguration).generate(testType, bundle, useXctestParser)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ class NmTestParser(
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
val app = bundleConfiguration.app
val bundle = AppleTestBundle(app, xctest, device.sdk)
val testApp = bundleConfiguration.testApp
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
return@withRetry parseTests(device, bundle)
} catch (e: CancellationException) {
throw e
Expand All @@ -48,7 +49,7 @@ class NmTestParser(
): List<Test> {
val testBinary = bundle.testBinary
val relativeTestBinaryPath = bundle.relativeBinaryPath
val xctest = bundle.testApplication
val xctest = bundle.xctestBundle

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class RemoteFileManager(private val device: AppleDevice) {
fun remoteXctestrunFile(): String = remoteFile(xctestrunFileName())

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

fun appUnderTestFileName(): String = "appUnderTest.app"
fun testRunnerFileName(): String = "xctestRunner.app"

private fun xcresultFileName(batch: TestBatch): String =
"${device.udid}.${batch.id}.xcresult"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ class XCTestParser<T: AppleDevice>(
val bundleConfiguration = vendorConfiguration.bundleConfiguration()
val xctest = bundleConfiguration?.xctest ?: throw IllegalArgumentException("No test bundle provided")
val app = bundleConfiguration.app
val bundle = AppleTestBundle(app, xctest, device.sdk)
val testApp = bundleConfiguration.testApp
val bundle = AppleTestBundle(app, testApp, xctest, device.sdk)
return@withRetry parseTests(device, bundle, applicationInstaller)
} catch (e: CancellationException) {
throw e
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.malinskiy.marathon.apple.extensions

import com.malinskiy.marathon.apple.model.AppleTestBundle
import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.config.vendor.apple.AppleTestBundleConfiguration
import com.malinskiy.marathon.config.vendor.apple.ios.XcresultConfiguration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,23 @@ import java.nio.file.Paths

class AppleTestBundle(
val application: File?,
val testApplication: File,
val testApplication: File?,
val xctestBundle: File,
val sdk: Sdk,
) : TestBundle() {
private val logger = MarathonLogging.logger {}
override val id: String
get() = testApplication.absolutePath
get() = xctestBundle.absolutePath

val applicationBundleInfo: BundleInfo? by lazy {
application?.let {
PropertyList.from<NSDictionary, BundleInfo>(
when (sdk) {
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> File(it, "Info.plist")
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> File(
it,
"Info.plist"
)

Sdk.MACOS -> Paths.get(it.absolutePath, "Contents", "Info.plist").toFile()
}
)
Expand All @@ -34,36 +39,67 @@ class AppleTestBundle(

val testBundleInfo: BundleInfo by lazy {
val file = when (sdk) {
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> File(testApplication, "Info.plist")
Sdk.MACOS -> Paths.get(testApplication.absolutePath, "Contents", "Info.plist").toFile()
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> File(
xctestBundle,
"Info.plist"
)

Sdk.MACOS -> Paths.get(xctestBundle.absolutePath, "Contents", "Info.plist").toFile()
}
PropertyList.from(file)
}
val testBundleId = (testBundleInfo.naming.bundleName ?: testApplication.nameWithoutExtension).replace("[- ]".toRegex(), "_")
val testBundleId = (testBundleInfo.naming.bundleName ?: xctestBundle.nameWithoutExtension).replace("[- ]".toRegex(), "_")

val testBinary: File by lazy {
val possibleTestBinaries = when (sdk) {
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 == "" }
?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> xctestBundle.listFiles()
?.filter { it.isFile && it.extension == "" }
?: throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")

Sdk.MACOS -> Paths.get(testApplication.absolutePath, *relativeBinaryPath).toFile().listFiles()
Sdk.MACOS -> Paths.get(xctestBundle.absolutePath, *relativeBinaryPath).toFile().listFiles()
?.filter { it.isFile && it.extension == "" }
?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
?: throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")
}
when (possibleTestBinaries.size) {
0 -> throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
0 -> throw ConfigurationException("missing test binaries in xctest folder at $xctestBundle")
1 -> possibleTestBinaries[0]
else -> {
logger.warn { "Multiple test binaries present in xctest folder" }
possibleTestBinaries.find { it.name == testApplication.nameWithoutExtension } ?: possibleTestBinaries.first()
logger.warn { "Multiple test binaries present [${possibleTestBinaries.joinToString(",") { it.name }}] in xctest folder" }
possibleTestBinaries.find { it.name == xctestBundle.nameWithoutExtension } ?: possibleTestBinaries.first()
}
}
}

val testRunnerBinary: File by lazy {
if (testApplication == null) {
throw ConfigurationException("no test application provided")
}

val possibleTestRunnerBinaries = when (sdk) {
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 == "" }
?: throw ConfigurationException("missing test binaries in test runner folder at $testApplication")

Sdk.MACOS -> Paths.get(testApplication.absolutePath, *relativeBinaryPath).toFile().listFiles()
?.filter { it.isFile && it.extension == "" }
?: throw ConfigurationException("missing test binaries in test runner folder at $testApplication")
}
when (possibleTestRunnerBinaries.size) {
0 -> throw ConfigurationException("missing test binaries in test runner folder at $testApplication")
1 -> possibleTestRunnerBinaries[0]
else -> {
logger.warn { "Multiple test binaries present [${possibleTestRunnerBinaries.joinToString(",") { it.name }}] in test runner folder" }
possibleTestRunnerBinaries.find { it.name == testApplication.nameWithoutExtension } ?: possibleTestRunnerBinaries.first()
}
}
}

val applicationBinary: File? by lazy {
application?.let { application ->
when (sdk) {
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 == "" }
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 == "" }

Sdk.MACOS -> Paths.get(application.absolutePath, *relativeBinaryPath).toFile().listFiles()
?.filter { it.isFile && it.extension == "" }
}?.let { possibleBinaries ->
Expand All @@ -84,7 +120,7 @@ class AppleTestBundle(
*/
val relativeBinaryPath: Array<String> by lazy {
when (sdk) {
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION , Sdk.VISION_SIMULATOR -> emptyArray()
Sdk.IPHONEOS, Sdk.IPHONESIMULATOR, Sdk.TV, Sdk.TV_SIMULATOR, Sdk.WATCH, Sdk.WATCH_SIMULATOR, Sdk.VISION, Sdk.VISION_SIMULATOR -> emptyArray()
Sdk.MACOS -> arrayOf("Contents", "MacOS")
}
}
Expand Down
Loading

0 comments on commit f2162d5

Please sign in to comment.