Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.10.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.CrashDump" Version="2.4.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.HangDump" Version="2.4.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.HtmlReport" Version="2.4.0" />
<PackageVersion Include="Microsoft.Testing.Extensions.Telemetry" Version="2.4.0" />
<!-- 2.4.0's controller-backed TRX path kills the test host on Ctrl+C instead of allowing hook cleanup. -->
<PackageVersion Include="Microsoft.Testing.Extensions.TrxReport" Version="2.3.3" />
Expand Down
1 change: 1 addition & 0 deletions TUnit.CI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
<Project Path="examples/TUnit.Example.FsCheck.TestProject/TUnit.Example.FsCheck.TestProject.csproj" />
<Project Path="tests/TUnit.TestProject/TUnit.TestProject.csproj" />
<Project Path="tests/TUnit.TestProject.FSharp/TUnit.TestProject.FSharp.fsproj" />
<Project Path="tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj" />
<Project Path="tests/TUnit.TestProject.Library/TUnit.TestProject.Library.csproj" />
<Project Path="tests/TUnit.TestProject.VB.NET/TUnit.TestProject.VB.NET.vbproj" />
</Folder>
Expand Down
1 change: 1 addition & 0 deletions TUnit.Dev.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
<Project Path="tests/TUnit.OpenTelemetry.Tests/TUnit.OpenTelemetry.Tests.csproj" />
<Project Path="tests/TUnit.PublicAPI/TUnit.PublicAPI.csproj" />
<Project Path="tests/TUnit.RpcTests/TUnit.RpcTests.csproj" />
<Project Path="tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj" />
<Project Path="tests/TUnit.TestProject.Library/TUnit.TestProject.Library.csproj" />
<Project Path="tests/TUnit.UnitTests/TUnit.UnitTests.csproj" />
</Folder>
Expand Down
1 change: 1 addition & 0 deletions TUnit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
<Project Path="tests/TUnit.RpcTests/TUnit.RpcTests.csproj" />
<Project Path="tests/TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj" />
<Project Path="tests/TUnit.Templates.Tests/TUnit.Templates.Tests.csproj" />
<Project Path="tests/TUnit.TestProject.HtmlReportDefaults/TUnit.TestProject.HtmlReportDefaults.csproj" />
<Project Path="tests/TUnit.UnitTests/TUnit.UnitTests.csproj" />
<Project Path="tests/TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj" />
<Project Path="tests/TUnit.Aspire.Tests.ApiService/TUnit.Aspire.Tests.ApiService.csproj" />
Expand Down
18 changes: 17 additions & 1 deletion docs/docs/guides/html-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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

Expand Down
11 changes: 9 additions & 2 deletions docs/docs/reference/command-line-flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<bool> IsEnabledAsync() => extension.IsEnabledAsync();

Expand All @@ -21,6 +27,18 @@ internal class HtmlReporterCommandProvider(IExtension extension) : ICommandLineO

public IReadOnlyCollection<CommandLineOption> 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(
Expand All @@ -32,15 +50,15 @@ public IReadOnlyCollection<CommandLineOption> GetCommandLineOptions()
ReportHtmlFilename,
"Path for the HTML test report file (default: TestResults/{AssemblyName}-report.html)",
ArgumentArity.ExactlyOne,
false)
false),
];
}

public Task<ValidationResult> 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");
}
Expand Down
7 changes: 7 additions & 0 deletions src/TUnit.Engine/Extensions/HtmlCliMode.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TUnit.Engine.Extensions;

public enum HtmlCliMode
{
Default,
Namespaced,
}
17 changes: 10 additions & 7 deletions src/TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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);

Expand Down Expand Up @@ -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<IConfiguration>().GetTestResultDirectory());

return junitReporter;
Expand All @@ -96,22 +99,22 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
var commandLineOptions = serviceProvider.GetRequiredService<ICommandLineOptions>();

// 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
// OnTestSessionFinishingAsync (called before the bus is drained/disabled).
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<IConfiguration>().GetTestResultDirectory());

return htmlReporter;
Expand Down
8 changes: 8 additions & 0 deletions src/TUnit.Engine/Framework/TestingPlatformBuilderHook.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
11 changes: 11 additions & 0 deletions src/TUnit.Engine/TUnit.Engine.props
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,15 @@
</TestingPlatformBuilderHook>
</ItemGroup>

<Target Name="_TUnitConfigureHtmlReportCommandLineOptions"
BeforeTargets="_GenerateSelfRegisteredExtensionsFileInputCache">
<!-- Microsoft.Testing.Extensions.HtmlReport declares this builder-hook identity in its
build props. If upstream changes it, update this condition and integration tests. -->
<ItemGroup Condition="'@(TestingPlatformBuilderHook->WithMetadataValue('Identity', 'A6E2BCC3-9B4D-4B6D-8AE3-2C1E12A54F4D'))' != ''">
<TestingPlatformBuilderHook Condition="'%(Identity)' == '6ADF853A-6945-4A06-9A4B-D99BC1DC1094'">
<TypeFullName>TUnit.Engine.Framework.NamespacedHtmlReportTestingPlatformBuilderHook</TypeFullName>
</TestingPlatformBuilderHook>
</ItemGroup>
</Target>

</Project>
132 changes: 132 additions & 0 deletions tests/TUnit.Engine.Tests/HtmlReportCliTests.cs
Original file line number Diff line number Diff line change
@@ -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<BufferedCommandResult> RunTUnitOnlyProject(
string resultsDirectory,
bool reflection,
params string[] htmlArguments)
{
var testProject = Sourcy.DotNet.Projects.TUnit_TestProject_HtmlReportDefaults;
List<string> 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<string, string?>
{
["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}
""");
}
}
Loading
Loading