diff --git a/build-tools/create-packs/Microsoft.Android.Sdk.proj b/build-tools/create-packs/Microsoft.Android.Sdk.proj index e9e8af48982..ea9becb0dce 100644 --- a/build-tools/create-packs/Microsoft.Android.Sdk.proj +++ b/build-tools/create-packs/Microsoft.Android.Sdk.proj @@ -75,6 +75,8 @@ core workload SDK packs imported by WorkloadManifest.targets. + + diff --git a/src/Microsoft.Android.Run/AndroidTestAdapter.cs b/src/Microsoft.Android.Run/AndroidTestAdapter.cs index bd0983ec671..bd63505f0ab 100644 --- a/src/Microsoft.Android.Run/AndroidTestAdapter.cs +++ b/src/Microsoft.Android.Run/AndroidTestAdapter.cs @@ -1,4 +1,5 @@ using System.Xml.Linq; +using Microsoft.Testing.Extensions.TrxReport.Abstractions; using Microsoft.Testing.Platform.Capabilities.TestFramework; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.Messages; @@ -76,17 +77,30 @@ async Task RunAndReportAsync (ExecuteRequestContext context, SessionUid sessionU var testResults = ParseTrxFile (localTrxPath); foreach (var result in testResults) { + // Build the failure message including stack trace for non-TRX consumers + var failureMessage = result.ErrorMessage ?? "Test failed"; + if (!string.IsNullOrEmpty (result.StackTrace)) + failureMessage += "\n" + result.StackTrace; + var stateProperty = result.Outcome switch { TrxOutcome.Passed => (IProperty) new PassedTestNodeStateProperty (), - TrxOutcome.Failed => new FailedTestNodeStateProperty (result.ErrorMessage ?? "Test failed"), + TrxOutcome.Failed => new FailedTestNodeStateProperty (failureMessage), TrxOutcome.NotExecuted => new SkippedTestNodeStateProperty (result.ErrorMessage), _ => new PassedTestNodeStateProperty (), }; + var properties = new List { stateProperty }; + + // Add TRX report properties required by ITrxReportCapability + if (!string.IsNullOrEmpty (result.ClassName)) + properties.Add (new TrxFullyQualifiedTypeNameProperty (result.ClassName)); + if (result.Outcome == TrxOutcome.Failed && (!string.IsNullOrEmpty (result.ErrorMessage) || !string.IsNullOrEmpty (result.StackTrace))) + properties.Add (new TrxExceptionProperty (result.ErrorMessage, result.StackTrace)); + var testNode = new TestNode { Uid = new TestNodeUid (result.FullyQualifiedName), DisplayName = result.TestName, - Properties = new PropertyBag (stateProperty), + Properties = new PropertyBag (properties.ToArray ()), }; await context.MessageBus.PublishAsync (this, new TestNodeUpdateMessage (sessionUid, testNode)); @@ -235,18 +249,14 @@ static List ParseTrxFile (string trxPath) ? $"{className}.{testName}" : testName; - // Extract error message if present + // Extract error message and stack trace if present string? errorMessage = null; + string? stackTrace = null; var outputElement = unitTestResult.Element (ns + "Output"); var errorInfo = outputElement?.Element (ns + "ErrorInfo"); if (errorInfo != null) { - var message = errorInfo.Element (ns + "Message")?.Value; - var stackTrace = errorInfo.Element (ns + "StackTrace")?.Value; - errorMessage = message; - if (!string.IsNullOrEmpty (stackTrace)) - errorMessage = string.IsNullOrEmpty (errorMessage) - ? stackTrace - : $"{errorMessage}\n{stackTrace}"; + errorMessage = errorInfo.Element (ns + "Message")?.Value; + stackTrace = errorInfo.Element (ns + "StackTrace")?.Value; } var trxOutcome = outcome switch { @@ -256,7 +266,7 @@ static List ParseTrxFile (string trxPath) _ => TrxOutcome.Passed, }; - results.Add (new TrxTestResult (fullyQualifiedName, testName, trxOutcome, errorMessage)); + results.Add (new TrxTestResult (fullyQualifiedName, testName, className, trxOutcome, errorMessage, stackTrace)); } } @@ -284,10 +294,22 @@ enum TrxOutcome record TrxTestResult ( string FullyQualifiedName, string TestName, + string? ClassName, TrxOutcome Outcome, - string? ErrorMessage); + string? ErrorMessage, + string? StackTrace); class AndroidTestCapabilities : ITestFrameworkCapabilities { - public IReadOnlyCollection Capabilities { get; } = []; + public IReadOnlyCollection Capabilities { get; } = [new AndroidTrxReportCapability ()]; +} + +class AndroidTrxReportCapability : ITrxReportCapability +{ + public bool IsSupported => true; + + public void Enable () + { + // No-op: TRX properties are always added to test nodes. + } } diff --git a/src/Microsoft.Android.Run/Microsoft.Android.Run.csproj b/src/Microsoft.Android.Run/Microsoft.Android.Run.csproj index b33678f9fcf..1f2466451e5 100644 --- a/src/Microsoft.Android.Run/Microsoft.Android.Run.csproj +++ b/src/Microsoft.Android.Run/Microsoft.Android.Run.csproj @@ -17,6 +17,7 @@ + diff --git a/src/Microsoft.Android.Run/Program.cs b/src/Microsoft.Android.Run/Program.cs index 2da984571d2..7b95b9288b5 100644 --- a/src/Microsoft.Android.Run/Program.cs +++ b/src/Microsoft.Android.Run/Program.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Microsoft.Testing.Extensions; using Mono.Options; using Xamarin.Android.Tools; @@ -317,6 +318,13 @@ async Task RunDotnetTestAsync (List mtpArgs) // since MTP needs them to set up the test communication channel. mtpArgs.AddRange (["--server", "dotnettestcli", "--dotnet-test-pipe", validatedDotnetTestPipe]); + // MTP defaults its working directory to the DLL location (SDK tools directory), + // not Environment.CurrentDirectory. Pass --results-directory explicitly so TRX + // reports are written to the project directory, matching dotnet test conventions. + if (!mtpArgs.Contains ("--results-directory")) { + mtpArgs.AddRange (["--results-directory", Path.Combine (Environment.CurrentDirectory, "TestResults")]); + } + var testApplicationBuilder = await Microsoft.Testing.Platform.Builder.TestApplication.CreateBuilderAsync (mtpArgs.ToArray ()); var adapter = new AndroidTestAdapter ( @@ -330,6 +338,8 @@ async Task RunDotnetTestAsync (List mtpArgs) _ => new AndroidTestCapabilities (), (_, _) => adapter); + testApplicationBuilder.AddTrxReportProvider (); + using var testApplication = await testApplicationBuilder.BuildAsync (); return await testApplication.RunAsync (); } diff --git a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs index af4af861bbb..0b046c3d3c5 100644 --- a/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs +++ b/tests/MSBuildDeviceIntegration/Tests/InstallAndRunTests.cs @@ -2443,10 +2443,13 @@ public void DotNetNewAndroidTest (string mode, AndroidRuntime runtime) Assert.IsTrue (dotnet.Build (target: "Install", parameters: buildParameters.ToArray ()), "`dotnet build -t:Install` should succeed"); // Run based on mode - var runParameters = buildParameters.Select (p => $"/p:{p}").ToArray (); + var runParameters = buildParameters.Select (p => $"/p:{p}").ToList (); + if (mode == "test") + runParameters.Add ("--report-trx"); + using var process = mode == "run" - ? dotnet.StartRun (waitForExit: true, parameters: runParameters) - : dotnet.StartTest (parameters: runParameters); + ? dotnet.StartRun (waitForExit: true, parameters: runParameters.ToArray ()) + : dotnet.StartTest (parameters: runParameters.ToArray ()); var locker = new Lock (); var output = new StringBuilder (); @@ -2499,6 +2502,17 @@ public void DotNetNewAndroidTest (string mode, AndroidRuntime runtime) StringAssert.Contains ("succeeded: 1", outputText, $"Output should report 1 passed test. See {logPath} for details."); StringAssert.Contains ("failed: 1", outputText, $"Output should report 1 failed test. See {logPath} for details."); StringAssert.Contains ("skipped: 1", outputText, $"Output should report 1 skipped test. See {logPath} for details."); + + // Verify that a TRX file was produced by --report-trx + var trxFiles = Directory.GetFiles (projectDirectory, "*.trx", SearchOption.AllDirectories); + Assert.IsTrue (trxFiles.Length > 0, $"Expected at least one .trx file in {projectDirectory}. See {logPath} for details."); + + TestContext.AddTestAttachment (trxFiles [0]); + + var trxDoc = XDocument.Load (trxFiles [0]); + var trxNs = trxDoc.Root?.Name.Namespace ?? XNamespace.None; + var resultSummary = trxDoc.Root?.Element (trxNs + "ResultSummary"); + Assert.IsNotNull (resultSummary, $"TRX file should contain a ResultSummary element. File: {trxFiles [0]}"); } }