Skip to content

Commit

Permalink
Merge pull request #912 from MarathonLabs/feature/simulator-log
Browse files Browse the repository at this point in the history
feat(apple): add simulator log dump per batch at device-logs
  • Loading branch information
Malinskiy authored Mar 27, 2024
2 parents e20eda5 + ec5c59f commit 4b8a0f7
Show file tree
Hide file tree
Showing 9 changed files with 122 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ sealed class VendorConfiguration {
@JsonProperty("xcodebuildTestArgs") val xcodebuildTestArgs: Map<String, String> = emptyMap(),
@JsonProperty("dataContainerClear") val dataContainerClear: Boolean = false,
@JsonProperty("testParserConfiguration") val testParserConfiguration: AppleTestParserConfiguration = AppleTestParserConfiguration.NmTestParserConfiguration(),
@JsonProperty("deviceLog") val deviceLog: Boolean = false,

@JsonProperty("signing") val signing: SigningConfiguration = SigningConfiguration(),
) : VendorConfiguration() {
Expand Down
1 change: 1 addition & 0 deletions core/src/main/kotlin/com/malinskiy/marathon/io/FileType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ enum class FileType(val dir: String, val suffix: String) {
TEST("tests", "xml"),
TEST_RESULT("test_result", "json"),
LOG("logs", "log"),
DEVICE_LOG("device-logs", "log"),
DEVICE_INFO("devices", "json"),
VIDEO("video", "mp4"),
SCREENSHOT("screenshot", "gif"),
Expand Down
8 changes: 8 additions & 0 deletions docs/runner/apple/configure/ios.md
Original file line number Diff line number Diff line change
Expand Up @@ -609,6 +609,14 @@ testParserConfiguration:
</TabItem>
</Tabs>

### Capture device log

By default, marathon does not pull device system logs. To investigate potential issues users might need to collect these.
System logs will be available at `$output-folder/device-files` per each batch (not per test!) due to latency issues

```yaml
deviceLog: true
```

[1]: ../workers.md
[2]: ../../configuration/dynamic-configuration.md
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ class RemoteFileManager(private val device: AppleDevice) {
return remoteFileForTest(screenshotFileName(udid, type))
}

fun remoteLog(): String {
return remoteFile("device.log")
}

private fun remoteFileForTest(filename: String): String {
return "${outputDir}$FILE_SEPARATOR$filename"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.DeviceService
import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.IoService
import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.PrivacyService
import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.SimulatorService
import com.malinskiy.marathon.apple.bin.xcrun.simctl.service.SpawnService
import com.malinskiy.marathon.apple.cmd.CommandExecutor
import com.malinskiy.marathon.config.Configuration
import com.malinskiy.marathon.config.vendor.VendorConfiguration
Expand All @@ -23,4 +24,5 @@ class Simctl(
val io = IoService(commandExecutor, timeoutConfiguration)
val privacy = PrivacyService(commandExecutor, timeoutConfiguration)
val application = ApplicationService(commandExecutor, timeoutConfiguration)
val spawn = SpawnService(commandExecutor, timeoutConfiguration)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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.apple.cmd.CommandSession
import com.malinskiy.marathon.apple.extensions.Durations
import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration
import java.time.Duration

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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.apple.cmd.CommandSession
import com.malinskiy.marathon.config.vendor.apple.ios.Permission
import com.malinskiy.marathon.config.vendor.apple.TimeoutConfiguration
import java.time.Duration

class SpawnService(commandExecutor: CommandExecutor,
private val timeoutConfiguration: TimeoutConfiguration,
) : SimctlService(commandExecutor) {
/**
* Spawn a process by executing a given executable on a device
*/
suspend fun spawn(udid: String, args: Array<String>, timeout: Duration = timeoutConfiguration.shell): CommandResult {
return criticalExec(
timeout = timeout,
"spawn", udid, *args
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ 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.ios.listener.DataContainerClearListener
import com.malinskiy.marathon.apple.ios.listener.log.DeviceLogListener
import com.malinskiy.marathon.apple.listener.AppleTestRunListener
import com.malinskiy.marathon.apple.listener.CompositeTestRunListener
import com.malinskiy.marathon.apple.listener.DebugTestRunListener
Expand Down Expand Up @@ -354,6 +355,7 @@ class AppleSimulatorDevice(
attachmentProviders
),
logListener,
DeviceLogListener(this, vendorConfiguration.deviceLog, devicePoolId, testBatch),
DebugTestRunListener(this),
diagnosticLogsPathFinder,
sessionResultsPathFinder,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.malinskiy.marathon.apple.ios.listener.log

import com.malinskiy.marathon.apple.extensions.Durations
import com.malinskiy.marathon.apple.ios.AppleSimulatorDevice
import com.malinskiy.marathon.apple.listener.AppleTestRunListener
import com.malinskiy.marathon.apple.model.Sdk
import com.malinskiy.marathon.device.DevicePoolId
import com.malinskiy.marathon.device.toDeviceInfo
import com.malinskiy.marathon.io.FileType
import com.malinskiy.marathon.log.MarathonLogging
import com.malinskiy.marathon.test.TestBatch
import kotlin.system.measureTimeMillis

class DeviceLogListener(
private val device: AppleSimulatorDevice,
private val enabled: Boolean,
private val pool: DevicePoolId,
private val testBatch: TestBatch,
) : AppleTestRunListener {

private var pid: String? = null
private val logger = MarathonLogging.logger {}
private val supported by lazy {
when (device.sdk) {
Sdk.IPHONESIMULATOR, Sdk.TV_SIMULATOR, Sdk.WATCH_SIMULATOR, Sdk.VISION_SIMULATOR -> {
true
}

else -> false
}
}

override suspend fun beforeTestRun() {
super.beforeTestRun()
if (!enabled) return

if (!supported) {
logger.warn { "Device ${device.serialNumber} does not support capturing device logs" }
return
}

val remoteLogPath = device.remoteFileManager.remoteLog()
val result =
device.commandExecutor.criticalExecute(
Durations.INFINITE,
"sh",
"-c",
"xcrun simctl spawn ${device.udid} log stream --type log --color none --style compact 2>/dev/null > $remoteLogPath & echo $!"
)
val possiblePid = result.combinedStdout.trim()
if (result.successful && possiblePid.toIntOrNull() != null) {
pid = result.combinedStdout.trim()
}
}

override suspend fun afterTestRun() {
super.afterTestRun()
if (!enabled) return
if (!supported) {
logger.warn { "Device ${device.serialNumber} does not support capturing device logs" }
return
}

if (pid != null) {
device.executeWorkerCommand(listOf("sh", "-c", "kill -s INT $pid"))
pullLogfile()
}
}

private suspend fun pullLogfile() {
val localLogFile =
device.fileManager.createFile(FileType.DEVICE_LOG, pool, device.toDeviceInfo(), test = null, testBatchId = testBatch.id)
val remoteFilePath = device.remoteFileManager.remoteLog()
val millis = measureTimeMillis {
device.pullFile(remoteFilePath, localLogFile)
}
logger.debug { "Pulling finished in ${millis}ms $remoteFilePath " }
}
}

0 comments on commit 4b8a0f7

Please sign in to comment.