From ef292e7e2f462352f29538e621d6066ecf056d21 Mon Sep 17 00:00:00 2001 From: Stanislaw Szczepanowski <37585349+stan-sz@users.noreply.github.com> Date: Wed, 9 Sep 2026 13:22:56 +0200 Subject: [PATCH] Upgrade MSTest to 4.4. and enable highest MSTestAnalysisMode See https://github.com/microsoft/testfx/blob/main/docs/Changelog.md#4.4.0 and https://learn.microsoft.com/en-us/dotnet/core/testing/mstest-analyzers/overview#mstestanalysismode --- Directory.Build.props | 1 + global.json | 2 +- src/Tests/AnalyzerTests.cs | 60 +++++++++---------- .../CollectDeclaredReferencesTaskTests.cs | 8 +-- src/Tests/E2ETests.cs | 20 +++---- src/Tests/MsvcLoggerTests.cs | 6 +- 6 files changed, 49 insertions(+), 48 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 649270c..f3ca6d5 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,7 @@ true true Recommended + All true diff --git a/global.json b/global.json index 4a062a8..edf4d5d 100644 --- a/global.json +++ b/global.json @@ -1,7 +1,7 @@ { "msbuild-sdks": { "Microsoft.Build.NoTargets": "3.7.56", - "MSTest.Sdk": "4.1.0" + "MSTest.Sdk": "4.4.0" }, "test": { "runner": "Microsoft.Testing.Platform" diff --git a/src/Tests/AnalyzerTests.cs b/src/Tests/AnalyzerTests.cs index ede799e..3ef0a72 100644 --- a/src/Tests/AnalyzerTests.cs +++ b/src/Tests/AnalyzerTests.cs @@ -9,7 +9,7 @@ namespace ReferenceTrimmer.Tests; [TestClass] -public sealed class AnalyzerTests +public sealed class AnalyzerTests(TestContext testContext) { [TestMethod] public async Task UsedViaMethodCall() @@ -28,7 +28,7 @@ public async Task UnusedReportsDiagnostic() var diagnostics = await RunAnalyzerAsync( "class C { }", dep); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -239,9 +239,9 @@ public async Task MultipleDepsOnlyUnusedReported() "class C : Used.Foo { }", [(used.Reference, used.Path, "ProjectReference", "../Used/Used.csproj"), (unused.Reference, unused.Path, "ProjectReference", "../Unused/Unused.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - Assert.IsTrue(diagnostics[0].GetMessage(CultureInfo.InvariantCulture).Contains("Unused")); + Assert.Contains("Unused", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -298,7 +298,7 @@ public async Task UnusedViaCrefWhenDocModeDisabled() class C { }", [(dep.Reference, dep.Path, "ProjectReference", "../Dependency/Dependency.csproj")], new CSharpParseOptions(documentationMode: DocumentationMode.None)); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -314,14 +314,14 @@ public async Task UsedViaTypeForwarding() var facadeTree = CSharpSyntaxTree.ParseText(@" using System.Runtime.CompilerServices; [assembly: TypeForwardedTo(typeof(Dep.Foo))] - "); + ", cancellationToken: testContext.CancellationToken); var facadeComp = CSharpCompilation.Create( "Facade", [facadeTree], [CorlibRef, runtime.Reference], new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); string facadePath = Path.Combine(Path.GetTempPath(), $"RT_Test_Facade_{Guid.NewGuid():N}.dll"); - var facadeResult = facadeComp.Emit(facadePath); + var facadeResult = facadeComp.Emit(facadePath, cancellationToken: testContext.CancellationToken); Assert.IsTrue(facadeResult.Success, $"Facade compilation failed:\n{string.Join("\n", facadeResult.Diagnostics)}"); var facadeRef = MetadataReference.CreateFromFile(facadePath); @@ -445,9 +445,9 @@ public async Task UnusedPackageReportsRT0003() var diagnostics = await RunAnalyzerAsync( "class C { }", [(dep.Reference, dep.Path, "PackageReference", "Dep.Package")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0003", diagnostics[0].Id); - Assert.IsTrue(diagnostics[0].GetMessage(CultureInfo.InvariantCulture).Contains("Dep.Package")); + Assert.Contains("Dep.Package", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -484,7 +484,7 @@ public async Task UnusedBareReferenceReportsRT0001() var diagnostics = await RunAnalyzerAsync( "class C { }", [(dep.Reference, dep.Path, "Reference", dep.Path)]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0001", diagnostics[0].Id); } @@ -544,7 +544,7 @@ public async Task UnusedDelegateNoExternalTypesReportsDiagnostic() var diagnostics = await RunAnalyzerAsync( "public class Local {} public delegate Local Produce(Local x);", dep); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -693,9 +693,9 @@ public async Task UnrelatedReferenceNotMarkedByOverride() [(baseAsm.Reference, baseAsm.Path, "ProjectReference", "../Base/Base.csproj"), (derivedAsm.Reference, derivedAsm.Path, "ProjectReference", "../Derived/Derived.csproj"), (unrelated.Reference, unrelated.Path, "ProjectReference", "../Unrelated/Unrelated.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "Unrelated"); + Assert.Contains("Unrelated", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -850,9 +850,9 @@ public async Task UnrelatedReferenceNotMarkedByInheritance() [(aAsm.Reference, aAsm.Path, "ProjectReference", "../A/A.csproj"), (bAsm.Reference, bAsm.Path, "ProjectReference", "../B/B.csproj"), (unrelated.Reference, unrelated.Path, "ProjectReference", "../Unrelated/Unrelated.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "Unrelated"); + Assert.Contains("Unrelated", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -977,9 +977,9 @@ public Consumer() : base(""1"") { } [(provider.Reference, provider.Path, "ProjectReference", "../Provider/Provider.csproj"), (providerDep.Reference, providerDep.Path, "ProjectReference", "../ProviderDependency/ProviderDependency.csproj"), (unrelated.Reference, unrelated.Path, "ProjectReference", "../Unrelated/Unrelated.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "Unrelated"); + Assert.Contains("Unrelated", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -1245,9 +1245,9 @@ public void Foo(int i) { } @"public class Consumer { void M(Provider.P p) { p.Foo(""x""); } }", [(provider.Reference, provider.Path, "ProjectReference", "../Provider/Provider.csproj"), (dep.Reference, dep.Path, "ProjectReference", "../Dep/Dep.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "Dep"); + Assert.Contains("Dep", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -1272,9 +1272,9 @@ public void Bar(Dep.T t) { } @"public class Consumer { void M(Provider.P p) { p.Foo(""x""); } }", [(provider.Reference, provider.Path, "ProjectReference", "../Provider/Provider.csproj"), (dep.Reference, dep.Path, "ProjectReference", "../Dep/Dep.csproj")]); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "Dep"); + Assert.Contains("Dep", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -1310,7 +1310,7 @@ public async Task UnusedCompilationReferenceStillReportsDiagnostic(bool useSymbo [(dependency.Reference, dependency.Path, "ProjectReference", "../Dependency/Dependency.csproj", dependency.Identity)], useSymbolAnalysis: useSymbolAnalysis); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -1334,7 +1334,7 @@ public async Task CompilationReferenceRequiresFullAssemblyIdentityMatch(bool use [(dependency.Reference, dependency.Path, "ProjectReference", "../Dependency/Dependency.csproj", differentVersionIdentity)], useSymbolAnalysis: useSymbolAnalysis); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -1416,9 +1416,9 @@ public async Task DuplicateCompilationReferenceIdentityUnionsTransitiveDependenc useSymbolAnalysis: useSymbolAnalysis, disableTransitiveProjectReferences: true); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "../Unrelated/Unrelated.csproj"); + Assert.Contains("../Unrelated/Unrelated.csproj", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -1471,7 +1471,7 @@ public async Task UnparseableProjectAssemblyIdentityDoesNotSuppressPeBackedDiagn [(dependency.Reference, dependency.Path, "ProjectReference", "../Dependency/Dependency.csproj", "LogicalAssembly, Version=invalid")], useSymbolAnalysis: useSymbolAnalysis); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); } @@ -1501,9 +1501,9 @@ public async Task CompilationReferenceDoesNotSuppressPeBackedLegacyDiagnostic( (unused.Reference, unused.Path, "ProjectReference", "../Unused/Unused.csproj", unusedIdentity)], useSymbolAnalysis: useSymbolAnalysis); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "../Unused/Unused.csproj"); + Assert.Contains("../Unused/Unused.csproj", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } [TestMethod] @@ -1533,9 +1533,9 @@ public async Task CompilationReferenceTransitiveChainRetainsDependenciesButNotUn useSymbolAnalysis: useSymbolAnalysis, disableTransitiveProjectReferences: true); - Assert.AreEqual(1, diagnostics.Length); + Assert.HasCount(1, diagnostics); Assert.AreEqual("RT0002", diagnostics[0].Id); - StringAssert.Contains(diagnostics[0].GetMessage(CultureInfo.InvariantCulture), "../C/C.csproj"); + Assert.Contains("../C/C.csproj", diagnostics[0].GetMessage(CultureInfo.InvariantCulture)); } // ────────────────────────────────────────────────────────────────────── diff --git a/src/Tests/CollectDeclaredReferencesTaskTests.cs b/src/Tests/CollectDeclaredReferencesTaskTests.cs index 73618df..157ac4d 100644 --- a/src/Tests/CollectDeclaredReferencesTaskTests.cs +++ b/src/Tests/CollectDeclaredReferencesTaskTests.cs @@ -30,7 +30,7 @@ public void ExecuteWithAllNullCollectionInputsDoesNotThrow() bool result = task.Execute(); Assert.IsTrue(result, "Task should succeed when all collection inputs are null. Errors: " + string.Join("; ", engine.Errors)); - Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + Assert.IsEmpty(engine.Errors, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); } finally { @@ -65,7 +65,7 @@ public void ExecuteWithNullResolvedReferencesAndUnresolvableReferenceDoesNotThro bool result = task.Execute(); Assert.IsTrue(result, "Task should succeed even when ResolvedReferences is null. Errors: " + string.Join("; ", engine.Errors)); - Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + Assert.IsEmpty(engine.Errors, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); } finally { @@ -99,7 +99,7 @@ public void ExecuteWithPackageReferencesAndNullProjectAssetsFileDoesNotThrow() bool result = task.Execute(); Assert.IsTrue(result, "Task should succeed when ProjectAssetsFile is null. Errors: " + string.Join("; ", engine.Errors)); - Assert.AreEqual(0, engine.Errors.Count, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); + Assert.IsEmpty(engine.Errors, "No errors should be logged. Errors: " + string.Join("; ", engine.Errors)); } finally { @@ -171,7 +171,7 @@ public void ProjectAssemblyIdentityParticipatesInIncrementalHash() .SingleOrDefault(include => include?.Contains("_ReferenceTrimmerProjectReferences", StringComparison.Ordinal) == true); Assert.IsNotNull(projectHashInput); - StringAssert.Contains(projectHashInput, "%(FusionName)"); + Assert.Contains("%(FusionName)", projectHashInput); } private sealed class MockBuildEngine : IBuildEngine diff --git a/src/Tests/E2ETests.cs b/src/Tests/E2ETests.cs index 2f909a6..5d88775 100644 --- a/src/Tests/E2ETests.cs +++ b/src/Tests/E2ETests.cs @@ -8,7 +8,7 @@ namespace ReferenceTrimmer.Tests; [TestClass] -public sealed class E2ETests +public sealed class E2ETests(TestContext testContext) { private readonly record struct Warning(string Message, string Project, IEnumerable? AltMessages = null); @@ -19,15 +19,15 @@ public sealed class E2ETests @".+: (warning|error) (?.+) \[(?.+)\]", RegexOptions.Compiled | RegexOptions.ExplicitCapture); - public TestContext? TestContext { get; set; } - [ClassInitialize] public static void ClassInitialize(TestContext _) { // Delete the package cache to avoid reusing old content if (Directory.Exists("Packages")) { +#pragma warning disable MSTEST0077 // Avoid hardcoded or shared filesystem paths in a parallelized test Directory.Delete("Packages", recursive: true); +#pragma warning restore MSTEST0077 // Avoid hardcoded or shared filesystem paths in a parallelized test } } @@ -821,14 +821,14 @@ private async Task RunMSBuildAsync( bool useSymbolAnalysis = false, IReadOnlyDictionary? globalProperties = null) { - var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", TestContext?.TestName ?? string.Empty)); + var testDataSourcePath = Path.GetFullPath(Path.Combine("TestData", testContext.TestName ?? string.Empty)); string logDirBase = Path.Combine(testDataSourcePath, "Logs"); string binlogFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".binlog"); string warningsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".warnings.log"); string errorsFilePath = Path.Combine(logDirBase, Path.GetFileName(projectFile) + ".errors.log"); - TestContext?.WriteLine($"Log directory: {logDirBase}"); + testContext.WriteLine($"Log directory: {logDirBase}"); string unusedLibraryLogPath = Path.Combine(testDataSourcePath, ForwardingLogger.HelpKeyword + ".json.log"); if (File.Exists(unusedLibraryLogPath)) @@ -868,19 +868,19 @@ private async Task RunMSBuildAsync( }.WithVsDevEnvironment()); Assert.IsNotNull(process); - string stdOut = await process.StandardOutput.ReadToEndAsync(); - string stdErr = await process.StandardError.ReadToEndAsync(); + string stdOut = await process.StandardOutput.ReadToEndAsync(testContext!.CancellationToken); + string stdErr = await process.StandardError.ReadToEndAsync(testContext.CancellationToken); - await process.WaitForExitAsync(); + await process.WaitForExitAsync(testContext.CancellationToken); Assert.AreEqual(0, process.ExitCode, $"Build of {projectFile} was not successful.{Environment.NewLine}StandardError: {stdErr}{Environment.NewLine}StandardOutput: {stdOut}"); Assert.AreEqual(File.Exists(unusedLibraryLogPath), expectUnusedMsvcLibrariesLog); - string errors = await File.ReadAllTextAsync(errorsFilePath); + string errors = await File.ReadAllTextAsync(errorsFilePath, testContext.CancellationToken); Assert.AreEqual(0, errors.Length, $"Build of {projectFile} was not successful.{Environment.NewLine}Error log: {errors}"); List actualWarnings = new(); - foreach (string line in await File.ReadAllLinesAsync(warningsFilePath)) + foreach (string line in await File.ReadAllLinesAsync(warningsFilePath, testContext.CancellationToken)) { Match match = WarningErrorRegex.Match(line); if (match.Success) diff --git a/src/Tests/MsvcLoggerTests.cs b/src/Tests/MsvcLoggerTests.cs index dd053fe..40d3234 100644 --- a/src/Tests/MsvcLoggerTests.cs +++ b/src/Tests/MsvcLoggerTests.cs @@ -6,7 +6,7 @@ namespace ReferenceTrimmer.Tests; [TestClass] -public sealed class MsvcLoggerTests +public sealed class MsvcLoggerTests(TestContext testContext) { private sealed class MockEventSource : IEventSource { @@ -372,7 +372,7 @@ public async Task CentralLogger_ProcessesUnusedLibEventsFromPrimaryNode() centralLogger.Shutdown(); Assert.IsTrue(File.Exists(jsonPath)); - string json = await File.ReadAllTextAsync(jsonPath); + string json = await File.ReadAllTextAsync(jsonPath, testContext.CancellationToken); Assert.Contains("user32.lib", json); } @@ -394,7 +394,7 @@ public async Task CentralLogger_JsonOnUnusedLibEvents() centralLogger.Shutdown(); Assert.IsTrue(File.Exists(jsonPath)); Assert.AreEqual($"[{Environment.NewLine}{{ \"aProp\": \"aValue\" }},{Environment.NewLine}{{ \"aProp2\": \"aValue2\" }}{Environment.NewLine}]{Environment.NewLine}", - await File.ReadAllTextAsync(jsonPath)); + await File.ReadAllTextAsync(jsonPath, testContext.CancellationToken)); } private static void SendLinkTaskStarted(