From 86d4207cb7d2494a6ba2d06826aadb6e168618c7 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 19:46:14 +0200 Subject: [PATCH 01/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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/15] 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 2ee208429af1d83533082dce1eaa03389baa5628 Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 23:33:55 +0200 Subject: [PATCH 09/15] Migrate System.CommandLine.StaticCompletions.Tests to MSTest.Sdk - Switch SDK to MSTest.Sdk; set UseMSTestSdk=true to opt out of test/Directory.Build.targets xUnit defaults. - Replace [Fact] with [TestMethod]. - Replace Verify.XunitV3 with Verify.MSTest and use VerifyBase for MSTest TestContext wiring. - Replace ITestOutputHelper with TestContext. - Drop Microsoft.NET.TestFramework reference (not used). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- eng/dependabot/Packages.props | 1 + .../BashShellProviderTests.cs | 20 +++++----- .../FishShellProviderTests.cs | 39 ++++++++++--------- .../HelpExtensionsTests.cs | 9 +++-- .../PowershellProviderTests.cs | 20 +++++----- ...CommandLine.StaticCompletions.Tests.csproj | 8 ++-- .../VerifyExtensions.cs | 7 ++-- .../ZshShellProviderTests.cs | 15 +++---- 8 files changed, 63 insertions(+), 56 deletions(-) diff --git a/eng/dependabot/Packages.props b/eng/dependabot/Packages.props index e5a597a4eea3..21717e5a0288 100644 --- a/eng/dependabot/Packages.props +++ b/eng/dependabot/Packages.props @@ -8,6 +8,7 @@ + diff --git a/test/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs b/test/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs index 9e368a5fbff3..51b6f33c4503 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/BashShellProviderTests.cs @@ -7,39 +7,41 @@ namespace System.CommandLine.StaticCompletions.Tests; using System.CommandLine.StaticCompletions.Shells; -public class BashShellProviderTests(ITestOutputHelper log) +[TestClass] +public class BashShellProviderTests : VerifyMSTest.VerifyBase { private IShellProvider provider = new BashShellProvider(); - [Fact] + + [TestMethod] public async Task GenericCompletions() { - await provider.Verify(new("mycommand"), log); + await provider.Verify(new("mycommand"), TestContext); } - [Fact] + [TestMethod] public async Task SimpleOptionCompletion() { await provider.Verify(new("mycommand") { new Option("--name") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task SubcommandAndOptionInTopLevelList() { await provider.Verify(new("mycommand") { new Option("--name"), new Command("subcommand") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task NestedSubcommandCompletion() { await provider.Verify(new("mycommand") { new Command("subcommand") { new Command("nested") } - }, log); + }, TestContext); } } diff --git a/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs b/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs index c8f385a5013b..f1cfe7571aac 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/FishShellProviderTests.cs @@ -7,44 +7,45 @@ namespace System.CommandLine.StaticCompletions.Tests; using System.CommandLine.StaticCompletions.Shells; -public class FishShellProviderTests(ITestOutputHelper log) +[TestClass] +public class FishShellProviderTests : VerifyMSTest.VerifyBase { private IShellProvider provider = new FishShellProvider(); - [Fact] + [TestMethod] public async Task GenericCompletions() { - await provider.Verify(new("mycommand"), log); + await provider.Verify(new("mycommand"), TestContext); } - [Fact] + [TestMethod] public async Task SimpleOptionCompletion() { await provider.Verify(new("mycommand") { new Option("--name") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task SubcommandAndOptionInTopLevelList() { await provider.Verify(new("mycommand") { new Option("--name"), new Command("subcommand") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task NestedSubcommandCompletion() { await provider.Verify(new("mycommand") { new Command("subcommand") { new Command("nested") } - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task DynamicCompletionsGeneration() { var dynamicOption = new Option("--dynamic") @@ -60,10 +61,10 @@ public async Task DynamicCompletionsGeneration() dynamicOption, dynamicArg }; - await provider.Verify(command, log); + await provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task StaticOptionValues() { var staticOption = new Option("--verbosity"); @@ -72,10 +73,10 @@ public async Task StaticOptionValues() { staticOption }; - await provider.Verify(command, log); + await provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task BoundedMultiValueOption() { var multiOption = new Option("--sources") @@ -88,10 +89,10 @@ public async Task BoundedMultiValueOption() multiOption, new Command("subcommand") }; - await provider.Verify(command, log); + await provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task UnboundedMultiValueOption() { var unboundedOption = new Option("--items") @@ -105,10 +106,10 @@ public async Task UnboundedMultiValueOption() new Option("--name"), new Command("subcommand") }; - await provider.Verify(command, log); + await provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task MixedArityOptions() { var singleOption = new Option("--config"); @@ -132,6 +133,6 @@ public async Task MixedArityOptions() unboundedOption, new Command("build") }; - await provider.Verify(command, log); + await provider.Verify(command, TestContext); } } diff --git a/test/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs b/test/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs index 9cd8949bfb24..e719e81a5618 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/HelpExtensionsTests.cs @@ -8,27 +8,28 @@ namespace System.CommandLine.StaticCompletions.Tests; using System.CommandLine.Help; using System.CommandLine.StaticCompletions; +[TestClass] public class HelpExtensionsTests { - [Fact] + [TestMethod] public void HelpOptionOnlyShowsUsefulNames() { new HelpOption().Names().Should().BeEquivalentTo(["--help", "-h"]); } - [Fact] + [TestMethod] public void OptionNamesListNameThenAliases() { new Option("--name", "-n", "--nombre").Names().Should().Equal(["--name", "-n", "--nombre"]); } - [Fact] + [TestMethod] public void OptionsWithNoAliasesHaveOnlyOneName() { new Option("--name").Names().Should().Equal(["--name"]); } - [Fact] + [TestMethod] public void HeirarchicalOptionsAreFlattened() { var parentCommand = new Command("parent"); diff --git a/test/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs b/test/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs index 48c00bd72a06..c86b2f654118 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/PowershellProviderTests.cs @@ -6,42 +6,42 @@ namespace System.CommandLine.StaticCompletions.Tests; using System.CommandLine.StaticCompletions.Shells; -using EmptyFiles; -public class PowershellProviderTests(ITestOutputHelper log) +[TestClass] +public class PowershellProviderTests : VerifyMSTest.VerifyBase { private IShellProvider provider = new PowerShellShellProvider(); - [Fact] + [TestMethod] public async Task GenericCompletions() { - await provider.Verify(new("mycommand"), log); + await provider.Verify(new("mycommand"), TestContext); } - [Fact] + [TestMethod] public async Task SimpleOptionCompletion() { await provider.Verify(new("mycommand") { new Option("--name") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task SubcommandAndOptionInTopLevelList() { await provider.Verify(new("mycommand") { new Option("--name"), new Command("subcommand") - }, log); + }, TestContext); } - [Fact] + [TestMethod] public async Task NestedSubcommandCompletion() { await provider.Verify(new("mycommand") { new Command("subcommand") { new Command("nested") } - }, log); + }, TestContext); } } diff --git a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index 349ac13587c3..8ecdf2f6e0bc 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -1,5 +1,6 @@ - + + true $(SdkTargetFramework) enable true @@ -9,14 +10,15 @@ + - + + - diff --git a/test/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs b/test/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs index a365e6478bce..a21588923978 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/VerifyExtensions.cs @@ -1,7 +1,6 @@ // 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; using System.CommandLine.StaticCompletions.Shells; using System.Runtime.CompilerServices; @@ -9,7 +8,7 @@ namespace System.CommandLine.StaticCompletions.Tests; public static class VerifyExtensions { - public static async Task Verify(this IShellProvider provider, Command command, ITestOutputHelper log, [CallerFilePath] string sourceFile = "") + public static async Task Verify(this IShellProvider provider, Command command, TestContext testContext, [CallerFilePath] string sourceFile = "") { // Can't use sourceFile directly because in CI the file may be rooted at a different location than the compile-time location // We do have the source code available, just at a different root, so we can use that compute @@ -23,12 +22,12 @@ public static async Task Verify(this IShellProvider provider, Command command, I { throw new DirectoryNotFoundException($"The directory ({runtimeSnapshotDir}) containing the source file ({sourceFile}) does not exist.\nVerify is going to try to recreate the directory and that won't work in CI.\nThe closest existing directory is ({closestExistingDirectory}). The current directory is ({Environment.CurrentDirectory})."); } - log.WriteLine($"CI environment detected, using snapshots directory in the runtime snapshots dir {runtimeSnapshotDir}"); + testContext.WriteLine($"CI environment detected, using snapshots directory in the runtime snapshots dir {runtimeSnapshotDir}"); settings.UseDirectory(runtimeSnapshotDir.FullName); } else { - log.WriteLine($"Using snapshots from local repository because $USER {Environment.GetEnvironmentVariable("USER")} is not helix-related"); + testContext.WriteLine($"Using snapshots from local repository because $USER {Environment.GetEnvironmentVariable("USER")} is not helix-related"); settings.UseDirectory(Path.Combine("snapshots", provider.ArgumentName)); } var completions = provider.GenerateCompletions(command); diff --git a/test/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs b/test/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs index 96bda10a9b9d..22b616195d2e 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs +++ b/test/System.CommandLine.StaticCompletions.Tests/ZshShellProviderTests.cs @@ -8,11 +8,12 @@ namespace System.CommandLine.StaticCompletions.Tests; using System.CommandLine.Help; using System.CommandLine.StaticCompletions.Shells; -public class ZshShellProviderTests(ITestOutputHelper log) +[TestClass] +public class ZshShellProviderTests : VerifyMSTest.VerifyBase { private IShellProvider _provider = new ZshShellProvider(); - [Fact] + [TestMethod] public async Task GenericCompletions() { Command command = new Command("my-app") { @@ -34,10 +35,10 @@ public async Task GenericCompletions() new Command("test") } }; - await _provider.Verify(command, log); + await _provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task DynamicCompletionsGeneration() { var staticOption = new Option("--static") @@ -62,10 +63,10 @@ public async Task DynamicCompletionsGeneration() staticOption, dynamicArg }; - await _provider.Verify(command, log); + await _provider.Verify(command, TestContext); } - [Fact] + [TestMethod] public async Task CustomStaticCompletionsGeneration() { var staticOption = new Option("--static"); @@ -84,6 +85,6 @@ public async Task CustomStaticCompletionsGeneration() staticOption, dynamicArg }; - await _provider.Verify(command, log); + await _provider.Verify(command, TestContext); } } From 98910615b615f324eb8ff6ec475e691f6f80e5d7 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:37:19 +0200 Subject: [PATCH 10/15] 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.HotReload.Watch.Aspire.Tests.csproj | 3 --- .../System.CommandLine.StaticCompletions.Tests.csproj | 3 +-- 2 files changed, 1 insertion(+), 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 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/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index 8ecdf2f6e0bc..d5e59fa94061 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -1,6 +1,5 @@ - + - true $(SdkTargetFramework) enable true From faf95537043396fcb4295394d8f99d8942504c10 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:53:54 +0200 Subject: [PATCH 11/15] 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. --- .../System.CommandLine.StaticCompletions.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index d5e59fa94061..f2df643db6f0 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -5,7 +5,6 @@ true Exe true - false From b3561c0f60f878fca81a2948b43bfc2382806008 Mon Sep 17 00:00:00 2001 From: Evangelink <32149626+Evangelink@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:20:19 +0200 Subject: [PATCH 12/15] 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> --- .../System.CommandLine.StaticCompletions.Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj index f2df643db6f0..5521d79df80c 100644 --- a/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj +++ b/test/System.CommandLine.StaticCompletions.Tests/System.CommandLine.StaticCompletions.Tests.csproj @@ -8,11 +8,9 @@ - - From 1925e7803faa649b667723365165db71503092f3 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Tue, 16 Jun 2026 05:58:25 +0200 Subject: [PATCH 13/15] Deploy Aspire launcher runtime closure for integration tests AspireLauncherIntegrationTests exec the Aspire launcher (Microsoft.DotNet.HotReload.Watch.Aspire) as a child process. ExcludeAssets=Runtime omitted the launcher's transitive runtime dependencies (Microsoft.CodeAnalysis*) from the test output, so the launched process crashed at startup with FileNotFoundException and the tests failed with Assert.NotNull. Removing ExcludeAssets=Runtime deploys the full closure next to the launcher. 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 3b45b8eb3031..b31410e85e75 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -19,7 +19,7 @@ - + From 7f195b2f0666069029a6bc46cda8646690bfe44f Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Tue, 16 Jun 2026 22:34:15 +0200 Subject: [PATCH 14/15] Address review feedback Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs index 8b42e41c7371..99744d91dd84 100644 --- a/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs +++ b/test/HelixTasks/SDKCustomCreateXUnitWorkItemsWithTestExclusion.cs @@ -259,11 +259,14 @@ private async Task ExecuteAsync() string envPrefix; if (IsPosixShell) { - envPrefix = $"HELIX_WORK_ITEM_TIMEOUT={timeout} "; + string testExecutionDirectoryEnv = string.IsNullOrEmpty(testExecutionDirectory) ? "" : "DOTNET_SDK_TEST_EXECUTION_DIRECTORY=$TestExecutionDirectory "; + envPrefix = $"HELIX_WORK_ITEM_TIMEOUT={timeout} {testExecutionDirectoryEnv}"; } else { - envPrefix = $"set HELIX_WORK_ITEM_TIMEOUT={timeout}&& "; + string testExecutionDirectoryEnv = string.IsNullOrEmpty(testExecutionDirectory) ? "" : "set DOTNET_SDK_TEST_EXECUTION_DIRECTORY=%TestExecutionDirectory%&& "; + string msbuildAdditionalSdkResolverFolderEnv = string.IsNullOrEmpty(msbuildAdditionalSdkResolverFolder) ? "" : "set DOTNET_SDK_TEST_MSBUILDSDKRESOLVER_FOLDER=%HELIX_CORRELATION_PAYLOAD%\\r&& "; + envPrefix = $"set HELIX_WORK_ITEM_TIMEOUT={timeout}&& {testExecutionDirectoryEnv}{msbuildAdditionalSdkResolverFolderEnv}"; } string diagArg = IsPosixShell From 4e7b8c54a247a908e05bb4166dc69950051b6f30 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Tue, 16 Jun 2026 22:25:29 +0200 Subject: [PATCH 15/15] Fix MSTest analyzer violations breaking main (Authoring.Tasks.IntegrationTests) PR #54766 enabled the Recommended MSTest analyzers as errors; #54758 merged shortly after with violations that are now build errors, leaving main red and blocking all open migration PRs. Fix the two affected files: - CommandResultAssertions.MSTest.cs: MSTEST0037 (IsTrue(a==b)->AreEqual, IsFalse(a==b) ->AreNotEqual) and MSTEST0023 (IsTrue(!x)->IsFalse(x)). - LocalizeTemplateTests.cs: MSTEST0037 (AreEqual(n, x.Length)->HasCount(n, x)). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> (cherry picked from commit 6ac4ef0e38f72e20e16a4e9048d61ba3ec847973) --- .../CommandResultAssertions.MSTest.cs | 16 ++++++++-------- .../LocalizeTemplateTests.cs | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs index afc231a5f934..e825a8174996 100644 --- a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/CommandResultAssertions.MSTest.cs @@ -28,19 +28,19 @@ internal CommandResultAssertions(CommandResult commandResult) internal CommandResultAssertions ExitWith(int expectedExitCode) { - Assert.IsTrue(expectedExitCode == _commandResult.ExitCode, AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not.")); + Assert.AreEqual(expectedExitCode, _commandResult.ExitCode, AppendDiagnosticsTo($"Expected command to exit with {expectedExitCode} but it did not.")); return this; } internal CommandResultAssertions Pass() { - Assert.IsTrue(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to pass but it did not.")); + Assert.AreEqual(0, _commandResult.ExitCode, AppendDiagnosticsTo("Expected command to pass but it did not.")); return this; } internal CommandResultAssertions Fail() { - Assert.IsFalse(_commandResult.ExitCode == 0, AppendDiagnosticsTo("Expected command to fail but it passed.")); + Assert.AreNotEqual(0, _commandResult.ExitCode, AppendDiagnosticsTo("Expected command to fail but it passed.")); return this; } @@ -53,7 +53,7 @@ internal CommandResultAssertions HaveStdOut() internal CommandResultAssertions HaveStdOut(string expectedOutput) { Assert.IsNotNull(_commandResult.StdOut); - Assert.IsTrue(expectedOutput == _commandResult.StdOut, AppendDiagnosticsTo($"Expected standard output to be '{expectedOutput}' but it was not.")); + Assert.AreEqual(expectedOutput, _commandResult.StdOut, AppendDiagnosticsTo($"Expected standard output to be '{expectedOutput}' but it was not.")); return this; } @@ -74,7 +74,7 @@ internal CommandResultAssertions HaveStdOutContaining(Func predica internal CommandResultAssertions NotHaveStdOutContaining(string pattern) { Assert.IsNotNull(_commandResult.StdOut); - Assert.IsTrue(!_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to not contain '{pattern}' but it did.")); + Assert.IsFalse(_commandResult.StdOut.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard output to not contain '{pattern}' but it did.")); return this; } @@ -103,7 +103,7 @@ internal CommandResultAssertions HaveStdOutMatching(string pattern, RegexOptions internal CommandResultAssertions NotHaveStdOutMatching(string pattern, RegexOptions options = RegexOptions.None) { Assert.IsNotNull(_commandResult.StdOut); - Assert.IsTrue(!Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to not match pattern '{pattern}' but it did.")); + Assert.IsFalse(Regex.Match(_commandResult.StdOut, pattern, options).Success, AppendDiagnosticsTo($"Expected standard output to not match pattern '{pattern}' but it did.")); return this; } @@ -117,7 +117,7 @@ internal CommandResultAssertions HaveStdErr() internal CommandResultAssertions HaveStdErr(string expectedOutput) { Assert.IsNotNull(_commandResult.StdErr); - Assert.IsTrue(expectedOutput == _commandResult.StdErr, AppendDiagnosticsTo($"Expected standard error to be '{expectedOutput}' but it was not.")); + Assert.AreEqual(expectedOutput, _commandResult.StdErr, AppendDiagnosticsTo($"Expected standard error to be '{expectedOutput}' but it was not.")); return this; } @@ -131,7 +131,7 @@ internal CommandResultAssertions HaveStdErrContaining(string pattern) internal CommandResultAssertions NotHaveStdErrContaining(string pattern) { Assert.IsNotNull(_commandResult.StdErr); - Assert.IsTrue(!_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); + Assert.IsFalse(_commandResult.StdErr.Contains(pattern, StringComparison.Ordinal), AppendDiagnosticsTo($"Expected standard error to contain '{pattern}' but it did not.")); return this; } diff --git a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs index f960e6b0d6ea..a3b4f1f72ae5 100644 --- a/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs +++ b/test/TemplateEngine/Microsoft.TemplateEngine.Authoring.Tasks.IntegrationTests/LocalizeTemplateTests.cs @@ -39,7 +39,7 @@ public void CanRunTask() string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); Assert.IsTrue(Directory.Exists(locFolder)); - Assert.AreEqual(14, Directory.GetFiles(locFolder).Length); + Assert.HasCount(14, Directory.GetFiles(locFolder)); Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); Directory.Delete(tmpDir, true); @@ -69,7 +69,7 @@ public void CanRunTaskSelectedLangs() string locFolder = Path.Combine(tmpDir, "content/TemplateWithSourceName/.template.config/localize"); Assert.IsTrue(Directory.Exists(locFolder)); - Assert.AreEqual(2, Directory.GetFiles(locFolder).Length); + Assert.HasCount(2, Directory.GetFiles(locFolder)); Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); Assert.IsFalse(File.Exists(Path.Combine(locFolder, "templatestrings.fr.json"))); @@ -101,7 +101,7 @@ public void CanRunTaskSelectedTemplates() string noLocFolder = Path.Combine(tmpDir, "content/non-localized/.template.config/localize"); Assert.IsTrue(Directory.Exists(locFolder)); - Assert.AreEqual(14, Directory.GetFiles(locFolder).Length); + Assert.HasCount(14, Directory.GetFiles(locFolder)); Assert.IsTrue(File.Exists(Path.Combine(locFolder, "templatestrings.de.json"))); Assert.IsFalse(Directory.Exists(noLocFolder));