diff --git a/cli/build.gradle.kts b/cli/build.gradle.kts index 03289e496..bcc61c9e1 100644 --- a/cli/build.gradle.kts +++ b/cli/build.gradle.kts @@ -35,6 +35,7 @@ distributions { dependencies { implementation(project(":core")) implementation(project(":vendor:vendor-apple:ios")) + implementation(project(":vendor:vendor-apple:macos")) implementation(project(":vendor:vendor-android")) implementation(project(":analytics:usage")) implementation(Libraries.kotlinStdLib) diff --git a/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt b/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt index 06923d63f..0a162c5cf 100644 --- a/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt +++ b/cli/src/main/kotlin/com/malinskiy/marathon/cli/ApplicationView.kt @@ -21,6 +21,7 @@ import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.di.marathonStartKoin import com.malinskiy.marathon.exceptions.ExceptionsReporterFactory import com.malinskiy.marathon.apple.ios.IosVendor +import com.malinskiy.marathon.apple.macos.MacosVendor import com.malinskiy.marathon.log.MarathonLogging import org.koin.core.context.stopKoin import org.koin.dsl.module @@ -71,6 +72,9 @@ private fun execute(cliConfiguration: CliConfiguration) { is VendorConfiguration.AndroidConfiguration -> { AndroidVendor + module { single { vendorConfiguration } } + listOf(adamModule) } + is VendorConfiguration.MacosConfiguration -> { + MacosVendor + module { single { vendorConfiguration } } + } else -> throw ConfigurationException("No vendor config present in ${marathonStartConfiguration.marathonfile.absolutePath}") } 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 4ebb2a9b6..9b157638f 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 @@ -13,20 +13,19 @@ import com.malinskiy.marathon.config.vendor.android.SerialStrategy import com.malinskiy.marathon.config.vendor.android.TestAccessConfiguration import com.malinskiy.marathon.config.vendor.android.TestParserConfiguration import com.malinskiy.marathon.config.vendor.android.ThreadingConfiguration -import com.malinskiy.marathon.config.vendor.android.TimeoutConfiguration import com.malinskiy.marathon.config.vendor.apple.AppleTestBundleConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.LifecycleConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.PermissionsConfiguration import com.malinskiy.marathon.config.vendor.apple.RsyncConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.SigningConfiguration import com.malinskiy.marathon.config.vendor.apple.SshConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration as AppleTimeoutConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.XcresultConfiguration import java.io.File +import com.malinskiy.marathon.config.vendor.android.TimeoutConfiguration as AndroidTimeoutConfiguration import com.malinskiy.marathon.config.vendor.apple.TestParserConfiguration as AppleTestParserConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.ScreenRecordConfiguration as IosScreenRecordConfiguration import com.malinskiy.marathon.config.vendor.apple.ThreadingConfiguration as AppleThreadingConfiguration -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration as IosTimeoutConfiguration -import com.malinskiy.marathon.config.vendor.apple.macos.TimeoutConfiguration as MacosTimeoutConfiguration1 const val DEFAULT_INIT_TIMEOUT_MILLIS = 30_000 const val DEFAULT_AUTO_GRANT_PERMISSION = false @@ -66,7 +65,7 @@ sealed class VendorConfiguration { @JsonProperty("screenRecordConfiguration") val screenRecordConfiguration: ScreenRecordConfiguration = ScreenRecordConfiguration(), @JsonProperty("waitForDevicesTimeoutMillis") val waitForDevicesTimeoutMillis: Long = DEFAULT_WAIT_FOR_DEVICES_TIMEOUT, @JsonProperty("allureConfiguration") val allureConfiguration: AllureConfiguration = AllureConfiguration(), - @JsonProperty("timeoutConfiguration") val timeoutConfiguration: TimeoutConfiguration = TimeoutConfiguration(), + @JsonProperty("timeoutConfiguration") val timeoutConfiguration: AndroidTimeoutConfiguration = AndroidTimeoutConfiguration(), @JsonProperty("fileSyncConfiguration") val fileSyncConfiguration: FileSyncConfiguration = FileSyncConfiguration(), @JsonProperty("threadingConfiguration") val threadingConfiguration: ThreadingConfiguration = ThreadingConfiguration(), @JsonProperty("testParserConfiguration") val testParserConfiguration: TestParserConfiguration = TestParserConfiguration.LocalTestParserConfiguration, @@ -114,7 +113,7 @@ sealed class VendorConfiguration { var screenRecordConfiguration: ScreenRecordConfiguration = ScreenRecordConfiguration() var waitForDevicesTimeoutMillis: Long = DEFAULT_WAIT_FOR_DEVICES_TIMEOUT var allureConfiguration: AllureConfiguration = AllureConfiguration() - var timeoutConfiguration: TimeoutConfiguration = TimeoutConfiguration() + var timeoutConfiguration: AndroidTimeoutConfiguration = AndroidTimeoutConfiguration() var fileSyncConfiguration: FileSyncConfiguration = FileSyncConfiguration() var threadingConfiguration: ThreadingConfiguration = ThreadingConfiguration() var testParserConfiguration: TestParserConfiguration = TestParserConfiguration.LocalTestParserConfiguration @@ -159,7 +158,7 @@ sealed class VendorConfiguration { @JsonProperty("xctestrunEnv") val xctestrunEnv: Map = emptyMap(), @JsonProperty("lifecycle") val lifecycleConfiguration: LifecycleConfiguration = LifecycleConfiguration(), @JsonProperty("permissions") val permissions: PermissionsConfiguration = PermissionsConfiguration(), - @JsonProperty("timeoutConfiguration") val timeoutConfiguration: IosTimeoutConfiguration = IosTimeoutConfiguration(), + @JsonProperty("timeoutConfiguration") val timeoutConfiguration: AppleTimeoutConfiguration = AppleTimeoutConfiguration(), @JsonProperty("threadingConfiguration") val threadingConfiguration: AppleThreadingConfiguration = AppleThreadingConfiguration(), @JsonProperty("hideRunnerOutput") val hideRunnerOutput: Boolean = false, @JsonProperty("compactOutput") val compactOutput: Boolean = false, @@ -185,7 +184,7 @@ sealed class VendorConfiguration { @JsonProperty("xcresult") val xcresult: XcresultConfiguration = XcresultConfiguration(), @JsonProperty("xctestrunEnv") val xctestrunEnv: Map = emptyMap(), - @JsonProperty("timeoutConfiguration") val timeoutConfiguration: MacosTimeoutConfiguration1 = MacosTimeoutConfiguration1(), + @JsonProperty("timeoutConfiguration") val timeoutConfiguration: AppleTimeoutConfiguration = AppleTimeoutConfiguration(), @JsonProperty("threadingConfiguration") val threadingConfiguration: AppleThreadingConfiguration = AppleThreadingConfiguration(), @JsonProperty("hideRunnerOutput") val hideRunnerOutput: Boolean = false, @JsonProperty("compactOutput") val compactOutput: Boolean = false, diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/ios/TimeoutConfiguration.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/TimeoutConfiguration.kt similarity index 96% rename from configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/ios/TimeoutConfiguration.kt rename to configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/TimeoutConfiguration.kt index 46435e6b1..f7775139e 100644 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/ios/TimeoutConfiguration.kt +++ b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/TimeoutConfiguration.kt @@ -1,4 +1,4 @@ -package com.malinskiy.marathon.config.vendor.apple.ios +package com.malinskiy.marathon.config.vendor.apple import com.fasterxml.jackson.annotation.JsonProperty import java.time.Duration diff --git a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/macos/TimeoutConfiguration.kt b/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/macos/TimeoutConfiguration.kt deleted file mode 100644 index 0a23fcdae..000000000 --- a/configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/macos/TimeoutConfiguration.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.malinskiy.marathon.config.vendor.apple.macos - -import com.fasterxml.jackson.annotation.JsonProperty -import java.time.Duration -import java.time.temporal.ChronoUnit - -/** - * @param shell default timeout for shell commands - * @param shellIdle default idling timeout for shell commands - * @param reachability timeout for inactive remote host - */ -data class TimeoutConfiguration( - @JsonProperty("shell") var shell: Duration = Duration.ofSeconds(30), - @JsonProperty("shellIdle") var shellIdle: Duration = Duration.ofSeconds(30), - @JsonProperty("reachability") var reachability: Duration = Duration.ofSeconds(5), -) diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleApplicationInstaller.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleApplicationInstaller.kt index 93f392fd9..0615aefbc 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleApplicationInstaller.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleApplicationInstaller.kt @@ -24,7 +24,8 @@ open class AppleApplicationInstaller( 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) + val bundle = AppleTestBundle(app, xctest, device.sdk) + val relativeTestBinaryPath = bundle.relativeTestBinaryPath logger.debug { "Moving xctest to ${device.serialNumber}" } val remoteXctest = device.remoteFileManager.remoteXctestFile() @@ -37,22 +38,12 @@ open class AppleApplicationInstaller( } logger.debug { "Generating test root for ${device.serialNumber}" } - 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 remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, testBinary.name) + val testBinary = bundle.testBinary + val remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, *relativeTestBinaryPath, testBinary.name) val testType = getTestTypeFor(device, device.sdk, remoteTestBinary) TestRootFactory(device, xctestrunEnv, xcresultConfiguration).generate(testType, bundle, useXctestParser) afterInstall(device as T) - bundleConfiguration.extraApplications?.forEach { if (it.isDirectory && it.extension == "app") { logger.debug { "Installing extra application $it to ${device.serialNumber}" } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleLogConfigurator.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleLogConfigurator.kt index 72ce6dfc5..5539e32e6 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleLogConfigurator.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/AppleLogConfigurator.kt @@ -7,7 +7,6 @@ import ch.qos.logback.classic.PatternLayout import ch.qos.logback.classic.spi.ILoggingEvent import ch.qos.logback.core.ConsoleAppender import ch.qos.logback.core.encoder.LayoutWrappingEncoder -import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.log.MarathonLogConfigurator import com.malinskiy.marathon.report.timeline.TimelineSummaryProvider import net.schmizz.sshj.DefaultConfig @@ -16,11 +15,10 @@ import net.schmizz.sshj.transport.kex.Curve25519SHA256 import net.schmizz.sshj.transport.random.BouncyCastleRandom import org.slf4j.LoggerFactory -class AppleLogConfigurator(private val vendorConfiguration: VendorConfiguration.IOSConfiguration) : MarathonLogConfigurator { +class AppleLogConfigurator(private val compactOutput: Boolean) : MarathonLogConfigurator { override fun configure() { val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext - val compactOutput = vendorConfiguration.compactOutput val layout = PatternLayout() layout.pattern = if (compactOutput) { "%highlight(%.-1level [%thread] <%logger{48}> %msg%n)" diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/NmTestParser.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/NmTestParser.kt index 434bfc1d2..ffd9fb306 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/NmTestParser.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/NmTestParser.kt @@ -1,6 +1,6 @@ package com.malinskiy.marathon.apple -import com.malinskiy.marathon.apple.extensions.testBundle +import com.malinskiy.marathon.apple.extensions.bundleConfiguration import com.malinskiy.marathon.apple.model.AppleTestBundle import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.exceptions.ConfigurationException @@ -25,10 +25,13 @@ class NmTestParser( private val logger = MarathonLogging.logger(NmTestParser::class.java.simpleName) override suspend fun extract(device: Device): List { - val bundle = vendorConfiguration.testBundle() return withRetry(3, 0) { try { val device = device as? AppleDevice ?: throw ConfigurationException("Unexpected device type for remote test parsing") + 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) return@withRetry parseTests(device, bundle) } catch (e: CancellationException) { throw e @@ -44,6 +47,7 @@ class NmTestParser( bundle: AppleTestBundle, ): List { val testBinary = bundle.testBinary + val relativeTestBinaryPath = bundle.relativeTestBinaryPath val xctest = bundle.testApplication logger.debug { "Found test binary $testBinary for xctest $xctest" } @@ -54,7 +58,7 @@ class NmTestParser( if (!device.pushFile(xctest, remoteXctest)) { throw TestParsingException("failed to push xctest for test parsing") } - val remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, testBinary.name) + val remoteTestBinary = device.remoteFileManager.joinPath(remoteXctest, *relativeTestBinaryPath, testBinary.name) val rawSwiftTests = device.binaryEnvironment.nm.swiftTests(remoteTestBinary) val swiftTests = rawSwiftTests.map { it.trim().split('.') } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/RemoteFileManager.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/RemoteFileManager.kt index 327fcd9ff..6e56c14e3 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/RemoteFileManager.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/RemoteFileManager.kt @@ -136,6 +136,8 @@ class RemoteFileManager(private val device: AppleDevice) { ) } + private fun String.bashEscape() = "'" + replace("'", "'\\''") + "'" + companion object { const val FILE_SEPARATOR = "/" } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/XCTestParser.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/XCTestParser.kt index 88e80fcea..c8eab8362 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/XCTestParser.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/XCTestParser.kt @@ -1,7 +1,6 @@ package com.malinskiy.marathon.apple import com.malinskiy.marathon.apple.extensions.bundleConfiguration -import com.malinskiy.marathon.apple.extensions.testBundle import com.malinskiy.marathon.apple.model.AppleTestBundle import com.malinskiy.marathon.apple.test.TestEvent import com.malinskiy.marathon.apple.test.TestRequest @@ -23,7 +22,7 @@ import kotlin.io.path.outputStream class XCTestParser( private val configuration: Configuration, - private val vendorConfiguration: VendorConfiguration.IOSConfiguration, + private val vendorConfiguration: VendorConfiguration.MacosConfiguration, private val testBundleIdentifier: AppleTestBundleIdentifier, private val applicationInstaller: AppleApplicationInstaller, ) : RemoteTestParser, LineListener { @@ -34,7 +33,11 @@ class XCTestParser( try { val device = device as? T ?: throw ConfigurationException("Unexpected device type for remote test parsing") - return@withRetry parseTests(device, configuration, vendorConfiguration, applicationInstaller) + 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) + return@withRetry parseTests(device, bundle, applicationInstaller) } catch (e: CancellationException) { throw e } catch (e: Exception) { @@ -46,13 +49,12 @@ class XCTestParser( private suspend fun parseTests( device: AppleDevice, - configuration: Configuration, - vendorConfiguration: VendorConfiguration, + bundle: AppleTestBundle, applicationInstaller: AppleApplicationInstaller, ): List { applicationInstaller.prepareInstallation(device, useXctestParser = true) - val platform = "iPhoneSimulator" + val platform = device.sdk.platformName val dylib = javaClass.getResourceAsStream("/libxctest-parser/$platform/libxctest-parser.dylib") val tempFile = kotlin.io.path.createTempFile().apply { outputStream().use { @@ -86,11 +88,12 @@ class XCTestParser( when (event) { is TestStarted -> { //Target name is never printed via xcodebuild. We create it using the bundle id in com.malinskiy.marathon.ios.xctestrun.TestRootFactory - val testWithTargetName = event.id.copy(pkg = vendorConfiguration.testBundle().testBundleId) + val testWithTargetName = event.id.copy(pkg = bundle.testBundleId) tests.add(testWithTargetName) } else -> Unit } + } } @@ -104,25 +107,12 @@ class XCTestParser( device.removeLineListener(this) } - val xctest = vendorConfiguration.bundleConfiguration()?.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() - } - } - if (tests.size == 0) { logger.warn { "XCTestParser failed to parse tests. xcodebuild output:" + System.lineSeparator() + "$lineBuffer" } } - val testBundle = AppleTestBundle(vendorConfiguration.bundleConfiguration()?.app, xctest) val result = tests.toList() - result.forEach { testBundleIdentifier.put(it, testBundle) } + result.forEach { testBundleIdentifier.put(it, bundle) } return result } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/AppleBinaryEnvironment.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/AppleBinaryEnvironment.kt index e404eb7b1..407ff3a3f 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/AppleBinaryEnvironment.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/AppleBinaryEnvironment.kt @@ -3,23 +3,25 @@ package com.malinskiy.marathon.apple.bin import com.google.gson.Gson import com.malinskiy.marathon.apple.bin.codesign.Codesign import com.malinskiy.marathon.apple.bin.getconf.Getconf +import com.malinskiy.marathon.apple.bin.ioreg.Ioreg import com.malinskiy.marathon.apple.bin.lipo.Lipo import com.malinskiy.marathon.apple.bin.nm.Nm import com.malinskiy.marathon.apple.bin.plistbuddy.PlistBuddy +import com.malinskiy.marathon.apple.bin.swvers.SwVers +import com.malinskiy.marathon.apple.bin.systemprofiler.SystemProfiler import com.malinskiy.marathon.apple.bin.xcodeselect.Xcodeselect import com.malinskiy.marathon.apple.bin.xcrun.Xcrun import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.vendor.VendorConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration class AppleBinaryEnvironment( commandExecutor: CommandExecutor, configuration: Configuration, - vendorConfiguration: VendorConfiguration.IOSConfiguration, + timeoutConfiguration: TimeoutConfiguration, gson: Gson ) { - private val timeoutConfiguration = vendorConfiguration.timeoutConfiguration - val codesign: Codesign = Codesign(commandExecutor, timeoutConfiguration) val getconf: Getconf = Getconf(commandExecutor, timeoutConfiguration) @@ -27,5 +29,8 @@ class AppleBinaryEnvironment( val nm: Nm = Nm(commandExecutor, timeoutConfiguration) val plistBuddy = PlistBuddy(commandExecutor, timeoutConfiguration) val xcodeselect: Xcodeselect = Xcodeselect(commandExecutor, timeoutConfiguration) - val xcrun: Xcrun = Xcrun(commandExecutor, configuration, vendorConfiguration, gson) + val ioreg: Ioreg = Ioreg(commandExecutor, timeoutConfiguration) + val systemProfiler: SystemProfiler = SystemProfiler(commandExecutor, timeoutConfiguration) + val swvers: SwVers = SwVers(commandExecutor, timeoutConfiguration) + val xcrun: Xcrun = Xcrun(commandExecutor, configuration, timeoutConfiguration, gson) } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/codesign/Codesign.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/codesign/Codesign.kt index a2cba274b..5fea2e88c 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/codesign/Codesign.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/codesign/Codesign.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.codesign import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.exceptions.DeviceSetupException import java.time.Duration diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/getconf/Getconf.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/getconf/Getconf.kt index f98d5cc0e..fd6ca9623 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/getconf/Getconf.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/getconf/Getconf.kt @@ -1,7 +1,7 @@ package com.malinskiy.marathon.apple.bin.getconf import com.malinskiy.marathon.apple.cmd.CommandExecutor -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration /** * retrieve standard configuration variables diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/ioreg/Ioreg.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/ioreg/Ioreg.kt new file mode 100644 index 000000000..20e2d214a --- /dev/null +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/ioreg/Ioreg.kt @@ -0,0 +1,28 @@ +package com.malinskiy.marathon.apple.bin.ioreg + +import com.malinskiy.marathon.apple.cmd.CommandExecutor +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration +import com.malinskiy.marathon.exceptions.DeviceSetupException + +/** + * show I/O Kit registry + */ +class Ioreg( + private val commandExecutor: CommandExecutor, + private val timeoutConfiguration: TimeoutConfiguration, +) { + suspend fun getUDID() = getParam("IOPlatformUUID") + + suspend fun getManufacturer() = getParam("manufacturer") + + suspend fun getModel() = getParam("model") + + private suspend fun getParam(name: String): String { + return commandExecutor.criticalExecute( + timeoutConfiguration.shell, + "sh", "-c", + "'ioreg -ad2 -c IOPlatformExpertDevice | plutil -extract IORegistryEntryChildren.0.$name raw -'", + ).successfulOrNull()?.combinedStdout?.trim() ?: throw DeviceSetupException("failed to detect UDID") + } +} + diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/lipo/Lipo.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/lipo/Lipo.kt index 50343aed2..75e908e6d 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/lipo/Lipo.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/lipo/Lipo.kt @@ -3,7 +3,7 @@ package com.malinskiy.marathon.apple.bin.lipo import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult import com.malinskiy.marathon.apple.model.Arch -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.exceptions.DeviceSetupException import java.time.Duration diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/nm/Nm.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/nm/Nm.kt index 8033ffd33..5df673d7a 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/nm/Nm.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/nm/Nm.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.nm import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.exceptions.DeviceSetupException import com.malinskiy.marathon.log.MarathonLogging import java.time.Duration diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/plistbuddy/PlistBuddy.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/plistbuddy/PlistBuddy.kt index 63b725839..565ee59ca 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/plistbuddy/PlistBuddy.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/plistbuddy/PlistBuddy.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.plistbuddy import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration /** * read and write values to plists diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/swvers/SwVers.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/swvers/SwVers.kt new file mode 100644 index 000000000..a725785ae --- /dev/null +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/swvers/SwVers.kt @@ -0,0 +1,21 @@ +package com.malinskiy.marathon.apple.bin.swvers + +import com.malinskiy.marathon.apple.cmd.CommandExecutor +import com.malinskiy.marathon.apple.cmd.CommandResult +import com.malinskiy.marathon.apple.model.Arch +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration +import com.malinskiy.marathon.exceptions.DeviceSetupException +import java.time.Duration + +/** + * create or operate on universal files + */ +class SwVers( + private val commandExecutor: CommandExecutor, + private val timeoutConfiguration: TimeoutConfiguration, +) { + suspend fun getVersion(): String { + return commandExecutor.criticalExecute(timeoutConfiguration.shell, "sw_vers", "--productVersion").successfulOrNull()?.combinedStdout?.trim() + ?: throw DeviceSetupException("failed to detect operating system version") + } +} diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/systemprofiler/SystemProfiler.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/systemprofiler/SystemProfiler.kt new file mode 100644 index 000000000..b256cbf9d --- /dev/null +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/systemprofiler/SystemProfiler.kt @@ -0,0 +1,21 @@ +package com.malinskiy.marathon.apple.bin.systemprofiler + +import com.malinskiy.marathon.apple.cmd.CommandExecutor +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration +import com.malinskiy.marathon.exceptions.DeviceSetupException + +/** + * create or operate on universal files + */ +class SystemProfiler( + private val commandExecutor: CommandExecutor, + private val timeoutConfiguration: TimeoutConfiguration, +) { + suspend fun getProvisioningUdid(): String { + val stdout = commandExecutor.criticalExecute( + timeoutConfiguration.shell, "sh", "-c", + "'system_profiler SPHardwareDataType'", + ).successfulOrNull()?.combinedStdout?.trim() + return stdout?.lines()?.find { it.contains("Provisioning UDID") }?.split(":")?.getOrNull(1)?.trim() ?: "" + } +} diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcodeselect/Xcodeselect.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcodeselect/Xcodeselect.kt index 52b21509e..f78ce7fa1 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcodeselect/Xcodeselect.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcodeselect/Xcodeselect.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.xcodeselect import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.model.Sdk -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration /** * Manages the active developer directory for Xcode and BSD tools diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/Xcrun.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/Xcrun.kt index c35b08dcc..b9539066a 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/Xcrun.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/Xcrun.kt @@ -7,18 +7,17 @@ import com.malinskiy.marathon.apple.bin.xcrun.xcresulttool.Xcresulttool import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.model.Sdk import com.malinskiy.marathon.config.Configuration -import com.malinskiy.marathon.config.vendor.VendorConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration class Xcrun( private val commandExecutor: CommandExecutor, configuration: Configuration, - vendorConfiguration: VendorConfiguration.IOSConfiguration, + private val timeoutConfiguration: TimeoutConfiguration, gson: Gson ) { - private val timeoutConfiguration = vendorConfiguration.timeoutConfiguration - val simctl = Simctl(commandExecutor, configuration, vendorConfiguration, gson) - val xcodebuild = Xcodebuild(commandExecutor, configuration, vendorConfiguration, timeoutConfiguration) + val simctl = Simctl(commandExecutor, timeoutConfiguration, gson) + val xcodebuild = Xcodebuild(commandExecutor, configuration, timeoutConfiguration) val xcresulttool = Xcresulttool(commandExecutor, timeoutConfiguration) suspend fun getSdkPlatformPath(sdk: Sdk): String { diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/Simctl.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/Simctl.kt index 3685fcdce..825117aad 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/Simctl.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/Simctl.kt @@ -9,17 +9,14 @@ import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.SimulatorService import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.vendor.VendorConfiguration -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.log.MarathonLogging class Simctl( private val commandExecutor: CommandExecutor, - private val configuration: Configuration, - private val vendorConfiguration: VendorConfiguration.IOSConfiguration, + private val timeoutConfiguration: TimeoutConfiguration, private val gson: Gson ) { - private val logger = MarathonLogging.logger {} - private val timeoutConfiguration: TimeoutConfiguration = vendorConfiguration.timeoutConfiguration val device = DeviceService(commandExecutor, timeoutConfiguration, gson) val simulator = SimulatorService(commandExecutor, timeoutConfiguration) diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/SimctlService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/SimctlService.kt index e1ac681b3..9151f1021 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/SimctlService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/SimctlService.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.xcrun.simctl import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import java.time.Duration abstract class SimctlService(private val commandExecutor: CommandExecutor) { diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/ApplicationService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/ApplicationService.kt index 0b5d92b76..4019b6a6f 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/ApplicationService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/ApplicationService.kt @@ -3,7 +3,7 @@ package com.malinskiy.marathon.apple.bin.xcrun.simctl.service import com.malinskiy.marathon.apple.bin.xcrun.simctl.SimctlService import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration class ApplicationService (commandExecutor: CommandExecutor, private val timeoutConfiguration: TimeoutConfiguration, diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/DeviceService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/DeviceService.kt index f120a59ed..fb3c421a9 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/DeviceService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/DeviceService.kt @@ -8,7 +8,7 @@ import com.malinskiy.marathon.apple.bin.xcrun.simctl.SimctlService import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureException import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureReason -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.log.MarathonLogging class DeviceService( diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/IoService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/IoService.kt index 788c162f4..c533cecce 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/IoService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/IoService.kt @@ -6,7 +6,7 @@ import com.malinskiy.marathon.apple.cmd.CommandResult import com.malinskiy.marathon.config.vendor.apple.ios.Codec import com.malinskiy.marathon.config.vendor.apple.ios.Display import com.malinskiy.marathon.config.vendor.apple.ios.Mask -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.config.vendor.apple.ios.Type import com.malinskiy.marathon.log.MarathonLogging diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/PrivacyService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/PrivacyService.kt index edaad1c98..99dc08590 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/PrivacyService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/PrivacyService.kt @@ -4,7 +4,7 @@ import com.malinskiy.marathon.apple.bin.xcrun.simctl.SimctlService import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult import com.malinskiy.marathon.config.vendor.apple.ios.Permission -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration class PrivacyService(commandExecutor: CommandExecutor, private val timeoutConfiguration: TimeoutConfiguration, diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/SimulatorService.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/SimulatorService.kt index b7ca10cd0..ccce6f0f3 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/SimulatorService.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/simctl/service/SimulatorService.kt @@ -2,7 +2,7 @@ package com.malinskiy.marathon.apple.bin.xcrun.simctl.service import com.malinskiy.marathon.apple.bin.xcrun.simctl.SimctlService import com.malinskiy.marathon.apple.cmd.CommandExecutor -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.log.MarathonLogging class SimulatorService( diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcodebuild/Xcodebuild.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcodebuild/Xcodebuild.kt index b1cb03fb4..89bdcbbfb 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcodebuild/Xcodebuild.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcodebuild/Xcodebuild.kt @@ -2,11 +2,12 @@ package com.malinskiy.marathon.apple.bin.xcrun.xcodebuild import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandSession +import com.malinskiy.marathon.apple.model.Platform +import com.malinskiy.marathon.apple.model.Sdk import com.malinskiy.marathon.apple.model.XcodeVersion import com.malinskiy.marathon.apple.test.TestRequest import com.malinskiy.marathon.config.Configuration -import com.malinskiy.marathon.config.vendor.VendorConfiguration -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.log.MarathonLogging import java.time.Duration @@ -16,18 +17,17 @@ import java.time.Duration class Xcodebuild( private val commandExecutor: CommandExecutor, private val configuration: Configuration, - private val vendorConfiguration: VendorConfiguration.IOSConfiguration, private val timeoutConfiguration: TimeoutConfiguration, ) { private val logger = MarathonLogging.logger {} - suspend fun testWithoutBuilding(udid: String, request: TestRequest): CommandSession { + suspend fun testWithoutBuilding(udid: String, sdk: Sdk, request: TestRequest, xcodebuildTestArgs: Map): CommandSession { val args = mutableMapOf().apply { - putAll(vendorConfiguration.xcodebuildTestArgs) + putAll(xcodebuildTestArgs) put("-enableCodeCoverage", codeCoverageFlag(request)) request.xcresult?.let { put("-resultBundlePath", it) } put("-destination-timeout", timeoutConfiguration.testDestination.seconds.toString()) - put("-destination", "\'platform=iOS simulator,id=$udid\'") + put("-destination", "\'platform=${sdk.destination},arch=arm64,id=$udid\'") } .filterKeys { it != "-xctestrun" } .toList() diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcresulttool/Xcresulttool.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcresulttool/Xcresulttool.kt index 251dc19ce..3db1019db 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcresulttool/Xcresulttool.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/xcrun/xcresulttool/Xcresulttool.kt @@ -6,7 +6,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature import com.fasterxml.jackson.module.kotlin.KotlinModule import com.google.gson.JsonSyntaxException import com.malinskiy.marathon.apple.cmd.CommandExecutor -import com.malinskiy.marathon.config.vendor.apple.ios.TimeoutConfiguration +import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration import com.malinskiy.marathon.log.MarathonLogging /** diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/cmd/remote/rsync/RsyncFileBridge.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/cmd/remote/rsync/RsyncFileBridge.kt index 92b812db1..57a7179ff 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/cmd/remote/rsync/RsyncFileBridge.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/cmd/remote/rsync/RsyncFileBridge.kt @@ -3,6 +3,11 @@ package com.malinskiy.marathon.apple.cmd.remote.rsync import com.github.fracpete.rsync4j.RSync import com.malinskiy.marathon.apple.cmd.FileBridge import com.malinskiy.marathon.apple.cmd.remote.rsync.a +import com.malinskiy.marathon.config.Configuration +import com.malinskiy.marathon.config.vendor.VendorConfiguration.IOSConfiguration +import com.malinskiy.marathon.config.vendor.apple.RsyncConfiguration +import com.malinskiy.marathon.config.vendor.apple.SshAuthentication +import com.malinskiy.marathon.config.vendor.apple.SshConfiguration import kotlinx.coroutines.sync.withLock import java.io.File import java.util.concurrent.ConcurrentHashMap @@ -17,9 +22,10 @@ import java.util.concurrent.locks.Lock */ class RsyncFileBridge( private val target: RsyncTarget, - private val configuration: com.malinskiy.marathon.config.Configuration, - private val vendorConfiguration: com.malinskiy.marathon.config.vendor.VendorConfiguration.IOSConfiguration, - private val authentication: com.malinskiy.marathon.config.vendor.apple.SshAuthentication?, + private val configuration: Configuration, + private val sshConfiguration: SshConfiguration, + private val rsyncConfiguration: RsyncConfiguration, + private val authentication: SshAuthentication?, ) : FileBridge { private val logger = com.malinskiy.marathon.log.MarathonLogging.logger {} @@ -87,42 +93,42 @@ class RsyncFileBridge( private val rsyncVersion: String get() { val output = com.github.fracpete.processoutput4j.output.CollectingProcessOutput() - output.monitor(com.github.fracpete.rsync4j.RSync().source("/tmp").destination("/tmp").version(true).builder()) + output.monitor(RSync().source("/tmp").destination("/tmp").version(true).builder()) return output.stdOut.replace("""(?s)\n.*\z""".toRegex(), "") } - private fun getRsyncBase(): com.github.fracpete.rsync4j.RSync { - return com.github.fracpete.rsync4j.RSync() + private fun getRsyncBase(): RSync { + return RSync() .a() .partial(true) .partialDir(".rsync-partial") .delayUpdates(true) - .rsyncPath(vendorConfiguration.rsync.remotePath) + .rsyncPath(rsyncConfiguration.remotePath) .verbose(configuration.debug) } - private fun com.github.fracpete.rsync4j.RSync.authenticate(): com.github.fracpete.rsync4j.RSync { + private fun RSync.authenticate(): RSync { return when (authentication) { - is com.malinskiy.marathon.config.vendor.apple.SshAuthentication.PasswordAuthentication -> { + is SshAuthentication.PasswordAuthentication -> { sshPass( com.github.fracpete.rsync4j.SshPass().password(authentication.password) ).rsh( "ssh -o 'StrictHostKeyChecking no' -F /dev/null " + "-l ${authentication.username} " + "-p ${target.port} " + - when (configuration.debug && vendorConfiguration.ssh.debug) { + when (configuration.debug && sshConfiguration.debug) { true -> "-vvv" else -> "" } ) } - is com.malinskiy.marathon.config.vendor.apple.SshAuthentication.PublicKeyAuthentication -> { + is SshAuthentication.PublicKeyAuthentication -> { rsh( "ssh -o 'StrictHostKeyChecking no' -F /dev/null " + "-i ${authentication.key} " + "-l ${authentication.username} " + "-p ${target.port} " + - when (configuration.debug && vendorConfiguration.ssh.debug) { + when (configuration.debug && sshConfiguration.debug) { true -> "-vvv" else -> "" } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/device/ConnectionFactory.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/device/ConnectionFactory.kt index f081479d1..43ff7b1d8 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/device/ConnectionFactory.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/device/ConnectionFactory.kt @@ -11,6 +11,9 @@ import com.malinskiy.marathon.apple.cmd.remote.ssh.sshj.SshjCommandExecutorFacto import com.malinskiy.marathon.apple.configuration.Transport import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureException import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureReason +import com.malinskiy.marathon.config.Configuration +import com.malinskiy.marathon.config.vendor.apple.RsyncConfiguration +import com.malinskiy.marathon.config.vendor.apple.SshConfiguration import net.schmizz.sshj.connection.ConnectionException import net.schmizz.sshj.transport.TransportException import net.schmizz.sshj.transport.verification.HostKeyVerifier @@ -19,11 +22,17 @@ import net.schmizz.sshj.transport.verification.PromiscuousVerifier import java.io.IOException import java.net.InetAddress import java.net.UnknownHostException +import java.time.Duration /** * Simple implementation of reference counting for closing connections */ -class ConnectionFactory(private val configuration: com.malinskiy.marathon.config.Configuration, private val vendorConfiguration: com.malinskiy.marathon.config.vendor.VendorConfiguration.IOSConfiguration) { +class ConnectionFactory( + private val configuration: Configuration, + private val sshConfiguration: SshConfiguration, + private val rsyncConfiguration: RsyncConfiguration, + private val reachabilityTimeout: Duration +) { private val logger = com.malinskiy.marathon.log.MarathonLogging.logger {} private val fileBridges = hashMapOf() private val sshCommandExecutors = hashMapOf() @@ -62,7 +71,7 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config } fun createRemote(transport: Transport.Ssh): Pair { - return if (vendorConfiguration.ssh.shareWorkerConnection) { + return if (sshConfiguration.shareWorkerConnection) { Pair(getOrCreateSshCommandExecutor(transport), getOrCreateFileBridge(transport.addr, transport.port, transport.authentication)) } else { Pair(createRemoteCommandExecutor(transport), getOrCreateFileBridge(transport.addr, transport.port, transport.authentication)) @@ -73,7 +82,7 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config return try { val hostAddress = transport.toInetAddressOrNull() ?: throw DeviceFailureException(DeviceFailureReason.UnreachableHost) val connectionId = "${hostAddress.hostAddress}:${transport.port}" - val authConfig = transport.authentication ?: vendorConfiguration.ssh.authentication + val authConfig = transport.authentication ?: sshConfiguration.authentication val sshAuthentication = when (authConfig) { is com.malinskiy.marathon.config.vendor.apple.SshAuthentication.PasswordAuthentication -> com.malinskiy.marathon.apple.cmd.remote.ssh.sshj.auth.SshAuthentication.PasswordAuthentication( authConfig.username, @@ -87,7 +96,7 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config null -> throw com.malinskiy.marathon.config.exceptions.ConfigurationException("no ssh auth provided for ${transport.addr}:${transport.port}") } - val hostKeyVerifier: HostKeyVerifier = vendorConfiguration.ssh.knownHostsPath?.let { + val hostKeyVerifier: HostKeyVerifier = sshConfiguration.knownHostsPath?.let { OpenSSHKnownHosts(it) } ?: PromiscuousVerifier() return try { @@ -96,7 +105,7 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config port = transport.port, authentication = sshAuthentication, hostKeyVerifier = hostKeyVerifier, - debug = vendorConfiguration.ssh.debug, + debug = sshConfiguration.debug, ) } catch (e: TransportException) { throw DeviceFailureException(DeviceFailureReason.UnreachableHost, e) @@ -121,7 +130,7 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config } return if (this.checkReachability) { if (try { - address.isReachable(vendorConfiguration.timeoutConfiguration.reachability.toMillis().toInt()) + address.isReachable(reachabilityTimeout.toMillis().toInt()) } catch (e: IOException) { logger.error("Error checking reachability of $this: $e") false @@ -147,15 +156,20 @@ class ConnectionFactory(private val configuration: com.malinskiy.marathon.config /** * Rsync doesn't work in parallel for the same host, so we have to share the same bridge */ - private fun getOrCreateFileBridge(addr: String, port: Int, authentication: com.malinskiy.marathon.config.vendor.apple.SshAuthentication?): FileBridge { + private fun getOrCreateFileBridge( + addr: String, + port: Int, + authentication: com.malinskiy.marathon.config.vendor.apple.SshAuthentication? + ): FileBridge { synchronized(fileBridges) { val rsyncSshTarget = RsyncTarget(addr, port) return fileBridges.getOrElse(rsyncSshTarget) { val defaultBridge = RsyncFileBridge( rsyncSshTarget, configuration, - vendorConfiguration, - authentication ?: vendorConfiguration.ssh.authentication + sshConfiguration, + rsyncConfiguration, + authentication ?: sshConfiguration.authentication ) fileBridges[rsyncSshTarget] = defaultBridge defaultBridge diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/ConfigurationExtensions.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/ConfigurationExtensions.kt index 219a21df3..92f2e3d12 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/ConfigurationExtensions.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/ConfigurationExtensions.kt @@ -46,11 +46,3 @@ fun VendorConfiguration.xcresultConfiguration(): XcresultConfiguration? { else -> null } } - -fun VendorConfiguration.testBundle(): AppleTestBundle { - return bundleConfiguration()?.let { - val xctest = it.xctest - val app = it.app - AppleTestBundle(app, xctest) - } ?: throw IllegalArgumentException("No test bundle provided") -} diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/StringExtensions.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/StringExtensions.kt new file mode 100644 index 000000000..30a06acd6 --- /dev/null +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/StringExtensions.kt @@ -0,0 +1,3 @@ +package com.malinskiy.marathon.apple.extensions + +fun String.bashEscape() = "'" + replace("'", "'\\''") + "'" diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/listener/TestResultsListener.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/listener/TestResultsListener.kt similarity index 98% rename from vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/listener/TestResultsListener.kt rename to vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/listener/TestResultsListener.kt index e49087c31..844f50d8a 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/listener/TestResultsListener.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/listener/TestResultsListener.kt @@ -1,9 +1,8 @@ -package com.malinskiy.marathon.apple.ios.listener +package com.malinskiy.marathon.apple.listener import com.malinskiy.marathon.apple.RemoteFileManager import com.malinskiy.marathon.apple.bin.xcrun.xcresulttool.ResultBundleFormat import com.malinskiy.marathon.apple.bin.xcrun.xcresulttool.Xcresulttool -import com.malinskiy.marathon.apple.listener.AccumulatingTestResultListener import com.malinskiy.marathon.device.Device import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.device.toDeviceInfo diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/AppleTestBundle.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/AppleTestBundle.kt index ffd0f4455..f1d51ed6c 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/AppleTestBundle.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/AppleTestBundle.kt @@ -7,10 +7,12 @@ import com.malinskiy.marathon.config.exceptions.ConfigurationException import com.malinskiy.marathon.execution.bundle.TestBundle import com.malinskiy.marathon.log.MarathonLogging import java.io.File +import java.nio.file.Paths class AppleTestBundle( val application: File?, val testApplication: File, + val sdk: Sdk, ) : TestBundle() { private val logger = MarathonLogging.logger {} override val id: String @@ -19,22 +21,35 @@ class AppleTestBundle( val applicationBundleInfo: BundleInfo? by lazy { application?.let { PropertyList.from( - File( - it, - "Info.plist" - ) + when (sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> File(it, "Info.plist") + Sdk.MACOS -> Paths.get(it.absolutePath, "Contents", "Info.plist").toFile() + } ) } } val appId = - applicationBundleInfo?.identification?.bundleIdentifier ?: throw ConfigurationException("No bundle identifier specified in $application") + applicationBundleInfo?.identification?.bundleIdentifier + ?: throw ConfigurationException("No bundle identifier specified in $application") - val testBundleInfo: BundleInfo by lazy { PropertyList.from(File(testApplication, "Info.plist")) } + val testBundleInfo: BundleInfo by lazy { + val file = when (sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> File(testApplication, "Info.plist") + Sdk.MACOS -> Paths.get(testApplication.absolutePath, "Contents", "Info.plist").toFile() + } + PropertyList.from(file) + } 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") + val possibleTestBinaries = when (sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> testApplication.listFiles()?.filter { it.isFile && it.extension == "" } + ?: throw ConfigurationException("missing test binaries in xctest folder at $testApplication") + + Sdk.MACOS -> Paths.get(testApplication.absolutePath, *relativeTestBinaryPath).toFile().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] @@ -44,4 +59,14 @@ class AppleTestBundle( } } } + + /** + * Path of the test binary relative to the xctest folder + */ + val relativeTestBinaryPath: Array by lazy { + when (sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> emptyArray() + Sdk.MACOS -> arrayOf("Contents", "MacOS") + } + } } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/Sdk.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/Sdk.kt index 89ee90a39..2936ef3d8 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/Sdk.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/model/Sdk.kt @@ -2,12 +2,25 @@ package com.malinskiy.marathon.apple.model enum class Sdk(val value: String) { IPHONEOS("iphoneos"), - IPHONESIMULATOR("iphonesimulator"); + IPHONESIMULATOR("iphonesimulator"), + MACOS("macosx"); val platformName: String by lazy { when(this) { IPHONEOS -> "iPhoneOS" IPHONESIMULATOR -> "iPhoneSimulator" + MACOS -> "MacOSX" + } + } + + /** + * destination platform for xcodebuild argument + */ + val destination: String by lazy { + when(this) { + IPHONEOS -> "iOS" + IPHONESIMULATOR -> "iOS Simulator" + MACOS -> "OS X" } } } diff --git a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/TestRootFactory.kt b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/TestRootFactory.kt index 1501e2cfc..9e72cb1c6 100644 --- a/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/TestRootFactory.kt +++ b/vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/xctestrun/TestRootFactory.kt @@ -1,6 +1,5 @@ package com.malinskiy.marathon.apple.xctestrun -import com.fasterxml.jackson.annotation.JsonProperty import com.malinskiy.marathon.apple.AppleDevice import com.malinskiy.marathon.apple.RemoteFileManager import com.malinskiy.marathon.apple.model.AppleTestBundle @@ -23,7 +22,7 @@ class TestRootFactory( private val device: AppleDevice, private val xctestrunEnv: Map, private val xcresultConfiguration: XcresultConfiguration, - ) { +) { suspend fun generate(testType: TestType, bundle: AppleTestBundle, useXctestParser: Boolean) { val remoteFileManager = device.remoteFileManager @@ -63,10 +62,13 @@ class TestRootFactory( if (!device.pushFolder(testApp, remoteTestApp)) { throw DeviceSetupException("failed to push app under test to remote device") } - val runnerPlugins = remoteFileManager.joinPath(testRunnerApp, "PlugIns") + val runnerPlugins = when (device.sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> remoteFileManager.joinPath(testRunnerApp, "PlugIns") + Sdk.MACOS -> remoteFileManager.joinPath(testRunnerApp, "Contents", "PlugIns") + } remoteFileManager.createRemoteDirectory(runnerPlugins) val remoteXctest = remoteFileManager.remoteXctestFile() - remoteFileManager.copy(remoteXctest, runnerPlugins) + remoteFileManager.copy(remoteXctest, runnerPlugins, override = false) if (device.sdk == Sdk.IPHONEOS) { TODO("generate phone provisioning") @@ -237,19 +239,27 @@ class TestRootFactory( platformLibraryPath: String, bundle: AppleTestBundle, ): String { - val testBinary = joinPath(device.remoteFileManager.remoteXctestFile(), bundle.testApplication.nameWithoutExtension) + val remoteTestBinary = joinPath( + device.remoteFileManager.remoteXctestFile(), + *bundle.relativeTestBinaryPath, + bundle.testBinary.nameWithoutExtension + ) val baseApp = joinPath(platformLibraryPath, "Xcode", "Agents", "XCTRunner.app") val runnerBinaryName = "${bundle.testBundleId}-Runner" val testRunnerApp = joinPath(testRoot, "$runnerBinaryName.app") device.remoteFileManager.copy(baseApp, testRunnerApp) - val baseTestRunnerBinary = joinPath(testRunnerApp, "XCTRunner") - val testRunnerBinary = joinPath(testRunnerApp, runnerBinaryName) + val baseTestRunnerBinary = joinPath(testRunnerApp, *bundle.relativeTestBinaryPath, "XCTRunner") + val testRunnerBinary = joinPath(testRunnerApp, *bundle.relativeTestBinaryPath, runnerBinaryName) device.remoteFileManager.copy(baseTestRunnerBinary, testRunnerBinary) - matchArchitectures(testBinary, testRunnerBinary) + matchArchitectures(remoteTestBinary, testRunnerBinary) + + val plist = when (device.sdk) { + Sdk.IPHONEOS, Sdk.IPHONESIMULATOR -> joinPath(testRunnerApp, "Info.plist") + Sdk.MACOS -> joinPath(testRunnerApp, "Contents", "Info.plist") + } - val plist = joinPath(testRunnerApp, "Info.plist") device.binaryEnvironment.plistBuddy.apply { set(plist, "CFBundleName", runnerBinaryName) set(plist, "CFBundleExecutable", runnerBinaryName) diff --git a/vendor/vendor-apple/base/src/main/resources/libxctest-parser/MacOSX/libxctest-parser.dylib b/vendor/vendor-apple/base/src/main/resources/libxctest-parser/MacOSX/libxctest-parser.dylib index f6e8ff88b..8a787baaf 100755 Binary files a/vendor/vendor-apple/base/src/main/resources/libxctest-parser/MacOSX/libxctest-parser.dylib and b/vendor/vendor-apple/base/src/main/resources/libxctest-parser/MacOSX/libxctest-parser.dylib differ diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt index b4ae43aef..726803afd 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/AppleSimulatorDevice.kt @@ -11,13 +11,13 @@ import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.ApplicationService import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult import com.malinskiy.marathon.apple.cmd.FileBridge -import com.malinskiy.marathon.apple.extensions.testBundle +import com.malinskiy.marathon.apple.extensions.bundleConfiguration import com.malinskiy.marathon.apple.ios.listener.DataContainerClearListener import com.malinskiy.marathon.apple.listener.AppleTestRunListener import com.malinskiy.marathon.apple.listener.CompositeTestRunListener import com.malinskiy.marathon.apple.listener.DebugTestRunListener import com.malinskiy.marathon.apple.listener.ResultBundleRunListener -import com.malinskiy.marathon.apple.ios.listener.TestResultsListener +import com.malinskiy.marathon.apple.listener.TestResultsListener import com.malinskiy.marathon.apple.listener.TestRunListenerAdapter import com.malinskiy.marathon.apple.ios.listener.screenshot.ScreenCapturerTestRunListener import com.malinskiy.marathon.apple.ios.listener.video.ScreenRecordingListener @@ -26,6 +26,7 @@ import com.malinskiy.marathon.apple.ios.model.Simulator import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureException import com.malinskiy.marathon.apple.logparser.parser.DiagnosticLogsPathFinder import com.malinskiy.marathon.apple.logparser.parser.SessionResultsPathFinder +import com.malinskiy.marathon.apple.model.AppleTestBundle import com.malinskiy.marathon.apple.model.XcodeVersion import com.malinskiy.marathon.apple.test.TestEvent import com.malinskiy.marathon.apple.test.TestRequest @@ -127,6 +128,7 @@ class AppleSimulatorDevice( override val remoteFileManager: RemoteFileManager = RemoteFileManager(this) override val storagePath = "${AppleDevice.SHARED_PATH}/$udid" private lateinit var xcodeVersion: XcodeVersion + private lateinit var testBundle: AppleTestBundle /** * Called only once per device's lifetime @@ -191,6 +193,11 @@ class AppleSimulatorDevice( AppleSimulatorApplicationInstaller( vendorConfiguration, ).prepareInstallation(this@AppleSimulatorDevice) + testBundle = vendorConfiguration.bundleConfiguration()?.let { + val xctest = it.xctest + val app = it.app + AppleTestBundle(app, xctest, sdk) + } ?: throw IllegalArgumentException("No test bundle provided") }) add(async { handleLifecycle(vendorConfiguration.lifecycleConfiguration.onPrepare) @@ -240,7 +247,7 @@ class AppleSimulatorDevice( executionLineListeners = lineListeners.onEach { addLineListener(it) } AppleDeviceTestRunner(this@AppleSimulatorDevice, testBundleIdentifier).execute( configuration, - vendorConfiguration.testBundle(), + testBundle, testBatch, listener ) @@ -340,7 +347,7 @@ class AppleSimulatorDevice( override suspend fun executeTestRequest(request: TestRequest): ReceiveChannel> { return produce { - binaryEnvironment.xcrun.xcodebuild.testWithoutBuilding(udid, request).use { session -> + binaryEnvironment.xcrun.xcodebuild.testWithoutBuilding(udid, sdk, request, vendorConfiguration.xcodebuildTestArgs).use { session -> withContext(Dispatchers.IO) { val deferredStdout = supervisorScope { async { diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/IosVendor.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/IosVendor.kt index fac1c8a02..02886bb87 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/IosVendor.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/IosVendor.kt @@ -63,8 +63,8 @@ val IosVendor = module { else -> NmTestParser(get(), get(), TestParserConfiguration.NmTestParserConfiguration(), get()) } } - single> { AppleApplicationInstaller(get()) } - single { AppleLogConfigurator(get()) } + single> { AppleSimulatorApplicationInstaller(get()) } + single { AppleLogConfigurator(get().compactOutput) } val appleTestBundleIdentifier = AppleTestBundleIdentifier() single { appleTestBundleIdentifier } diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt index 2463e5138..2891d0949 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/AppleSimulatorProvider.kt @@ -68,7 +68,12 @@ class AppleSimulatorProvider( private val devices = ConcurrentHashMap() private val channel: Channel = unboundedChannel() - private val connectionFactory = ConnectionFactory(configuration, vendorConfiguration) + private val connectionFactory = ConnectionFactory( + configuration, + vendorConfiguration.ssh, + vendorConfiguration.rsync, + vendorConfiguration.timeoutConfiguration.reachability + ) private val environmentVariableSubstitutor = StringSubstitutor(StringLookupFactory.INSTANCE.environmentVariableStringLookup()) private val simulatorFactory = SimulatorFactory(configuration, vendorConfiguration, testBundleIdentifier, gson, track, timer) @@ -115,7 +120,7 @@ class AppleSimulatorProvider( return } - val bin = AppleBinaryEnvironment(commandExecutor, configuration, vendorConfiguration, gson) + val bin = AppleBinaryEnvironment(commandExecutor, configuration, vendorConfiguration.timeoutConfiguration, gson) val plan = plan(transport, bin, targets) val deferredExisting = createExisting(plan, transport) val deferredProvisioning = createNew(transport, plan, bin) diff --git a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/SimulatorFactory.kt b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/SimulatorFactory.kt index 818e2f7e8..5192da2be 100644 --- a/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/SimulatorFactory.kt +++ b/vendor/vendor-apple/ios/src/main/kotlin/com/malinskiy/marathon/apple/ios/device/SimulatorFactory.kt @@ -36,7 +36,7 @@ class SimulatorFactory( fileBridge: FileBridge, udid: String, ): AppleSimulatorDevice { - val bin = AppleBinaryEnvironment(commandExecutor, configuration, vendorConfiguration, gson) + val bin = AppleBinaryEnvironment(commandExecutor, configuration, vendorConfiguration.timeoutConfiguration, gson) val simctlDevice = try { val simctlDevices = bin.xcrun.simctl.device.listDevices() simctlDevices.find { it.udid == udid }?.apply { diff --git a/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/AppleMacosProvider.kt b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/AppleMacosProvider.kt new file mode 100644 index 000000000..6325f18ec --- /dev/null +++ b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/AppleMacosProvider.kt @@ -0,0 +1,233 @@ +package com.malinskiy.marathon.apple.macos + +import com.fasterxml.jackson.databind.JsonMappingException +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.module.kotlin.readValue +import com.google.gson.Gson +import com.malinskiy.marathon.actor.unboundedChannel +import com.malinskiy.marathon.analytics.internal.pub.Track +import com.malinskiy.marathon.apple.AppleTestBundleIdentifier +import com.malinskiy.marathon.apple.bin.AppleBinaryEnvironment +import com.malinskiy.marathon.apple.configuration.AppleTarget +import com.malinskiy.marathon.apple.configuration.Marathondevices +import com.malinskiy.marathon.apple.configuration.Transport +import com.malinskiy.marathon.apple.configuration.Worker +import com.malinskiy.marathon.apple.device.ConnectionFactory +import com.malinskiy.marathon.apple.model.Sdk +import com.malinskiy.marathon.config.Configuration +import com.malinskiy.marathon.config.vendor.VendorConfiguration +import com.malinskiy.marathon.device.Device +import com.malinskiy.marathon.device.DeviceProvider +import com.malinskiy.marathon.exceptions.NoDevicesException +import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.time.Timer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.withContext +import org.apache.commons.text.StringSubstitutor +import org.apache.commons.text.lookup.StringLookupFactory +import java.io.File +import java.util.concurrent.ConcurrentHashMap +import kotlin.coroutines.CoroutineContext + +class AppleMacosProvider( + private val configuration: Configuration, + private val vendorConfiguration: VendorConfiguration.MacosConfiguration, + private val testBundleIdentifier: AppleTestBundleIdentifier, + private val gson: Gson, + private val objectMapper: ObjectMapper, + private val track: Track, + private val timer: Timer +) : DeviceProvider, CoroutineScope { + + private val logger = MarathonLogging.logger(AppleMacosProvider::class.java.simpleName) + + private val dispatcher = + newFixedThreadPoolContext(vendorConfiguration.threadingConfiguration.deviceProviderThreads, "AppleDeviceProvider") + override val coroutineContext: CoroutineContext + get() = dispatcher + override val deviceInitializationTimeoutMillis = configuration.deviceInitializationTimeoutMillis + + private val job = Job() + + private val devices = ConcurrentHashMap() + private val channel: Channel = unboundedChannel() + private val connectionFactory = ConnectionFactory( + configuration, + vendorConfiguration.ssh, + vendorConfiguration.rsync, + vendorConfiguration.timeoutConfiguration.reachability + ) + private val environmentVariableSubstitutor = StringSubstitutor(StringLookupFactory.INSTANCE.environmentVariableStringLookup()) + private val fileManager = FileManager( + configuration.outputConfiguration.maxPath, + configuration.outputConfiguration.maxFilename, + configuration.outputDir + ) + + override fun subscribe() = channel + + override suspend fun initialize() = withContext(coroutineContext) { + logger.debug("Initializing AppleSimulatorProvider") + val file = vendorConfiguration.devicesFile ?: File(System.getProperty("user.dir"), "Marathondevices") + val devicesWithEnvironmentVariablesReplaced = environmentVariableSubstitutor.replace(file.readText()) + val workers: List = try { + objectMapper.readValue(devicesWithEnvironmentVariablesReplaced).workers + } catch (e: JsonMappingException) { + throw NoDevicesException("Invalid Marathondevices file ${file.absolutePath} format", e) + } + if (workers.isEmpty()) { + throw NoDevicesException("No workers found in the ${file.absolutePath}") + } + val hosts: Map> = mutableMapOf>().apply { + workers.map { + put(it.transport, it.devices) + } + } + + logger.debug { "Establishing communication with [${hosts.keys.joinToString()}]" } + val deferred = hosts.filter { + var use = false + it.value.forEach { device -> + when (device) { + is AppleTarget.Host -> use = true + else -> logger.warn { "macOS vendor runs do not use anything but host device for testing. Skipping" } + } + } + use + }.map { (transport, _) -> + async { + initializeForTransport(transport) + } + } + awaitAll(*deferred.toTypedArray()) + Unit + } + + override suspend fun borrow(): Device { + while (devices.isEmpty()) { + delay(200) + } + return devices.values.random() + } + + private suspend fun initializeForTransport(transport: Transport) { + val (commandExecutor, fileBridge) = connectionFactory.create(transport) + if (commandExecutor == null) { + return + } + val bin = AppleBinaryEnvironment(commandExecutor, configuration, vendorConfiguration.timeoutConfiguration, gson) + var udid = bin.systemProfiler.getProvisioningUdid() + if (udid.isBlank()) { + udid = bin.ioreg.getUDID() + } + val device = MacosDevice( + udid, + transport, + Sdk.MACOS, + bin, + testBundleIdentifier, + fileManager, + configuration, + vendorConfiguration, + commandExecutor, + fileBridge, + track, + timer + ) + track.trackProviderDevicePreparing(device) { + device.setup() + } + connect(transport, device) + } + + override suspend fun terminate() = withContext(NonCancellable) { + withContext(NonCancellable) { + logger.debug { "Terminating ${AppleMacosProvider::class.simpleName}" } + channel.close() + if (logger.isDebugEnabled) { + // print out final summary on attempted simulator connections + //printFailingSimulatorSummary() + } + val deferredDispose = devices.map { (uuid, device) -> + async { + try { + dispose(device) + connectionFactory.dispose(device.commandExecutor) + } catch (e: Exception) { + //We don't really care during termination about exceptions + } + logger.debug("Disposed device ${device.udid}") + } + } + deferredDispose.awaitAll() + devices.clear() + } + dispatcher.close() + } + +// suspend fun onDisconnect(device: AppleSimulatorDevice, remoteSimulator: AppleTarget.Simulator, reason: DeviceFailureReason) = +// withContext(coroutineContext + CoroutineName("onDisconnect")) { +// launch(context = coroutineContext + job + CoroutineName("disconnector")) { +// try { +// if (devices.remove(device.serialNumber, device)) { +// dispose(device) +// notifyDisconnected(device) +// } +// } catch (e: Exception) { +// logger.debug("Exception removing device ${device.udid}") +// } +// } +// +// if (reason == DeviceFailureReason.InvalidSimulatorIdentifier) { +// logger.error("device ${device.udid} does not exist on remote host") +// } else if (RemoteSimulatorConnectionCounter.get(device.udid) < MAX_CONNECTION_ATTEMPTS) { +// launch(context = coroutineContext + job + CoroutineName("reconnector")) { +// delay(499) +// RemoteSimulatorConnectionCounter.putAndGet(device.udid) +// simulatorFactory.createRemote(remoteSimulator)?.let { +// connect(it) +// } +// } +// } +// } + + private fun dispose(device: MacosDevice) { + device.dispose() + } + + private fun connect(transport: Transport, device: MacosDevice) { + devices.put(device.udid, device) + ?.let { + logger.error("replaced existing device $it with new $device.") + dispose(it) + } + notifyConnected(device) + } + + private fun notifyConnected(device: MacosDevice) = launch(context = coroutineContext) { + channel.send(element = DeviceProvider.DeviceEvent.DeviceConnected(device)) + } + + private fun notifyDisconnected(device: MacosDevice) = launch(context = coroutineContext) { + channel.send(element = DeviceProvider.DeviceEvent.DeviceDisconnected(device)) + } + +// private fun printFailingSimulatorSummary() { +// simulators +// .map { "${it.udid}@${it.transport}" to (RemoteSimulatorConnectionCounter.get(it.udid) - 1) } +// .filter { it.second > 0 } +// .sortedByDescending { it.second } +// .forEach { +// logger.debug(String.format("%3d %s", it.second, it.first)) +// } +// } +} diff --git a/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosDevice.kt b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosDevice.kt index 436962085..3c6e90414 100644 --- a/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosDevice.kt +++ b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosDevice.kt @@ -1,70 +1,234 @@ package com.malinskiy.marathon.apple.macos +import com.malinskiy.marathon.analytics.internal.pub.Track +import com.malinskiy.marathon.apple.AppleApplicationInstaller import com.malinskiy.marathon.apple.AppleDevice +import com.malinskiy.marathon.apple.AppleDeviceTestRunner +import com.malinskiy.marathon.apple.AppleTestBundleIdentifier import com.malinskiy.marathon.apple.RemoteFileManager import com.malinskiy.marathon.apple.bin.AppleBinaryEnvironment +import com.malinskiy.marathon.apple.cmd.CommandExecutor import com.malinskiy.marathon.apple.cmd.CommandResult +import com.malinskiy.marathon.apple.cmd.FileBridge +import com.malinskiy.marathon.apple.configuration.Transport +import com.malinskiy.marathon.apple.extensions.bundleConfiguration +import com.malinskiy.marathon.apple.listener.AppleTestRunListener +import com.malinskiy.marathon.apple.listener.CompositeTestRunListener +import com.malinskiy.marathon.apple.listener.DebugTestRunListener +import com.malinskiy.marathon.apple.listener.ResultBundleRunListener +import com.malinskiy.marathon.apple.listener.TestResultsListener +import com.malinskiy.marathon.apple.listener.TestRunListenerAdapter +import com.malinskiy.marathon.apple.logparser.XctestEventProducer +import com.malinskiy.marathon.apple.logparser.parser.DeviceFailureException +import com.malinskiy.marathon.apple.logparser.parser.DiagnosticLogsPathFinder +import com.malinskiy.marathon.apple.logparser.parser.SessionResultsPathFinder +import com.malinskiy.marathon.apple.model.AppleTestBundle import com.malinskiy.marathon.apple.model.Sdk +import com.malinskiy.marathon.apple.model.XcodeVersion import com.malinskiy.marathon.apple.test.TestEvent import com.malinskiy.marathon.apple.test.TestRequest import com.malinskiy.marathon.config.Configuration +import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.device.DeviceFeature import com.malinskiy.marathon.device.DevicePoolId import com.malinskiy.marathon.device.NetworkState import com.malinskiy.marathon.device.OperatingSystem +import com.malinskiy.marathon.device.file.measureFileTransfer +import com.malinskiy.marathon.device.file.measureFolderTransfer import com.malinskiy.marathon.device.screenshot.Rotation +import com.malinskiy.marathon.device.toDeviceInfo +import com.malinskiy.marathon.exceptions.DeviceLostException +import com.malinskiy.marathon.exceptions.DeviceSetupException import com.malinskiy.marathon.execution.TestBatchResults import com.malinskiy.marathon.execution.listener.LineListener +import com.malinskiy.marathon.execution.listener.LogListener +import com.malinskiy.marathon.io.FileManager +import com.malinskiy.marathon.log.MarathonLogging +import com.malinskiy.marathon.report.attachment.AttachmentProvider +import com.malinskiy.marathon.report.logs.LogWriter import com.malinskiy.marathon.test.TestBatch +import com.malinskiy.marathon.time.Timer import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.NonCancellable +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.channels.produce +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.supervisorScope +import kotlinx.coroutines.withContext import mu.KLogger +import net.schmizz.sshj.connection.ConnectionException +import net.schmizz.sshj.connection.channel.OpenFailException +import net.schmizz.sshj.transport.TransportException import java.awt.image.BufferedImage import java.io.File import java.time.Duration +import java.util.concurrent.CopyOnWriteArrayList +import kotlin.coroutines.CoroutineContext -class MacosDevice : AppleDevice { - override val udid: String - get() = TODO("Not yet implemented") - override val remoteFileManager: RemoteFileManager - get() = TODO("Not yet implemented") - override val storagePath: String - get() = TODO("Not yet implemented") - override val sdk: Sdk - get() = TODO("Not yet implemented") - override val binaryEnvironment: AppleBinaryEnvironment - get() = TODO("Not yet implemented") +class MacosDevice( + override val udid: String, + transport: Transport, + override var sdk: Sdk, + override val binaryEnvironment: AppleBinaryEnvironment, + private val testBundleIdentifier: AppleTestBundleIdentifier, + val fileManager: FileManager, + private val configuration: Configuration, + private val vendorConfiguration: VendorConfiguration.MacosConfiguration, + internal val commandExecutor: CommandExecutor, + private val fileBridge: FileBridge, + private val track: Track, + private val timer: Timer, +) : AppleDevice, CoroutineScope { + override val logger = MarathonLogging.logger {} + override var operatingSystem: OperatingSystem = OperatingSystem("Unknown") + override val serialNumber: String = "$udid@${commandExecutor.host.id}" + override var model: String = "Unknown" + override var manufacturer: String = "Unknown" + + override val networkState: NetworkState + get() = when (healthy) { + true -> NetworkState.CONNECTED + false -> NetworkState.DISCONNECTED + } + override var deviceFeatures: Collection = emptyList() + override val healthy: Boolean = true + override var abi: String = "Unknown" + private lateinit var version: String + + override val orientation: Rotation = Rotation.ROTATION_0 + private lateinit var runtimeVersion: String + private lateinit var runtimeBuildVersion: String + private lateinit var env: Map + private lateinit var home: String + private lateinit var logFile: String + private lateinit var devicePlistPath: String + private var deviceDescriptor: Map<*, *>? = null + private val dispatcher by lazy { + newFixedThreadPoolContext( + vendorConfiguration.threadingConfiguration.deviceThreads, + "AppleSimulatorDevice - execution - ${commandExecutor.host.id}" + ) + } + override val coroutineContext: CoroutineContext = dispatcher + override val remoteFileManager: RemoteFileManager = RemoteFileManager(this) + override val storagePath = "${AppleDevice.SHARED_PATH}/$udid" + private lateinit var xcodeVersion: XcodeVersion + private lateinit var testBundle: AppleTestBundle + + /** + * Called only once per device's lifetime + */ override suspend fun setup() { - TODO("Not yet implemented") + env = fetchEnvvars() + + xcodeVersion = binaryEnvironment.xcrun.xcodebuild.getVersion() + + home = env["HOME"] ?: "" + if (home.isBlank()) { + throw DeviceSetupException("macOS $udid: invalid value $home for environment variable HOME") + } + val logDirectory = "/private/var/log/" + logFile = "$logDirectory/system.log" + + + model = binaryEnvironment.ioreg.getModel() + manufacturer = binaryEnvironment.ioreg.getManufacturer() + operatingSystem = OperatingSystem(binaryEnvironment.swvers.getVersion()) + abi = executeWorkerCommand(listOf("uname", "-m"))?.let { + if (it.successful) { + it.combinedStdout.trim() + } else { + null + } + } ?: "Unknown" + + deviceFeatures = detectFeatures() } override suspend fun executeTestRequest(request: TestRequest): ReceiveChannel> { - TODO("Not yet implemented") + return produce { + binaryEnvironment.xcrun.xcodebuild.testWithoutBuilding(udid, sdk, request, vendorConfiguration.xcodebuildTestArgs).use { session -> + withContext(Dispatchers.IO) { + val deferredStdout = supervisorScope { + async { + val testEventProducer = + XctestEventProducer(request.testTargetName ?: "", timer) + for (line in session.stdout) { + testEventProducer.process(line)?.let { + send(it) + } + lineListeners.forEach { it.onLine(line) } + } + } + } + + val deferredStderr = supervisorScope { + async { + for (line in session.stderr) { + if (line.trim().isNotBlank()) { + logger.error { "simulator $udid: stderr=$line" } + } + } + } + } + + deferredStderr.await() + deferredStdout.await() + val exitCode = session.exitCode.await() + // 70 = no devices + // 65 = ** TEST EXECUTE FAILED **: crash + logger.debug { "Finished test batch execution with exit status $exitCode" } + close() + } + } + } + } override suspend fun executeWorkerCommand(command: List): CommandResult? { - TODO("Not yet implemented") + return commandExecutor.safeExecute( + command = command, + timeout = vendorConfiguration.timeoutConfiguration.shell, + idleTimeout = vendorConfiguration.timeoutConfiguration.shellIdle, + env = emptyMap(), + null + ) } override suspend fun pushFile(src: File, dst: String): Boolean { - TODO("Not yet implemented") + return measureFileTransfer(src) { + fileBridge.send(src, dst) + } } override suspend fun pullFile(src: String, dst: File): Boolean { - TODO("Not yet implemented") + return measureFileTransfer(dst) { + fileBridge.receive(src, dst) + } } override suspend fun pushFolder(src: File, dst: String): Boolean { - TODO("Not yet implemented") + return measureFolderTransfer(src) { + fileBridge.send(src, dst) + } } override suspend fun pullFolder(src: String, dst: File): Boolean { - TODO("Not yet implemented") + return measureFolderTransfer(dst) { + fileBridge.receive(src, dst) + } } override suspend fun install(remotePath: String): Boolean { - TODO("Not yet implemented") + return true } override suspend fun getScreenshot(timeout: Duration, dst: File): Boolean { @@ -91,27 +255,28 @@ class MacosDevice : AppleDevice { TODO("Not yet implemented") } - override val operatingSystem: OperatingSystem - get() = TODO("Not yet implemented") - override val serialNumber: String - get() = TODO("Not yet implemented") - override val model: String - get() = TODO("Not yet implemented") - override val manufacturer: String - get() = TODO("Not yet implemented") - override val networkState: NetworkState - get() = TODO("Not yet implemented") - override val deviceFeatures: Collection - get() = TODO("Not yet implemented") - override val healthy: Boolean - get() = TODO("Not yet implemented") - override val abi: String - get() = TODO("Not yet implemented") - override val logger: KLogger - get() = TODO("Not yet implemented") - override suspend fun prepare(configuration: Configuration) { - TODO("Not yet implemented") + async(CoroutineName("prepare $serialNumber")) { + supervisorScope { + track.trackDevicePreparing(this@MacosDevice) { + remoteFileManager.removeRemoteDirectory() + remoteFileManager.createRemoteDirectory() + remoteFileManager.createRemoteSharedDirectory() + mutableListOf>().apply { + add(async { + AppleApplicationInstaller( + vendorConfiguration, + ).prepareInstallation(this@MacosDevice) + testBundle = vendorConfiguration.bundleConfiguration()?.let { + val xctest = it.xctest + val app = it.app + AppleTestBundle(app, xctest, sdk) + } ?: throw IllegalArgumentException("No test bundle provided") + }) + }.awaitAll() + } + } + }.await() } override suspend fun execute( @@ -120,21 +285,125 @@ class MacosDevice : AppleDevice { testBatch: TestBatch, deferred: CompletableDeferred ) { - TODO("Not yet implemented") + try { + async(CoroutineName("execute $serialNumber")) { + supervisorScope { + var executionLineListeners = setOf() + try { + val (listener, lineListeners) = createExecutionListeners(devicePoolId, testBatch, deferred) + executionLineListeners = lineListeners.onEach { addLineListener(it) } + AppleDeviceTestRunner(this@MacosDevice, testBundleIdentifier).execute( + configuration, + testBundle, + testBatch, + listener + ) + } finally { + executionLineListeners.forEach { removeLineListener(it) } + } + } + }.await() + } catch (e: ConnectionException) { + throw DeviceLostException(e) + } catch (e: TransportException) { + throw DeviceLostException(e) + } catch (e: OpenFailException) { + throw DeviceLostException(e) + } catch (e: IllegalStateException) { + throw DeviceLostException(e) + } catch (e: DeviceFailureException) { + throw DeviceLostException(e) + } } override fun dispose() { - TODO("Not yet implemented") + dispatcher.close() } - override val orientation: Rotation - get() = TODO("Not yet implemented") + private suspend fun fetchEnvvars(): Map { + val commandResult = commandExecutor.safeExecute( + command = listOf("env"), + timeout = vendorConfiguration.timeoutConfiguration.shell, + idleTimeout = vendorConfiguration.timeoutConfiguration.shellIdle, + env = emptyMap(), + workdir = null + ) + if (commandResult?.successful != true) { + throw DeviceSetupException("macOS $udid: unable to detect environment variables") + } + + return commandResult.stdout + .map { it.trim() } + .filter { it.isNotEmpty() } + .associate { + val key = it.substringBefore('=').trim() + val value = it.substringAfter('=').trim() + Pair(key, value) + } + } + + private suspend fun detectFeatures(): List { + return emptyList() + } + + private val lineListeners = CopyOnWriteArrayList() override fun addLineListener(listener: LineListener) { - TODO("Not yet implemented") + lineListeners.add(listener) } override fun removeLineListener(listener: LineListener) { - TODO("Not yet implemented") + lineListeners.remove(listener) } + + private fun createExecutionListeners( + devicePoolId: DevicePoolId, + testBatch: TestBatch, + deferred: CompletableDeferred, + ): Pair> { + val logWriter = LogWriter(fileManager) + + val attachmentProviders = mutableListOf() + val recorderListener = object : AppleTestRunListener {} + + val logListener = TestRunListenerAdapter( + LogListener(toDeviceInfo(), this, devicePoolId, testBatch.id, logWriter) + .also { attachmentProviders.add(it) } + ) + + val diagnosticLogsPathFinder = DiagnosticLogsPathFinder() + val sessionResultsPathFinder = SessionResultsPathFinder() + val debugLogPrinter = + com.malinskiy.marathon.apple.logparser.parser.DebugLogPrinter(hideRunnerOutput = vendorConfiguration.hideRunnerOutput) + + val logListeners = setOf( + diagnosticLogsPathFinder, + sessionResultsPathFinder, + debugLogPrinter + ) + + return Pair( + CompositeTestRunListener( + listOf( + TestResultsListener( + testBatch, + this, + devicePoolId, + deferred, + timer, + remoteFileManager, + binaryEnvironment.xcrun.xcresulttool, + attachmentProviders + ), + logListener, + DebugTestRunListener(this), + diagnosticLogsPathFinder, + sessionResultsPathFinder, + recorderListener, + ResultBundleRunListener(this, vendorConfiguration.xcresult, devicePoolId, testBatch, fileManager), + ) + ), logListeners + ) + } + } diff --git a/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosVendor.kt b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosVendor.kt index 50a4c8f7e..285c509d4 100644 --- a/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosVendor.kt +++ b/vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/MacosVendor.kt @@ -1,22 +1,49 @@ package com.malinskiy.marathon.apple.macos +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator +import com.fasterxml.jackson.module.kotlin.KotlinFeature +import com.fasterxml.jackson.module.kotlin.KotlinModule +import com.google.gson.GsonBuilder +import com.malinskiy.marathon.apple.AppleApplicationInstaller import com.malinskiy.marathon.apple.AppleLogConfigurator import com.malinskiy.marathon.apple.AppleTestBundleIdentifier import com.malinskiy.marathon.apple.NmTestParser import com.malinskiy.marathon.apple.XCTestParser +import com.malinskiy.marathon.apple.bin.xcrun.simctl.model.SimctlDeviceList +import com.malinskiy.marathon.apple.bin.xcrun.simctl.model.SimctlDeviceListDeserializer import com.malinskiy.marathon.config.Configuration import com.malinskiy.marathon.config.vendor.VendorConfiguration import com.malinskiy.marathon.config.vendor.apple.TestParserConfiguration +import com.malinskiy.marathon.device.DeviceProvider import com.malinskiy.marathon.execution.TestParser import com.malinskiy.marathon.execution.bundle.TestBundleIdentifier import com.malinskiy.marathon.log.MarathonLogConfigurator import org.koin.dsl.module val MacosVendor = module { + single { + val gson = GsonBuilder() + .registerTypeAdapter(SimctlDeviceList::class.java, SimctlDeviceListDeserializer()) + .create() + val objectMapper = ObjectMapper(YAMLFactory().disable(YAMLGenerator.Feature.USE_NATIVE_TYPE_ID)) + .registerModule( + KotlinModule.Builder() + .withReflectionCacheSize(512) + .configure(KotlinFeature.NullToEmptyCollection, false) + .configure(KotlinFeature.NullToEmptyMap, false) + .configure(KotlinFeature.NullIsSameAsDefault, false) + .configure(KotlinFeature.SingletonSupport, true) + .configure(KotlinFeature.StrictNullChecks, false) + .build() + ) + AppleMacosProvider(get(), get(), get(), gson, objectMapper, get(), get()) + } single { val configuration = get() - val iosConfiguration = configuration.vendorConfiguration as? VendorConfiguration.IOSConfiguration - val testParserConfiguration = iosConfiguration?.testParserConfiguration + val macosConfiguration = configuration.vendorConfiguration as? VendorConfiguration.MacosConfiguration + val testParserConfiguration = macosConfiguration?.testParserConfiguration when { testParserConfiguration != null && testParserConfiguration is TestParserConfiguration.XCTestParserConfiguration -> XCTestParser( get(), @@ -35,7 +62,8 @@ val MacosVendor = module { else -> NmTestParser(get(), get(), TestParserConfiguration.NmTestParserConfiguration(), get()) } } - single { AppleLogConfigurator(get()) } + single> { AppleApplicationInstaller(get()) } + single { AppleLogConfigurator(get().compactOutput) } val appleTestBundleIdentifier = AppleTestBundleIdentifier() single { appleTestBundleIdentifier }