From 4da16b4b645ad2298adcde6e0c7351cc1c26dc0c Mon Sep 17 00:00:00 2001 From: Jonathan Peppers Date: Tue, 7 Jul 2026 11:45:56 -0500 Subject: [PATCH] [dotnet] Fix `dotnet test` on macOS and Mac Catalyst templates `dotnet test` on the `macostest` and `maccatalysttest` templates was failing with "Zero tests ran" and Microsoft.Testing.Platform (MTP) handshake failures. Two bugs, both needed for the desktop platforms to work: 1. The desktop test templates' `Main.cs` was dropping the incoming `args` when calling `TestApplication.CreateBuilderAsync`. `dotnet test` invokes the app with MTP protocol arguments appended to the command line (the server type and the named-pipe path for the test host), so discarding `args` disables the MTP handshake. Forward the current process' command-line arguments into `CreateBuilderAsync` alongside the existing `--results-directory` / `--report-trx` args. iOS and tvOS aren't affected because mlaunch forwards MTP env vars into the simulator and MTP picks up the protocol config from those env vars. 2. On macOS and Mac Catalyst the default `dotnet run/test` launch path goes through `open` (LaunchServices), which does not propagate the parent process' environment variables and does not reliably forward trailing CLI arguments to the app. Default `RunWithOpen` to `false` in `Xamarin.Shared.Sdk.MSTest.props` so MSTest projects launch the app binary directly. This is a no-op for iOS/tvOS (`RunWithOpen` is only consulted by the desktop targets). Re-enable the previously-commented-out `[TestCase]` entries for `maccatalysttest` and `macostest` in `DotNetTestTest`, and skip the simulator boot/shutdown steps for desktop platforms. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../maccatalysttest/csharp/Main.cs | 6 +++ .../macostest/csharp/Main.cs | 6 +++ .../targets/Xamarin.Shared.Sdk.MSTest.props | 8 ++++ tests/dotnet/UnitTests/DotNetTestTest.cs | 40 +++++++++---------- 4 files changed, 40 insertions(+), 20 deletions(-) diff --git a/dotnet/Templates/Microsoft.MacCatalyst.Templates/maccatalysttest/csharp/Main.cs b/dotnet/Templates/Microsoft.MacCatalyst.Templates/maccatalysttest/csharp/Main.cs index c66e487fc13f..4ea38ded034b 100644 --- a/dotnet/Templates/Microsoft.MacCatalyst.Templates/maccatalysttest/csharp/Main.cs +++ b/dotnet/Templates/Microsoft.MacCatalyst.Templates/maccatalysttest/csharp/Main.cs @@ -63,7 +63,13 @@ public void WillConnect (UIScene scene, UISceneSession session, UISceneConnectio var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments); var resultsPath = Path.Combine (documentsPath, "TestResults"); + // Forward args from 'dotnet test', which sends the MTP protocol config + // (--server / --dotnet-test-pipe) via the command line. + // Environment.GetCommandLineArgs()[0] is the executable path, not an argument. + string [] cliArgs = Environment.GetCommandLineArgs (); + string [] mtpArgs = cliArgs.Length > 1 ? cliArgs [1..] : []; var builder = await TestApplication.CreateBuilderAsync ([ + .. mtpArgs, "--results-directory", resultsPath, "--report-trx" ]); diff --git a/dotnet/Templates/Microsoft.macOS.Templates/macostest/csharp/Main.cs b/dotnet/Templates/Microsoft.macOS.Templates/macostest/csharp/Main.cs index 3c3c16808d33..d829f575772e 100644 --- a/dotnet/Templates/Microsoft.macOS.Templates/macostest/csharp/Main.cs +++ b/dotnet/Templates/Microsoft.macOS.Templates/macostest/csharp/Main.cs @@ -52,7 +52,13 @@ public override void DidFinishLaunching (NSNotification notification) var documentsPath = Environment.GetFolderPath (Environment.SpecialFolder.MyDocuments); var resultsPath = Path.Combine (documentsPath, "TestResults"); + // Forward args from 'dotnet test', which sends the MTP protocol config + // (--server / --dotnet-test-pipe) via the command line. + // Environment.GetCommandLineArgs()[0] is the executable path, not an argument. + string [] cliArgs = Environment.GetCommandLineArgs (); + string [] mtpArgs = cliArgs.Length > 1 ? cliArgs [1..] : []; var builder = await TestApplication.CreateBuilderAsync ([ + .. mtpArgs, "--results-directory", resultsPath, "--report-trx" ]); diff --git a/dotnet/targets/Xamarin.Shared.Sdk.MSTest.props b/dotnet/targets/Xamarin.Shared.Sdk.MSTest.props index 83ced6200365..63f64f433235 100644 --- a/dotnet/targets/Xamarin.Shared.Sdk.MSTest.props +++ b/dotnet/targets/Xamarin.Shared.Sdk.MSTest.props @@ -11,6 +11,14 @@ false true + + false diff --git a/tests/dotnet/UnitTests/DotNetTestTest.cs b/tests/dotnet/UnitTests/DotNetTestTest.cs index fed4f817a74b..5b1faf96e471 100644 --- a/tests/dotnet/UnitTests/DotNetTestTest.cs +++ b/tests/dotnet/UnitTests/DotNetTestTest.cs @@ -13,10 +13,8 @@ public class DotNetTestTest : TestBaseClass { [Test] [TestCase (ApplePlatform.iOS, "iostest")] [TestCase (ApplePlatform.TVOS, "tvostest")] - // macOS and Mac Catalyst don't use mlaunch (they use 'open' via Desktop.targets), - // so MTP support requires a different approach for these platforms. - // [TestCase (ApplePlatform.MacCatalyst, "maccatalysttest")] - // [TestCase (ApplePlatform.MacOSX, "macostest")] + [TestCase (ApplePlatform.MacCatalyst, "maccatalysttest")] + [TestCase (ApplePlatform.MacOSX, "macostest")] public void DotNetTest (ApplePlatform platform, string template) { Configuration.IgnoreIfIgnoredPlatform (platform); @@ -26,24 +24,21 @@ public void DotNetTest (ApplePlatform platform, string template) DotNet.AssertNew (outputDir, template); var proj = Path.Combine (outputDir, $"{template}.csproj"); - // Boot a simulator so that ComputeRunArguments can find a device + var isDesktop = platform == ApplePlatform.MacOSX || platform == ApplePlatform.MacCatalyst; var log = ConsoleLogger.Instance; var simService = new SimulatorService (log); - var runtimeService = new RuntimeService (log); - var device = GetOrCreateDevice (platform, simService, runtimeService); + SimulatorDeviceInfo? device = null; - if (!device.IsBooted) - Assert.That (simService.Boot (device.Udid), Is.True, $"Failed to boot simulator {device.Udid}."); + if (!isDesktop) { + // Boot a simulator so that ComputeRunArguments can find a device + var runtimeService = new RuntimeService (log); + device = GetOrCreateDevice (platform, simService, runtimeService); - try { - // dotnet test internally calls ComputeRunArguments via MSBuild API without - // forwarding /p: properties, so we must set them in the project file directly. - var csproj = File.ReadAllText (proj); - csproj = csproj.Replace ( - "", - $" true\n {device.Udid}\n "); - File.WriteAllText (proj, csproj); + if (!device.IsBooted) + Assert.That (simService.Boot (device.Udid), Is.True, $"Failed to boot simulator {device.Udid}."); + } + try { // Replace generated tests with a single passing test var testFile = Path.Combine (outputDir, "Test1.cs"); File.WriteAllText (testFile, $@"namespace {template}; @@ -59,21 +54,26 @@ public void TestMethod1 () // Run 'dotnet test' directly using Execution.RunAsync. // dotnet test's MTP flow doesn't forward /p: properties to its internal - // ComputeRunArguments MSBuild API call, so properties must be in the csproj. + // ComputeRunArguments MSBuild API call, so pass properties as environment + // variables instead — MSBuild picks those up as global properties in the + // child process. var env = new Dictionary (); env ["MSBuildSDKsPath"] = null; env ["MSBUILD_EXE_PATH"] = null; + env ["UseFloatingTargetPlatformVersion"] = "true"; + if (device is not null) + env ["Device"] = device.Udid; var binlog = Path.Combine (outputDir, "log-test.binlog"); var testArgs = new List { "test", proj, - $"--device:{device.Udid}", $"/bl:{binlog}" }; var testResult = Execution.RunAsync (DotNet.Executable, testArgs, env, Console.Out, workingDirectory: outputDir, timeout: TimeSpan.FromMinutes (10)).Result; Assert.That (testResult.ExitCode, Is.EqualTo (0), $"'dotnet test' failed with exit code {testResult.ExitCode}.\nBinlog: {binlog}\nOutput:\n{testResult.Output.MergedOutput}"); } finally { - simService.Shutdown (device.Udid); + if (device is not null) + simService.Shutdown (device.Udid); } }