From 48adc9aa9fe8d92329147741943d74cf0ee3c301 Mon Sep 17 00:00:00 2001 From: Anton Malinskiy Date: Sat, 17 Feb 2024 22:29:40 +1000 Subject: [PATCH] feat(apple): macOS parsing --- cli/build.gradle.kts | 1 + .../malinskiy/marathon/cli/ApplicationView.kt | 4 + .../config/vendor/VendorConfiguration.kt | 13 +- .../apple/{ios => }/TimeoutConfiguration.kt | 2 +- .../apple/macos/TimeoutConfiguration.kt | 16 - .../apple/AppleApplicationInstaller.kt | 17 +- .../marathon/apple/AppleLogConfigurator.kt | 4 +- .../malinskiy/marathon/apple/NmTestParser.kt | 10 +- .../marathon/apple/RemoteFileManager.kt | 2 + .../malinskiy/marathon/apple/XCTestParser.kt | 32 +- .../apple/bin/AppleBinaryEnvironment.kt | 13 +- .../marathon/apple/bin/codesign/Codesign.kt | 2 +- .../marathon/apple/bin/getconf/Getconf.kt | 2 +- .../marathon/apple/bin/ioreg/Ioreg.kt | 28 ++ .../malinskiy/marathon/apple/bin/lipo/Lipo.kt | 2 +- .../com/malinskiy/marathon/apple/bin/nm/Nm.kt | 2 +- .../apple/bin/plistbuddy/PlistBuddy.kt | 2 +- .../marathon/apple/bin/swvers/SwVers.kt | 21 + .../bin/systemprofiler/SystemProfiler.kt | 21 + .../apple/bin/xcodeselect/Xcodeselect.kt | 2 +- .../marathon/apple/bin/xcrun/Xcrun.kt | 9 +- .../marathon/apple/bin/xcrun/simctl/Simctl.kt | 7 +- .../apple/bin/xcrun/simctl/SimctlService.kt | 2 +- .../simctl/service/ApplicationService.kt | 2 +- .../bin/xcrun/simctl/service/DeviceService.kt | 2 +- .../bin/xcrun/simctl/service/IoService.kt | 2 +- .../xcrun/simctl/service/PrivacyService.kt | 2 +- .../xcrun/simctl/service/SimulatorService.kt | 2 +- .../apple/bin/xcrun/xcodebuild/Xcodebuild.kt | 12 +- .../bin/xcrun/xcresulttool/Xcresulttool.kt | 2 +- .../apple/cmd/remote/rsync/RsyncFileBridge.kt | 30 +- .../apple/device/ConnectionFactory.kt | 32 +- .../extensions/ConfigurationExtensions.kt | 8 - .../apple/extensions/StringExtensions.kt | 3 + .../apple}/listener/TestResultsListener.kt | 3 +- .../marathon/apple/model/AppleTestBundle.kt | 41 +- .../com/malinskiy/marathon/apple/model/Sdk.kt | 15 +- .../apple/xctestrun/TestRootFactory.kt | 28 +- .../MacOSX/libxctest-parser.dylib | Bin 173560 -> 206152 bytes .../apple/ios/AppleSimulatorDevice.kt | 15 +- .../malinskiy/marathon/apple/ios/IosVendor.kt | 4 +- .../ios/device/AppleSimulatorProvider.kt | 9 +- .../apple/ios/device/SimulatorFactory.kt | 2 +- .../apple/macos/AppleMacosProvider.kt | 233 ++++++++++++ .../marathon/apple/macos/MacosDevice.kt | 359 +++++++++++++++--- .../marathon/apple/macos/MacosVendor.kt | 34 +- 46 files changed, 851 insertions(+), 203 deletions(-) rename configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/{ios => }/TimeoutConfiguration.kt (96%) delete mode 100644 configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/apple/macos/TimeoutConfiguration.kt create mode 100644 vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/ioreg/Ioreg.kt create mode 100644 vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/swvers/SwVers.kt create mode 100644 vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/bin/systemprofiler/SystemProfiler.kt create mode 100644 vendor/vendor-apple/base/src/main/kotlin/com/malinskiy/marathon/apple/extensions/StringExtensions.kt rename vendor/vendor-apple/{ios/src/main/kotlin/com/malinskiy/marathon/apple/ios => base/src/main/kotlin/com/malinskiy/marathon/apple}/listener/TestResultsListener.kt (98%) create mode 100644 vendor/vendor-apple/macos/src/main/kotlin/com/malinskiy/marathon/apple/macos/AppleMacosProvider.kt 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 f6e8ff88b7ae16f85e8be8e9ad8985aa11d8f370..8a787baaf263eefadc8d8eec2499634f87995311 100755 GIT binary patch literal 206152 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 literal 173560 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 { 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 }