Skip to content

Commit 4573531

Browse files
authored
feat: use results directory provided by Microsoft Testing Platform in HtmlReporter (#5294)
* feat: use results directory provided by MTP in HtmlReporter * feat: use results directory provided by MTP in JUnitReporter
1 parent e5bf845 commit 4573531

3 files changed

Lines changed: 45 additions & 19 deletions

File tree

TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using TUnit.Engine.Framework;
88
using TUnit.Engine.Reporters;
99
using Microsoft.Testing.Platform.CommandLine;
10+
using Microsoft.Testing.Platform.Configurations;
1011

1112
#pragma warning disable TPEXP
1213

@@ -79,6 +80,11 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
7980
{
8081
junitReporter.SetOutputPath(pathArgs[0]);
8182
}
83+
84+
// Set results directory as specified by --results-directory,
85+
// so it can be used in the default output path if --report-html-filename is not provided
86+
junitReporter.SetResultsDirectory(serviceProvider.GetRequiredService<IConfiguration>().GetTestResultDirectory());
87+
8288
return junitReporter;
8389
});
8490
testApplicationBuilder.TestHost.AddTestHostApplicationLifetime(_ => junitReporter);
@@ -106,6 +112,10 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
106112
// OnTestSessionFinishingAsync (called before the bus is drained/disabled).
107113
htmlReporter.SetMessageBus(serviceProvider.GetMessageBus());
108114

115+
// Set results directory as specified by --results-directory,
116+
// so it can be used in the default output path if --report-html-filename is not provided
117+
htmlReporter.SetResultsDirectory(serviceProvider.GetRequiredService<IConfiguration>().GetTestResultDirectory());
118+
109119
return htmlReporter;
110120
});
111121
}

TUnit.Engine/Reporters/Html/HtmlReporter.cs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ internal sealed class HtmlReporter(IExtension extension) : IDataConsumer, IDataP
2323
{
2424
private string? _outputPath;
2525
private IMessageBus? _messageBus;
26+
private string _resultsDirectory = "TestResults";
2627
private readonly ConcurrentDictionary<string, ConcurrentQueue<TestNodeUpdateMessage>> _updates = [];
2728

2829
#if NET
@@ -64,11 +65,6 @@ public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationTo
6465

6566
public Task BeforeRunAsync(CancellationToken cancellationToken)
6667
{
67-
if (string.IsNullOrEmpty(_outputPath))
68-
{
69-
_outputPath = GetDefaultOutputPath();
70-
}
71-
7268
#if NET
7369
_activityCollector = new ActivityCollector();
7470
_activityCollector.Start();
@@ -117,6 +113,11 @@ public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionCon
117113
return;
118114
}
119115

116+
if (string.IsNullOrEmpty(_outputPath))
117+
{
118+
_outputPath = GetDefaultOutputPath();
119+
}
120+
120121
var outputPath = _outputPath!;
121122
// WriteFileAsync returns false if all retry attempts are exhausted (locked file, bad path, etc.).
122123
// Artifact publishing is gated on a successful write — no file means no artifact.
@@ -178,6 +179,13 @@ internal void SetMessageBus(IMessageBus? messageBus)
178179
_messageBus = messageBus;
179180
}
180181

182+
// Called by the AddTestSessionLifetimeHandler factory at startup, before any session events fire,
183+
// so _resultsDirectory is guaranteed to be set before OnTestSessionFinishingAsync is invoked.
184+
internal void SetResultsDirectory(string path)
185+
{
186+
_resultsDirectory = path;
187+
}
188+
181189
private ReportData BuildReportData()
182190
{
183191
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
@@ -527,13 +535,13 @@ private static (string Status, ReportExceptionData? Exception, string? SkipReaso
527535
};
528536
}
529537

530-
private static string GetDefaultOutputPath()
538+
private string GetDefaultOutputPath()
531539
{
532540
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
533541
var sanitizedName = string.Concat(assemblyName.Split(Path.GetInvalidFileNameChars()));
534542
var os = GetShortOsName();
535543
var tfm = GetShortFrameworkName();
536-
return Path.GetFullPath(Path.Combine("TestResults", $"{sanitizedName}-{os}-{tfm}-report.html"));
544+
return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-{os}-{tfm}-report.html"));
537545
}
538546

539547
private static string GetShortOsName()

TUnit.Engine/Reporters/JUnitReporter.cs

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ public class JUnitReporter(IExtension extension) : IDataConsumer, ITestHostAppli
1616
{
1717
private string _outputPath = null!;
1818
private bool _isEnabled;
19+
private string _resultsDirectory = "TestResults";
1920

2021
public async Task<bool> IsEnabledAsync()
2122
{
@@ -35,16 +36,6 @@ public async Task<bool> IsEnabledAsync()
3536
return false;
3637
}
3738

38-
// Determine output path (only if not already set via command-line argument)
39-
if (string.IsNullOrEmpty(_outputPath))
40-
{
41-
var envPath = Environment.GetEnvironmentVariable(EnvironmentConstants.JUnitXmlOutputPath);
42-
43-
_outputPath = envPath is not null
44-
? PathValidator.ValidateAndNormalizePath(envPath, nameof(EnvironmentConstants.JUnitXmlOutputPath))
45-
: GetDefaultOutputPath();
46-
}
47-
4839
_isEnabled = true;
4940
return await extension.IsEnabledAsync();
5041
}
@@ -97,6 +88,16 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellation)
9788
return;
9889
}
9990

91+
// Determine output path (only if not already set via command-line argument)
92+
if (string.IsNullOrEmpty(_outputPath))
93+
{
94+
var envPath = Environment.GetEnvironmentVariable(EnvironmentConstants.JUnitXmlOutputPath);
95+
96+
_outputPath = envPath is not null
97+
? PathValidator.ValidateAndNormalizePath(envPath, nameof(EnvironmentConstants.JUnitXmlOutputPath))
98+
: GetDefaultOutputPath();
99+
}
100+
100101
// Write to file with retry logic
101102
await WriteXmlFileAsync(_outputPath, xmlContent, cancellation);
102103
}
@@ -108,14 +109,21 @@ internal void SetOutputPath(string path)
108109
_outputPath = PathValidator.ValidateAndNormalizePath(path, nameof(path));
109110
}
110111

111-
private static string GetDefaultOutputPath()
112+
// Called by the AddTestSessionLifetimeHandler factory at startup, before any session events fire,
113+
// so _resultsDirectory is guaranteed to be set before AfterRunAsync is invoked.
114+
internal void SetResultsDirectory(string path)
115+
{
116+
_resultsDirectory = path;
117+
}
118+
119+
private string GetDefaultOutputPath()
112120
{
113121
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
114122

115123
// Sanitize assembly name to remove any characters that could be used for path traversal
116124
var sanitizedName = string.Concat(assemblyName.Split(Path.GetInvalidFileNameChars()));
117125

118-
return Path.GetFullPath(Path.Combine("TestResults", $"{sanitizedName}-junit.xml"));
126+
return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-junit.xml"));
119127
}
120128

121129
private static async Task WriteXmlFileAsync(string path, string content, CancellationToken cancellationToken)

0 commit comments

Comments
 (0)