Skip to content
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
42 changes: 34 additions & 8 deletions src/Microsoft.DotNet.XHarness.Apple/AppOperations/AppTester.cs
Original file line number Diff line number Diff line change
Expand Up @@ -341,15 +341,28 @@ private async Task RunSimulatorTests(
// On iOS 18 and later, transferring results over a TCP tunnel isn’t supported.
// Instead, copy the results file from the device to the host machine.
if (deviceListener.TestLog != null &&
!await resultFileHandler.CopyResultsAsync(
resultFileHandler.IsVersionSupported(simulator.OSVersion, true))
{
bool resultsCopied = await resultFileHandler.CopyResultsAsync(
runMode,
true,
simulator.OSVersion,
simulator.UDID,
appInformation.BundleIdentifier,
deviceListener.TestLog.FullPath))
{
throw new InvalidOperationException("Failed to copy test results from simulator to host.");
deviceListener.TestLog.FullPath);

// If results weren't copied, it likely means the app crashed before tests could run
// Try to retrieve the crash report
if (!resultsCopied)
{
_mainLog.WriteLine("Test results file not found, app may have crashed before tests started.");
await resultFileHandler.CopyCrashReportAsync(
simulator.UDID,
simulator.Name,
appInformation,
_mainLog,
isSimulator: true);
}
}
}

Expand Down Expand Up @@ -428,15 +441,28 @@ private async Task RunDeviceTests(
// On iOS 18 and later, transferring results over a TCP tunnel isn’t supported.
// Instead, copy the results file from the device to the host machine.
if (deviceListener.TestLog != null &&
!await resultFileHandler.CopyResultsAsync(
resultFileHandler.IsVersionSupported(device.OSVersion, false))
{
bool resultsCopied = await resultFileHandler.CopyResultsAsync(
runMode,
false,
device.OSVersion,
device.UDID,
appInformation.BundleIdentifier,
deviceListener.TestLog.FullPath))
{
throw new InvalidOperationException("Failed to copy test results from device to host.");
deviceListener.TestLog.FullPath);

// If results weren't copied, it likely means the app crashed before tests could run
// Try to retrieve the crash report
if (!resultsCopied)
{
_mainLog.WriteLine("Test results file not found, app may have crashed before tests started.");
await resultFileHandler.CopyCrashReportAsync(
device.UDID,
device.Name,
appInformation,
_mainLog,
isSimulator: false);
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions src/Microsoft.DotNet.XHarness.iOS.Shared/IResultFileHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,14 @@ Task<bool> CopyResultsAsync(
string udid,
string bundleIdentifier,
string hostDestinationPath);

/// <summary>
/// Copy the latest crash report from the device and dumps its content to the log.
/// </summary>
Task CopyCrashReportAsync(
string deviceUdid,
string? deviceName,
AppBundleInformation appInformation,
Common.Logging.ILog outputLog,
bool isSimulator);
}
96 changes: 96 additions & 0 deletions src/Microsoft.DotNet.XHarness.iOS.Shared/ResultFileHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.Common.Execution;
using Microsoft.DotNet.XHarness.Common.Logging;
using Microsoft.DotNet.XHarness.iOS.Shared.Execution;

Expand Down Expand Up @@ -104,4 +106,98 @@ await _processManager.ExecuteCommandAsync(

return true;
}

public async Task CopyCrashReportAsync(
string deviceUdid,
string? deviceName,
AppBundleInformation appInformation,
ILog outputLog,
bool isSimulator)
{
_mainLog.WriteLine("Attempting to retrieve crash report from device...");

// List all crash reports on the device
string tempCrashListFile = Path.GetTempFileName();

MlaunchArguments listArgs = new MlaunchArguments(new ListCrashReportsArgument(tempCrashListFile));

if (!string.IsNullOrEmpty(deviceName))
{
listArgs.Add(new DeviceNameArgument(deviceName));
}

ProcessExecutionResult listResult = await _processManager.ExecuteCommandAsync(
listArgs,
_mainLog,
TimeSpan.FromMinutes(1));

if (!listResult.Succeeded || !File.Exists(tempCrashListFile))
{
_mainLog.WriteLine("Failed to list crash reports from device.");
return;
}

List<string> crashReports = File.ReadAllLines(tempCrashListFile)
.Where(line => !string.IsNullOrWhiteSpace(line))
.ToList();

if (crashReports.Count == 0)
{
_mainLog.WriteLine("No crash reports found on device.");
return;
}

// Filter for crash reports that might be related to our app
// .ips files typically follow the format: AppName-YYYY-MM-DD-HHMMSS.ips or similar
List<string> appRelatedCrashes = crashReports
.Where(crash => crash.Contains(appInformation.AppName, StringComparison.OrdinalIgnoreCase) ||
crash.EndsWith(".ips", StringComparison.OrdinalIgnoreCase))
.ToList();

// Use the last crash report (most recent) from the filtered list
string latestCrashReport = (appRelatedCrashes.Count > 0 ? appRelatedCrashes : crashReports).Last();

_mainLog.WriteLine($"Found crash report: {latestCrashReport}");

// Download the crash report
string? uploadRoot = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
string crashReportContent;

if (!string.IsNullOrEmpty(uploadRoot) && Directory.Exists(uploadRoot))
{
string crashFileName = Path.GetFileName(latestCrashReport);
crashReportContent = Path.Combine(uploadRoot, crashFileName);
}
else
{
crashReportContent = Path.GetTempFileName();
}

MlaunchArguments downloadArgs = new MlaunchArguments(
new DownloadCrashReportArgument(latestCrashReport),
new DownloadCrashReportToArgument(crashReportContent));

if (!string.IsNullOrEmpty(deviceName))
{
downloadArgs.Add(new DeviceNameArgument(deviceName));
}

ProcessExecutionResult downloadResult = await _processManager.ExecuteCommandAsync(
downloadArgs,
_mainLog,
TimeSpan.FromMinutes(1));

if (!downloadResult.Succeeded || !File.Exists(crashReportContent))
{
_mainLog.WriteLine("Failed to download crash report from device.");
return;
}

// Dump the crash report content to the log
_mainLog.WriteLine($"==================== Crash report ====================");
_mainLog.WriteLine($"Crash report file: {crashReportContent}");
string crashContent = await File.ReadAllTextAsync(crashReportContent);
Comment thread
kotlarmilos marked this conversation as resolved.
_mainLog.WriteLine(crashContent);
_mainLog.WriteLine($"==================== End of Crash report ====================");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.DotNet.XHarness.Common.Execution;
using Microsoft.DotNet.XHarness.Common.Logging;
using Microsoft.DotNet.XHarness.iOS.Shared;
using Microsoft.DotNet.XHarness.iOS.Shared.Execution;
using Moq;
using Xunit;
Expand Down Expand Up @@ -169,4 +173,90 @@ public async Task DeviceOsVersion18FileMissingReturnsFalse()
Assert.False(result);
log.Verify(l => l.WriteLine($"Failed to copy results file from device. Expected at: {_tempFile}"), Times.Once);
}

[Fact]
public async Task CopyCrashReportUsesHelixUploadRootWhenAvailable()
{
// Skip on Windows as mlaunch is not available
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return;
}

string originalUploadRoot = Environment.GetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT");
string uploadRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(uploadRoot);

try
{
Environment.SetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT", uploadRoot);

Mock<IMlaunchProcessManager> pm = new Mock<IMlaunchProcessManager>();
Mock<IFileBackedLog> log = new Mock<IFileBackedLog>();
ResultFileHandler handler = CreateHandler(pm, log);

string crashReportName = "MyApp-2025-11-25-223847.ips";
string expectedDownloadPath = Path.Combine(uploadRoot, crashReportName);
string crashContent = "Dummy crash content";
string actualDownloadPath = null;

int callCount = 0;

pm.Setup(m => m.ExecuteCommandAsync(
It.IsAny<MlaunchArguments>(),
It.IsAny<ILog>(),
It.IsAny<TimeSpan>(),
It.IsAny<Dictionary<string, string>>(),
It.IsAny<int>(),
It.IsAny<CancellationToken?>()))
.Returns((MlaunchArguments args, ILog _, TimeSpan _, Dictionary<string, string> _, int _, CancellationToken? _) =>
{
callCount++;
if (callCount == 1)
{
string listFilePath = GetArgumentValue(args, "list-crash-reports");
File.WriteAllLines(listFilePath, new[] { crashReportName });
}
else if (callCount == 2)
{
actualDownloadPath = GetArgumentValue(args, "download-crash-report-to");
File.WriteAllText(actualDownloadPath, crashContent);
}

return Task.FromResult(new ProcessExecutionResult { ExitCode = 0 });
});

var appInfo = new AppBundleInformation("MyApp", "com.example.myapp", "/tmp", "/tmp", supports32b: false);

await handler.CopyCrashReportAsync("device-udid", null, appInfo, log.Object, isSimulator: false);

Assert.Equal(expectedDownloadPath, actualDownloadPath);
Assert.True(File.Exists(expectedDownloadPath));

log.Verify(l => l.WriteLine("Attempting to retrieve crash report from device..."), Times.Once);
log.Verify(l => l.WriteLine($"Found crash report: {crashReportName}"), Times.Once);
log.Verify(l => l.WriteLine("==================== Crash report ===================="), Times.Once);
log.Verify(l => l.WriteLine($"Crash report file: {expectedDownloadPath}"), Times.Once);
log.Verify(l => l.WriteLine(crashContent), Times.Once);
log.Verify(l => l.WriteLine("==================== End of Crash report ===================="), Times.Once);
}
finally
{
Environment.SetEnvironmentVariable("HELIX_WORKITEM_UPLOAD_ROOT", originalUploadRoot);

if (Directory.Exists(uploadRoot))
{
Directory.Delete(uploadRoot, true);
}
}
}

private static string GetArgumentValue(MlaunchArguments args, string argumentName)
{
string prefix = $"--{argumentName}=";
string argument = args.Select(a => a.AsCommandLineArgument())
.First(a => a.StartsWith(prefix, StringComparison.Ordinal));

return argument.Substring(prefix.Length).Trim('"');
}
}
Loading