From 8580a88dbf75c468b37e0526de9e259bc03243c7 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:13:32 +0200 Subject: [PATCH 1/2] Add `UseMicrosoftTestingPlatform` setting to `ITestTaskSettings` and enhance `dotnet test` argument customization. - Introduced `UseMicrosoftTestingPlatform` for flexibility in running tests with Microsoft.Testing.Platform or VSTest. - Updated `TestTask` to support platform-specific arguments (e.g., TRX, Cobertura coverage). - Added comprehensive unit tests to validate behavior across various settings and scenarios. --- .../Templates/Settings/ITestTaskSettings.cs | 21 ++ .../Tasks/Templates/TestTask.cs | 61 +++- .../Tasks/Templates/TestTaskTests.cs | 269 +++++++++++++++++- 3 files changed, 345 insertions(+), 6 deletions(-) diff --git a/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/Settings/ITestTaskSettings.cs b/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/Settings/ITestTaskSettings.cs index 2c05c885..d713cc2f 100644 --- a/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/Settings/ITestTaskSettings.cs +++ b/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/Settings/ITestTaskSettings.cs @@ -25,5 +25,26 @@ IEnumerable TestProjects bool GenerateCoverageReport => true; + /// + /// Gets a value that indicates whether dotnet test is run in Microsoft.Testing.Platform (MTP) mode + /// instead of VSTest mode. + /// + /// + /// to pass Microsoft.Testing.Platform arguments to dotnet test; otherwise, + /// to pass VSTest arguments. The default is . + /// + /// + /// This setting only selects the arguments passed to dotnet test; it does not switch the test mode of the + /// .NET SDK. The repository must enable the Microsoft.Testing.Platform mode in its global.json + /// ("test": { "runner": "Microsoft.Testing.Platform" }). The test projects must reference + /// Microsoft.Testing.Extensions.TrxReport and, if is + /// , Microsoft.Testing.Extensions.CodeCoverage. In this mode, the TRX file + /// (<Project>.trx) is written to instead of + /// , and the coverage report is written in Cobertura format to + /// <Project>.cobertura.xml in . Because these files are + /// named after the test project file, the file names of all test projects must be unique. + /// + bool UseMicrosoftTestingPlatform => false; + DirectoryPath TestSourceDir => Context.RootDir.Combine("tests"); } diff --git a/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/TestTask.cs b/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/TestTask.cs index 43407291..cb18ea74 100755 --- a/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/TestTask.cs +++ b/source/CakeBuild/CreativeCoders.CakeBuild/Tasks/Templates/TestTask.cs @@ -1,6 +1,7 @@ using Cake.Common.Diagnostics; using Cake.Common.Tools.DotNet; using Cake.Common.Tools.DotNet.Test; +using Cake.Core; using Cake.Core.IO; using CreativeCoders.CakeBuild.Tasks.Templates.Settings; using JetBrains.Annotations; @@ -16,6 +17,10 @@ protected override Task RunAsyncCore(T context, ITestTaskSettings taskSettings) context.Information($"Found {testProjects.Length} test project(s)"); + context.Information(taskSettings.UseMicrosoftTestingPlatform + ? "Test platform: Microsoft Testing Platform" + : "Test platform: VSTest"); + foreach (var testProject in testProjects) { context.Information($"Test project found: {testProject.GetFilename()}"); @@ -27,25 +32,71 @@ protected override Task RunAsyncCore(T context, ITestTaskSettings taskSettings) return Task.CompletedTask; } + /// + /// Customizes the dotnet test settings for a single test project before the test run starts. + /// + /// The build context. + /// The dotnet test settings to customize. + /// + /// The hook is called after the base settings (configuration, build, results directory and, in VSTest mode, + /// loggers and collectors) have been applied. If is + /// , the Microsoft.Testing.Platform arguments are appended after the hook has run; an + /// set by the hook is kept and runs before + /// these arguments. In this mode, do not set or + /// , because dotnet test does not support them. + /// protected virtual void ApplyDotNetTestSettings(T context, DotNetTestSettings dotNetBuildSettings) { } private DotNetTestSettings CreateDotNetBuildSettings(T context, FilePath testProject, ITestTaskSettings testSettings) { - var testResultFile = - context.TestResultsDir.CombineWithFilePath($"{testProject.GetFilenameWithoutExtension()}.trx"); - var dotNetTestSettings = new DotNetTestSettings { Configuration = context.BuildConfiguration, NoBuild = context.HasExecutedTask(typeof(BuildTask)), - Loggers = [$"trx;LogFileName={testResultFile}"], - Collectors = testSettings.GenerateCoverageReport ? ["XPlat Code Coverage"] : [], ResultsDirectory = context.CodeCoverageDir }; + if (testSettings.UseMicrosoftTestingPlatform) + { + dotNetTestSettings.PathType = DotNetTestPathType.Project; + } + else + { + var testResultFile = + context.TestResultsDir.CombineWithFilePath($"{testProject.GetFilenameWithoutExtension()}.trx"); + + dotNetTestSettings.Loggers = [$"trx;LogFileName={testResultFile}"]; + dotNetTestSettings.Collectors = testSettings.GenerateCoverageReport ? ["XPlat Code Coverage"] : []; + } + ApplyDotNetTestSettings(context, dotNetTestSettings); + if (testSettings.UseMicrosoftTestingPlatform) + { + var customArgumentCustomization = dotNetTestSettings.ArgumentCustomization; + + dotNetTestSettings.ArgumentCustomization = args => + { + args = customArgumentCustomization?.Invoke(args) ?? args; + + args.Append("--report-trx"); + args.Append("--report-trx-filename"); + args.AppendQuoted($"{testProject.GetFilenameWithoutExtension()}.trx"); + + if (testSettings.GenerateCoverageReport) + { + args.Append("--coverage"); + args.Append("--coverage-output-format"); + args.Append("cobertura"); + args.Append("--coverage-output"); + args.AppendQuoted($"{testProject.GetFilenameWithoutExtension()}.cobertura.xml"); + } + + return args; + }; + } + if (dotNetTestSettings.NoBuild) { context.Information("Skip build"); diff --git a/tests/CreativeCoders.CakeBuild.Tests/Tasks/Templates/TestTaskTests.cs b/tests/CreativeCoders.CakeBuild.Tests/Tasks/Templates/TestTaskTests.cs index 1f42be55..a8fb02fe 100644 --- a/tests/CreativeCoders.CakeBuild.Tests/Tasks/Templates/TestTaskTests.cs +++ b/tests/CreativeCoders.CakeBuild.Tests/Tasks/Templates/TestTaskTests.cs @@ -1,4 +1,6 @@ using AwesomeAssertions; +using Cake.Common.Tools.DotNet.Test; +using Cake.Core; using Cake.Core.IO; using CreativeCoders.CakeBuild.Tasks.Templates; using CreativeCoders.CakeBuild.Tasks.Templates.Settings; @@ -9,11 +11,57 @@ namespace CreativeCoders.CakeBuild.Tests.Tasks.Templates; public class TestTaskTests { - private class TestTestBuildContext(Cake.Core.ICakeContext context) + private class TestTestBuildContext(ICakeContext context, bool generateCoverageReport = true) : CakeBuildContext(context), ITestTaskSettings { public IEnumerable TestProjects => [new FilePath("/repo/tests/Project.Tests.csproj")]; + + public bool GenerateCoverageReport { get; } = generateCoverageReport; + } + + private class MtpTestBuildContext( + ICakeContext context, + bool generateCoverageReport = true, + IEnumerable? testProjects = null) + : CakeBuildContext(context), ITestTaskSettings + { + public IEnumerable TestProjects { get; } = + testProjects ?? [new FilePath("/repo/tests/Project.Tests.csproj")]; + + public bool GenerateCoverageReport { get; } = generateCoverageReport; + + public bool UseMicrosoftTestingPlatform => true; + } + + private class FrameworkTestTask : TestTask + { + protected override void ApplyDotNetTestSettings(MtpTestBuildContext context, + DotNetTestSettings dotNetBuildSettings) + { + dotNetBuildSettings.Framework = "net10.0"; + } + } + + private class ArgumentCustomizationTestTask : TestTask + { + protected override void ApplyDotNetTestSettings(MtpTestBuildContext context, + DotNetTestSettings dotNetBuildSettings) + { + dotNetBuildSettings.ArgumentCustomization = args => args.Append("--custom-argument"); + } + } + + private static ICakeContext CreateCakeContext(List capturedCalls) + { + var cakeContext = CakeTestHelper.CreateCakeContext("dotnet-test"); + CakeTestHelper.SetupFileSystem(cakeContext, "/repo", "/repo/test.sln"); + + A.CallTo(() => cakeContext.ProcessRunner.Start("dotnet-test", A._)) + .Invokes((FilePath _, ProcessSettings settings) => + capturedCalls.Add(settings.Arguments.Select(arg => arg.Render()).ToArray())); + + return cakeContext; } [Fact] @@ -41,4 +89,223 @@ await act .Contains("\"/repo/tests/Project.Tests.csproj\"")))) .MustHaveHappenedOnceExactly(); } + + [Fact] + public void UseMicrosoftTestingPlatform_NotOverridden_IsFalse() + { + // Arrange + var cakeContext = CakeTestHelper.CreateCakeContext("dotnet-test"); + CakeTestHelper.SetupFileSystem(cakeContext, "/repo", "/repo/test.sln"); + + ITestTaskSettings settings = new TestTestBuildContext(cakeContext); + + // Act + var useMicrosoftTestingPlatform = settings.UseMicrosoftTestingPlatform; + + // Assert + useMicrosoftTestingPlatform + .Should() + .BeFalse(); + } + + [Fact] + public async Task RunAsync_DefaultSettings_PassesVsTestArguments() + { + // Arrange + var calls = new List(); + var context = new TestTestBuildContext(CreateCakeContext(calls)); + var task = new TestTask(); + + var expectedLogger = $"\"trx;LogFileName={context.TestResultsDir.FullPath}/Project.Tests.trx\""; + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .HaveElementAt(0, "test") + .And.HaveElementAt(1, "\"/repo/tests/Project.Tests.csproj\"") + .And.ContainInConsecutiveOrder("--logger", expectedLogger) + .And.ContainInConsecutiveOrder("--collect", "\"XPlat Code Coverage\"") + .And.NotContain("--project") + .And.NotContain("--report-trx") + .And.NotContain("--coverage"); + } + + [Fact] + public async Task RunAsync_VsTestWithoutCoverage_OmitsCollector() + { + // Arrange + var calls = new List(); + var context = new TestTestBuildContext(CreateCakeContext(calls), false); + var task = new TestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .Contain("--logger") + .And.NotContain("--collect") + .And.NotContain("\"XPlat Code Coverage\""); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformWithCoverage_PassesMtpArguments() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls)); + var task = new TestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .HaveElementAt(0, "test") + .And.ContainInConsecutiveOrder("--project", "\"/repo/tests/Project.Tests.csproj\"") + .And.ContainInConsecutiveOrder("--results-directory", $"\"{context.CodeCoverageDir.FullPath}\"") + .And.ContainInConsecutiveOrder("--report-trx", "--report-trx-filename", "\"Project.Tests.trx\"") + .And.ContainInConsecutiveOrder("--coverage", "--coverage-output-format", "cobertura") + .And.ContainInConsecutiveOrder("--coverage-output", "\"Project.Tests.cobertura.xml\"") + .And.NotContain("--logger") + .And.NotContain("--collect"); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformWithoutCoverage_OmitsCoverageArguments() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls), false); + var task = new TestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .ContainInConsecutiveOrder("--project", "\"/repo/tests/Project.Tests.csproj\"") + .And.ContainInConsecutiveOrder("--report-trx", "--report-trx-filename", "\"Project.Tests.trx\"") + .And.NotContain("--coverage") + .And.NotContain("--coverage-output-format") + .And.NotContain("--coverage-output") + .And.NotContain("--logger") + .And.NotContain("--collect"); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformWithMultipleProjects_PassesProjectSpecificArguments() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls), testProjects: + [ + new FilePath("/repo/tests/B.Tests.csproj"), + new FilePath("/repo/tests/A.Tests.csproj") + ]); + var task = new TestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .HaveCount(2); + + calls[0] + .Should() + .ContainInConsecutiveOrder("--project", "\"/repo/tests/A.Tests.csproj\"") + .And.ContainInConsecutiveOrder("--report-trx-filename", "\"A.Tests.trx\""); + + calls[1] + .Should() + .ContainInConsecutiveOrder("--project", "\"/repo/tests/B.Tests.csproj\"") + .And.ContainInConsecutiveOrder("--report-trx-filename", "\"B.Tests.trx\""); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformAfterBuildTask_PassesNoBuild() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls)); + context.AddExecutedTask(new BuildTask()); + var task = new TestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .Contain("--no-build") + .And.ContainInConsecutiveOrder("--project", "\"/repo/tests/Project.Tests.csproj\"") + .And.Contain("--report-trx"); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformWithApplyDotNetTestSettingsOverride_KeepsMtpArguments() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls)); + var task = new FrameworkTestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .ContainInConsecutiveOrder("--framework", "net10.0") + .And.ContainInConsecutiveOrder("--report-trx", "--report-trx-filename", "\"Project.Tests.trx\"") + .And.ContainInConsecutiveOrder("--coverage", "--coverage-output-format", "cobertura"); + } + + [Fact] + public async Task RunAsync_UseMicrosoftTestingPlatformWithArgumentCustomizationOverride_KeepsBothArguments() + { + // Arrange + var calls = new List(); + var context = new MtpTestBuildContext(CreateCakeContext(calls)); + var task = new ArgumentCustomizationTestTask(); + + // Act + await task.RunAsync(context); + + // Assert + calls + .Should() + .ContainSingle() + .Which + .Should() + .Contain("--custom-argument") + .And.ContainInConsecutiveOrder("--report-trx", "--report-trx-filename", "\"Project.Tests.trx\"") + .And.ContainInConsecutiveOrder("--coverage", "--coverage-output-format", "cobertura"); + } } From ad2195d0baab32e372c05dbc103304c174b2f732 Mon Sep 17 00:00:00 2001 From: darthsharp <48331467+darthsharp@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:30:57 +0200 Subject: [PATCH 2/2] Upgrade NuGet package versions in `Directory.Packages.props` for `coverlet.collector`, `log4net`, and `System.IO.Abstractions` to enhance compatibility and security. --- Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 2b873f95..c7ff8cae 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -7,11 +7,11 @@ - + - + @@ -45,8 +45,8 @@ - - + +