Skip to content

Commit

Permalink
feat(ios): add dataContainerClear
Browse files Browse the repository at this point in the history
  • Loading branch information
Malinskiy committed Sep 8, 2023
1 parent ca7f42a commit 6589cb6
Show file tree
Hide file tree
Showing 10 changed files with 137 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ sealed class VendorConfiguration {
@JsonProperty("compactOutput") val compactOutput: Boolean = false,
@JsonProperty("rsync") val rsync: RsyncConfiguration = RsyncConfiguration(),
@JsonProperty("xcodebuildTestArgs") val xcodebuildTestArgs: Map<String, String> = emptyMap(),
@JsonProperty("dataContainerClear") val dataContainerClear: Boolean = false,

@JsonProperty("signing") val signing: SigningConfiguration = SigningConfiguration(),
) : VendorConfiguration() {
Expand Down
9 changes: 9 additions & 0 deletions docs/docs/ios/configure.md
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,15 @@ rsync:
remotePath: "/usr/bin/rsync-custom"
```

### Clear state between test batch executions
By default, marathon does not clear state between test batch executions. To mitigate potential test side effects, one could add an option to
clear the container data between test runs. Keep in mind that test side effects might still be present.
If you want to isolate tests even further, then you should consider reducing the batch size.

```yaml
dataContainerClear: true
```

[1]: workers.md
[2]: /configuration/dynamic-configuration.md
[3]: https://en.wikipedia.org/wiki/ISO_8601
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.config.vendor.ios.TestType
import com.malinskiy.marathon.exceptions.DeviceSetupException
import com.malinskiy.marathon.execution.withRetry
import com.malinskiy.marathon.ios.extensions.testBundle
import com.malinskiy.marathon.ios.model.Sdk
import com.malinskiy.marathon.ios.xctestrun.TestRootFactory
import com.malinskiy.marathon.log.MarathonLogging
Expand All @@ -18,9 +19,9 @@ class AppleApplicationInstaller(
private val logger = MarathonLogging.logger {}

suspend fun prepareInstallation(device: AppleSimulatorDevice) {
val bundle = vendorConfiguration.bundle ?: throw ConfigurationException("no xctest found for configuration")
val bundle = vendorConfiguration.testBundle() ?: throw ConfigurationException("no xctest found for configuration")

val xctest = bundle.xctest
val xctest = bundle.testApplication
logger.debug { "Moving xctest to ${device.serialNumber}" }
val remoteXctest = device.remoteFileManager.remoteXctestFile()
withRetry(3, 1000L) {
Expand All @@ -47,7 +48,7 @@ class AppleApplicationInstaller(
TestRootFactory(device, vendorConfiguration).generate(testType, bundle)
grantPermissions(device)

bundle.extraApplications?.forEach {
vendorConfiguration.bundle?.extraApplications?.forEach {
if (it.isDirectory && it.extension == "app") {
logger.debug { "Installing extra application $it to ${device.serialNumber}" }
val remoteExtraApplication = device.remoteFileManager.remoteExtraApplication(it.name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import com.malinskiy.marathon.test.toTestName
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.channels.ReceiveChannel

class AppleDeviceTestRunner(private val device: AppleSimulatorDevice) {
class AppleDeviceTestRunner(private val device: AppleSimulatorDevice, private val bundleIdentifier: AppleTestBundleIdentifier) {
private val logger = MarathonLogging.logger {}

suspend fun execute(
Expand All @@ -43,7 +43,15 @@ class AppleDeviceTestRunner(private val device: AppleSimulatorDevice) {
)
var channel: ReceiveChannel<List<TestEvent>>? = null
try {
clearData(vendorConfiguration)
if (vendorConfiguration.dataContainerClear) {
val bundleIds = rawTestBatch.tests.map {
bundleIdentifier.identify(it).appId
}.toSet()
bundleIds.forEach {
device.clearAppContainer(it)
}
}

listener.beforeTestRun()

val localChannel = device.executeTestRequest(runnerRequest)
Expand Down Expand Up @@ -77,11 +85,4 @@ class AppleDeviceTestRunner(private val device: AppleSimulatorDevice) {
}
}
}

private suspend fun clearData(vendorConfiguration: VendorConfiguration.IOSConfiguration) {
// if (vendorConfiguration.eraseSimulatorOnStart) {
// device.shutdown()
// device.erase()
// }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.malinskiy.marathon.execution.listener.LineListener
import com.malinskiy.marathon.execution.listener.LogListener
import com.malinskiy.marathon.io.FileManager
import com.malinskiy.marathon.ios.bin.AppleBinaryEnvironment
import com.malinskiy.marathon.ios.bin.xcrun.simctl.service.ApplicationService
import com.malinskiy.marathon.ios.cmd.CommandExecutor
import com.malinskiy.marathon.ios.cmd.CommandResult
import com.malinskiy.marathon.ios.cmd.FileBridge
Expand Down Expand Up @@ -105,12 +106,13 @@ class AppleSimulatorDevice(
val arch: Arch
get() = when {
sdk == Sdk.IPHONESIMULATOR -> {
when(abi) {
when (abi) {
"x86_64" -> Arch.x86_64
"arm64" -> Arch.arm64
else -> Arch.arm64
}
}

udid.contains('-') -> Arch.arm64e
else -> Arch.arm64
}
Expand All @@ -126,7 +128,10 @@ class AppleSimulatorDevice(
private lateinit var devicePlistPath: String
private var deviceDescriptor: Map<*, *>? = null
private val dispatcher by lazy {
newFixedThreadPoolContext(vendorConfiguration.threadingConfiguration.deviceThreads, "AppleSimulatorDevice - execution - ${commandExecutor.host.id}")
newFixedThreadPoolContext(
vendorConfiguration.threadingConfiguration.deviceThreads,
"AppleSimulatorDevice - execution - ${commandExecutor.host.id}"
)
}
override val coroutineContext: CoroutineContext = dispatcher
override val remoteFileManager: RemoteFileManager = RemoteFileManager(this)
Expand Down Expand Up @@ -244,7 +249,7 @@ class AppleSimulatorDevice(
try {
val (listener, lineListeners) = createExecutionListeners(devicePoolId, testBatch, deferred)
executionLineListeners = lineListeners.onEach { addLineListener(it) }
AppleDeviceTestRunner(this@AppleSimulatorDevice).execute(configuration, vendorConfiguration, testBatch, listener)
AppleDeviceTestRunner(this@AppleSimulatorDevice, testBundleIdentifier).execute(configuration, vendorConfiguration, testBatch, listener)
} finally {
executionLineListeners.forEach { removeLineListener(it) }
}
Expand Down Expand Up @@ -750,4 +755,15 @@ class AppleSimulatorDevice(
suspend fun grant(permission: Permission, bundleId: String): Boolean {
return binaryEnvironment.xcrun.simctl.privacy.grant(udid, permission, bundleId).successful
}

suspend fun clearAppContainer(bundleId: String) {
binaryEnvironment.xcrun.simctl.application.terminateApplication(udid, bundleId)

val containerPath = binaryEnvironment.xcrun.simctl.application.containerPath(udid, bundleId, ApplicationService.ContainerType.DATA)
if (containerPath.successful) {
remoteFileManager.removeRemotePath(containerPath.combinedStdout.trim())
} else {
logger.warn { "Failed to clear app container:\nstdout: ${containerPath.combinedStdout}\nstderr: ${containerPath.combinedStderr}" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import com.malinskiy.marathon.config.Configuration
import com.malinskiy.marathon.config.exceptions.ConfigurationException
import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.device.Device
import com.malinskiy.marathon.device.DeviceProvider
import com.malinskiy.marathon.exceptions.TestParsingException
import com.malinskiy.marathon.execution.RemoteTestParser
import com.malinskiy.marathon.execution.withRetry
import com.malinskiy.marathon.ios.extensions.testBundle
import com.malinskiy.marathon.ios.model.AppleTestBundle
import com.malinskiy.marathon.log.MarathonLogging
import com.malinskiy.marathon.test.Test
Expand All @@ -22,23 +22,11 @@ class AppleTestParser(
private val logger = MarathonLogging.logger(AppleTestParser::class.java.simpleName)

override suspend fun extract(device: Device): List<Test> {
val app = vendorConfiguration.bundle?.app ?: throw IllegalArgumentException("No application bundle provided")
val xctest = vendorConfiguration.bundle?.xctest ?: throw IllegalArgumentException("No test bundle provided")
val possibleTestBinaries = xctest.listFiles()?.filter { it.isFile && it.extension == "" }
?: throw ConfigurationException("missing test binaries in xctest folder at $xctest")
val testBinary = when (possibleTestBinaries.size) {
0 -> throw ConfigurationException("missing test binaries in xctest folder at $xctest")
1 -> possibleTestBinaries[0]
else -> {
logger.warn { "Multiple test binaries present in xctest folder" }
possibleTestBinaries.find { it.name == xctest.nameWithoutExtension } ?: possibleTestBinaries.first()
}
}

val bundle = vendorConfiguration.testBundle()
return withRetry(3, 0) {
try {
val device = device as? AppleSimulatorDevice ?: throw ConfigurationException("Unexpected device type for remote test parsing")
return@withRetry parseTests(device, xctest, testBinary)
return@withRetry parseTests(device, bundle)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Expand All @@ -50,9 +38,10 @@ class AppleTestParser(

private suspend fun parseTests(
device: AppleSimulatorDevice,
xctest: File,
testBinary: File
bundle: AppleTestBundle,
): List<Test> {
val testBinary = bundle.testBinary
val xctest = bundle.testApplication

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

Expand Down Expand Up @@ -86,7 +75,7 @@ class AppleTestParser(
}


val testBundle = AppleTestBundle(vendorConfiguration.bundle?.application, xctest, testBinary)
val testBundle = AppleTestBundle(vendorConfiguration.bundle?.application, xctest)
swiftTests.forEach { testBundleIdentifier.put(it, testBundle) }
objectiveCTests.forEach { testBundleIdentifier.put(it, testBundle) }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,24 @@ class ApplicationService (commandExecutor: CommandExecutor,
)
}

suspend fun containerPath(udid: String, bundleId: String, containerType: ContainerType): CommandResult {
return criticalExec(
timeout = timeoutConfiguration.shell,
"get_app_container", udid, bundleId, containerType.value
)
}

enum class ContainerType(val value: String) {
APPLICATION("app"),
DATA("data"),
GROUPS("groups")
}

/**
* Terminates a running application with the given bundle ID on this device
*/
suspend fun terminateApplication(udid: String, bundleId: String): CommandResult {
return criticalExec(
suspend fun terminateApplication(udid: String, bundleId: String): CommandResult? {
return safeExecute(
timeout = timeoutConfiguration.shell,
"terminate", udid, bundleId
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.malinskiy.marathon.ios.extensions

import com.malinskiy.marathon.config.vendor.VendorConfiguration
import com.malinskiy.marathon.ios.model.AppleTestBundle

fun VendorConfiguration.IOSConfiguration.testBundle(): AppleTestBundle {
val xctest = bundle?.xctest ?: throw IllegalArgumentException("No test bundle provided")
val app = bundle?.app ?: throw IllegalArgumentException("No application bundle provided")

return AppleTestBundle(app, xctest)
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,47 @@
package com.malinskiy.marathon.ios.model

import com.dd.plist.NSDictionary
import com.malinskiy.marathon.config.exceptions.ConfigurationException
import com.malinskiy.marathon.execution.bundle.TestBundle
import com.malinskiy.marathon.ios.plist.PropertyList
import com.malinskiy.marathon.ios.plist.bundle.BundleInfo
import com.malinskiy.marathon.log.MarathonLogging
import java.io.File

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

val applicationBundleInfo: BundleInfo? by lazy {
application?.let {
PropertyList.from<NSDictionary, BundleInfo>(
File(
it,
"Info.plist"
)
)
}
}
val appId =
applicationBundleInfo?.identification?.bundleIdentifier ?: throw ConfigurationException("No bundle identifier specified in $application")

val testBundleInfo: BundleInfo by lazy { PropertyList.from(File(testApplication, "Info.plist")) }
val testBundleId = (testBundleInfo.naming.bundleName ?: testApplication.nameWithoutExtension).replace('-', '_')

val testBinary: File by lazy {
val possibleTestBinaries = testApplication.listFiles()?.filter { it.isFile && it.extension == "" }
?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
when (possibleTestBinaries.size) {
0 -> throw ConfigurationException("missing test binaries in xctest folder at $testApplication")
1 -> possibleTestBinaries[0]
else -> {
logger.warn { "Multiple test binaries present in xctest folder" }
possibleTestBinaries.find { it.name == testApplication.nameWithoutExtension } ?: possibleTestBinaries.first()
}
}
}
}
Loading

0 comments on commit 6589cb6

Please sign in to comment.