diff --git a/Directory.Packages.props b/Directory.Packages.props index bc42be3347f..1d35ef2cad4 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -54,6 +54,7 @@ + diff --git a/TUnit.CI.slnx b/TUnit.CI.slnx index d5cd80e607a..a0688c90109 100644 --- a/TUnit.CI.slnx +++ b/TUnit.CI.slnx @@ -112,6 +112,7 @@ + diff --git a/TUnit.Dev.slnx b/TUnit.Dev.slnx index 87c935fb3c7..180772f47fb 100644 --- a/TUnit.Dev.slnx +++ b/TUnit.Dev.slnx @@ -66,6 +66,7 @@ + diff --git a/TUnit.slnx b/TUnit.slnx index ac3754c10a9..40c186ec05f 100644 --- a/TUnit.slnx +++ b/TUnit.slnx @@ -88,6 +88,7 @@ + diff --git a/docs/docs/guides/html-report.md b/docs/docs/guides/html-report.md index 16c02002948..88fc42e52c9 100644 --- a/docs/docs/guides/html-report.md +++ b/docs/docs/guides/html-report.md @@ -39,10 +39,20 @@ Running many test projects and want **one combined report instead of one per pro ### Custom Output Path +When TUnit is the only HTML report provider, the existing option remains available: + ```bash dotnet run -- --report-html-filename my-custom-report.html ``` +When the `Microsoft.Testing.Extensions.HtmlReport` package is referenced, Microsoft owns the +`--report-html` and `--report-html-filename` options. TUnit automatically switches to its +namespaced filename option: + +```bash +dotnet run -- --tunit-report-html-filename my-custom-report.html +``` + ### Disable Report Generation Set the environment variable: @@ -57,7 +67,13 @@ For version-controlled project configuration, set `context.Settings.Reporting.Ht ### Deprecated: `--report-html` Flag -The `--report-html` flag is deprecated since the report is now generated by default. Using it will show a deprecation warning but will not cause an error. +Without `Microsoft.Testing.Extensions.HtmlReport`, TUnit continues to accept the deprecated +`--report-html` flag for compatibility. TUnit's report is generated automatically, so the flag +has no effect beyond displaying a deprecation warning. Use `TUNIT_DISABLE_HTML_REPORTER=true` +when you need to disable TUnit's report generation. + +When `Microsoft.Testing.Extensions.HtmlReport` is referenced, TUnit does not register either +legacy option; their behavior is provided by the Microsoft extension. ## GitHub Actions Integration diff --git a/docs/docs/reference/command-line-flags.md b/docs/docs/reference/command-line-flags.md index ef2d60f2ca9..25e88bab6a7 100644 --- a/docs/docs/reference/command-line-flags.md +++ b/docs/docs/reference/command-line-flags.md @@ -136,12 +136,19 @@ Please note that for the coverage and trx report, you need to install [additiona File name prefix for the JSON report produced by --output-json. --report-html - (Deprecated) The HTML report is now generated by default. + Available when Microsoft.Testing.Extensions.HtmlReport is not referenced. + (Deprecated) The TUnit HTML report is now generated by default. Disable it with TUNIT_DISABLE_HTML_REPORTER or set context.Settings.Reporting.HtmlReportEnabled = false. --report-html-filename - Path for the HTML test report file + Available when Microsoft.Testing.Extensions.HtmlReport is not referenced. + Path for the TUnit HTML test report file + (default: TestResults/{'{AssemblyName}'}-report.html). + + --tunit-report-html-filename + Available when Microsoft.Testing.Extensions.HtmlReport is referenced. + Path for the TUnit HTML test report file (default: TestResults/{'{AssemblyName}'}-report.html). --junit-output-path diff --git a/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs b/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs index 2f1b7fd7499..f76e3febe22 100644 --- a/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs +++ b/src/TUnit.Engine/CommandLineProviders/HtmlReporterCommandProvider.cs @@ -1,13 +1,19 @@ using Microsoft.Testing.Platform.CommandLine; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.CommandLine; +using TUnit.Engine.Extensions; namespace TUnit.Engine.CommandLineProviders; -internal class HtmlReporterCommandProvider(IExtension extension) : ICommandLineOptionsProvider +internal class HtmlReporterCommandProvider(IExtension extension, HtmlCliMode htmlCliMode = HtmlCliMode.Default) : ICommandLineOptionsProvider { public const string ReportHtml = "report-html"; public const string ReportHtmlFilename = "report-html-filename"; + public const string TUnitReportHtmlFilename = "tunit-report-html-filename"; + + public string ReportHtmlFilenameOption => htmlCliMode == HtmlCliMode.Namespaced + ? TUnitReportHtmlFilename + : ReportHtmlFilename; public Task IsEnabledAsync() => extension.IsEnabledAsync(); @@ -21,6 +27,18 @@ internal class HtmlReporterCommandProvider(IExtension extension) : ICommandLineO public IReadOnlyCollection GetCommandLineOptions() { + if (htmlCliMode == HtmlCliMode.Namespaced) + { + return + [ + new CommandLineOption( + TUnitReportHtmlFilename, + "Path for the HTML test report file (default: TestResults/{AssemblyName}-report.html)", + ArgumentArity.ExactlyOne, + false), + ]; + } + return [ new CommandLineOption( @@ -32,7 +50,7 @@ public IReadOnlyCollection GetCommandLineOptions() ReportHtmlFilename, "Path for the HTML test report file (default: TestResults/{AssemblyName}-report.html)", ArgumentArity.ExactlyOne, - false) + false), ]; } @@ -40,7 +58,7 @@ public Task ValidateOptionArgumentsAsync( CommandLineOption commandOption, string[] arguments) { - if (commandOption.Name == ReportHtmlFilename && arguments.Length != 1) + if (commandOption.Name == ReportHtmlFilenameOption && arguments.Length != 1) { return ValidationResult.InvalidTask("A single output path must be provided for the HTML report"); } diff --git a/src/TUnit.Engine/Extensions/HtmlCliMode.cs b/src/TUnit.Engine/Extensions/HtmlCliMode.cs new file mode 100644 index 00000000000..5e160ca7bcc --- /dev/null +++ b/src/TUnit.Engine/Extensions/HtmlCliMode.cs @@ -0,0 +1,7 @@ +namespace TUnit.Engine.Extensions; + +public enum HtmlCliMode +{ + Default, + Namespaced, +} diff --git a/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs b/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs index 2a69e0b2901..cc181f856f6 100644 --- a/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs +++ b/src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs @@ -15,7 +15,10 @@ namespace TUnit.Engine.Extensions; public static class TestApplicationBuilderExtensions { - public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) + public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) => + testApplicationBuilder.AddTUnit(HtmlCliMode.Default); + + public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder, HtmlCliMode htmlCliMode) { TUnitExtension extension = new(); @@ -26,7 +29,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) var junitReporterCommandProvider = new JUnitReporterCommandProvider(extension); var htmlReporter = new Reporters.Html.HtmlReporter(extension); - var htmlReporterCommandProvider = new HtmlReporterCommandProvider(extension); + var htmlReporterCommandProvider = new HtmlReporterCommandProvider(extension, htmlCliMode); htmlReporter.SetGitHubReporter(githubReporter); @@ -80,7 +83,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) } // Set results directory as specified by --results-directory, - // so it can be used in the default output path if --report-html-filename is not provided + // so it can be used in the default output path if --junit-output-path is not provided junitReporter.SetResultsDirectory(serviceProvider.GetRequiredService().GetTestResultDirectory()); return junitReporter; @@ -96,14 +99,14 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) var commandLineOptions = serviceProvider.GetRequiredService(); // Deprecated: --report-html is now a no-op (reporter is always-on) - if (commandLineOptions.IsOptionSet(HtmlReporterCommandProvider.ReportHtml)) + if (htmlCliMode == HtmlCliMode.Default && commandLineOptions.IsOptionSet(HtmlReporterCommandProvider.ReportHtml)) { Console.WriteLine("Warning: --report-html is deprecated. The HTML report is now generated by default. Use TUNIT_DISABLE_HTML_REPORTER=true to disable."); } - if (commandLineOptions.TryGetOptionArgumentList(HtmlReporterCommandProvider.ReportHtmlFilename, out var pathArgs)) + if (commandLineOptions.TryGetOptionArgumentList(htmlReporterCommandProvider.ReportHtmlFilenameOption, out var pathArgs)) { - htmlReporter.SetOutputPath(Helpers.PathValidator.ValidateAndNormalizePath(pathArgs[0], HtmlReporterCommandProvider.ReportHtmlFilename)); + htmlReporter.SetOutputPath(Helpers.PathValidator.ValidateAndNormalizePath(pathArgs[0], htmlReporterCommandProvider.ReportHtmlFilenameOption)); } // Inject the application-level message bus so PublishArtifactAsync works in @@ -111,7 +114,7 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder) htmlReporter.SetMessageBus(serviceProvider.GetMessageBus()); // Set results directory as specified by --results-directory, - // so it can be used in the default output path if --report-html-filename is not provided + // so it can be used in the default output path if the HTML report filename option is not provided htmlReporter.SetResultsDirectory(serviceProvider.GetRequiredService().GetTestResultDirectory()); return htmlReporter; diff --git a/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs b/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs index 083a88346eb..975f1818969 100644 --- a/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs +++ b/src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs @@ -10,3 +10,11 @@ public static void AddExtensions( string[] _) => testApplicationBuilder.AddTUnit(); } + +public static class NamespacedHtmlReportTestingPlatformBuilderHook +{ + public static void AddExtensions( + ITestApplicationBuilder testApplicationBuilder, + string[] _) => + testApplicationBuilder.AddTUnit(HtmlCliMode.Namespaced); +} diff --git a/src/TUnit.Engine/TUnit.Engine.props b/src/TUnit.Engine/TUnit.Engine.props index 296dc59a7ac..eae66ae0cfc 100644 --- a/src/TUnit.Engine/TUnit.Engine.props +++ b/src/TUnit.Engine/TUnit.Engine.props @@ -44,4 +44,15 @@ + + + + + TUnit.Engine.Framework.NamespacedHtmlReportTestingPlatformBuilderHook + + + + diff --git a/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs new file mode 100644 index 00000000000..4a1839a641e --- /dev/null +++ b/tests/TUnit.Engine.Tests/HtmlReportCliTests.cs @@ -0,0 +1,132 @@ +using CliWrap; +using CliWrap.Buffered; +using Shouldly; +using TUnit.Engine.Tests.Enums; + +namespace TUnit.Engine.Tests; + +public class HtmlReportCliTests(TestMode testMode) : InvokableTestBase(testMode) +{ + [Test] + public async Task Combined_Html_Report_Packages_Accept_TUnit_Namespaced_Option() + { + await RunTestsWithFilter( + "/*/*/BasicTests/SynchronousTest", + [ + result => result.ResultSummary.Outcome.ShouldBe("Completed"), + result => result.ResultSummary.Counters.Total.ShouldBe(1), + result => result.ResultSummary.Counters.Passed.ShouldBe(1), + result => result.ResultSummary.Counters.Failed.ShouldBe(0), + ], + new RunOptions() + .WithArgument("--tunit-report-html-filename") + .WithArgument("tunit-report.html")); + } +} + +public class DefaultHtmlReportCliTests +{ + [Test] + [Arguments(false)] + [Arguments(true)] + public async Task TUnit_Only_Project_Accepts_Legacy_Options(bool reflection) + { + var tempDirectory = CreateTempDirectory(); + var reportPath = Path.Combine(tempDirectory, "custom-report.html"); + + try + { + var result = await RunTUnitOnlyProject( + tempDirectory, + reflection, + "--report-html", + "--report-html-filename", + reportPath); + + AssertSuccessful(result); + File.Exists(reportPath).ShouldBeTrue(); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + [Test] + [Arguments(false)] + [Arguments(true)] + public async Task TUnit_Only_Project_Uses_Default_Output_Path(bool reflection) + { + var tempDirectory = CreateTempDirectory(); + + try + { + var result = await RunTUnitOnlyProject(tempDirectory, reflection); + + AssertSuccessful(result); + + var reports = Directory.GetFiles(tempDirectory, "*-report.html"); + reports.Length.ShouldBe(1); + Path.GetFileName(reports[0]).ShouldStartWith("TUnit.TestProject.HtmlReportDefaults-"); + } + finally + { + Directory.Delete(tempDirectory, recursive: true); + } + } + + private static string CreateTempDirectory() + { + var path = Path.Combine(Path.GetTempPath(), $"tunit-html-defaults-{Guid.NewGuid():N}"); + Directory.CreateDirectory(path); + return path; + } + + private static Task RunTUnitOnlyProject( + string resultsDirectory, + bool reflection, + params string[] htmlArguments) + { + var testProject = Sourcy.DotNet.Projects.TUnit_TestProject_HtmlReportDefaults; + List arguments = + [ + "run", + "--no-build", + "--project", testProject.FullName, + "--framework", "net10.0", + "--configuration", "Release", + "--", + "--treenode-filter", "/*/*/DefaultHtmlReportTests/Pass", + "--results-directory", resultsDirectory, + ..htmlArguments, + ]; + + if (reflection) + { + arguments.Add("--reflection"); + } + + return Cli.Wrap("dotnet") + .WithArguments(arguments) + .WithWorkingDirectory(testProject.DirectoryName!) + .WithEnvironmentVariables(new Dictionary + { + ["TUNIT_DISABLE_HTML_REPORTER"] = "false", + ["TUNIT_DISABLE_JSON_REPORT"] = "true", + ["TUNIT_DISABLE_ARTIFACT_UPLOAD"] = "true", + }) + .WithValidation(CommandResultValidation.None) + .ExecuteBufferedAsync(); + } + + private static void AssertSuccessful(BufferedCommandResult result) + { + result.ExitCode.ShouldBe(0, $""" + Standard output: + {result.StandardOutput} + + Standard error: + {result.StandardError} + """); + } +} diff --git a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs index 05123ed29cc..f47fc4619ab 100644 --- a/tests/TUnit.Engine.Tests/HtmlReporterTests.cs +++ b/tests/TUnit.Engine.Tests/HtmlReporterTests.cs @@ -3,11 +3,16 @@ using System.IO.Compression; using System.Text; using System.Text.RegularExpressions; +using Microsoft.Testing.Platform.CommandLine; +using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions.CommandLine; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.TestHost; using Shouldly; using TUnit.Core; using TUnit.Core.Enums; +using TUnit.Engine.CommandLineProviders; +using TUnit.Engine.Extensions; using TUnit.Engine.Reporters; using TUnit.Engine.Reporters.Html; @@ -15,6 +20,65 @@ namespace TUnit.Engine.Tests; public class HtmlReporterTests { + [Test] + public void CommandProvider_DefaultMode_Exposes_Legacy_Options() + { + var provider = new HtmlReporterCommandProvider(new MockExtension()); + + var options = provider.GetCommandLineOptions(); + + options.Count.ShouldBe(2); + options.Single(x => x.Name == "report-html").Arity.ShouldBe(ArgumentArity.Zero); + options.Single(x => x.Name == "report-html-filename").Arity.ShouldBe(ArgumentArity.ExactlyOne); + options.Select(x => x.Name).ShouldNotContain("tunit-report-html-filename"); + } + + [Test] + public void CommandProvider_NamespacedMode_Exposes_Only_TUnitSpecific_Filename_Option() + { + var provider = new HtmlReporterCommandProvider(new MockExtension(), HtmlCliMode.Namespaced); + + var options = provider.GetCommandLineOptions(); + + options.Count.ShouldBe(1); + options.Single().Name.ShouldBe("tunit-report-html-filename"); + options.Single().Arity.ShouldBe(ArgumentArity.ExactlyOne); + options.Select(x => x.Name).ShouldNotContain("report-html"); + options.Select(x => x.Name).ShouldNotContain("report-html-filename"); + } + + [Test] + public async Task CommandProvider_Accepts_One_Filename_Argument() + { + foreach (var mode in Enum.GetValues()) + { + var provider = new HtmlReporterCommandProvider(new MockExtension(), mode); + var option = provider.GetCommandLineOptions().Single(x => x.Arity == ArgumentArity.ExactlyOne); + + var result = await provider.ValidateOptionArgumentsAsync(option, ["report.html"]); + + result.IsValid.ShouldBeTrue(); + } + } + + [Test] + [Arguments(0)] + [Arguments(2)] + public async Task CommandProvider_Rejects_Filename_Argument_Count_Other_Than_One(int argumentCount) + { + foreach (var mode in Enum.GetValues()) + { + var provider = new HtmlReporterCommandProvider(new MockExtension(), mode); + var option = provider.GetCommandLineOptions().Single(x => x.Arity == ArgumentArity.ExactlyOne); + var arguments = Enumerable.Repeat("report.html", argumentCount).ToArray(); + + var result = await provider.ValidateOptionArgumentsAsync(option, arguments); + + result.IsValid.ShouldBeFalse(); + result.ErrorMessage.ShouldBe("A single output path must be provided for the HTML report"); + } + } + [Test] public void HtmlReporter_Implements_IDataProducer() { diff --git a/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs index 1ca19d4ecfe..9d2f34a94a6 100644 --- a/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs +++ b/tests/TUnit.Engine.Tests/ReportingSettingsTests.cs @@ -15,7 +15,7 @@ public async Task Discovery_Hook_Can_Disable_Reporting(CancellationToken cancell try { var options = new RunOptions() - .WithArgument("--report-html-filename") + .WithArgument("--tunit-report-html-filename") .WithArgument(reportPath) .WithEnvironmentVariable("TUNIT_DISABLE_HTML_REPORTER", "false") .WithEnvironmentVariable("TUNIT_DISABLE_JSON_REPORT", "false") diff --git a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj index a16423b43f1..eae576b9440 100644 --- a/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj +++ b/tests/TUnit.Engine.Tests/TUnit.Engine.Tests.csproj @@ -28,6 +28,7 @@ + diff --git a/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs b/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs new file mode 100644 index 00000000000..f577e26add0 --- /dev/null +++ b/tests/TUnit.TestProject.HtmlReportDefaults/DefaultHtmlReportTests.cs @@ -0,0 +1,9 @@ +namespace TUnit.TestProject.HtmlReportDefaults; + +public class DefaultHtmlReportTests +{ + [Test] + public void Pass() + { + } +} diff --git a/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj b/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj new file mode 100644 index 00000000000..9f7b9c9c358 --- /dev/null +++ b/tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj @@ -0,0 +1,15 @@ + + + + + + net10.0 + + + + + + + + + diff --git a/tests/TUnit.TestProject/TUnit.TestProject.csproj b/tests/TUnit.TestProject/TUnit.TestProject.csproj index bb2f9a84854..a18335e32a7 100644 --- a/tests/TUnit.TestProject/TUnit.TestProject.csproj +++ b/tests/TUnit.TestProject/TUnit.TestProject.csproj @@ -30,6 +30,7 @@ +