From 86d4207cb7d2494a6ba2d06826aadb6e168618c7 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 19:46:14 +0200 Subject: [PATCH 01/17] Migrate Microsoft.DotNet.HotReload.Watch.Aspire.Tests to MSTest.Sdk on MTP This is a pathfinder PR for migrating the test suite to MSTest on Microsoft.Testing.Platform (MTP). Microsoft.DotNet.HotReload.Watch.Aspire.Tests was chosen because it has no dependency on the shared Microsoft.NET.TestFramework (which is xUnit-coupled and referenced by ~57 of 78 test projects), so it can migrate in isolation without unblocking dependents first. Changes: * global.json: add MSTest.Sdk 4.3.0-preview.26307.5 to msbuild-sdks. * test/Directory.Build.targets: gate the xUnit defaults (TestRunnerName=XUnitV3, Using Include=Xunit, etc.) behind $(UseMSTestSdk) != true, so MSTest.Sdk projects opt out cleanly. * test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests: - csproj now uses Sdk="MSTest.Sdk", sets UseMSTestSdk=true, references AwesomeAssertions and only the Watch.Aspire project. MTP is on by default via MSTest.Sdk (EnableMSTestRunner + TestingPlatformDotnetTestSupport). - All 4 unit-test files converted from xUnit to MSTest attributes/asserts ([Fact]/[Theory] -> [TestMethod]/[DataRow], Assert.* equivalents, Assert.IsInstanceOfType, Assert.HasCount, Assert.IsEmpty). - Local AssertEx.SequenceEqual helper replaces the xUnit-coupled one from HotReload.Test.Utilities. * Move the 2 integration tests (AspireLauncherTests + PipeUtilities) to test/dotnet-watch.Tests/Aspire/ so the Aspire.Tests project stays a pure MSTest unit-test project. They keep xUnit because they depend on WatchSdkTest, WatchableApp, [PlatformSpecificFact], ITestOutputHelper and TestAssets from Microsoft.NET.TestFramework. AspireLauncherTests was renamed to AspireLauncherIntegrationTests to reflect its new role. * src/Dotnet.Watch/Watch.Aspire/Properties/AssemblyInfo.cs: grant InternalsVisibleTo to dotnet-watch.Tests (needed by PipeUtilities, which uses internal WatchStatusEvent). * test/dotnet-watch.Tests/dotnet-watch.Tests.csproj: add ProjectReference to Watch.Aspire (ExcludeAssets=Runtime) so the moved integration tests compile. Verification: * Microsoft.DotNet.HotReload.Watch.Aspire.Tests builds with MSTest.Sdk and all 58 unit tests pass under MTP (705 ms). * test/dotnet-watch.Tests builds successfully with the moved files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Watch.Aspire/Properties/AssemblyInfo.cs | 1 + .../AspireHostLauncherCliTests.cs | 111 ++++++------ .../AspireHostLauncherTests.cs | 57 +++--- .../AspireResourceLauncherCliTests.cs | 163 +++++++++--------- .../AspireServerLauncherCliTests.cs | 87 +++++----- .../AssertEx.cs | 30 ++++ ...DotNet.HotReload.Watch.Aspire.Tests.csproj | 16 +- .../Aspire/AspireLauncherIntegrationTests.cs} | 2 +- .../Aspire}/PipeUtilities.cs | 0 .../dotnet-watch.Tests.csproj | 2 + 10 files changed, 255 insertions(+), 214 deletions(-) create mode 100644 test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs rename test/{Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs => dotnet-watch.Tests/Aspire/AspireLauncherIntegrationTests.cs} (98%) rename test/{Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities => dotnet-watch.Tests/Aspire}/PipeUtilities.cs (100%) diff --git a/src/Dotnet.Watch/Watch.Aspire/Properties/AssemblyInfo.cs b/src/Dotnet.Watch/Watch.Aspire/Properties/AssemblyInfo.cs index 74a1e15ab7a8..3d48d3fc6f5e 100644 --- a/src/Dotnet.Watch/Watch.Aspire/Properties/AssemblyInfo.cs +++ b/src/Dotnet.Watch/Watch.Aspire/Properties/AssemblyInfo.cs @@ -4,4 +4,5 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.DotNet.HotReload.Watch.Aspire.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("dotnet-watch.Tests, PublicKey = 0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs index b7d9c7f69059..23edc5e39889 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs @@ -1,145 +1,146 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch.UnitTests; +[TestClass] public class AspireHostLauncherCliTests { - [Fact] + [TestMethod] public void RequiredSdkOption() { // --sdk option is missing var args = new[] { "host", "--entrypoint", "proj", "a", "b" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void RequiredEntryPointOption() { // --entrypoint option is missing var args = new[] { "host", "--sdk", "sdk", "--verbose" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void ProjectAndSdkPaths() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "myproject.csproj" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("sdk", launcher.EnvironmentOptions.SdkDirectory); - Assert.True(launcher.EntryPoint.IsProjectFile); - Assert.Equal("myproject.csproj", launcher.EntryPoint.PhysicalPath); - Assert.Empty(launcher.ApplicationArguments); - Assert.Equal(LogLevel.Information, launcher.GlobalOptions.LogLevel); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); + Assert.IsTrue(launcher.EntryPoint.IsProjectFile); + Assert.AreEqual("myproject.csproj", launcher.EntryPoint.PhysicalPath); + Assert.IsEmpty(launcher.ApplicationArguments); + Assert.AreEqual(LogLevel.Information, launcher.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void FilePath() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "file.cs" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("sdk", launcher.EnvironmentOptions.SdkDirectory); - Assert.False(launcher.EntryPoint.IsProjectFile); - Assert.Equal("file.cs", launcher.EntryPoint.EntryPointFilePath); - Assert.Empty(launcher.ApplicationArguments); - Assert.Equal(LogLevel.Information, launcher.GlobalOptions.LogLevel); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); + Assert.IsFalse(launcher.EntryPoint.IsProjectFile); + Assert.AreEqual("file.cs", launcher.EntryPoint.EntryPointFilePath); + Assert.IsEmpty(launcher.ApplicationArguments); + Assert.AreEqual(LogLevel.Information, launcher.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void ApplicationArguments() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--verbose", "a", "b" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); - Assert.Equal(LogLevel.Debug, launcher.GlobalOptions.LogLevel); + Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void VerboseOption() { // With verbose flag var argsVerbose = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--verbose" }; - var launcherVerbose = Assert.IsType(AspireLauncher.TryCreate(argsVerbose)); - Assert.Equal(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); + var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); // Without verbose flag var argsNotVerbose = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherNotVerbose = Assert.IsType(AspireLauncher.TryCreate(argsNotVerbose)); - Assert.Equal(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); + var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void QuietOption() { // With quiet flag var argsQuiet = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--quiet" }; - var launcherQuiet = Assert.IsType(AspireLauncher.TryCreate(argsQuiet)); - Assert.Equal(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); + var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); // Without quiet flag var argsNotQuiet = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherNotQuiet = Assert.IsType(AspireLauncher.TryCreate(argsNotQuiet)); - Assert.Equal(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); + var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void NoLaunchProfileOption() { // With no-launch-profile flag var argsNoProfile = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--no-launch-profile" }; - var launcherNoProfile = Assert.IsType(AspireLauncher.TryCreate(argsNoProfile)); - Assert.False(launcherNoProfile.LaunchProfileName.HasValue); + var launcherNoProfile = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); + Assert.IsFalse(launcherNoProfile.LaunchProfileName.HasValue); // Without no-launch-profile flag var argsDefault = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherDefault = Assert.IsType(AspireLauncher.TryCreate(argsDefault)); - Assert.True(launcherDefault.LaunchProfileName.HasValue); - Assert.Null(launcherDefault.LaunchProfileName.Value); + var launcherDefault = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsDefault)); + Assert.IsTrue(launcherDefault.LaunchProfileName.HasValue); + Assert.IsNull(launcherDefault.LaunchProfileName.Value); } - [Fact] + [TestMethod] public void LaunchProfileOption() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--launch-profile", "MyProfile" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.True(launcher.LaunchProfileName.HasValue); - Assert.Equal("MyProfile", launcher.LaunchProfileName.Value); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.IsTrue(launcher.LaunchProfileName.HasValue); + Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } - [Fact] + [TestMethod] public void ConflictingOptions() { // Cannot specify both --quiet and --verbose var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--quiet", "--verbose" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void EntryPoint_MultipleValues() { // EntryPoint option should only accept one value; extra values become application arguments var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj1", "proj2" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("proj1", launcher.EntryPoint.ProjectOrEntryPointFilePath); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("proj1", launcher.EntryPoint.ProjectOrEntryPointFilePath); AssertEx.SequenceEqual(["proj2"], launcher.ApplicationArguments); } - [Fact] + [TestMethod] public void AllOptionsSet() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "myapp.csproj", "--verbose", "--no-launch-profile", "arg1", "arg2", "arg3" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); - Assert.True(launcher.EntryPoint.IsProjectFile); - Assert.Equal("myapp.csproj", launcher.EntryPoint.PhysicalPath); - Assert.Equal("sdk", launcher.EnvironmentOptions.SdkDirectory); - Assert.Equal(LogLevel.Debug, launcher.GlobalOptions.LogLevel); - Assert.False(launcher.LaunchProfileName.HasValue); + Assert.IsTrue(launcher.EntryPoint.IsProjectFile); + Assert.AreEqual("myapp.csproj", launcher.EntryPoint.PhysicalPath); + Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); + Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); + Assert.IsFalse(launcher.LaunchProfileName.HasValue); AssertEx.SequenceEqual(["arg1", "arg2", "arg3"], launcher.ApplicationArguments); } -} +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs index 94abac0ac2ca..33cf07e5aa9e 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; @@ -6,6 +6,7 @@ namespace Microsoft.DotNet.Watch.UnitTests; +[TestClass] public class AspireHostLauncherTests { private static AspireHostLauncher CreateLauncher( @@ -33,13 +34,13 @@ private static AspireHostLauncher CreateLauncher( private static void AssertCommonProperties(ProjectOptions options, AspireHostLauncher launcher) { - Assert.True(options.IsMainProject); - Assert.Equal("run", options.Command); - Assert.Equal(launcher.EntryPoint, options.Representation); - Assert.Empty(options.LaunchEnvironmentVariables); + Assert.IsTrue(options.IsMainProject); + Assert.AreEqual("run", options.Command); + Assert.AreEqual(launcher.EntryPoint, options.Representation); + Assert.IsEmpty(options.LaunchEnvironmentVariables); } - [Fact] + [TestMethod] public void GetProjectOptions_ProjectFile_UsesProjectFlag() { var launcher = CreateLauncher("myapp.csproj"); @@ -47,11 +48,11 @@ public void GetProjectOptions_ProjectFile_UsesProjectFlag() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.False(options.LaunchProfileName.HasValue); + Assert.IsFalse(options.LaunchProfileName.HasValue); AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_EntryPointFile_UsesFileFlag() { var launcher = CreateLauncher("Program.cs"); @@ -59,11 +60,11 @@ public void GetProjectOptions_EntryPointFile_UsesFileFlag() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.False(options.LaunchProfileName.HasValue); + Assert.IsFalse(options.LaunchProfileName.HasValue); AssertEx.SequenceEqual(["--file", "Program.cs", "--no-launch-profile"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_WithLaunchProfile_AddsLaunchProfileArguments() { var launcher = CreateLauncher("myapp.csproj", launchProfileName: "MyProfile"); @@ -71,12 +72,12 @@ public void GetProjectOptions_WithLaunchProfile_AddsLaunchProfileArguments() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.True(options.LaunchProfileName.HasValue); - Assert.Equal("MyProfile", options.LaunchProfileName.Value); + Assert.IsTrue(options.LaunchProfileName.HasValue); + Assert.AreEqual("MyProfile", options.LaunchProfileName.Value); AssertEx.SequenceEqual(["--project", "myapp.csproj", "--launch-profile", "MyProfile"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_NoLaunchProfile_AddsNoLaunchProfileFlag() { var launcher = CreateLauncher("myapp.csproj", launchProfileName: Optional.NoValue); @@ -84,11 +85,11 @@ public void GetProjectOptions_NoLaunchProfile_AddsNoLaunchProfileFlag() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.False(options.LaunchProfileName.HasValue); + Assert.IsFalse(options.LaunchProfileName.HasValue); AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_NullLaunchProfile_UsesDefault() { // null value (HasValue=true) means use default launch profile - no --launch-profile or --no-launch-profile flag @@ -97,12 +98,12 @@ public void GetProjectOptions_NullLaunchProfile_UsesDefault() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.True(options.LaunchProfileName.HasValue); - Assert.Null(options.LaunchProfileName.Value); + Assert.IsTrue(options.LaunchProfileName.HasValue); + Assert.IsNull(options.LaunchProfileName.Value); AssertEx.SequenceEqual(["--project", "myapp.csproj"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_WithApplicationArguments_AppendsArguments() { var launcher = CreateLauncher("myapp.csproj", launchProfileName: "Profile", applicationArguments: ["arg1", "arg2"]); @@ -110,12 +111,12 @@ public void GetProjectOptions_WithApplicationArguments_AppendsArguments() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.True(options.LaunchProfileName.HasValue); - Assert.Equal("Profile", options.LaunchProfileName.Value); + Assert.IsTrue(options.LaunchProfileName.HasValue); + Assert.AreEqual("Profile", options.LaunchProfileName.Value); AssertEx.SequenceEqual(["--project", "myapp.csproj", "--launch-profile", "Profile", "arg1", "arg2"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_SetsCustomWorkingDirectory() { var launcher = CreateLauncher("myapp.csproj", workingDirectory: "/custom/path"); @@ -123,10 +124,10 @@ public void GetProjectOptions_SetsCustomWorkingDirectory() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.Equal("/custom/path", options.WorkingDirectory); + Assert.AreEqual("/custom/path", options.WorkingDirectory); } - [Fact] + [TestMethod] public void GetProjectOptions_EntryPointFile_WithLaunchProfileAndArguments() { var launcher = CreateLauncher("Program.cs", launchProfileName: "Dev", applicationArguments: ["--port", "8080"]); @@ -134,12 +135,12 @@ public void GetProjectOptions_EntryPointFile_WithLaunchProfileAndArguments() var options = launcher.GetHostProjectOptions()!; AssertCommonProperties(options, launcher); - Assert.True(options.LaunchProfileName.HasValue); - Assert.Equal("Dev", options.LaunchProfileName.Value); + Assert.IsTrue(options.LaunchProfileName.HasValue); + Assert.AreEqual("Dev", options.LaunchProfileName.Value); AssertEx.SequenceEqual(["--file", "Program.cs", "--launch-profile", "Dev", "--port", "8080"], options.CommandArguments); } - [Fact] + [TestMethod] public void GetProjectOptions_NoLaunchProfile_WithApplicationArguments() { var launcher = CreateLauncher("myapp.csproj", launchProfileName: Optional.NoValue, applicationArguments: ["--urls", "http://localhost:5000"]); @@ -147,7 +148,7 @@ public void GetProjectOptions_NoLaunchProfile_WithApplicationArguments() var options = launcher.GetHostProjectOptions(); AssertCommonProperties(options, launcher); - Assert.False(options.LaunchProfileName.HasValue); + Assert.IsFalse(options.LaunchProfileName.HasValue); AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile", "--urls", "http://localhost:5000"], options.CommandArguments); } -} +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs index d0151c2102fb..ef5a3347ddde 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; @@ -6,175 +6,176 @@ namespace Microsoft.DotNet.Watch.UnitTests; +[TestClass] public class AspireResourceLauncherCliTests { - [Fact] + [TestMethod] public void RequiredServerOption() { // --server option is missing var args = new[] { "resource", "--entrypoint", "proj" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void RequiredEntryPointOption() { // --entrypoint option is missing var args = new[] { "resource", "--server", "pipe1" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void MinimalRequiredOptions() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj.csproj" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("pipe1", launcher.ServerPipeName); - Assert.Equal("proj.csproj", launcher.EntryPoint); - Assert.Empty(launcher.ApplicationArguments); - Assert.Empty(launcher.EnvironmentVariables); - Assert.True(launcher.LaunchProfileName.HasValue); - Assert.Null(launcher.LaunchProfileName.Value); - Assert.Equal(LogLevel.Information, launcher.GlobalOptions.LogLevel); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("pipe1", launcher.ServerPipeName); + Assert.AreEqual("proj.csproj", launcher.EntryPoint); + Assert.IsEmpty(launcher.ApplicationArguments); + Assert.IsEmpty(launcher.EnvironmentVariables); + Assert.IsTrue(launcher.LaunchProfileName.HasValue); + Assert.IsNull(launcher.LaunchProfileName.Value); + Assert.AreEqual(LogLevel.Information, launcher.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void ApplicationArguments() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "a", "b" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); } - [Fact] + [TestMethod] public void EnvironmentOption_SingleVariable() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY=value" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Single(launcher.EnvironmentVariables); - Assert.Equal("value", launcher.EnvironmentVariables["KEY"]); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.HasCount(1, launcher.EnvironmentVariables); + Assert.AreEqual("value", launcher.EnvironmentVariables["KEY"]); } - [Fact] + [TestMethod] public void EnvironmentOption_MultipleVariables() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY1=val1", "-e", "KEY2=val2" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal(2, launcher.EnvironmentVariables.Count); - Assert.Equal("val1", launcher.EnvironmentVariables["KEY1"]); - Assert.Equal("val2", launcher.EnvironmentVariables["KEY2"]); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual(2, launcher.EnvironmentVariables.Count); + Assert.AreEqual("val1", launcher.EnvironmentVariables["KEY1"]); + Assert.AreEqual("val2", launcher.EnvironmentVariables["KEY2"]); } - [Fact] + [TestMethod] public void EnvironmentOption_ValueWithEquals() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "CONN=Server=localhost;Port=5432" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("Server=localhost;Port=5432", launcher.EnvironmentVariables["CONN"]); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("Server=localhost;Port=5432", launcher.EnvironmentVariables["CONN"]); } - [Fact] + [TestMethod] public void EnvironmentOption_EmptyValue() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY=" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("", launcher.EnvironmentVariables["KEY"]); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("", launcher.EnvironmentVariables["KEY"]); } - [Fact] + [TestMethod] public void EnvironmentOption_NoEquals() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("", launcher.EnvironmentVariables["KEY"]); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("", launcher.EnvironmentVariables["KEY"]); } - [Fact] + [TestMethod] public void NoLaunchProfileOption() { // With no-launch-profile flag var argsNoProfile = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--no-launch-profile" }; - var launcherNoProfile = Assert.IsType(AspireLauncher.TryCreate(argsNoProfile)); - Assert.False(launcherNoProfile.LaunchProfileName.HasValue); + var launcherNoProfile = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); + Assert.IsFalse(launcherNoProfile.LaunchProfileName.HasValue); // Without no-launch-profile flag var argsDefault = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherDefault = Assert.IsType(AspireLauncher.TryCreate(argsDefault)); - Assert.True(launcherDefault.LaunchProfileName.HasValue); - Assert.Null(launcherDefault.LaunchProfileName.Value); + var launcherDefault = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsDefault)); + Assert.IsTrue(launcherDefault.LaunchProfileName.HasValue); + Assert.IsNull(launcherDefault.LaunchProfileName.Value); } - [Fact] + [TestMethod] public void LaunchProfileOption() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--launch-profile", "MyProfile" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.True(launcher.LaunchProfileName.HasValue); - Assert.Equal("MyProfile", launcher.LaunchProfileName.Value); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.IsTrue(launcher.LaunchProfileName.HasValue); + Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } - [Fact] + [TestMethod] public void LaunchProfileOption_ShortForm() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-lp", "MyProfile" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.True(launcher.LaunchProfileName.HasValue); - Assert.Equal("MyProfile", launcher.LaunchProfileName.Value); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.IsTrue(launcher.LaunchProfileName.HasValue); + Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } - [Fact] + [TestMethod] public void VerboseOption() { var argsVerbose = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--verbose" }; - var launcherVerbose = Assert.IsType(AspireLauncher.TryCreate(argsVerbose)); - Assert.Equal(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); + var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); var argsNotVerbose = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherNotVerbose = Assert.IsType(AspireLauncher.TryCreate(argsNotVerbose)); - Assert.Equal(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); + var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void QuietOption() { var argsQuiet = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--quiet" }; - var launcherQuiet = Assert.IsType(AspireLauncher.TryCreate(argsQuiet)); - Assert.Equal(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); + var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); var argsNotQuiet = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherNotQuiet = Assert.IsType(AspireLauncher.TryCreate(argsNotQuiet)); - Assert.Equal(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); + var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void ConflictingOptions() { // Cannot specify both --quiet and --verbose var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--quiet", "--verbose" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void AllOptionsSet() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "myapp.csproj", "-e", "K1=V1", "-e", "K2=V2", "--launch-profile", "Dev", "--verbose", "arg1", "arg2" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); - Assert.Equal("pipe1", launcher.ServerPipeName); - Assert.Equal("myapp.csproj", launcher.EntryPoint); - Assert.Equal(LogLevel.Debug, launcher.GlobalOptions.LogLevel); - Assert.True(launcher.LaunchProfileName.HasValue); - Assert.Equal("Dev", launcher.LaunchProfileName.Value); + Assert.AreEqual("pipe1", launcher.ServerPipeName); + Assert.AreEqual("myapp.csproj", launcher.EntryPoint); + Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); + Assert.IsTrue(launcher.LaunchProfileName.HasValue); + Assert.AreEqual("Dev", launcher.LaunchProfileName.Value); AssertEx.SequenceEqual(["arg1", "arg2"], launcher.ApplicationArguments); - Assert.Equal(2, launcher.EnvironmentVariables.Count); - Assert.Equal("V1", launcher.EnvironmentVariables["K1"]); - Assert.Equal("V2", launcher.EnvironmentVariables["K2"]); + Assert.AreEqual(2, launcher.EnvironmentVariables.Count); + Assert.AreEqual("V1", launcher.EnvironmentVariables["K1"]); + Assert.AreEqual("V2", launcher.EnvironmentVariables["K2"]); } - [Fact] + [TestMethod] public void EnvironmentOption_Duplicates() { var command = new AspireResourceCommandDefinition(); @@ -187,7 +188,7 @@ public void EnvironmentOption_Duplicates() result.Errors.Should().BeEmpty(); } - [Fact] + [TestMethod] public void EnvironmentOption_Duplicates_CasingDifference() { var command = new AspireResourceCommandDefinition(); @@ -212,7 +213,7 @@ public void EnvironmentOption_Duplicates_CasingDifference() result.Errors.Should().BeEmpty(); } - [Fact] + [TestMethod] public void EnvironmentOption_MultiplePerToken() { var command = new AspireResourceCommandDefinition(); @@ -230,7 +231,7 @@ public void EnvironmentOption_MultiplePerToken() result.Errors.Should().BeEmpty(); } - [Fact] + [TestMethod] public void EnvironmentOption_NoValue() { var command = new AspireResourceCommandDefinition(); @@ -243,7 +244,7 @@ public void EnvironmentOption_NoValue() result.Errors.Should().BeEmpty(); } - [Fact] + [TestMethod] public void EnvironmentOption_WhitespaceTrimming() { var command = new AspireResourceCommandDefinition(); @@ -256,11 +257,11 @@ public void EnvironmentOption_WhitespaceTrimming() result.Errors.Should().BeEmpty(); } - [Theory] - [InlineData("")] - [InlineData("=")] - [InlineData("= X")] - [InlineData(" \u2002 = X")] + [TestMethod] + [DataRow("")] + [DataRow("=")] + [DataRow("= X")] + [DataRow(" \u2002 = X")] public void EnvironmentOption_Errors(string token) { var command = new AspireResourceCommandDefinition(); @@ -271,4 +272,4 @@ public void EnvironmentOption_Errors(string token) $"Incorrectly formatted environment variables '{token}'" ], result.Errors.Select(e => e.Message)); } -} +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs index 39ba1fdff22b..91dcfe91bdf7 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs @@ -1,129 +1,130 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Extensions.Logging; namespace Microsoft.DotNet.Watch.UnitTests; +[TestClass] public class AspireServerLauncherCliTests { - [Fact] + [TestMethod] public void RequiredServerOption() { // --server option is missing var args = new[] { "server", "--sdk", "sdk" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void RequiredSdkOption() { // --sdk option is missing var args = new[] { "server", "--server", "pipe1" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void MinimalRequiredOptions() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("pipe1", launcher.ServerPipeName); - Assert.Equal(LogLevel.Information, launcher.GlobalOptions.LogLevel); - Assert.Empty(launcher.ResourcePaths); - Assert.Null(launcher.StatusPipeName); - Assert.Null(launcher.ControlPipeName); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("pipe1", launcher.ServerPipeName); + Assert.AreEqual(LogLevel.Information, launcher.GlobalOptions.LogLevel); + Assert.IsEmpty(launcher.ResourcePaths); + Assert.IsNull(launcher.StatusPipeName); + Assert.IsNull(launcher.ControlPipeName); } - [Fact] + [TestMethod] public void ResourceOption_SingleValue() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj"], launcher.ResourcePaths); } - [Fact] + [TestMethod] public void ResourceOption_MultipleValues() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "proj2.csproj", "file.cs" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj", "file.cs"], launcher.ResourcePaths); } - [Fact] + [TestMethod] public void ResourceOption_MultipleFlags() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "--resource", "proj2.csproj" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); } - [Fact] + [TestMethod] public void StatusPipeOption() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--status-pipe", "status1" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("status1", launcher.StatusPipeName); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("status1", launcher.StatusPipeName); } - [Fact] + [TestMethod] public void ControlPipeOption() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--control-pipe", "control1" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); - Assert.Equal("control1", launcher.ControlPipeName); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + Assert.AreEqual("control1", launcher.ControlPipeName); } - [Fact] + [TestMethod] public void VerboseOption() { // With verbose flag var argsVerbose = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--verbose" }; - var launcherVerbose = Assert.IsType(AspireLauncher.TryCreate(argsVerbose)); - Assert.Equal(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); + var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); // Without verbose flag var argsNotVerbose = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcherNotVerbose = Assert.IsType(AspireLauncher.TryCreate(argsNotVerbose)); - Assert.Equal(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); + var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void QuietOption() { // With quiet flag var argsQuiet = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--quiet" }; - var launcherQuiet = Assert.IsType(AspireLauncher.TryCreate(argsQuiet)); - Assert.Equal(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); + var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); // Without quiet flag var argsNotQuiet = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcherNotQuiet = Assert.IsType(AspireLauncher.TryCreate(argsNotQuiet)); - Assert.Equal(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); + var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } - [Fact] + [TestMethod] public void ConflictingOptions() { // Cannot specify both --quiet and --verbose var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--quiet", "--verbose" }; var launcher = AspireLauncher.TryCreate(args); - Assert.Null(launcher); + Assert.IsNull(launcher); } - [Fact] + [TestMethod] public void AllOptionsSet() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "proj2.csproj", "--status-pipe", "status1", "--control-pipe", "control1", "--verbose" }; - var launcher = Assert.IsType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); - Assert.Equal("pipe1", launcher.ServerPipeName); - Assert.Equal(LogLevel.Debug, launcher.GlobalOptions.LogLevel); + Assert.AreEqual("pipe1", launcher.ServerPipeName); + Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); - Assert.Equal("status1", launcher.StatusPipeName); - Assert.Equal("control1", launcher.ControlPipeName); + Assert.AreEqual("status1", launcher.StatusPipeName); + Assert.AreEqual("control1", launcher.ControlPipeName); } -} +} \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs new file mode 100644 index 000000000000..67ae04e4e745 --- /dev/null +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.DotNet.Watch.UnitTests; + +/// +/// Small project-local helper that mirrors the shape of the xUnit-based AssertEx +/// in for the subset of +/// helpers used by the unit tests in this project. Keeping it local lets this project +/// stay independent from the xUnit-coupled shared test utilities. +/// +internal static class AssertEx +{ + public static void SequenceEqual(IEnumerable expected, IEnumerable actual, string? message = null) + { + Assert.IsNotNull(actual); + + if (!expected.SequenceEqual(actual)) + { + var expectedString = string.Join(Environment.NewLine, expected.Select(FormatItem)); + var actualString = string.Join(Environment.NewLine, actual.Select(FormatItem)); + Assert.Fail( + (message is null ? string.Empty : message + Environment.NewLine) + + $"Expected:{Environment.NewLine}{expectedString}{Environment.NewLine}" + + $"Actual:{Environment.NewLine}{actualString}"); + } + + static string FormatItem(T item) => item?.ToString() ?? ""; + } +} diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj index e65ce1a88a56..3aa8c6452145 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj @@ -1,19 +1,23 @@ - + + + true + $(SdkTargetFramework) - Exe Microsoft.DotNet.Watch.Aspire.UnitTests MicrosoftAspNetCore - + + + + - - - + \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs b/test/dotnet-watch.Tests/Aspire/AspireLauncherIntegrationTests.cs similarity index 98% rename from test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs rename to test/dotnet-watch.Tests/Aspire/AspireLauncherIntegrationTests.cs index ceab4de2f838..2255ed3aa721 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireLauncherTests.cs +++ b/test/dotnet-watch.Tests/Aspire/AspireLauncherIntegrationTests.cs @@ -6,7 +6,7 @@ namespace Microsoft.DotNet.Watch.UnitTests; -public class AspireLauncherTests(ITestOutputHelper logger) : WatchSdkTest(logger) +public class AspireLauncherIntegrationTests(ITestOutputHelper logger) : WatchSdkTest(logger) { private WatchableApp CreateHostApp() => new( diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs b/test/dotnet-watch.Tests/Aspire/PipeUtilities.cs similarity index 100% rename from test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Utilities/PipeUtilities.cs rename to test/dotnet-watch.Tests/Aspire/PipeUtilities.cs diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 72312ae26fb1..6e31001e68f0 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -18,6 +18,8 @@ + + From 52eade30e7344c1b2a22a9d97883d5b7a8d829d8 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 22:02:57 +0200 Subject: [PATCH 02/17] Bump MSTest.Sdk to 4.3.0-preview.26311.10 + apply skill review fixes - Bumps MSTest.Sdk to the latest internal preview to pick up the newest 4.3 assertion APIs (Assert.ContainsSingle, Assert.Contains for strings with the more natural (needle, haystack) signature, etc.). - Applies the assertion mapping flagged by the migrate-xunit-to-mstest skill in this repo (.github/skills/migrate-xunit-to-mstest, PR #54727): Assert.HasCount(1, x) -> Assert.ContainsSingle(x) (one occurrence in AspireResourceLauncherCliTests.cs) Verified: 58/58 tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- global.json | 2 +- .../AspireResourceLauncherCliTests.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 1dc269111cae..2b0f56d196c0 100644 --- a/global.json +++ b/global.json @@ -26,6 +26,6 @@ "Microsoft.Build.NoTargets": "3.7.134", "Microsoft.Build.Traversal": "4.1.82", "Microsoft.WixToolset.Sdk": "6.0.3-dotnet.4", - "MSTest.Sdk": "4.3.0-preview.26307.5" + "MSTest.Sdk": "4.3.0-preview.26311.10" } } diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs index ef5a3347ddde..5640079c6a7d 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs @@ -54,7 +54,7 @@ public void EnvironmentOption_SingleVariable() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY=value" }; var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); - Assert.HasCount(1, launcher.EnvironmentVariables); + Assert.ContainsSingle(launcher.EnvironmentVariables); Assert.AreEqual("value", launcher.EnvironmentVariables["KEY"]); } From 75a20cf293aa17907166b6ce634372af0d7b56dd Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 22:16:38 +0200 Subject: [PATCH 03/17] Address review: use Assert.IsExactInstanceOfType for xUnit IsType parity Per reviewer feedback: MSTest 4.1.0+ exposes Assert.IsExactInstanceOfType(value) which returns T and enforces exact-type semantics -- the proper equivalent of xUnit's Assert.IsType(x). Assert.IsInstanceOfType is the equivalent of xUnit's Assert.IsAssignableFrom (assignable, not exact), which would be a silent semantic regression for the IsType originals. All 39 occurrences across AspireHostLauncherCliTests.cs, AspireResourceLauncherCliTests.cs, AspireServerLauncherCliTests.cs, and AspireLauncherIntegrationTests.cs were originally Assert.IsType in xUnit (verified against main), so all 39 are flipped to Assert.IsExactInstanceOfType. Note: the migrate-xunit-to-mstest skill cheatsheet at .github/skills/migrate-xunit-to-mstest/references/mapping-cheatsheet.md recommends `Assert.IsInstanceOfType` plus an extra typeof-check for exact-type semantics; that guidance predates IsExactInstanceOfType being available. Follow-up upstream (dotnet/skills) suggested. Verified: 58/58 Aspire tests still pass; dotnet-watch.Tests builds clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AspireHostLauncherCliTests.cs | 24 +++++++------- .../AspireResourceLauncherCliTests.cs | 32 +++++++++---------- .../AspireServerLauncherCliTests.cs | 22 ++++++------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs index 23edc5e39889..40db79bbb9a4 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs @@ -30,7 +30,7 @@ public void RequiredEntryPointOption() public void ProjectAndSdkPaths() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "myproject.csproj" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); Assert.IsTrue(launcher.EntryPoint.IsProjectFile); Assert.AreEqual("myproject.csproj", launcher.EntryPoint.PhysicalPath); @@ -42,7 +42,7 @@ public void ProjectAndSdkPaths() public void FilePath() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "file.cs" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); Assert.IsFalse(launcher.EntryPoint.IsProjectFile); Assert.AreEqual("file.cs", launcher.EntryPoint.EntryPointFilePath); @@ -54,7 +54,7 @@ public void FilePath() public void ApplicationArguments() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--verbose", "a", "b" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); } @@ -64,12 +64,12 @@ public void VerboseOption() { // With verbose flag var argsVerbose = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--verbose" }; - var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + var launcherVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); // Without verbose flag var argsNotVerbose = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + var launcherNotVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } @@ -78,12 +78,12 @@ public void QuietOption() { // With quiet flag var argsQuiet = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--quiet" }; - var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + var launcherQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); // Without quiet flag var argsNotQuiet = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + var launcherNotQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } @@ -92,12 +92,12 @@ public void NoLaunchProfileOption() { // With no-launch-profile flag var argsNoProfile = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--no-launch-profile" }; - var launcherNoProfile = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); + var launcherNoProfile = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); Assert.IsFalse(launcherNoProfile.LaunchProfileName.HasValue); // Without no-launch-profile flag var argsDefault = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj" }; - var launcherDefault = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsDefault)); + var launcherDefault = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsDefault)); Assert.IsTrue(launcherDefault.LaunchProfileName.HasValue); Assert.IsNull(launcherDefault.LaunchProfileName.Value); } @@ -106,7 +106,7 @@ public void NoLaunchProfileOption() public void LaunchProfileOption() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--launch-profile", "MyProfile" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.IsTrue(launcher.LaunchProfileName.HasValue); Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } @@ -125,7 +125,7 @@ public void EntryPoint_MultipleValues() { // EntryPoint option should only accept one value; extra values become application arguments var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj1", "proj2" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("proj1", launcher.EntryPoint.ProjectOrEntryPointFilePath); AssertEx.SequenceEqual(["proj2"], launcher.ApplicationArguments); } @@ -134,7 +134,7 @@ public void EntryPoint_MultipleValues() public void AllOptionsSet() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "myapp.csproj", "--verbose", "--no-launch-profile", "arg1", "arg2", "arg3" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.IsTrue(launcher.EntryPoint.IsProjectFile); Assert.AreEqual("myapp.csproj", launcher.EntryPoint.PhysicalPath); diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs index 5640079c6a7d..06efb2e94417 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs @@ -31,7 +31,7 @@ public void RequiredEntryPointOption() public void MinimalRequiredOptions() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj.csproj" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("pipe1", launcher.ServerPipeName); Assert.AreEqual("proj.csproj", launcher.EntryPoint); Assert.IsEmpty(launcher.ApplicationArguments); @@ -45,7 +45,7 @@ public void MinimalRequiredOptions() public void ApplicationArguments() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "a", "b" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); } @@ -53,7 +53,7 @@ public void ApplicationArguments() public void EnvironmentOption_SingleVariable() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY=value" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.ContainsSingle(launcher.EnvironmentVariables); Assert.AreEqual("value", launcher.EnvironmentVariables["KEY"]); } @@ -62,7 +62,7 @@ public void EnvironmentOption_SingleVariable() public void EnvironmentOption_MultipleVariables() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY1=val1", "-e", "KEY2=val2" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual(2, launcher.EnvironmentVariables.Count); Assert.AreEqual("val1", launcher.EnvironmentVariables["KEY1"]); Assert.AreEqual("val2", launcher.EnvironmentVariables["KEY2"]); @@ -72,7 +72,7 @@ public void EnvironmentOption_MultipleVariables() public void EnvironmentOption_ValueWithEquals() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "CONN=Server=localhost;Port=5432" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("Server=localhost;Port=5432", launcher.EnvironmentVariables["CONN"]); } @@ -80,7 +80,7 @@ public void EnvironmentOption_ValueWithEquals() public void EnvironmentOption_EmptyValue() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY=" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("", launcher.EnvironmentVariables["KEY"]); } @@ -88,7 +88,7 @@ public void EnvironmentOption_EmptyValue() public void EnvironmentOption_NoEquals() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-e", "KEY" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("", launcher.EnvironmentVariables["KEY"]); } @@ -97,12 +97,12 @@ public void NoLaunchProfileOption() { // With no-launch-profile flag var argsNoProfile = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--no-launch-profile" }; - var launcherNoProfile = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); + var launcherNoProfile = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNoProfile)); Assert.IsFalse(launcherNoProfile.LaunchProfileName.HasValue); // Without no-launch-profile flag var argsDefault = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherDefault = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsDefault)); + var launcherDefault = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsDefault)); Assert.IsTrue(launcherDefault.LaunchProfileName.HasValue); Assert.IsNull(launcherDefault.LaunchProfileName.Value); } @@ -111,7 +111,7 @@ public void NoLaunchProfileOption() public void LaunchProfileOption() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--launch-profile", "MyProfile" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.IsTrue(launcher.LaunchProfileName.HasValue); Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } @@ -120,7 +120,7 @@ public void LaunchProfileOption() public void LaunchProfileOption_ShortForm() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "-lp", "MyProfile" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.IsTrue(launcher.LaunchProfileName.HasValue); Assert.AreEqual("MyProfile", launcher.LaunchProfileName.Value); } @@ -129,11 +129,11 @@ public void LaunchProfileOption_ShortForm() public void VerboseOption() { var argsVerbose = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--verbose" }; - var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + var launcherVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); var argsNotVerbose = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + var launcherNotVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } @@ -141,11 +141,11 @@ public void VerboseOption() public void QuietOption() { var argsQuiet = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "--quiet" }; - var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + var launcherQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); var argsNotQuiet = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj" }; - var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + var launcherNotQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } @@ -162,7 +162,7 @@ public void ConflictingOptions() public void AllOptionsSet() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "myapp.csproj", "-e", "K1=V1", "-e", "K2=V2", "--launch-profile", "Dev", "--verbose", "arg1", "arg2" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("pipe1", launcher.ServerPipeName); Assert.AreEqual("myapp.csproj", launcher.EntryPoint); diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs index 91dcfe91bdf7..b1bf32c9b82f 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs @@ -30,7 +30,7 @@ public void RequiredSdkOption() public void MinimalRequiredOptions() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("pipe1", launcher.ServerPipeName); Assert.AreEqual(LogLevel.Information, launcher.GlobalOptions.LogLevel); Assert.IsEmpty(launcher.ResourcePaths); @@ -42,7 +42,7 @@ public void MinimalRequiredOptions() public void ResourceOption_SingleValue() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj"], launcher.ResourcePaths); } @@ -50,7 +50,7 @@ public void ResourceOption_SingleValue() public void ResourceOption_MultipleValues() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "proj2.csproj", "file.cs" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj", "file.cs"], launcher.ResourcePaths); } @@ -58,7 +58,7 @@ public void ResourceOption_MultipleValues() public void ResourceOption_MultipleFlags() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "--resource", "proj2.csproj" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); } @@ -66,7 +66,7 @@ public void ResourceOption_MultipleFlags() public void StatusPipeOption() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--status-pipe", "status1" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("status1", launcher.StatusPipeName); } @@ -74,7 +74,7 @@ public void StatusPipeOption() public void ControlPipeOption() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--control-pipe", "control1" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("control1", launcher.ControlPipeName); } @@ -83,12 +83,12 @@ public void VerboseOption() { // With verbose flag var argsVerbose = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--verbose" }; - var launcherVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); + var launcherVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsVerbose)); Assert.AreEqual(LogLevel.Debug, launcherVerbose.GlobalOptions.LogLevel); // Without verbose flag var argsNotVerbose = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcherNotVerbose = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); + var launcherNotVerbose = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotVerbose)); Assert.AreEqual(LogLevel.Information, launcherNotVerbose.GlobalOptions.LogLevel); } @@ -97,12 +97,12 @@ public void QuietOption() { // With quiet flag var argsQuiet = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--quiet" }; - var launcherQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); + var launcherQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsQuiet)); Assert.AreEqual(LogLevel.Warning, launcherQuiet.GlobalOptions.LogLevel); // Without quiet flag var argsNotQuiet = new[] { "server", "--server", "pipe1", "--sdk", "sdk" }; - var launcherNotQuiet = Assert.IsInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); + var launcherNotQuiet = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(argsNotQuiet)); Assert.AreEqual(LogLevel.Information, launcherNotQuiet.GlobalOptions.LogLevel); } @@ -119,7 +119,7 @@ public void ConflictingOptions() public void AllOptionsSet() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "proj2.csproj", "--status-pipe", "status1", "--control-pipe", "control1", "--verbose" }; - var launcher = Assert.IsInstanceOfType(AspireLauncher.TryCreate(args)); + var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("pipe1", launcher.ServerPipeName); Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); From 27da8c4aa0d5b7ff5aa66d59d52f5631d6e7552d Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:32:03 +0200 Subject: [PATCH 04/17] Remove redundant Using of Microsoft.VisualStudio.TestTools.UnitTesting MSTest.Sdk already adds this as an implicit global using. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj index 3aa8c6452145..a7be15d830b3 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj @@ -13,7 +13,6 @@ - From 7a551ebccb67adbfb3ad1b17db1e8fce28e6961d Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 23:03:36 +0200 Subject: [PATCH 05/17] Replace AssertEx.SequenceEqual with Assert.AreSequenceEqual MSTest 4.3+ provides Assert.AreSequenceEqual for element-wise IEnumerable compare with a nice diff message, so the project-local AssertEx helper is no longer needed. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AspireHostLauncherCliTests.cs | 6 ++-- .../AspireHostLauncherTests.cs | 16 +++++----- .../AspireResourceLauncherCliTests.cs | 6 ++-- .../AspireServerLauncherCliTests.cs | 8 ++--- .../AssertEx.cs | 30 ------------------- 5 files changed, 18 insertions(+), 48 deletions(-) delete mode 100644 test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs index 40db79bbb9a4..c5b3e40cf567 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherCliTests.cs @@ -55,7 +55,7 @@ public void ApplicationArguments() { var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj", "--verbose", "a", "b" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); - AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); + Assert.AreSequenceEqual(["a", "b"], launcher.ApplicationArguments); Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); } @@ -127,7 +127,7 @@ public void EntryPoint_MultipleValues() var args = new[] { "host", "--sdk", "sdk", "--entrypoint", "proj1", "proj2" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); Assert.AreEqual("proj1", launcher.EntryPoint.ProjectOrEntryPointFilePath); - AssertEx.SequenceEqual(["proj2"], launcher.ApplicationArguments); + Assert.AreSequenceEqual(["proj2"], launcher.ApplicationArguments); } [TestMethod] @@ -141,6 +141,6 @@ public void AllOptionsSet() Assert.AreEqual("sdk", launcher.EnvironmentOptions.SdkDirectory); Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); Assert.IsFalse(launcher.LaunchProfileName.HasValue); - AssertEx.SequenceEqual(["arg1", "arg2", "arg3"], launcher.ApplicationArguments); + Assert.AreSequenceEqual(["arg1", "arg2", "arg3"], launcher.ApplicationArguments); } } \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs index 33cf07e5aa9e..23ca92043e70 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireHostLauncherTests.cs @@ -49,7 +49,7 @@ public void GetProjectOptions_ProjectFile_UsesProjectFlag() AssertCommonProperties(options, launcher); Assert.IsFalse(options.LaunchProfileName.HasValue); - AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); } [TestMethod] @@ -61,7 +61,7 @@ public void GetProjectOptions_EntryPointFile_UsesFileFlag() AssertCommonProperties(options, launcher); Assert.IsFalse(options.LaunchProfileName.HasValue); - AssertEx.SequenceEqual(["--file", "Program.cs", "--no-launch-profile"], options.CommandArguments); + Assert.AreSequenceEqual(["--file", "Program.cs", "--no-launch-profile"], options.CommandArguments); } [TestMethod] @@ -74,7 +74,7 @@ public void GetProjectOptions_WithLaunchProfile_AddsLaunchProfileArguments() AssertCommonProperties(options, launcher); Assert.IsTrue(options.LaunchProfileName.HasValue); Assert.AreEqual("MyProfile", options.LaunchProfileName.Value); - AssertEx.SequenceEqual(["--project", "myapp.csproj", "--launch-profile", "MyProfile"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj", "--launch-profile", "MyProfile"], options.CommandArguments); } [TestMethod] @@ -86,7 +86,7 @@ public void GetProjectOptions_NoLaunchProfile_AddsNoLaunchProfileFlag() AssertCommonProperties(options, launcher); Assert.IsFalse(options.LaunchProfileName.HasValue); - AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj", "--no-launch-profile"], options.CommandArguments); } [TestMethod] @@ -100,7 +100,7 @@ public void GetProjectOptions_NullLaunchProfile_UsesDefault() AssertCommonProperties(options, launcher); Assert.IsTrue(options.LaunchProfileName.HasValue); Assert.IsNull(options.LaunchProfileName.Value); - AssertEx.SequenceEqual(["--project", "myapp.csproj"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj"], options.CommandArguments); } [TestMethod] @@ -113,7 +113,7 @@ public void GetProjectOptions_WithApplicationArguments_AppendsArguments() AssertCommonProperties(options, launcher); Assert.IsTrue(options.LaunchProfileName.HasValue); Assert.AreEqual("Profile", options.LaunchProfileName.Value); - AssertEx.SequenceEqual(["--project", "myapp.csproj", "--launch-profile", "Profile", "arg1", "arg2"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj", "--launch-profile", "Profile", "arg1", "arg2"], options.CommandArguments); } [TestMethod] @@ -137,7 +137,7 @@ public void GetProjectOptions_EntryPointFile_WithLaunchProfileAndArguments() AssertCommonProperties(options, launcher); Assert.IsTrue(options.LaunchProfileName.HasValue); Assert.AreEqual("Dev", options.LaunchProfileName.Value); - AssertEx.SequenceEqual(["--file", "Program.cs", "--launch-profile", "Dev", "--port", "8080"], options.CommandArguments); + Assert.AreSequenceEqual(["--file", "Program.cs", "--launch-profile", "Dev", "--port", "8080"], options.CommandArguments); } [TestMethod] @@ -149,6 +149,6 @@ public void GetProjectOptions_NoLaunchProfile_WithApplicationArguments() AssertCommonProperties(options, launcher); Assert.IsFalse(options.LaunchProfileName.HasValue); - AssertEx.SequenceEqual(["--project", "myapp.csproj", "--no-launch-profile", "--urls", "http://localhost:5000"], options.CommandArguments); + Assert.AreSequenceEqual(["--project", "myapp.csproj", "--no-launch-profile", "--urls", "http://localhost:5000"], options.CommandArguments); } } \ No newline at end of file diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs index 06efb2e94417..a4cb31a69526 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireResourceLauncherCliTests.cs @@ -46,7 +46,7 @@ public void ApplicationArguments() { var args = new[] { "resource", "--server", "pipe1", "--entrypoint", "proj", "a", "b" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); - AssertEx.SequenceEqual(["a", "b"], launcher.ApplicationArguments); + Assert.AreSequenceEqual(["a", "b"], launcher.ApplicationArguments); } [TestMethod] @@ -169,7 +169,7 @@ public void AllOptionsSet() Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); Assert.IsTrue(launcher.LaunchProfileName.HasValue); Assert.AreEqual("Dev", launcher.LaunchProfileName.Value); - AssertEx.SequenceEqual(["arg1", "arg2"], launcher.ApplicationArguments); + Assert.AreSequenceEqual(["arg1", "arg2"], launcher.ApplicationArguments); Assert.AreEqual(2, launcher.EnvironmentVariables.Count); Assert.AreEqual("V1", launcher.EnvironmentVariables["K1"]); Assert.AreEqual("V2", launcher.EnvironmentVariables["K2"]); @@ -267,7 +267,7 @@ public void EnvironmentOption_Errors(string token) var command = new AspireResourceCommandDefinition(); var result = command.Parse(["--server", "S", "--entrypoint", "E", "-e", token]); - AssertEx.SequenceEqual( + Assert.AreSequenceEqual( [ $"Incorrectly formatted environment variables '{token}'" ], result.Errors.Select(e => e.Message)); diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs index b1bf32c9b82f..127b20e9ca0f 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AspireServerLauncherCliTests.cs @@ -43,7 +43,7 @@ public void ResourceOption_SingleValue() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); - AssertEx.SequenceEqual(["proj1.csproj"], launcher.ResourcePaths); + Assert.AreSequenceEqual(["proj1.csproj"], launcher.ResourcePaths); } [TestMethod] @@ -51,7 +51,7 @@ public void ResourceOption_MultipleValues() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "proj2.csproj", "file.cs" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); - AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj", "file.cs"], launcher.ResourcePaths); + Assert.AreSequenceEqual(["proj1.csproj", "proj2.csproj", "file.cs"], launcher.ResourcePaths); } [TestMethod] @@ -59,7 +59,7 @@ public void ResourceOption_MultipleFlags() { var args = new[] { "server", "--server", "pipe1", "--sdk", "sdk", "--resource", "proj1.csproj", "--resource", "proj2.csproj" }; var launcher = Assert.IsExactInstanceOfType(AspireLauncher.TryCreate(args)); - AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); + Assert.AreSequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); } [TestMethod] @@ -123,7 +123,7 @@ public void AllOptionsSet() Assert.AreEqual("pipe1", launcher.ServerPipeName); Assert.AreEqual(LogLevel.Debug, launcher.GlobalOptions.LogLevel); - AssertEx.SequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); + Assert.AreSequenceEqual(["proj1.csproj", "proj2.csproj"], launcher.ResourcePaths); Assert.AreEqual("status1", launcher.StatusPipeName); Assert.AreEqual("control1", launcher.ControlPipeName); } diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs deleted file mode 100644 index 67ae04e4e745..000000000000 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/AssertEx.cs +++ /dev/null @@ -1,30 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.DotNet.Watch.UnitTests; - -/// -/// Small project-local helper that mirrors the shape of the xUnit-based AssertEx -/// in for the subset of -/// helpers used by the unit tests in this project. Keeping it local lets this project -/// stay independent from the xUnit-coupled shared test utilities. -/// -internal static class AssertEx -{ - public static void SequenceEqual(IEnumerable expected, IEnumerable actual, string? message = null) - { - Assert.IsNotNull(actual); - - if (!expected.SequenceEqual(actual)) - { - var expectedString = string.Join(Environment.NewLine, expected.Select(FormatItem)); - var actualString = string.Join(Environment.NewLine, actual.Select(FormatItem)); - Assert.Fail( - (message is null ? string.Empty : message + Environment.NewLine) + - $"Expected:{Environment.NewLine}{expectedString}{Environment.NewLine}" + - $"Actual:{Environment.NewLine}{actualString}"); - } - - static string FormatItem(T item) => item?.ToString() ?? ""; - } -} From cb5098995948cabacda6d93f175533cddedfde04 Mon Sep 17 00:00:00 2001 From: Evangelink <32149626+Evangelink@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:19:48 +0200 Subject: [PATCH 06/17] Move FluentAssertions Using + AwesomeAssertions PackageReference to test/Directory.Build.targets Per @Evangelink: keep per-csproj boilerplate minimal. FluentAssertions is now a global using for any test project (gated on IsTestProject OR UsingMSTestSdk), and AwesomeAssertions is added as a PackageReference for MSTest.Sdk projects. xUnit projects continue to pick it up transitively via Microsoft.NET.TestFramework. The Microsoft.NET.TestFramework.* and Xunit usings remain gated on the xUnit branch (UsingMSTestSdk != true) because MSTest projects in this repo do not reference Microsoft.NET.TestFramework; making those usings global would fail with CS0246. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj | 5 ----- 1 file changed, 5 deletions(-) diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj index a7be15d830b3..2ae0f2fe0244 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj @@ -10,11 +10,6 @@ MicrosoftAspNetCore - - - - - From ead0658e82a0946b58fa2decf327f229603696ee Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 16:07:51 +0200 Subject: [PATCH 07/17] Restore Watch.Aspire ProjectReference in dotnet-watch.Tests.csproj The recent `Helix dispatcher: gate --report-trx on TrxReport extension being loaded` commit accidentally removed the ProjectReference to Microsoft.DotNet.HotReload.Watch.Aspire from dotnet-watch.Tests.csproj (introduced in the `Migrate Microsoft.DotNet.HotReload.Watch.Aspire.Tests to MSTest.Sdk on MTP` commit to allow the moved AspireLauncherIntegrationTests and PipeUtilities to compile). Without that reference, the build fails with: error CS0246: The type or namespace name 'WatchStatusEvent' could not be found (are you missing a using directive or an assembly reference?) [test/dotnet-watch.Tests/Aspire/PipeUtilities.cs] Re-add the ProjectReference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/dotnet-watch.Tests/dotnet-watch.Tests.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 6e31001e68f0..3b45b8eb3031 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -1,4 +1,4 @@ - + Exe $(SdkTargetFramework) From fe718beb6b20d0e227ad7410054bd7a949f05b4f Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 18:29:09 +0200 Subject: [PATCH 08/17] Add shared Microsoft.DotNet.Test.MSTest.Utilities project Introduces an MSTest-flavored counterpart to the xUnit-based Microsoft.DotNet.HotReload.Test.Utilities so that test helpers that need MSTest's TestContext (e.g. TestLogger, TestLoggerFactory) can be shared across MSTest.Sdk test projects instead of being copy-pasted per project. This commit also migrates the inline TestLogger from Microsoft.DotNet.HotReload.Client.Tests to consume the shared project, which serves as the first reference consumer. Subsequent migration PRs (DeltaApplier.Tests, Containers.UnitTests) will adopt the same project reference instead of adding their own TestLogger/TestLoggerFactory copies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk.slnx | 1 + src/Dotnet.Watch/dotnet-watch.slnf | 1 + ...osoft.DotNet.HotReload.Client.Tests.csproj | 4 + .../StaticWebAssetsManifestTests.cs | 2 +- .../InMemoryLoggerProvider.cs | 39 ++++++++ ...rosoft.DotNet.Test.MSTest.Utilities.csproj | 42 ++++++++ .../TestLogger.cs | 14 ++- .../TestLoggerFactory.cs | 97 +++++++++++++++++++ 8 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj rename test/{Microsoft.DotNet.HotReload.Client.Tests/Utilities => Microsoft.DotNet.Test.MSTest.Utilities}/TestLogger.cs (70%) create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs diff --git a/sdk.slnx b/sdk.slnx index df9722223799..07d879bec7b9 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -367,6 +367,7 @@ + diff --git a/src/Dotnet.Watch/dotnet-watch.slnf b/src/Dotnet.Watch/dotnet-watch.slnf index 48e156d1458e..b5600195a12e 100644 --- a/src/Dotnet.Watch/dotnet-watch.slnf +++ b/src/Dotnet.Watch/dotnet-watch.slnf @@ -33,6 +33,7 @@ "test\\dotnet-watch-test-browser\\dotnet-watch-test-browser.csproj", "test\\Microsoft.DotNet.HotReload.Test.Utilities\\Microsoft.DotNet.HotReload.Test.Utilities.csproj", "test\\Microsoft.DotNet.HotReload.Watch.Aspire.Tests\\Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj", + "test\\Microsoft.DotNet.Test.MSTest.Utilities\\Microsoft.DotNet.Test.MSTest.Utilities.csproj", "test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj" ] diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj b/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj index 7500dad99340..ef519bdfcde4 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs b/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs index 87d3eadc3176..e82b59ba8884 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs +++ b/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.Watch.UnitTests; +using Microsoft.DotNet.Test.MSTest.Utilities; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.DotNet.HotReload.UnitTests; diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs new file mode 100644 index 000000000000..3715dac3febe --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Test.MSTest.Utilities; + +/// +/// An that appends every log entry to a caller-supplied list. +/// Useful for tests that need to assert on the exact sequence of log entries produced by a +/// component under test (without dragging in MSTest's TestContext sink). +/// +public sealed class InMemoryLoggerProvider(List<(LogLevel, string)> messagesCollection) : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) => new InMemoryLogger(messagesCollection); + + public void Dispose() + { + } + + private sealed class InMemoryLogger(List<(LogLevel, string)> messagesCollection) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + => messagesCollection.Add((logLevel, formatter(state, exception))); + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } +} diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj b/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj new file mode 100644 index 000000000000..73adf7653621 --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj @@ -0,0 +1,42 @@ + + + + + + $(SdkTargetFramework);$(NetFrameworkToolCurrent) + Library + Microsoft.DotNet.Test.MSTest.Utilities + MicrosoftAspNetCore + false + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs similarity index 70% rename from test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs rename to test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs index 6bb15080c57d..87c8c1248f62 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs @@ -1,12 +1,18 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Microsoft.DotNet.Watch.UnitTests; +namespace Microsoft.DotNet.Test.MSTest.Utilities; -internal class TestLogger(TestContext? output = null) : ILogger +/// +/// An that captures messages in memory and optionally echoes them to an +/// MSTest . Designed to be shared across MSTest.Sdk test projects so +/// the same pattern doesn't have to be duplicated per project. +/// +public class TestLogger(TestContext? testContext = null) : ILogger { public readonly object Guard = new(); private readonly List _messages = []; @@ -31,7 +37,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except HasWarning |= logLevel is LogLevel.Warning; _messages.Add(message); - output?.WriteLine(message); + testContext?.WriteLine(message); } } diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs new file mode 100644 index 000000000000..15485a93f137 --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.DotNet.Test.MSTest.Utilities; + +/// +/// An that writes log messages to an MSTest +/// (when provided) and a simple console sink. Useful for tests that +/// need a real (e.g. for components that take one in their ctor). +/// +public sealed class TestLoggerFactory : ILoggerFactory +{ + private readonly List _loggerProviders = new(); + private readonly List _factories = new(); + + public TestLoggerFactory(TestContext? testContext = null) + { + if (testContext is not null) + { + _loggerProviders.Add(new TestContextLoggerProvider(testContext)); + } + } + + public void Dispose() + { + while (_factories.Count > 0) + { + ILoggerFactory factory = _factories[0]; + _factories.RemoveAt(0); + factory.Dispose(); + } + } + + public ILogger CreateLogger(string categoryName) + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + + foreach (ILoggerProvider loggerProvider in _loggerProviders) + { + builder.AddProvider(loggerProvider); + } + + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "[yyyy-MM-dd HH:mm:ss.fff] "; + options.IncludeScopes = true; + }); + }); + + _factories.Add(loggerFactory); + return loggerFactory.CreateLogger(categoryName); + } + + public ILogger CreateLogger() => CreateLogger("Test Host"); + + public void AddProvider(ILoggerProvider provider) => _loggerProviders.Add(provider); + + private sealed class TestContextLoggerProvider(TestContext testContext) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) => new TestContextLogger(testContext, categoryName); + + public void Dispose() + { + } + } + + private sealed class TestContextLogger(TestContext testContext, string categoryName) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + testContext.WriteLine($"{logLevel}: {categoryName}: {formatter(state, exception)}"); + if (exception is not null) + { + testContext.WriteLine(exception.ToString()); + } + } + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } +} From 1a4c1917532834929f2d4830d950186afaecb633 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 21:26:49 +0200 Subject: [PATCH 09/17] Migrate containerize.UnitTests to MSTest.Sdk on MTP Follow-up to the pathfinder migration. containerize.UnitTests only uses xUnit Fact/Theory/InlineData/Assert APIs - it does not use any types from Microsoft.NET.TestFramework. So the project can drop its TestFramework ProjectReference and switch to MSTest.Sdk in one self-contained PR. Changes: * test/containerize.UnitTests/containerize.UnitTests.csproj: - Use Sdk="MSTest.Sdk", set UseMSTestSdk=true (opts the project out of the xUnit defaults in test/Directory.Build.targets - gate introduced in PR #54722). - Drop the ProjectReference to Microsoft.NET.TestFramework (unused). - Drop OutputType=Exe (MSTest.Sdk handles it). - Add Microsoft.VisualStudio.TestTools.UnitTesting global using. * test/containerize.UnitTests/ParserTests.cs: xUnit -> MSTest mechanical conversion: [Fact]/[Theory] -> [TestMethod], [InlineData] -> [DataRow], add [TestClass], Assert.NotNull/Equal/Empty/Single -> IsNotNull/AreEqual/IsEmpty/HasCount(1, ...). Verification: 9/9 tests pass on MTP in ~400 ms. Stacks on top of PR #54722 (pathfinder). Merge after that one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/ParserTests.cs | 81 ++++++++++--------- .../containerize.UnitTests.csproj | 11 ++- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/test/containerize.UnitTests/ParserTests.cs b/test/containerize.UnitTests/ParserTests.cs index 4240089eca13..ab6edd0d1c84 100644 --- a/test/containerize.UnitTests/ParserTests.cs +++ b/test/containerize.UnitTests/ParserTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; @@ -6,9 +6,10 @@ namespace containerize.UnitTests; +[TestClass] public class ParserTests { - [Fact] + [TestMethod] public void CanParseLabels() { ContainerizeCommand command = new(); @@ -41,17 +42,17 @@ public void CanParseLabels() Dictionary? labels = parseResult.GetValue(command.LabelsOption); - Assert.NotNull(labels); - Assert.Equal(6, labels.Count); - Assert.Empty(labels["NoValue"]); - Assert.Equal("Val2", labels["Valid2"]); - Assert.Equal("Val 3", labels["Valid3"]); - Assert.Equal("\"Val4\"", labels["Valid4"]); - Assert.Equal("\"Un1", labels["Unbalanced1"]); - Assert.Equal("Un2\"", labels["Unbalanced2"]); + Assert.IsNotNull(labels); + Assert.AreEqual(6, labels.Count); + Assert.IsEmpty(labels["NoValue"]); + Assert.AreEqual("Val2", labels["Valid2"]); + Assert.AreEqual("Val 3", labels["Valid3"]); + Assert.AreEqual("\"Val4\"", labels["Valid4"]); + Assert.AreEqual("\"Un1", labels["Unbalanced1"]); + Assert.AreEqual("Un2\"", labels["Unbalanced2"]); } - [Fact] + [TestMethod] public void CanParseLabels2() { ContainerizeCommand command = new(); @@ -79,15 +80,15 @@ public void CanParseLabels2() Dictionary? labels = parseResult.GetValue(command.LabelsOption); - Assert.NotNull(labels); - Assert.Equal(2, labels.Count); - Assert.Empty(labels["NoValue"]); - Assert.Equal("Val2", labels["Valid2"]); + Assert.IsNotNull(labels); + Assert.AreEqual(2, labels.Count); + Assert.IsEmpty(labels["NoValue"]); + Assert.AreEqual("Val2", labels["Valid2"]); } - [Theory] - [InlineData("not-a-label")] - [InlineData("not", "a", "label")] + [TestMethod] + [DataRow("not-a-label")] + [DataRow("not", "a", "label")] public void CanHandleInvalidLabels(params string[] labelStr) { ContainerizeCommand command = new(); @@ -114,12 +115,12 @@ public void CanHandleInvalidLabels(params string[] labelStr) } ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Single(parseResult.Errors); + Assert.HasCount(1, parseResult.Errors); - Assert.Equal($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); + Assert.AreEqual($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); } - [Fact] + [TestMethod] public void CanParseEnvironmentVariables() { ContainerizeCommand command = new(); @@ -149,21 +150,21 @@ public void CanParseEnvironmentVariables() ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Empty(parseResult.Errors); + Assert.IsEmpty(parseResult.Errors); Dictionary? envVars = parseResult.GetValue(command.EnvVarsOption); - Assert.NotNull(envVars); - Assert.Equal(6, envVars.Count); - Assert.Empty(envVars["NoValue"]); - Assert.Equal("Val2", envVars["Valid2"]); - Assert.Equal("Val 3", envVars["Valid3"]); - Assert.Equal("\"Val4\"", envVars["Valid4"]); - Assert.Equal("\"Un1", envVars["Unbalanced1"]); - Assert.Equal("Un2\"", envVars["Unbalanced2"]); + Assert.IsNotNull(envVars); + Assert.AreEqual(6, envVars.Count); + Assert.IsEmpty(envVars["NoValue"]); + Assert.AreEqual("Val2", envVars["Valid2"]); + Assert.AreEqual("Val 3", envVars["Valid3"]); + Assert.AreEqual("\"Val4\"", envVars["Valid4"]); + Assert.AreEqual("\"Un1", envVars["Unbalanced1"]); + Assert.AreEqual("Un2\"", envVars["Unbalanced2"]); } - [Fact] + [TestMethod] public void CanParsePorts() { ContainerizeCommand command = new(); @@ -191,22 +192,22 @@ public void CanParsePorts() ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Empty(parseResult.Errors); + Assert.IsEmpty(parseResult.Errors); Port[]? ports = parseResult.GetValue(command.PortsOption); - Assert.NotNull(ports); - Assert.Equal(4, ports.Length); + Assert.IsNotNull(ports); + Assert.AreEqual(4, ports.Length); Assert.Contains(new Port(1500, PortType.tcp), ports); Assert.Contains(new Port(1501, PortType.udp), ports); Assert.Contains(new Port(1501, PortType.tcp), ports); Assert.Contains(new Port(1502, PortType.tcp), ports); } - [Theory] - [InlineData("1501/smth", "(InvalidPortType)")] - [InlineData("1501\\tcp", "(InvalidPortNumber)")] - [InlineData("not-a-number", "(InvalidPortNumber)")] + [TestMethod] + [DataRow("1501/smth", "(InvalidPortType)")] + [DataRow("1501\\tcp", "(InvalidPortNumber)")] + [DataRow("not-a-number", "(InvalidPortNumber)")] public void CanHandleInvalidPorts(string portStr, string reason) { string errorMessage = $"Incorrectly formatted ports:{Environment.NewLine}\t{portStr}:\t{reason}{Environment.NewLine}"; @@ -232,9 +233,9 @@ public void CanHandleInvalidPorts(string portStr, string reason) baseArgs.Add(portStr); ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Single(parseResult.Errors); + Assert.HasCount(1, parseResult.Errors); - Assert.Equal(errorMessage, parseResult.Errors[0].Message); + Assert.AreEqual(errorMessage, parseResult.Errors[0].Message); } } diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 215c72557027..b5f2fbc8d3e6 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -1,4 +1,4 @@ - + $(SdkTargetFramework) @@ -6,12 +6,15 @@ false true MicrosoftShared - Exe + true + + + + - - + \ No newline at end of file From bf18d59df5cdc96ddec0d9e6df2044ae8c43d038 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 22:06:26 +0200 Subject: [PATCH 10/17] Apply skill review fixes: Assert.HasCount(1,x) -> Assert.ContainsSingle(x) Applies the assertion mapping flagged by the migrate-xunit-to-mstest skill (.github/skills/migrate-xunit-to-mstest, PR #54727). Two occurrences in ParserTests.cs converted from `Assert.HasCount(1, parseResult.Errors)` to the more idiomatic `Assert.ContainsSingle(parseResult.Errors)`, which matches the xUnit original (`Assert.Single`) more directly. Verified: 9/9 tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/ParserTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/containerize.UnitTests/ParserTests.cs b/test/containerize.UnitTests/ParserTests.cs index ab6edd0d1c84..d9273253b6ce 100644 --- a/test/containerize.UnitTests/ParserTests.cs +++ b/test/containerize.UnitTests/ParserTests.cs @@ -115,7 +115,7 @@ public void CanHandleInvalidLabels(params string[] labelStr) } ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.HasCount(1, parseResult.Errors); + Assert.ContainsSingle(parseResult.Errors); Assert.AreEqual($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); } @@ -233,7 +233,7 @@ public void CanHandleInvalidPorts(string portStr, string reason) baseArgs.Add(portStr); ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.HasCount(1, parseResult.Errors); + Assert.ContainsSingle(parseResult.Errors); Assert.AreEqual(errorMessage, parseResult.Errors[0].Message); } From 72bc00559c5f65eb3aaf9f31ea656b6c915ae9ea Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:33:13 +0200 Subject: [PATCH 11/17] Remove redundant Using of Microsoft.VisualStudio.TestTools.UnitTesting MSTest.Sdk already adds this as an implicit global using. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/containerize.UnitTests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index b5f2fbc8d3e6..76241b728e9f 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -9,10 +9,6 @@ true - - - - From 439fef53223027e42c83172af2c6c5a592678713 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:53:47 +0200 Subject: [PATCH 12/17] Remove redundant false (MSTest.Sdk default) MSTest.Sdk's Runner/Common.targets already sets `false` for every MSTest.Sdk project, so the per-project declaration is redundant. --- test/containerize.UnitTests/containerize.UnitTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 76241b728e9f..44bc1d2dba79 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -3,7 +3,6 @@ $(SdkTargetFramework) enable - false true MicrosoftShared true From 7750f28b80c5d4043b0e01446b6003d1c057f0fe Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 21:28:40 +0200 Subject: [PATCH 13/17] Migrate Microsoft.DotNet.ApiCompat.Tests to MSTest.Sdk on MTP Same pattern as containerize.UnitTests (#54723). This project's single test file only uses xUnit Fact/Assert.Equal - no Microsoft.NET.TestFramework types - so the TestFramework ProjectReference can be dropped and the project switched to MSTest.Sdk in one self-contained change. Changes: * test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj: - Use Sdk="MSTest.Sdk", set UseMSTestSdk=true. - Drop ProjectReference to Microsoft.NET.TestFramework (unused). - Drop OutputType=Exe (MSTest.Sdk handles it). - Add Microsoft.VisualStudio.TestTools.UnitTesting global using. * test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs: [Fact] -> [TestMethod], add [TestClass], Assert.Equal -> Assert.AreEqual. Verification: 5/5 tests pass on MTP in ~250 ms. Stacks on top of #54723 which stacks on #54722. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 11 +++++---- .../RegexStringTransformerTests.cs | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 898749c592b6..34ce1e5f2506 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -1,10 +1,14 @@ - + $(SdkTargetFramework) - Exe + true + + + + @@ -12,10 +16,9 @@ - - + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs b/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs index bed3d912cfbd..683d3a626cbd 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs +++ b/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs @@ -1,13 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.ApiCompat; namespace Microsoft.DotNet.ApiCompatibility.Tests { + [TestClass] public class RegexStringTransformerTests { - [Fact] + [TestMethod] public void Transform_CaptureGroupPatternDoesNotMatchInput_ReturnsInput() { const string CaptureGroupPattern = "(abc)def"; @@ -16,10 +17,10 @@ public void Transform_CaptureGroupPatternDoesNotMatchInput_ReturnsInput() string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal(Input, output); + Assert.AreEqual(Input, output); } - [Fact] + [TestMethod] public void Transform_ReplacementPatternWithoutCaptureGroups_ReturnsReplacementPattern() { const string CaptureGroupPattern = "(abc)d*"; @@ -28,10 +29,10 @@ public void Transform_ReplacementPatternWithoutCaptureGroups_ReturnsReplacementP string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal(ReplacementPattern, output); + Assert.AreEqual(ReplacementPattern, output); } - [Fact] + [TestMethod] public void Transform_ReplacementPatternWithTooManyReplacementMarkers_ReturnOutputWithoutTransformedReplacementMarkers() { const string CaptureGroupPattern = "(abc)(def)ghi"; @@ -40,10 +41,10 @@ public void Transform_ReplacementPatternWithTooManyReplacementMarkers_ReturnOutp string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal("1:abc, 2:def, 3:$3", output); + Assert.AreEqual("1:abc, 2:def, 3:$3", output); } - [Fact] + [TestMethod] public void Transform_SameNumberOfGroupsAndMarkers_ReturnsExpected() { const string CaptureGroupPattern = @".+\\(.+)\\(.+)"; @@ -52,10 +53,10 @@ public void Transform_SameNumberOfGroupsAndMarkers_ReturnsExpected() string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal("lib/net7.0-android/System.Linq.dll", output); + Assert.AreEqual("lib/net7.0-android/System.Linq.dll", output); } - [Fact] + [TestMethod] public void Transform_MultiplePatterns_ReturnsExpected() { var patterns = new (string, string)[] @@ -69,7 +70,7 @@ public void Transform_MultiplePatterns_ReturnsExpected() string output = new RegexStringTransformer(patterns).Transform(Input); - Assert.Equal("runtimes/android/lib/net7.0/System.Linq.dll", output); + Assert.AreEqual("runtimes/android/lib/net7.0/System.Linq.dll", output); } } } From 4aa30242b57bfb62cf4d335db42d14eabde9b33b Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:34:20 +0200 Subject: [PATCH 14/17] Remove redundant Using of Microsoft.VisualStudio.TestTools.UnitTesting MSTest.Sdk already adds this as an implicit global using. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 34ce1e5f2506..52400ac52e9f 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -5,10 +5,6 @@ true - - - - From 65cf48652c9d0a383a589ce544deaa26ee5d7517 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:34:04 +0200 Subject: [PATCH 15/17] Use built-in $(UsingMSTestSdk) property instead of custom $(UseMSTestSdk) MSTest.Sdk already sets $(UsingMSTestSdk)=true in its Sdk.props before Directory.Build.props is evaluated, so the custom true opt-in property is redundant. This change: - Removes true from MSTest.Sdk csproj(s). - Renames $(UseMSTestSdk) -> $(UsingMSTestSdk) in test/Directory.Build.targets (the xUnit-defaults gating condition) and in xunit-runner/{XUnitPublish,XUnitRunner}.targets (Helix MTP dispatcher detection). --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 1 - .../Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj | 3 --- test/containerize.UnitTests/containerize.UnitTests.csproj | 1 - 3 files changed, 5 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 52400ac52e9f..3802f96de8ab 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -2,7 +2,6 @@ $(SdkTargetFramework) - true diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj index 2ae0f2fe0244..0aec8ad4a1ee 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj @@ -1,9 +1,6 @@ - - true $(SdkTargetFramework) Microsoft.DotNet.Watch.Aspire.UnitTests diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 44bc1d2dba79..3036a17b4558 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -5,7 +5,6 @@ enable true MicrosoftShared - true From c58302d12567bcf4f52ecd15a83a530b913eef6a Mon Sep 17 00:00:00 2001 From: Evangelink <32149626+Evangelink@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:19:59 +0200 Subject: [PATCH 16/17] Helix dispatcher: gate --report-trx on TrxReport extension being loaded --report-trx is an MTP CLI argument provided only when the Microsoft.Testing.Extensions.TrxReport extension is loaded on the test host. MSTest.Sdk's Default/AllMicrosoft profiles enable it by default, but other MTP runners (e.g. xUnit v3 MTP) do not bundle the extension, so passing --report-trx to those hosts fails with 'unknown argument'. XUnitPublish.targets now exposes the GetTrxReportEnabled target which returns the value of EnableMicrosoftTestingExtensionsTrxReport. XUnitRunner.targets calls that target and propagates the value as the EnableTrxReport metadata of SDKCustomXUnitProject items. SDKCustomCreateXUnitWorkItemsWithTestExclusion reads that metadata and only appends --report-trx to the MTP command line when it is true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/dotnet-watch.Tests/dotnet-watch.Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 3b45b8eb3031..24484402b0e1 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -18,8 +18,6 @@ - - From 6f740e7fe15c39ecbff6308d554c649dfda8366e Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 16:07:33 +0200 Subject: [PATCH 17/17] Restore Watch.Aspire ProjectReference in dotnet-watch.Tests.csproj The recent `Helix dispatcher: gate --report-trx on TrxReport extension being loaded` commit accidentally removed the ProjectReference to Microsoft.DotNet.HotReload.Watch.Aspire from dotnet-watch.Tests.csproj (introduced in the `Migrate Microsoft.DotNet.HotReload.Watch.Aspire.Tests to MSTest.Sdk on MTP` commit to allow the moved AspireLauncherIntegrationTests and PipeUtilities to compile). Without that reference, the build fails with: error CS0246: The type or namespace name 'WatchStatusEvent' could not be found (are you missing a using directive or an assembly reference?) [test/dotnet-watch.Tests/Aspire/PipeUtilities.cs] Re-add the ProjectReference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/dotnet-watch.Tests/dotnet-watch.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 24484402b0e1..3b45b8eb3031 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -18,6 +18,8 @@ + +