Skip to content

Commit

Permalink
feat(apple): macOS parsing
Browse files Browse the repository at this point in the history
  • Loading branch information
Malinskiy committed Feb 17, 2024
1 parent be1c3f2 commit 48adc9a
Show file tree
Hide file tree
Showing 46 changed files with 851 additions and 203 deletions.
1 change: 1 addition & 0 deletions cli/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()

Check warning on line 116 in configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt

View check run for this annotation

Codecov / codecov/patch

configuration/src/main/kotlin/com/malinskiy/marathon/config/vendor/VendorConfiguration.kt#L116

Added line #L116 was not covered by tests
var fileSyncConfiguration: FileSyncConfiguration = FileSyncConfiguration()
var threadingConfiguration: ThreadingConfiguration = ThreadingConfiguration()
var testParserConfiguration: TestParserConfiguration = TestParserConfiguration.LocalTestParserConfiguration
Expand Down Expand Up @@ -159,7 +158,7 @@ sealed class VendorConfiguration {
@JsonProperty("xctestrunEnv") val xctestrunEnv: Map<String, String> = 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,
Expand All @@ -185,7 +184,7 @@ sealed class VendorConfiguration {

@JsonProperty("xcresult") val xcresult: XcresultConfiguration = XcresultConfiguration(),
@JsonProperty("xctestrunEnv") val xctestrunEnv: Map<String, String> = 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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ open class AppleApplicationInstaller<in T: AppleDevice>(
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()
Expand All @@ -37,22 +38,12 @@ open class AppleApplicationInstaller<in T: AppleDevice>(
}
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}" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)"
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,10 +25,13 @@ class NmTestParser(
private val logger = MarathonLogging.logger(NmTestParser::class.java.simpleName)

override suspend fun extract(device: Device): List<Test> {
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
Expand All @@ -44,6 +47,7 @@ class NmTestParser(
bundle: AppleTestBundle,
): List<Test> {
val testBinary = bundle.testBinary
val relativeTestBinaryPath = bundle.relativeTestBinaryPath
val xctest = bundle.testApplication

logger.debug { "Found test binary $testBinary for xctest $xctest" }
Expand All @@ -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('.') }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ class RemoteFileManager(private val device: AppleDevice) {
)
}

private fun String.bashEscape() = "'" + replace("'", "'\\''") + "'"

companion object {
const val FILE_SEPARATOR = "/"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -23,7 +22,7 @@ import kotlin.io.path.outputStream

class XCTestParser<T: AppleDevice>(
private val configuration: Configuration,
private val vendorConfiguration: VendorConfiguration.IOSConfiguration,
private val vendorConfiguration: VendorConfiguration.MacosConfiguration,
private val testBundleIdentifier: AppleTestBundleIdentifier,
private val applicationInstaller: AppleApplicationInstaller<T>,
) : RemoteTestParser<DeviceProvider>, LineListener {
Expand All @@ -34,7 +33,11 @@ class XCTestParser<T: AppleDevice>(
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) {
Expand All @@ -46,13 +49,12 @@ class XCTestParser<T: AppleDevice>(

private suspend fun parseTests(
device: AppleDevice,
configuration: Configuration,
vendorConfiguration: VendorConfiguration,
bundle: AppleTestBundle,
applicationInstaller: AppleApplicationInstaller<T>,
): List<Test> {
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 {
Expand Down Expand Up @@ -86,11 +88,12 @@ class XCTestParser<T: AppleDevice>(
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
}

}
}

Expand All @@ -104,25 +107,12 @@ class XCTestParser<T: AppleDevice>(
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,34 @@ 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)
val lipo: Lipo = Lipo(commandExecutor, timeoutConfiguration)
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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit 48adc9a

Please sign in to comment.