Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(apple): add simulator log dump per batch at device-logs #912

Merged
merged 4 commits into from
Mar 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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 " }
}
}
Loading