From 86d4207cb7d2494a6ba2d06826aadb6e168618c7 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 19:46:14 +0200 Subject: [PATCH 01/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] 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/23] Add shared Microsoft.DotNet.Test.MSTest.Utilities project Introduces an MSTest-flavored counterpart to the xUnit-based Microsoft.DotNet.HotReload.Test.Utilities so that test helpers that need MSTest's TestContext (e.g. TestLogger, TestLoggerFactory) can be shared across MSTest.Sdk test projects instead of being copy-pasted per project. This commit also migrates the inline TestLogger from Microsoft.DotNet.HotReload.Client.Tests to consume the shared project, which serves as the first reference consumer. Subsequent migration PRs (DeltaApplier.Tests, Containers.UnitTests) will adopt the same project reference instead of adding their own TestLogger/TestLoggerFactory copies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sdk.slnx | 1 + src/Dotnet.Watch/dotnet-watch.slnf | 1 + ...osoft.DotNet.HotReload.Client.Tests.csproj | 4 + .../StaticWebAssetsManifestTests.cs | 2 +- .../InMemoryLoggerProvider.cs | 39 ++++++++ ...rosoft.DotNet.Test.MSTest.Utilities.csproj | 42 ++++++++ .../TestLogger.cs | 14 ++- .../TestLoggerFactory.cs | 97 +++++++++++++++++++ 8 files changed, 195 insertions(+), 5 deletions(-) create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj rename test/{Microsoft.DotNet.HotReload.Client.Tests/Utilities => Microsoft.DotNet.Test.MSTest.Utilities}/TestLogger.cs (70%) create mode 100644 test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs diff --git a/sdk.slnx b/sdk.slnx index df9722223799..07d879bec7b9 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -367,6 +367,7 @@ + diff --git a/src/Dotnet.Watch/dotnet-watch.slnf b/src/Dotnet.Watch/dotnet-watch.slnf index 48e156d1458e..b5600195a12e 100644 --- a/src/Dotnet.Watch/dotnet-watch.slnf +++ b/src/Dotnet.Watch/dotnet-watch.slnf @@ -33,6 +33,7 @@ "test\\dotnet-watch-test-browser\\dotnet-watch-test-browser.csproj", "test\\Microsoft.DotNet.HotReload.Test.Utilities\\Microsoft.DotNet.HotReload.Test.Utilities.csproj", "test\\Microsoft.DotNet.HotReload.Watch.Aspire.Tests\\Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj", + "test\\Microsoft.DotNet.Test.MSTest.Utilities\\Microsoft.DotNet.Test.MSTest.Utilities.csproj", "test\\Microsoft.Extensions.DotNetDeltaApplier.Tests\\Microsoft.Extensions.DotNetDeltaApplier.Tests.csproj", "test\\dotnet-watch.Tests\\dotnet-watch.Tests.csproj" ] diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj b/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj index 7500dad99340..ef519bdfcde4 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Client.Tests/Microsoft.DotNet.HotReload.Client.Tests.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs b/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs index 87d3eadc3176..e82b59ba8884 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs +++ b/test/Microsoft.DotNet.HotReload.Client.Tests/StaticWebAssetsManifestTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.Watch.UnitTests; +using Microsoft.DotNet.Test.MSTest.Utilities; using Microsoft.Extensions.Logging.Abstractions; namespace Microsoft.DotNet.HotReload.UnitTests; diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs new file mode 100644 index 000000000000..3715dac3febe --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/InMemoryLoggerProvider.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; + +namespace Microsoft.DotNet.Test.MSTest.Utilities; + +/// +/// An that appends every log entry to a caller-supplied list. +/// Useful for tests that need to assert on the exact sequence of log entries produced by a +/// component under test (without dragging in MSTest's TestContext sink). +/// +public sealed class InMemoryLoggerProvider(List<(LogLevel, string)> messagesCollection) : ILoggerProvider +{ + public ILogger CreateLogger(string categoryName) => new InMemoryLogger(messagesCollection); + + public void Dispose() + { + } + + private sealed class InMemoryLogger(List<(LogLevel, string)> messagesCollection) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + => messagesCollection.Add((logLevel, formatter(state, exception))); + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } +} diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj b/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj new file mode 100644 index 000000000000..73adf7653621 --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/Microsoft.DotNet.Test.MSTest.Utilities.csproj @@ -0,0 +1,42 @@ + + + + + + $(SdkTargetFramework);$(NetFrameworkToolCurrent) + Library + Microsoft.DotNet.Test.MSTest.Utilities + MicrosoftAspNetCore + false + + + + + + + + + + + + + + + + diff --git a/test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs similarity index 70% rename from test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs rename to test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs index 6bb15080c57d..87c8c1248f62 100644 --- a/test/Microsoft.DotNet.HotReload.Client.Tests/Utilities/TestLogger.cs +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLogger.cs @@ -1,12 +1,18 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Immutable; using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace Microsoft.DotNet.Watch.UnitTests; +namespace Microsoft.DotNet.Test.MSTest.Utilities; -internal class TestLogger(TestContext? output = null) : ILogger +/// +/// An that captures messages in memory and optionally echoes them to an +/// MSTest . Designed to be shared across MSTest.Sdk test projects so +/// the same pattern doesn't have to be duplicated per project. +/// +public class TestLogger(TestContext? testContext = null) : ILogger { public readonly object Guard = new(); private readonly List _messages = []; @@ -31,7 +37,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except HasWarning |= logLevel is LogLevel.Warning; _messages.Add(message); - output?.WriteLine(message); + testContext?.WriteLine(message); } } diff --git a/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs new file mode 100644 index 000000000000..15485a93f137 --- /dev/null +++ b/test/Microsoft.DotNet.Test.MSTest.Utilities/TestLoggerFactory.cs @@ -0,0 +1,97 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Microsoft.DotNet.Test.MSTest.Utilities; + +/// +/// An that writes log messages to an MSTest +/// (when provided) and a simple console sink. Useful for tests that +/// need a real (e.g. for components that take one in their ctor). +/// +public sealed class TestLoggerFactory : ILoggerFactory +{ + private readonly List _loggerProviders = new(); + private readonly List _factories = new(); + + public TestLoggerFactory(TestContext? testContext = null) + { + if (testContext is not null) + { + _loggerProviders.Add(new TestContextLoggerProvider(testContext)); + } + } + + public void Dispose() + { + while (_factories.Count > 0) + { + ILoggerFactory factory = _factories[0]; + _factories.RemoveAt(0); + factory.Dispose(); + } + } + + public ILogger CreateLogger(string categoryName) + { + ILoggerFactory loggerFactory = LoggerFactory.Create(builder => + { + builder.SetMinimumLevel(LogLevel.Trace); + + foreach (ILoggerProvider loggerProvider in _loggerProviders) + { + builder.AddProvider(loggerProvider); + } + + builder.AddSimpleConsole(options => + { + options.SingleLine = true; + options.TimestampFormat = "[yyyy-MM-dd HH:mm:ss.fff] "; + options.IncludeScopes = true; + }); + }); + + _factories.Add(loggerFactory); + return loggerFactory.CreateLogger(categoryName); + } + + public ILogger CreateLogger() => CreateLogger("Test Host"); + + public void AddProvider(ILoggerProvider provider) => _loggerProviders.Add(provider); + + private sealed class TestContextLoggerProvider(TestContext testContext) : ILoggerProvider + { + public ILogger CreateLogger(string categoryName) => new TestContextLogger(testContext, categoryName); + + public void Dispose() + { + } + } + + private sealed class TestContextLogger(TestContext testContext, string categoryName) : ILogger + { + public IDisposable? BeginScope(TState state) where TState : notnull => NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) => true; + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + testContext.WriteLine($"{logLevel}: {categoryName}: {formatter(state, exception)}"); + if (exception is not null) + { + testContext.WriteLine(exception.ToString()); + } + } + } + + private sealed class NullScope : IDisposable + { + public static readonly NullScope Instance = new(); + + public void Dispose() + { + } + } +} From 1a4c1917532834929f2d4830d950186afaecb633 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 21:26:49 +0200 Subject: [PATCH 09/23] Migrate containerize.UnitTests to MSTest.Sdk on MTP Follow-up to the pathfinder migration. containerize.UnitTests only uses xUnit Fact/Theory/InlineData/Assert APIs - it does not use any types from Microsoft.NET.TestFramework. So the project can drop its TestFramework ProjectReference and switch to MSTest.Sdk in one self-contained PR. Changes: * test/containerize.UnitTests/containerize.UnitTests.csproj: - Use Sdk="MSTest.Sdk", set UseMSTestSdk=true (opts the project out of the xUnit defaults in test/Directory.Build.targets - gate introduced in PR #54722). - Drop the ProjectReference to Microsoft.NET.TestFramework (unused). - Drop OutputType=Exe (MSTest.Sdk handles it). - Add Microsoft.VisualStudio.TestTools.UnitTesting global using. * test/containerize.UnitTests/ParserTests.cs: xUnit -> MSTest mechanical conversion: [Fact]/[Theory] -> [TestMethod], [InlineData] -> [DataRow], add [TestClass], Assert.NotNull/Equal/Empty/Single -> IsNotNull/AreEqual/IsEmpty/HasCount(1, ...). Verification: 9/9 tests pass on MTP in ~400 ms. Stacks on top of PR #54722 (pathfinder). Merge after that one. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/ParserTests.cs | 81 ++++++++++--------- .../containerize.UnitTests.csproj | 11 ++- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/test/containerize.UnitTests/ParserTests.cs b/test/containerize.UnitTests/ParserTests.cs index 4240089eca13..ab6edd0d1c84 100644 --- a/test/containerize.UnitTests/ParserTests.cs +++ b/test/containerize.UnitTests/ParserTests.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.CommandLine; @@ -6,9 +6,10 @@ namespace containerize.UnitTests; +[TestClass] public class ParserTests { - [Fact] + [TestMethod] public void CanParseLabels() { ContainerizeCommand command = new(); @@ -41,17 +42,17 @@ public void CanParseLabels() Dictionary? labels = parseResult.GetValue(command.LabelsOption); - Assert.NotNull(labels); - Assert.Equal(6, labels.Count); - Assert.Empty(labels["NoValue"]); - Assert.Equal("Val2", labels["Valid2"]); - Assert.Equal("Val 3", labels["Valid3"]); - Assert.Equal("\"Val4\"", labels["Valid4"]); - Assert.Equal("\"Un1", labels["Unbalanced1"]); - Assert.Equal("Un2\"", labels["Unbalanced2"]); + Assert.IsNotNull(labels); + Assert.AreEqual(6, labels.Count); + Assert.IsEmpty(labels["NoValue"]); + Assert.AreEqual("Val2", labels["Valid2"]); + Assert.AreEqual("Val 3", labels["Valid3"]); + Assert.AreEqual("\"Val4\"", labels["Valid4"]); + Assert.AreEqual("\"Un1", labels["Unbalanced1"]); + Assert.AreEqual("Un2\"", labels["Unbalanced2"]); } - [Fact] + [TestMethod] public void CanParseLabels2() { ContainerizeCommand command = new(); @@ -79,15 +80,15 @@ public void CanParseLabels2() Dictionary? labels = parseResult.GetValue(command.LabelsOption); - Assert.NotNull(labels); - Assert.Equal(2, labels.Count); - Assert.Empty(labels["NoValue"]); - Assert.Equal("Val2", labels["Valid2"]); + Assert.IsNotNull(labels); + Assert.AreEqual(2, labels.Count); + Assert.IsEmpty(labels["NoValue"]); + Assert.AreEqual("Val2", labels["Valid2"]); } - [Theory] - [InlineData("not-a-label")] - [InlineData("not", "a", "label")] + [TestMethod] + [DataRow("not-a-label")] + [DataRow("not", "a", "label")] public void CanHandleInvalidLabels(params string[] labelStr) { ContainerizeCommand command = new(); @@ -114,12 +115,12 @@ public void CanHandleInvalidLabels(params string[] labelStr) } ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Single(parseResult.Errors); + Assert.HasCount(1, parseResult.Errors); - Assert.Equal($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); + Assert.AreEqual($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); } - [Fact] + [TestMethod] public void CanParseEnvironmentVariables() { ContainerizeCommand command = new(); @@ -149,21 +150,21 @@ public void CanParseEnvironmentVariables() ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Empty(parseResult.Errors); + Assert.IsEmpty(parseResult.Errors); Dictionary? envVars = parseResult.GetValue(command.EnvVarsOption); - Assert.NotNull(envVars); - Assert.Equal(6, envVars.Count); - Assert.Empty(envVars["NoValue"]); - Assert.Equal("Val2", envVars["Valid2"]); - Assert.Equal("Val 3", envVars["Valid3"]); - Assert.Equal("\"Val4\"", envVars["Valid4"]); - Assert.Equal("\"Un1", envVars["Unbalanced1"]); - Assert.Equal("Un2\"", envVars["Unbalanced2"]); + Assert.IsNotNull(envVars); + Assert.AreEqual(6, envVars.Count); + Assert.IsEmpty(envVars["NoValue"]); + Assert.AreEqual("Val2", envVars["Valid2"]); + Assert.AreEqual("Val 3", envVars["Valid3"]); + Assert.AreEqual("\"Val4\"", envVars["Valid4"]); + Assert.AreEqual("\"Un1", envVars["Unbalanced1"]); + Assert.AreEqual("Un2\"", envVars["Unbalanced2"]); } - [Fact] + [TestMethod] public void CanParsePorts() { ContainerizeCommand command = new(); @@ -191,22 +192,22 @@ public void CanParsePorts() ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Empty(parseResult.Errors); + Assert.IsEmpty(parseResult.Errors); Port[]? ports = parseResult.GetValue(command.PortsOption); - Assert.NotNull(ports); - Assert.Equal(4, ports.Length); + Assert.IsNotNull(ports); + Assert.AreEqual(4, ports.Length); Assert.Contains(new Port(1500, PortType.tcp), ports); Assert.Contains(new Port(1501, PortType.udp), ports); Assert.Contains(new Port(1501, PortType.tcp), ports); Assert.Contains(new Port(1502, PortType.tcp), ports); } - [Theory] - [InlineData("1501/smth", "(InvalidPortType)")] - [InlineData("1501\\tcp", "(InvalidPortNumber)")] - [InlineData("not-a-number", "(InvalidPortNumber)")] + [TestMethod] + [DataRow("1501/smth", "(InvalidPortType)")] + [DataRow("1501\\tcp", "(InvalidPortNumber)")] + [DataRow("not-a-number", "(InvalidPortNumber)")] public void CanHandleInvalidPorts(string portStr, string reason) { string errorMessage = $"Incorrectly formatted ports:{Environment.NewLine}\t{portStr}:\t{reason}{Environment.NewLine}"; @@ -232,9 +233,9 @@ public void CanHandleInvalidPorts(string portStr, string reason) baseArgs.Add(portStr); ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.Single(parseResult.Errors); + Assert.HasCount(1, parseResult.Errors); - Assert.Equal(errorMessage, parseResult.Errors[0].Message); + Assert.AreEqual(errorMessage, parseResult.Errors[0].Message); } } diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 215c72557027..b5f2fbc8d3e6 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -1,4 +1,4 @@ - + $(SdkTargetFramework) @@ -6,12 +6,15 @@ false true MicrosoftShared - Exe + true + + + + - - + \ No newline at end of file From bf18d59df5cdc96ddec0d9e6df2044ae8c43d038 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 22:06:26 +0200 Subject: [PATCH 10/23] Apply skill review fixes: Assert.HasCount(1,x) -> Assert.ContainsSingle(x) Applies the assertion mapping flagged by the migrate-xunit-to-mstest skill (.github/skills/migrate-xunit-to-mstest, PR #54727). Two occurrences in ParserTests.cs converted from `Assert.HasCount(1, parseResult.Errors)` to the more idiomatic `Assert.ContainsSingle(parseResult.Errors)`, which matches the xUnit original (`Assert.Single`) more directly. Verified: 9/9 tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/ParserTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/containerize.UnitTests/ParserTests.cs b/test/containerize.UnitTests/ParserTests.cs index ab6edd0d1c84..d9273253b6ce 100644 --- a/test/containerize.UnitTests/ParserTests.cs +++ b/test/containerize.UnitTests/ParserTests.cs @@ -115,7 +115,7 @@ public void CanHandleInvalidLabels(params string[] labelStr) } ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.HasCount(1, parseResult.Errors); + Assert.ContainsSingle(parseResult.Errors); Assert.AreEqual($"Incorrectly formatted labels: {string.Join(";", labelStr)}", parseResult.Errors[0].Message); } @@ -233,7 +233,7 @@ public void CanHandleInvalidPorts(string portStr, string reason) baseArgs.Add(portStr); ParseResult parseResult = command.Parse(baseArgs.ToArray()); - Assert.HasCount(1, parseResult.Errors); + Assert.ContainsSingle(parseResult.Errors); Assert.AreEqual(errorMessage, parseResult.Errors[0].Message); } From 72bc00559c5f65eb3aaf9f31ea656b6c915ae9ea Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:33:13 +0200 Subject: [PATCH 11/23] Remove redundant Using of Microsoft.VisualStudio.TestTools.UnitTesting MSTest.Sdk already adds this as an implicit global using. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/containerize.UnitTests/containerize.UnitTests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index b5f2fbc8d3e6..76241b728e9f 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -9,10 +9,6 @@ true - - - - From 439fef53223027e42c83172af2c6c5a592678713 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:53:47 +0200 Subject: [PATCH 12/23] Remove redundant false (MSTest.Sdk default) MSTest.Sdk's Runner/Common.targets already sets `false` for every MSTest.Sdk project, so the per-project declaration is redundant. --- test/containerize.UnitTests/containerize.UnitTests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 76241b728e9f..44bc1d2dba79 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -3,7 +3,6 @@ $(SdkTargetFramework) enable - false true MicrosoftShared true From 7750f28b80c5d4043b0e01446b6003d1c057f0fe Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 21:28:40 +0200 Subject: [PATCH 13/23] Migrate Microsoft.DotNet.ApiCompat.Tests to MSTest.Sdk on MTP Same pattern as containerize.UnitTests (#54723). This project's single test file only uses xUnit Fact/Assert.Equal - no Microsoft.NET.TestFramework types - so the TestFramework ProjectReference can be dropped and the project switched to MSTest.Sdk in one self-contained change. Changes: * test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj: - Use Sdk="MSTest.Sdk", set UseMSTestSdk=true. - Drop ProjectReference to Microsoft.NET.TestFramework (unused). - Drop OutputType=Exe (MSTest.Sdk handles it). - Add Microsoft.VisualStudio.TestTools.UnitTesting global using. * test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs: [Fact] -> [TestMethod], add [TestClass], Assert.Equal -> Assert.AreEqual. Verification: 5/5 tests pass on MTP in ~250 ms. Stacks on top of #54723 which stacks on #54722. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 11 +++++---- .../RegexStringTransformerTests.cs | 23 ++++++++++--------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 898749c592b6..34ce1e5f2506 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -1,10 +1,14 @@ - + $(SdkTargetFramework) - Exe + true + + + + @@ -12,10 +16,9 @@ - - + \ No newline at end of file diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs b/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs index bed3d912cfbd..683d3a626cbd 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs +++ b/test/Microsoft.DotNet.ApiCompat.Tests/RegexStringTransformerTests.cs @@ -1,13 +1,14 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.ApiCompat; namespace Microsoft.DotNet.ApiCompatibility.Tests { + [TestClass] public class RegexStringTransformerTests { - [Fact] + [TestMethod] public void Transform_CaptureGroupPatternDoesNotMatchInput_ReturnsInput() { const string CaptureGroupPattern = "(abc)def"; @@ -16,10 +17,10 @@ public void Transform_CaptureGroupPatternDoesNotMatchInput_ReturnsInput() string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal(Input, output); + Assert.AreEqual(Input, output); } - [Fact] + [TestMethod] public void Transform_ReplacementPatternWithoutCaptureGroups_ReturnsReplacementPattern() { const string CaptureGroupPattern = "(abc)d*"; @@ -28,10 +29,10 @@ public void Transform_ReplacementPatternWithoutCaptureGroups_ReturnsReplacementP string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal(ReplacementPattern, output); + Assert.AreEqual(ReplacementPattern, output); } - [Fact] + [TestMethod] public void Transform_ReplacementPatternWithTooManyReplacementMarkers_ReturnOutputWithoutTransformedReplacementMarkers() { const string CaptureGroupPattern = "(abc)(def)ghi"; @@ -40,10 +41,10 @@ public void Transform_ReplacementPatternWithTooManyReplacementMarkers_ReturnOutp string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal("1:abc, 2:def, 3:$3", output); + Assert.AreEqual("1:abc, 2:def, 3:$3", output); } - [Fact] + [TestMethod] public void Transform_SameNumberOfGroupsAndMarkers_ReturnsExpected() { const string CaptureGroupPattern = @".+\\(.+)\\(.+)"; @@ -52,10 +53,10 @@ public void Transform_SameNumberOfGroupsAndMarkers_ReturnsExpected() string output = new RegexStringTransformer(CaptureGroupPattern, ReplacementPattern).Transform(Input); - Assert.Equal("lib/net7.0-android/System.Linq.dll", output); + Assert.AreEqual("lib/net7.0-android/System.Linq.dll", output); } - [Fact] + [TestMethod] public void Transform_MultiplePatterns_ReturnsExpected() { var patterns = new (string, string)[] @@ -69,7 +70,7 @@ public void Transform_MultiplePatterns_ReturnsExpected() string output = new RegexStringTransformer(patterns).Transform(Input); - Assert.Equal("runtimes/android/lib/net7.0/System.Linq.dll", output); + Assert.AreEqual("runtimes/android/lib/net7.0/System.Linq.dll", output); } } } From 4aa30242b57bfb62cf4d335db42d14eabde9b33b Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:34:20 +0200 Subject: [PATCH 14/23] Remove redundant Using of Microsoft.VisualStudio.TestTools.UnitTesting MSTest.Sdk already adds this as an implicit global using. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 34ce1e5f2506..52400ac52e9f 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -5,10 +5,6 @@ true - - - - From 65cf48652c9d0a383a589ce544deaa26ee5d7517 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:34:04 +0200 Subject: [PATCH 15/23] Use built-in $(UsingMSTestSdk) property instead of custom $(UseMSTestSdk) MSTest.Sdk already sets $(UsingMSTestSdk)=true in its Sdk.props before Directory.Build.props is evaluated, so the custom true opt-in property is redundant. This change: - Removes true from MSTest.Sdk csproj(s). - Renames $(UseMSTestSdk) -> $(UsingMSTestSdk) in test/Directory.Build.targets (the xUnit-defaults gating condition) and in xunit-runner/{XUnitPublish,XUnitRunner}.targets (Helix MTP dispatcher detection). --- .../Microsoft.DotNet.ApiCompat.Tests.csproj | 1 - .../Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj | 3 --- test/containerize.UnitTests/containerize.UnitTests.csproj | 1 - 3 files changed, 5 deletions(-) diff --git a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj index 52400ac52e9f..3802f96de8ab 100644 --- a/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj +++ b/test/Microsoft.DotNet.ApiCompat.Tests/Microsoft.DotNet.ApiCompat.Tests.csproj @@ -2,7 +2,6 @@ $(SdkTargetFramework) - true diff --git a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj index 2ae0f2fe0244..0aec8ad4a1ee 100644 --- a/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj +++ b/test/Microsoft.DotNet.HotReload.Watch.Aspire.Tests/Microsoft.DotNet.HotReload.Watch.Aspire.Tests.csproj @@ -1,9 +1,6 @@ - - true $(SdkTargetFramework) Microsoft.DotNet.Watch.Aspire.UnitTests diff --git a/test/containerize.UnitTests/containerize.UnitTests.csproj b/test/containerize.UnitTests/containerize.UnitTests.csproj index 44bc1d2dba79..3036a17b4558 100644 --- a/test/containerize.UnitTests/containerize.UnitTests.csproj +++ b/test/containerize.UnitTests/containerize.UnitTests.csproj @@ -5,7 +5,6 @@ enable true MicrosoftShared - true From c58302d12567bcf4f52ecd15a83a530b913eef6a Mon Sep 17 00:00:00 2001 From: Evangelink <32149626+Evangelink@users.noreply.github.com> Date: Fri, 12 Jun 2026 14:19:59 +0200 Subject: [PATCH 16/23] Helix dispatcher: gate --report-trx on TrxReport extension being loaded --report-trx is an MTP CLI argument provided only when the Microsoft.Testing.Extensions.TrxReport extension is loaded on the test host. MSTest.Sdk's Default/AllMicrosoft profiles enable it by default, but other MTP runners (e.g. xUnit v3 MTP) do not bundle the extension, so passing --report-trx to those hosts fails with 'unknown argument'. XUnitPublish.targets now exposes the GetTrxReportEnabled target which returns the value of EnableMicrosoftTestingExtensionsTrxReport. XUnitRunner.targets calls that target and propagates the value as the EnableTrxReport metadata of SDKCustomXUnitProject items. SDKCustomCreateXUnitWorkItemsWithTestExclusion reads that metadata and only appends --report-trx to the MTP command line when it is true. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/dotnet-watch.Tests/dotnet-watch.Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 3b45b8eb3031..24484402b0e1 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -18,8 +18,6 @@ - - From 6f740e7fe15c39ecbff6308d554c649dfda8366e Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 16:07:33 +0200 Subject: [PATCH 17/23] Restore Watch.Aspire ProjectReference in dotnet-watch.Tests.csproj The recent `Helix dispatcher: gate --report-trx on TrxReport extension being loaded` commit accidentally removed the ProjectReference to Microsoft.DotNet.HotReload.Watch.Aspire from dotnet-watch.Tests.csproj (introduced in the `Migrate Microsoft.DotNet.HotReload.Watch.Aspire.Tests to MSTest.Sdk on MTP` commit to allow the moved AspireLauncherIntegrationTests and PipeUtilities to compile). Without that reference, the build fails with: error CS0246: The type or namespace name 'WatchStatusEvent' could not be found (are you missing a using directive or an assembly reference?) [test/dotnet-watch.Tests/Aspire/PipeUtilities.cs] Re-add the ProjectReference. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- test/dotnet-watch.Tests/dotnet-watch.Tests.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj index 24484402b0e1..3b45b8eb3031 100644 --- a/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj +++ b/test/dotnet-watch.Tests/dotnet-watch.Tests.csproj @@ -18,6 +18,8 @@ + + From 063870dbf6edbaa142b7786bedb265fb68ea3218 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 21:35:06 +0200 Subject: [PATCH 18/23] Migrate Microsoft.AspNetCore.Watch.BrowserRefresh.Tests to MSTest.Sdk on MTP Same pattern as previous migrations. None of the 6 test files used any Microsoft.NET.TestFramework types - only xUnit primitives - so the TestFramework ProjectReference can be dropped and the project switched to MSTest.Sdk in one self-contained change. Changes: * test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/csproj: - Use Sdk="MSTest.Sdk", set UseMSTestSdk=true. - Drop ProjectReference to Microsoft.NET.TestFramework (unused). - Drop OutputType=Exe (MSTest.Sdk handles it). - Add Microsoft.VisualStudio.TestTools.UnitTesting global using. * 6 test files: xUnit -> MSTest mechanical conversion plus targeted fixes: - [Fact]/[Theory] -> [TestMethod], [InlineData] -> [DataRow], add [TestClass]. - Assert.True/False/Null/NotNull/Empty/NotEmpty/Equal/NotEqual -> IsTrue/IsFalse/IsNull/IsNotNull/IsEmpty/IsNotEmpty/AreEqual/AreNotEqual. - Assert.Single(x) -> Assert.HasCount(1, x). - String Assert.Contains(needle, haystack) -> StringAssert.Contains(haystack, needle) (xUnit and MSTest argument orders are reversed). - One Assert.DoesNotContain(needle, str) -> Assert.IsFalse(str.Contains(needle)). - Assert.Collection in BrowserScriptMiddlewareTest expanded to inline Assert.HasCount + indexed AreEqual checks. - Assert.ThrowsAsync -> Assert.ThrowsExactlyAsync. - Assert.AreEqual on byte[]/int[] -> Assert.AreSequenceEqual (MSTEST0065: MSTest does not do element-wise equality on collections; the analyzer catches a real semantic difference vs xUnit). - StringValues (Microsoft.Extensions.Primitives) ambiguity with Assert.AreEqual resolved by calling .ToString() at 5 call sites. - xUnit v3 TestContext.Current.CancellationToken -> CancellationToken.None (MSTest's TestContext.Current is experimental; tests do not actually require test-cancellation propagation). Verification: 110/110 tests pass on MTP in ~480 ms. Stacks on #54724 which stacks on #54723 which stacks on #54722. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BlazorWasmHotReloadMiddlewareTest.cs | 35 +-- .../BrowserRefreshMiddlewareTest.cs | 163 ++++++------- .../BrowserScriptMiddlewareTest.cs | 36 ++- .../HostingStartupTest.cs | 41 ++-- ...pNetCore.Watch.BrowserRefresh.Tests.csproj | 11 +- .../ResponseStreamWrapperCompressionTest.cs | 101 ++++---- .../ScriptInjectingStreamTests.cs | 228 +++++++++--------- 7 files changed, 308 insertions(+), 307 deletions(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BlazorWasmHotReloadMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BlazorWasmHotReloadMiddlewareTest.cs index efdcf548c524..f72bc30a5454 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BlazorWasmHotReloadMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BlazorWasmHotReloadMiddlewareTest.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.Text.Json; @@ -7,6 +7,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh { + [TestClass] public class BlazorWasmHotReloadMiddlewareTest { private readonly ILogger _logger; @@ -19,7 +20,7 @@ public BlazorWasmHotReloadMiddlewareTest() _middleware = new BlazorWasmHotReloadMiddleware(context => throw new TimeZoneNotFoundException(), _logger); } - [Fact] + [TestMethod] public async Task DeltasAreSavedOnPost() { var context = new DefaultHttpContext(); @@ -55,7 +56,7 @@ public async Task DeltasAreSavedOnPost() AssertUpdates([update], _middleware.Updates); } - [Fact] + [TestMethod] public async Task DuplicateDeltasOnPostAreIgnored() { var updates = new BlazorWasmHotReloadMiddleware.Update[] @@ -106,7 +107,7 @@ public async Task DuplicateDeltasOnPostAreIgnored() AssertUpdates(updates, _middleware.Updates); } - [Fact] + [TestMethod] public async Task MultipleDeltaPayloadsCanBeAccepted() { var update = new BlazorWasmHotReloadMiddleware.Update() @@ -178,7 +179,7 @@ public async Task MultipleDeltaPayloadsCanBeAccepted() AssertUpdates([update, newUpdate], _middleware.Updates); } - [Fact] + [TestMethod] public async Task Get_Returns204_IfNoDeltasPresent() { var context = new DefaultHttpContext(); @@ -186,10 +187,10 @@ public async Task Get_Returns204_IfNoDeltasPresent() await _middleware.InvokeAsync(context); - Assert.Equal(204, context.Response.StatusCode); + Assert.AreEqual(204, context.Response.StatusCode); } - [Fact] + [TestMethod] public async Task GetReturnsDeltas() { var context = new DefaultHttpContext(); @@ -226,30 +227,30 @@ public async Task GetReturnsDeltas() await _middleware.InvokeAsync(context); - Assert.Equal(200, context.Response.StatusCode); - Assert.Equal( + Assert.AreEqual(200, context.Response.StatusCode); + Assert.AreSequenceEqual( JsonSerializer.SerializeToUtf8Bytes(updates, new JsonSerializerOptions(JsonSerializerDefaults.Web)), stream.ToArray()); } private static void AssertUpdates(IReadOnlyList expected, IReadOnlyList actual) { - Assert.Equal(expected.Count, actual.Count); + Assert.AreEqual(expected.Count, actual.Count); for (var u = 0; u < expected.Count; u++) { var expectedUpdate = expected[u]; var actualUpdate = actual[u]; - Assert.Equal(expectedUpdate.Id, actualUpdate.Id); - Assert.Equal(expectedUpdate.Deltas.Length, expectedUpdate.Deltas.Length); + Assert.AreEqual(expectedUpdate.Id, actualUpdate.Id); + Assert.AreEqual(expectedUpdate.Deltas.Length, expectedUpdate.Deltas.Length); for (var i = 0; i < expectedUpdate.Deltas.Length; i++) { - Assert.Equal(expectedUpdate.Deltas[i].ILDelta, actualUpdate.Deltas[i].ILDelta); - Assert.Equal(expectedUpdate.Deltas[i].PdbDelta, actualUpdate.Deltas[i].PdbDelta); - Assert.Equal(expectedUpdate.Deltas[i].MetadataDelta, actualUpdate.Deltas[i].MetadataDelta); - Assert.Equal(expectedUpdate.Deltas[i].ModuleId, actualUpdate.Deltas[i].ModuleId); - Assert.Equal(expectedUpdate.Deltas[i].UpdatedTypes, actualUpdate.Deltas[i].UpdatedTypes); + Assert.AreEqual(expectedUpdate.Deltas[i].ILDelta, actualUpdate.Deltas[i].ILDelta); + Assert.AreEqual(expectedUpdate.Deltas[i].PdbDelta, actualUpdate.Deltas[i].PdbDelta); + Assert.AreEqual(expectedUpdate.Deltas[i].MetadataDelta, actualUpdate.Deltas[i].MetadataDelta); + Assert.AreEqual(expectedUpdate.Deltas[i].ModuleId, actualUpdate.Deltas[i].ModuleId); + Assert.AreSequenceEqual(expectedUpdate.Deltas[i].UpdatedTypes, actualUpdate.Deltas[i].UpdatedTypes); } } } diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs index d04585a7113d..31b81f325c01 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs @@ -10,12 +10,13 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh { + [TestClass] public class BrowserRefreshMiddlewareTest { - [Theory] - [InlineData("DELETE")] - [InlineData("head")] - [InlineData("Put")] + [TestMethod] + [DataRow("DELETE")] + [DataRow("head")] + [DataRow("Put")] public void IsBrowserDocumentRequest_ReturnsFalse_ForNonGetOrPostRequests(string method) { // Arrange @@ -35,10 +36,10 @@ public void IsBrowserDocumentRequest_ReturnsFalse_ForNonGetOrPostRequests(string var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public void IsBrowserDocumentRequest_ReturnsFalse_IsRequestDoesNotAcceptHtml() { // Arrange @@ -58,10 +59,10 @@ public void IsBrowserDocumentRequest_ReturnsFalse_IsRequestDoesNotAcceptHtml() var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public void IsBrowserDocumentRequest_ReturnsTrue_ForGetRequestsThatAcceptHtml() { // Arrange @@ -81,10 +82,10 @@ public void IsBrowserDocumentRequest_ReturnsTrue_ForGetRequestsThatAcceptHtml() var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Fact] + [TestMethod] public void IsBrowserDocumentRequest_ReturnsTrue_ForRequestsThatAcceptAnyHtml() { // Arrange @@ -104,10 +105,10 @@ public void IsBrowserDocumentRequest_ReturnsTrue_ForRequestsThatAcceptAnyHtml() var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Fact] + [TestMethod] public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestDoesNotHaveFetchMetadataRequestHeader() { // Arrange @@ -127,10 +128,10 @@ public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestDoesNotHaveFetchMetada var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Fact] + [TestMethod] public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHeaderIsEmpty() { // Arrange @@ -151,12 +152,12 @@ public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHe var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("document")] - [InlineData("Document")] + [TestMethod] + [DataRow("document")] + [DataRow("Document")] public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHeaderIsDocument(string headerValue) { // Arrange @@ -177,12 +178,12 @@ public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHe var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("frame")] - [InlineData("iframe")] + [TestMethod] + [DataRow("frame")] + [DataRow("iframe")] public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHeaderIsFrame(string headerValue) { // Arrange @@ -203,11 +204,11 @@ public void IsBrowserDocumentRequest_ReturnsTrue_IfRequestFetchMetadataRequestHe var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("serviceworker")] + [TestMethod] + [DataRow("serviceworker")] public void IsBrowserDocumentRequest_ReturnsFalse_IfRequestFetchMetadataRequestHeaderIsNotDocument(string headerValue) { // Arrange @@ -228,14 +229,14 @@ public void IsBrowserDocumentRequest_ReturnsFalse_IfRequestFetchMetadataRequestH var result = BrowserRefreshMiddleware.IsBrowserDocumentRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Theory] - [InlineData("DELETE")] - [InlineData("POST")] - [InlineData("head")] - [InlineData("Put")] + [TestMethod] + [DataRow("DELETE")] + [DataRow("POST")] + [DataRow("head")] + [DataRow("Put")] public void IsWebassemblyBootRequest_ReturnsFalse_ForNonGetRequests(string method) { // Arrange @@ -256,10 +257,10 @@ public void IsWebassemblyBootRequest_ReturnsFalse_ForNonGetRequests(string metho var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public void IsWebassemblyBootRequest_ReturnsFalse_IfRequestDoesNotAcceptJson() { // Arrange @@ -280,10 +281,10 @@ public void IsWebassemblyBootRequest_ReturnsFalse_IfRequestDoesNotAcceptJson() var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public void IsWebassemblyBootRequest_ReturnsTrue_ForGetRequestsThatAcceptJson() { // Arrange @@ -304,10 +305,10 @@ public void IsWebassemblyBootRequest_ReturnsTrue_ForGetRequestsThatAcceptJson() var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Fact] + [TestMethod] public void IsWebassemblyBootRequest_ReturnsTrue_ForGetRequestsThatAcceptAnyContentType() { // Arrange @@ -328,12 +329,12 @@ public void IsWebassemblyBootRequest_ReturnsTrue_ForGetRequestsThatAcceptAnyCont var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("/_framework/blazor.boot.json")] - [InlineData("/Blazor.boot.json")] + [TestMethod] + [DataRow("/_framework/blazor.boot.json")] + [DataRow("/Blazor.boot.json")] public void IsWebassemblyBootRequest_ReturnsTrue_ForFileNameRequestsToBlazorBootJson(string path) { // Arrange @@ -354,13 +355,13 @@ public void IsWebassemblyBootRequest_ReturnsTrue_ForFileNameRequestsToBlazorBoot var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("/_framework/other.txt")] - [InlineData("/other.txt")] - [InlineData("/Blazor.boot.json/other.txt")] + [TestMethod] + [DataRow("/_framework/other.txt")] + [DataRow("/other.txt")] + [DataRow("/Blazor.boot.json/other.txt")] public void IsWebassemblyBootRequest_ReturnsFalse_ForRequestsToOtherPathsThanBlazorBootJson(string path) { // Arrange @@ -381,10 +382,10 @@ public void IsWebassemblyBootRequest_ReturnsFalse_ForRequestsToOtherPathsThanBla var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestDoesNotHaveFetchMetadataRequestHeader() { // Arrange @@ -405,10 +406,10 @@ public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestDoesNotHaveFetchMetada var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Fact] + [TestMethod] public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestFetchMetadataRequestHeaderIsEmpty() { // Arrange @@ -430,12 +431,12 @@ public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestFetchMetadataRequestHe var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("empty")] - [InlineData("Empty")] + [TestMethod] + [DataRow("empty")] + [DataRow("Empty")] public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestFetchMetadataRequestHeaderIsEmptyValue(string headerValue) { // Arrange @@ -457,14 +458,14 @@ public void IsWebassemblyBootRequest_ReturnsTrue_IfRequestFetchMetadataRequestHe var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.True(result); + Assert.IsTrue(result); } - [Theory] - [InlineData("frame")] - [InlineData("iframe")] - [InlineData("serviceworker")] - [InlineData("document")] + [TestMethod] + [DataRow("frame")] + [DataRow("iframe")] + [DataRow("serviceworker")] + [DataRow("document")] public void IsWebassemblyBootRequest_ReturnsFalse_IfRequestFetchMetadataRequestHeaderIsEmptyValue(string headerValue) { // Arrange @@ -486,10 +487,10 @@ public void IsWebassemblyBootRequest_ReturnsFalse_IfRequestFetchMetadataRequestH var result = BrowserRefreshMiddleware.IsWebAssemblyBootRequest(context); // Assert - Assert.False(result); + Assert.IsFalse(result); } - [Fact] + [TestMethod] public async Task InvokeAsync_AttachesHeadersToResponse() { var stream = new MemoryStream(); @@ -528,11 +529,11 @@ public async Task InvokeAsync_AttachesHeadersToResponse() await middleware.InvokeAsync(context); // Assert - Assert.True(context.Response.Headers.ContainsKey("DOTNET-MODIFIABLE-ASSEMBLIES")); - Assert.True(context.Response.Headers.ContainsKey("ASPNETCORE-BROWSER-TOOLS")); + Assert.IsTrue(context.Response.Headers.ContainsKey("DOTNET-MODIFIABLE-ASSEMBLIES")); + Assert.IsTrue(context.Response.Headers.ContainsKey("ASPNETCORE-BROWSER-TOOLS")); } - [Fact] + [TestMethod] public async Task InvokeAsync_DoesNotAttachHeaders_WhenAlreadyAttached() { var stream = new MemoryStream(); @@ -574,33 +575,33 @@ public async Task InvokeAsync_DoesNotAttachHeaders_WhenAlreadyAttached() await middleware.InvokeAsync(context); // Assert - Assert.True(context.Response.Headers.ContainsKey("DOTNET-MODIFIABLE-ASSEMBLIES")); - Assert.Equal("true", context.Response.Headers["DOTNET-MODIFIABLE-ASSEMBLIES"]); - Assert.True(context.Response.Headers.ContainsKey("ASPNETCORE-BROWSER-TOOLS")); - Assert.Equal("true", context.Response.Headers["ASPNETCORE-BROWSER-TOOLS"]); + Assert.IsTrue(context.Response.Headers.ContainsKey("DOTNET-MODIFIABLE-ASSEMBLIES")); + Assert.AreEqual("true", context.Response.Headers["DOTNET-MODIFIABLE-ASSEMBLIES"].ToString()); + Assert.IsTrue(context.Response.Headers.ContainsKey("ASPNETCORE-BROWSER-TOOLS")); + Assert.AreEqual("true", context.Response.Headers["ASPNETCORE-BROWSER-TOOLS"].ToString()); } - [Theory] - [InlineData(500, "text/html")] - [InlineData(404, "text/html")] - [InlineData(200, "text/html")] + [TestMethod] + [DataRow(500, "text/html")] + [DataRow(404, "text/html")] + [DataRow(200, "text/html")] public async Task InvokeAsync_AddsScriptToThePage_ForSupportedStatusCodes(int statusCode, string contentType) { // Act & Assert var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content"); - Assert.Contains("", responseContent); + StringAssert.Contains(responseContent, ""); } - [Theory] - [InlineData(400, "text/html")] // Bad Request - [InlineData(401, "text/html")] // Unauthorized - [InlineData(404, "application/json")] // 404 with wrong content type - [InlineData(200, "application/json")] // 200 with wrong content type + [TestMethod] + [DataRow(400, "text/html")] // Bad Request + [DataRow(401, "text/html")] // Unauthorized + [DataRow(404, "application/json")] // 404 with wrong content type + [DataRow(200, "application/json")] // 200 with wrong content type public async Task InvokeAsync_DoesNotAddScript_ForUnsupportedStatusCodesOrContentTypes(int statusCode, string contentType) { // Act & Assert var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content", includeHtmlWrapper: false); - Assert.DoesNotContain("", responseContent); + Assert.IsFalse(responseContent.Contains("")); } private async Task TestBrowserRefreshMiddleware(int statusCode, string contentType, string content, bool includeHtmlWrapper = true) @@ -646,7 +647,7 @@ private async Task TestBrowserRefreshMiddleware(int statusCode, string c // Return response content and verify status code var responseContent = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Equal(statusCode, context.Response.StatusCode); + Assert.AreEqual(statusCode, context.Response.StatusCode); return responseContent; } diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs index 19621f84cc92..487c789f174c 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs @@ -6,6 +6,7 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh { + [TestClass] public class BrowserScriptMiddlewareTest { private readonly RequestDelegate _next = (context) => Task.CompletedTask; @@ -17,7 +18,7 @@ public BrowserScriptMiddlewareTest() _logger = loggerFactory.CreateLogger(); } - [Fact] + [TestMethod] public async Task InvokeAsync_ReturnsScript() { var context = new DefaultHttpContext(); @@ -33,12 +34,12 @@ public async Task InvokeAsync_ReturnsScript() stream.Position = 0; var script = Encoding.UTF8.GetString(stream.ToArray()); - Assert.Contains("// dotnet-watch browser reload script", script); - Assert.Contains("'some-host'", script); - Assert.Contains("'test-key'", script); + StringAssert.Contains(script, "// dotnet-watch browser reload script"); + StringAssert.Contains(script, "'some-host'"); + StringAssert.Contains(script, "'test-key'"); } - [Fact] + [TestMethod] public async Task InvokeAsync_ConfiguresHeaders() { var context = new DefaultHttpContext(); @@ -52,23 +53,14 @@ public async Task InvokeAsync_ConfiguresHeaders() await middleware.InvokeAsync(context); var response = context.Response; - Assert.Collection( - response.Headers.OrderBy(h => h.Key), - kvp => - { - Assert.Equal("Cache-Control", kvp.Key); - Assert.Equal("no-store", kvp.Value); - }, - kvp => - { - Assert.Equal("Content-Length", kvp.Key); - Assert.NotEqual(0, kvp.Value.Count); - }, - kvp => - { - Assert.Equal("Content-Type", kvp.Key); - Assert.Equal("application/javascript; charset=utf-8", kvp.Value); - }); + var headers = response.Headers.OrderBy(h => h.Key).ToArray(); + Assert.HasCount(3, headers); + Assert.AreEqual("Cache-Control", headers[0].Key); + Assert.AreEqual("no-store", headers[0].Value.ToString()); + Assert.AreEqual("Content-Length", headers[1].Key); + Assert.AreNotEqual(0, headers[1].Value.Count); + Assert.AreEqual("Content-Type", headers[2].Key); + Assert.AreEqual("application/javascript; charset=utf-8", headers[2].Value.ToString()); } } } diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/HostingStartupTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/HostingStartupTest.cs index 5050e4c8dd15..40dfc35d6c85 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/HostingStartupTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/HostingStartupTest.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 Microsoft.AspNetCore.Builder; @@ -7,9 +7,10 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh { + [TestClass] public class HostingStartupTest { - [Fact] + [TestMethod] public async Task ClearSiteDataWorks() { // Arrange @@ -21,11 +22,11 @@ public async Task ClearSiteDataWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - Assert.Equal("\"cache\"", context.Response.Headers["Clear-Site-Data"]); + Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.AreEqual("\"cache\"", context.Response.Headers["Clear-Site-Data"].ToString()); } - [Fact] + [TestMethod] public async Task GetBlazorHotReloadMiddlewareWorks() { // Arrange @@ -39,10 +40,10 @@ public async Task GetBlazorHotReloadMiddlewareWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode); + Assert.AreEqual(StatusCodes.Status204NoContent, context.Response.StatusCode); } - [Fact] + [TestMethod] public async Task PostBlazorHotReloadMiddlewareWorks() { var requestDelegate = GetRequestDelegate(); @@ -58,7 +59,7 @@ public async Task PostBlazorHotReloadMiddlewareWorks() await requestDelegate(context); - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode); context.Request.Path = "/_framework/blazor-hotreload"; context.Request.Method = "GET"; @@ -70,10 +71,10 @@ public async Task PostBlazorHotReloadMiddlewareWorks() var bodyJson = Encoding.UTF8.GetString(body.ToArray()); - Assert.Equal($"[{updateJson}]", bodyJson); + Assert.AreEqual($"[{updateJson}]", bodyJson); } - [Fact] + [TestMethod] public async Task GetBlazorHotReloadJsWorks() { // Arrange @@ -87,11 +88,11 @@ public async Task GetBlazorHotReloadJsWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - Assert.NotEmpty(responseBody.ToArray()); + Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.IsNotEmpty(responseBody.ToArray()); } - [Fact] + [TestMethod] public async Task GetAspNetCoreBrowserRefreshWorks() { // Arrange @@ -105,11 +106,11 @@ public async Task GetAspNetCoreBrowserRefreshWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); - Assert.NotEmpty(responseBody.ToArray()); + Assert.AreEqual(StatusCodes.Status200OK, context.Response.StatusCode); + Assert.IsNotEmpty(responseBody.ToArray()); } - [Fact] + [TestMethod] public async Task GetUnknownUrlWorks() { // Arrange @@ -121,10 +122,10 @@ public async Task GetUnknownUrlWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status418ImATeapot, context.Response.StatusCode); + Assert.AreEqual(StatusCodes.Status418ImATeapot, context.Response.StatusCode); } - [Fact] + [TestMethod] public async Task GetUnknownFrameworkPathWorks() { // Arrange @@ -155,7 +156,7 @@ public async Task GetUnknownFrameworkPathWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status206PartialContent, context.Response.StatusCode); + Assert.AreEqual(StatusCodes.Status206PartialContent, context.Response.StatusCode); // Act - 2 @@ -163,7 +164,7 @@ public async Task GetUnknownFrameworkPathWorks() await requestDelegate(context); // Assert - Assert.Equal(StatusCodes.Status226IMUsed, context.Response.StatusCode); + Assert.AreEqual(StatusCodes.Status226IMUsed, context.Response.StatusCode); } private static RequestDelegate GetRequestDelegate(Action? configureBuilder = null) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj index a9b32a2352b2..edb1c8195aee 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj @@ -1,16 +1,19 @@ - + $(SdkTargetFramework) Microsoft.AspNetCore.Watch.BrowserRefresh MicrosoftAspNetCore - Exe false + true + + + + - @@ -22,4 +25,4 @@ - + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs index 32c39452edb2..e5febf95ed4f 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs @@ -8,9 +8,10 @@ namespace Microsoft.AspNetCore.Watch.BrowserRefresh { + [TestClass] public class ResponseStreamWrapperCompressionTest { - [Fact] + [TestMethod] public async Task WriteAsync_HandlesGzipCompressedHtmlResponse() { // Arrange @@ -32,17 +33,17 @@ public async Task WriteAsync_HandlesGzipCompressedHtmlResponse() var wrapper = new ResponseStreamWrapper(context, NullLogger.Instance); // Act - await wrapper.WriteAsync(compressedData, TestContext.Current.CancellationToken); + await wrapper.WriteAsync(compressedData, CancellationToken.None); await wrapper.CompleteAsync(); // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task Write_HandlesGzipCompressedHtmlResponse() { // Arrange @@ -69,12 +70,12 @@ public async Task Write_HandlesGzipCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task WriteAsync_DoesNotModifyNonHtmlCompressedResponse() { // Arrange @@ -96,15 +97,15 @@ public async Task WriteAsync_DoesNotModifyNonHtmlCompressedResponse() var wrapper = new ResponseStreamWrapper(context, NullLogger.Instance); // Act - await wrapper.WriteAsync(compressedData, TestContext.Current.CancellationToken); + await wrapper.WriteAsync(compressedData, CancellationToken.None); await wrapper.CompleteAsync(); var result = outputStream.ToArray(); - Assert.Equal(compressedData, result); - Assert.True(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.AreSequenceEqual(compressedData, result); + Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } - [Fact] + [TestMethod] public async Task Write_DoesNotModifyNonHtmlCompressedResponse() { // Arrange @@ -130,11 +131,11 @@ public async Task Write_DoesNotModifyNonHtmlCompressedResponse() await wrapper.CompleteAsync(); var result = outputStream.ToArray(); - Assert.Equal(compressedData, result); - Assert.True(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.AreSequenceEqual(compressedData, result); + Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } - [Fact] + [TestMethod] public async Task WriteAsync_IgnoresNonGzipCompressionTypes() { // Arrange @@ -156,17 +157,17 @@ public async Task WriteAsync_IgnoresNonGzipCompressionTypes() var wrapper = new ResponseStreamWrapper(context, NullLogger.Instance); // Act - await wrapper.WriteAsync(data, TestContext.Current.CancellationToken); + await wrapper.WriteAsync(data, CancellationToken.None); await wrapper.CompleteAsync(); // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); // Should treat as regular data since we only handle gzip - Assert.Contains("", result); - Assert.True(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + StringAssert.Contains(result, ""); + Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } - [Fact] + [TestMethod] public async Task Write_IgnoresNonGzipCompressionTypes() { // Arrange @@ -194,11 +195,11 @@ public async Task Write_IgnoresNonGzipCompressionTypes() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); // Should treat as regular data since we only handle gzip - Assert.Contains("", result); - Assert.True(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + StringAssert.Contains(result, ""); + Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } - [Fact] + [TestMethod] public async Task WriteAsync_PreservesNonCompressedHtmlResponse() { // Arrange @@ -219,16 +220,16 @@ public async Task WriteAsync_PreservesNonCompressedHtmlResponse() var wrapper = new ResponseStreamWrapper(context, NullLogger.Instance); // Act - await wrapper.WriteAsync(data, TestContext.Current.CancellationToken); + await wrapper.WriteAsync(data, CancellationToken.None); await wrapper.CompleteAsync(); // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task Write_PreservesNonCompressedHtmlResponse() { // Arrange @@ -254,11 +255,11 @@ public async Task Write_PreservesNonCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task WriteAsync_GzipHtml_SingleByteChunks() { // Arrange: small HTML so compressed output is reasonably small; we will feed one byte at a time. @@ -289,12 +290,12 @@ public async Task WriteAsync_GzipHtml_SingleByteChunks() // Assert: script injected and content encoding removed var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task Write_GzipHtml_SingleByteChunks() { // Arrange: small HTML so compressed output is reasonably small; we will feed one byte at a time. @@ -325,12 +326,12 @@ public async Task Write_GzipHtml_SingleByteChunks() // Assert: script injected and content encoding removed var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task WriteAsync_GzipHtml_LargeChunk32K() { // Arrange: generate largely incompressible-ish HTML body so compressed data spans >= 32K @@ -346,7 +347,7 @@ public async Task WriteAsync_GzipHtml_LargeChunk32K() sb.Append(""); var htmlContent = sb.ToString(); var compressedData = CompressWithGzip(htmlContent); - Assert.True(compressedData.Length > 0, "Expected non-empty compressed payload"); + Assert.IsTrue(compressedData.Length > 0, "Expected non-empty compressed payload"); var outputStream = new MemoryStream(); var context = new DefaultHttpContext @@ -375,13 +376,13 @@ public async Task WriteAsync_GzipHtml_LargeChunk32K() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.Contains("", result); // Ensure full doc present - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + StringAssert.Contains(result, ""); // Ensure full doc present + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } - [Fact] + [TestMethod] public async Task Write_GzipHtml_LargeChunk32K() { // Arrange: generate largely incompressible-ish HTML body so compressed data spans >= 32K @@ -397,7 +398,7 @@ public async Task Write_GzipHtml_LargeChunk32K() sb.Append(""); var htmlContent = sb.ToString(); var compressedData = CompressWithGzip(htmlContent); - Assert.True(compressedData.Length > 0, "Expected non-empty compressed payload"); + Assert.IsTrue(compressedData.Length > 0, "Expected non-empty compressed payload"); var outputStream = new MemoryStream(); var context = new DefaultHttpContext @@ -426,10 +427,10 @@ public async Task Write_GzipHtml_LargeChunk32K() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - Assert.Contains("", result); - Assert.Contains("", result); // Ensure full doc present - Assert.False(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); - Assert.Null(context.Response.Headers.ContentLength); + StringAssert.Contains(result, ""); + StringAssert.Contains(result, ""); // Ensure full doc present + Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); + Assert.IsNull(context.Response.Headers.ContentLength); } private static byte[] CompressWithGzip(string content) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs index dd3bdbb24b68..9036ed1e4df7 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs @@ -1,13 +1,15 @@ -// 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. namespace Microsoft.AspNetCore.Watch.BrowserRefresh; +[TestClass] + public class ScriptInjectingStreamTests { private static readonly string s_injectedScript = ScriptInjectingStream.InjectedScript; - [Fact] + [TestMethod] public void Write_CompleteBodyTagInSingleWrite_InjectsScript() { // Arrange @@ -20,11 +22,11 @@ public void Write_CompleteBodyTagInSingleWrite_InjectsScript() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_CompleteBodyTagInSingleWrite_InjectsScript() { // Arrange @@ -33,25 +35,25 @@ public async Task WriteAsync_CompleteBodyTagInSingleWrite_InjectsScript() var html = "Content"; // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes(html), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes(html), CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Theory] - [InlineData("Content^<", "/body>")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^", "<", "/body>")] - [InlineData("C", "o", "ntent^", "<", "/body>")] - [InlineData("Content^", "")] - [InlineData("Content", "")] + [TestMethod] + [DataRow("Content^<", "/body>")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^", "<", "/body>")] + [DataRow("C", "o", "ntent^", "<", "/body>")] + [DataRow("Content^", "")] + [DataRow("Content", "")] public void Write_BodyTagSplitAcrossMultipleWrites_InjectsScript(params string[] parts) { // Arrange @@ -67,21 +69,21 @@ public void Write_BodyTagSplitAcrossMultipleWrites_InjectsScript(params string[] // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal(expectedResult, result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual(expectedResult, result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Theory] - [InlineData("Content^<", "/body>")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^")] - [InlineData("Content^", "<", "/body>")] - [InlineData("C", "o", "ntent^", "<", "/body>")] - [InlineData("Content^", "")] - [InlineData("Content", "")] + [TestMethod] + [DataRow("Content^<", "/body>")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^")] + [DataRow("Content^", "<", "/body>")] + [DataRow("C", "o", "ntent^", "<", "/body>")] + [DataRow("Content^", "")] + [DataRow("Content", "")] public async Task WriteAsync_BodyTagSplitAcrossMultipleWrites_InjectsScript(params string[] parts) { // Arrange @@ -92,16 +94,16 @@ public async Task WriteAsync_BodyTagSplitAcrossMultipleWrites_InjectsScript(para // Act foreach (var part in parts) { - await stream.WriteAsync(Encoding.UTF8.GetBytes(part.Replace("^", "")), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes(part.Replace("^", "")), CancellationToken.None); } // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal(expectedResult, result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual(expectedResult, result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Dispose_FlushesPartialBodyTagAtEndOfInput() { // Arrange @@ -116,12 +118,12 @@ public void Dispose_FlushesPartialBodyTagAtEndOfInput() var flushResult = Encoding.UTF8.GetString(baseStream.ToArray()); // Assert - Assert.Equal("Content", writeResult); - Assert.Equal("ContentContent", writeResult); + Assert.AreEqual("ContentContentContentContent", writeResult); - Assert.Equal("ContentContent", writeResult); + Assert.AreEqual("ContentContent{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_BodyTagSplitAcrossMultipleSingleByteWrites_InjectsScript() { // Arrange @@ -171,21 +173,21 @@ public async Task WriteAsync_BodyTagSplitAcrossMultipleSingleByteWrites_InjectsS var stream = new ScriptInjectingStream(baseStream); // Act - Split "" across 7 writes - await stream.WriteAsync(Encoding.UTF8.GetBytes("Content<"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("/"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("b"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("o"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("d"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("y"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes(">"), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("Content<"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("/"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("b"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("o"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("d"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("y"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes(">"), CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Write_FalsePositivePartialMatch_FlushesCorrectly() { // Arrange @@ -198,11 +200,11 @@ public void Write_FalsePositivePartialMatch_FlushesCorrectly() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content
Not a body tag{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content
Not a body tag{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_FalsePositivePartialMatch_FlushesCorrectly() { // Arrange @@ -210,16 +212,16 @@ public async Task WriteAsync_FalsePositivePartialMatch_FlushesCorrectly() var stream = new ScriptInjectingStream(baseStream); // Act - Start with partial match that turns out false - await stream.WriteAsync(Encoding.UTF8.GetBytes("ContentNot a body tag"), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("ContentNot a body tag"), CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content
Not a body tag{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content
Not a body tag{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Write_NoBodyTag_NoInjection() { // Arrange @@ -232,11 +234,11 @@ public void Write_NoBodyTag_NoInjection() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal(html, result); - Assert.False(stream.ScriptInjectionPerformed); + Assert.AreEqual(html, result); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_NoBodyTag_NoInjection() { // Arrange @@ -245,15 +247,15 @@ public async Task WriteAsync_NoBodyTag_NoInjection() var html = "
Content
"; // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes(html), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes(html), CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal(html, result); - Assert.False(stream.ScriptInjectionPerformed); + Assert.AreEqual(html, result); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Write_MultipleBodyTags_InjectsOnlyOnce() { // Arrange @@ -266,11 +268,11 @@ public void Write_MultipleBodyTags_InjectsOnlyOnce() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"First{s_injectedScript}Second", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"First{s_injectedScript}Second", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_MultipleBodyTags_InjectsOnlyOnce() { // Arrange @@ -278,16 +280,16 @@ public async Task WriteAsync_MultipleBodyTags_InjectsOnlyOnce() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes("First"), TestContext.Current.CancellationToken); - await stream.WriteAsync(Encoding.UTF8.GetBytes("Second"), TestContext.Current.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("First"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("Second"), CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"First{s_injectedScript}Second", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"First{s_injectedScript}Second", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void WriteByte_PassesThroughDirectly() { // Arrange @@ -300,11 +302,11 @@ public void WriteByte_PassesThroughDirectly() // Assert var result = baseStream.ToArray(); - Assert.Equal(new byte[] { 65, 66 }, result); - Assert.False(stream.ScriptInjectionPerformed); + Assert.AreSequenceEqual(new byte[] { 65, 66 }, result); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Write_EmptyBuffer_DoesNothing() { // Arrange @@ -315,11 +317,11 @@ public void Write_EmptyBuffer_DoesNothing() stream.Write(ReadOnlySpan.Empty); // Assert - Assert.Empty(baseStream.ToArray()); - Assert.False(stream.ScriptInjectionPerformed); + Assert.IsEmpty(baseStream.ToArray()); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_EmptyBuffer_DoesNothing() { // Arrange @@ -327,14 +329,14 @@ public async Task WriteAsync_EmptyBuffer_DoesNothing() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.WriteAsync(ReadOnlyMemory.Empty, TestContext.Current.CancellationToken); + await stream.WriteAsync(ReadOnlyMemory.Empty, CancellationToken.None); // Assert - Assert.Empty(baseStream.ToArray()); - Assert.False(stream.ScriptInjectionPerformed); + Assert.IsEmpty(baseStream.ToArray()); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_WithCancellation_PropagatesCancellation() { // Arrange @@ -344,7 +346,7 @@ public async Task WriteAsync_WithCancellation_PropagatesCancellation() cts.Cancel(); // Act & Assert - await Assert.ThrowsAsync(async () => + await Assert.ThrowsExactlyAsync(async () => { // Use a mock stream that respects cancellation var mockStream = new CancellationTestStream(); @@ -353,7 +355,7 @@ await Assert.ThrowsAsync(async () => }); } - [Fact] + [TestMethod] public void Write_ArrayOverload_CompleteBodyTag_InjectsScript() { // Arrange @@ -367,11 +369,11 @@ public void Write_ArrayOverload_CompleteBodyTag_InjectsScript() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_ArrayOverload_CompleteBodyTag_InjectsScript() { // Arrange @@ -381,15 +383,15 @@ public async Task WriteAsync_ArrayOverload_CompleteBodyTag_InjectsScript() var bytes = Encoding.UTF8.GetBytes(html); // Act - await stream.WriteAsync(bytes, 0, bytes.Length, TestContext.Current.CancellationToken); + await stream.WriteAsync(bytes, 0, bytes.Length, CancellationToken.None); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Write_ArrayOverloadWithOffset_InjectsScript() { // Arrange @@ -402,11 +404,11 @@ public void Write_ArrayOverloadWithOffset_InjectsScript() // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task WriteAsync_ArrayOverloadWithOffset_InjectsScript() { // Arrange @@ -415,15 +417,15 @@ public async Task WriteAsync_ArrayOverloadWithOffset_InjectsScript() var buffer = Encoding.UTF8.GetBytes("XXXContentYYY"); // Act - await stream.WriteAsync(buffer, 3, buffer.Length - 6, TestContext.Current.CancellationToken); // Skip XXX and YYY + await stream.WriteAsync(buffer, 3, buffer.Length - 6, CancellationToken.None); // Skip XXX and YYY // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); - Assert.Equal($"Content{s_injectedScript}", result); - Assert.True(stream.ScriptInjectionPerformed); + Assert.AreEqual($"Content{s_injectedScript}", result); + Assert.IsTrue(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public void Flush_WithoutAnyWrites_DoesNotCrash() { // Arrange @@ -434,11 +436,11 @@ public void Flush_WithoutAnyWrites_DoesNotCrash() stream.Flush(); // Assert - Assert.Empty(baseStream.ToArray()); - Assert.False(stream.ScriptInjectionPerformed); + Assert.IsEmpty(baseStream.ToArray()); + Assert.IsFalse(stream.ScriptInjectionPerformed); } - [Fact] + [TestMethod] public async Task FlushAsync_WithoutAnyWrites_DoesNotCrash() { // Arrange @@ -446,11 +448,11 @@ public async Task FlushAsync_WithoutAnyWrites_DoesNotCrash() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.FlushAsync(TestContext.Current.CancellationToken); + await stream.FlushAsync(CancellationToken.None); // Assert - Assert.Empty(baseStream.ToArray()); - Assert.False(stream.ScriptInjectionPerformed); + Assert.IsEmpty(baseStream.ToArray()); + Assert.IsFalse(stream.ScriptInjectionPerformed); } private class CancellationTestStream : Stream From 2b84511b8c56b7ac1c2c4faf23671a95c5c65957 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Thu, 11 Jun 2026 22:18:41 +0200 Subject: [PATCH 19/23] Apply skill review fixes: use 4.3 Assert.Contains / Assert.DoesNotContain for strings The migrate-xunit-to-mstest skill (.github/skills/migrate-xunit-to-mstest, PR #54727) notes that MSTest 3.8+ added Assert.Contains(needle, str) / Assert.DoesNotContain(needle, str) overloads for strings -- with the natural (needle, haystack) argument order matching xUnit's Assert.Contains. With the 4.3.0-preview MSTest.Sdk bump in #54722, these are now available throughout the migration. Replaced: - StringAssert.Contains(haystack, needle) -> Assert.Contains(needle, haystack) - Assert.IsFalse(haystack.Contains(needle)) -> Assert.DoesNotContain(needle, haystack) Touched files: - BrowserRefreshMiddlewareTest.cs - BrowserScriptMiddlewareTest.cs - ResponseStreamWrapperCompressionTest.cs Note: Assert.IsFalse(dict.ContainsKey(key)) call sites are unchanged (no Assert.DoesNotContainKey API exists). Verified: 110/110 tests still pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../BrowserRefreshMiddlewareTest.cs | 4 ++-- .../BrowserScriptMiddlewareTest.cs | 6 ++--- .../ResponseStreamWrapperCompressionTest.cs | 24 +++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs index 31b81f325c01..55416d986134 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserRefreshMiddlewareTest.cs @@ -589,7 +589,7 @@ public async Task InvokeAsync_AddsScriptToThePage_ForSupportedStatusCodes(int st { // Act & Assert var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content"); - StringAssert.Contains(responseContent, ""); + Assert.Contains("", responseContent); } [TestMethod] @@ -601,7 +601,7 @@ public async Task InvokeAsync_DoesNotAddScript_ForUnsupportedStatusCodesOrConten { // Act & Assert var responseContent = await TestBrowserRefreshMiddleware(statusCode, contentType, "Test Content", includeHtmlWrapper: false); - Assert.IsFalse(responseContent.Contains("")); + Assert.DoesNotContain("", responseContent); } private async Task TestBrowserRefreshMiddleware(int statusCode, string contentType, string content, bool includeHtmlWrapper = true) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs index 487c789f174c..d16809efb7d4 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/BrowserScriptMiddlewareTest.cs @@ -34,9 +34,9 @@ public async Task InvokeAsync_ReturnsScript() stream.Position = 0; var script = Encoding.UTF8.GetString(stream.ToArray()); - StringAssert.Contains(script, "// dotnet-watch browser reload script"); - StringAssert.Contains(script, "'some-host'"); - StringAssert.Contains(script, "'test-key'"); + Assert.Contains("// dotnet-watch browser reload script", script); + Assert.Contains("'some-host'", script); + Assert.Contains("'test-key'", script); } [TestMethod] diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs index e5febf95ed4f..a0735b9dff89 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ResponseStreamWrapperCompressionTest.cs @@ -38,7 +38,7 @@ public async Task WriteAsync_HandlesGzipCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -70,7 +70,7 @@ public async Task Write_HandlesGzipCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -163,7 +163,7 @@ public async Task WriteAsync_IgnoresNonGzipCompressionTypes() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); // Should treat as regular data since we only handle gzip - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } @@ -195,7 +195,7 @@ public async Task Write_IgnoresNonGzipCompressionTypes() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); // Should treat as regular data since we only handle gzip - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsTrue(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); } @@ -225,7 +225,7 @@ public async Task WriteAsync_PreservesNonCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -255,7 +255,7 @@ public async Task Write_PreservesNonCompressedHtmlResponse() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -290,7 +290,7 @@ public async Task WriteAsync_GzipHtml_SingleByteChunks() // Assert: script injected and content encoding removed var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -326,7 +326,7 @@ public async Task Write_GzipHtml_SingleByteChunks() // Assert: script injected and content encoding removed var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); + Assert.Contains("", result); Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -376,8 +376,8 @@ public async Task WriteAsync_GzipHtml_LargeChunk32K() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); - StringAssert.Contains(result, ""); // Ensure full doc present + Assert.Contains("", result); + Assert.Contains("", result); // Ensure full doc present Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } @@ -427,8 +427,8 @@ public async Task Write_GzipHtml_LargeChunk32K() // Assert var result = Encoding.UTF8.GetString(outputStream.ToArray()); - StringAssert.Contains(result, ""); - StringAssert.Contains(result, ""); // Ensure full doc present + Assert.Contains("", result); + Assert.Contains("", result); // Ensure full doc present Assert.IsFalse(context.Response.Headers.ContainsKey(HeaderNames.ContentEncoding)); Assert.IsNull(context.Response.Headers.ContentLength); } From 8c5f7572ba57257e27398d1997f0fa6382b697cc Mon Sep 17 00:00:00 2001 From: Evangelink Date: Thu, 11 Jun 2026 22:35:14 +0200 Subject: [PATCH 20/23] 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.AspNetCore.Watch.BrowserRefresh.Tests.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj index edb1c8195aee..96284b742cf7 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj @@ -8,10 +8,6 @@ true - - - - From 3bee4814abe6b52c5c42a219a8ae722bdedad996 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:34:39 +0200 Subject: [PATCH 21/23] 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.AspNetCore.Watch.BrowserRefresh.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj index 96284b742cf7..b8925064c9c8 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj @@ -5,7 +5,6 @@ Microsoft.AspNetCore.Watch.BrowserRefresh MicrosoftAspNetCore false - true From 6006b4d730b2c4762722b35767162436870f8055 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 13:53:50 +0200 Subject: [PATCH 22/23] 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. --- .../Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj index b8925064c9c8..0e6fe66f9235 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests.csproj @@ -4,7 +4,6 @@ $(SdkTargetFramework) Microsoft.AspNetCore.Watch.BrowserRefresh MicrosoftAspNetCore - false From c0659ed34b56ee4b474f406988a98874382840c4 Mon Sep 17 00:00:00 2001 From: Amaury Leveugle Date: Fri, 12 Jun 2026 14:03:53 +0200 Subject: [PATCH 23/23] Use TestContext.CancellationToken instead of CancellationToken.None The original xUnit v3 test used TestContext.Current.CancellationToken to propagate test cancellation into the WriteAsync/FlushAsync calls. The MSTest migration swapped that for CancellationToken.None, dropping cancellation propagation. Restore the original behavior by adding the standard MSTest `public TestContext TestContext { get; set; }` property on the test class and routing the cancellation token through it (TestContext.Current is also valid but is currently flagged as experimental via MSTESTEXP, hence the instance-property pattern). --- .../ScriptInjectingStreamTests.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs index 9036ed1e4df7..7c9a65aeb2de 100644 --- a/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs +++ b/test/Microsoft.AspNetCore.Watch.BrowserRefresh.Tests/ScriptInjectingStreamTests.cs @@ -9,6 +9,8 @@ public class ScriptInjectingStreamTests { private static readonly string s_injectedScript = ScriptInjectingStream.InjectedScript; + public TestContext TestContext { get; set; } = null!; + [TestMethod] public void Write_CompleteBodyTagInSingleWrite_InjectsScript() { @@ -35,7 +37,7 @@ public async Task WriteAsync_CompleteBodyTagInSingleWrite_InjectsScript() var html = "Content"; // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes(html), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes(html), TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -94,7 +96,7 @@ public async Task WriteAsync_BodyTagSplitAcrossMultipleWrites_InjectsScript(para // Act foreach (var part in parts) { - await stream.WriteAsync(Encoding.UTF8.GetBytes(part.Replace("^", "")), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes(part.Replace("^", "")), TestContext.CancellationToken); } // Assert @@ -131,7 +133,7 @@ public async Task DisposeAsync_FlushesPartialBodyTagAtEndOfInput() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes("ContentContent" across 7 writes - await stream.WriteAsync(Encoding.UTF8.GetBytes("Content<"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("/"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("b"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("o"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("d"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("y"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes(">"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("Content<"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("/"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("b"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("o"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("d"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("y"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes(">"), TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -212,8 +214,8 @@ public async Task WriteAsync_FalsePositivePartialMatch_FlushesCorrectly() var stream = new ScriptInjectingStream(baseStream); // Act - Start with partial match that turns out false - await stream.WriteAsync(Encoding.UTF8.GetBytes("ContentNot a body tag"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("ContentNot a body tag"), TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -247,7 +249,7 @@ public async Task WriteAsync_NoBodyTag_NoInjection() var html = "
Content
"; // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes(html), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes(html), TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -280,8 +282,8 @@ public async Task WriteAsync_MultipleBodyTags_InjectsOnlyOnce() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.WriteAsync(Encoding.UTF8.GetBytes("First"), CancellationToken.None); - await stream.WriteAsync(Encoding.UTF8.GetBytes("Second"), CancellationToken.None); + await stream.WriteAsync(Encoding.UTF8.GetBytes("First"), TestContext.CancellationToken); + await stream.WriteAsync(Encoding.UTF8.GetBytes("Second"), TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -329,7 +331,7 @@ public async Task WriteAsync_EmptyBuffer_DoesNothing() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.WriteAsync(ReadOnlyMemory.Empty, CancellationToken.None); + await stream.WriteAsync(ReadOnlyMemory.Empty, TestContext.CancellationToken); // Assert Assert.IsEmpty(baseStream.ToArray()); @@ -383,7 +385,7 @@ public async Task WriteAsync_ArrayOverload_CompleteBodyTag_InjectsScript() var bytes = Encoding.UTF8.GetBytes(html); // Act - await stream.WriteAsync(bytes, 0, bytes.Length, CancellationToken.None); + await stream.WriteAsync(bytes, 0, bytes.Length, TestContext.CancellationToken); // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -417,7 +419,7 @@ public async Task WriteAsync_ArrayOverloadWithOffset_InjectsScript() var buffer = Encoding.UTF8.GetBytes("XXXContentYYY"); // Act - await stream.WriteAsync(buffer, 3, buffer.Length - 6, CancellationToken.None); // Skip XXX and YYY + await stream.WriteAsync(buffer, 3, buffer.Length - 6, TestContext.CancellationToken); // Skip XXX and YYY // Assert var result = Encoding.UTF8.GetString(baseStream.ToArray()); @@ -448,7 +450,7 @@ public async Task FlushAsync_WithoutAnyWrites_DoesNotCrash() var stream = new ScriptInjectingStream(baseStream); // Act - await stream.FlushAsync(CancellationToken.None); + await stream.FlushAsync(TestContext.CancellationToken); // Assert Assert.IsEmpty(baseStream.ToArray());