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
10 changes: 10 additions & 0 deletions TUnit.Engine/Extensions/TestApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using TUnit.Engine.Framework;
using TUnit.Engine.Reporters;
using Microsoft.Testing.Platform.CommandLine;
using Microsoft.Testing.Platform.Configurations;

#pragma warning disable TPEXP

Expand Down Expand Up @@ -79,6 +80,11 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
{
junitReporter.SetOutputPath(pathArgs[0]);
}

// 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
junitReporter.SetResultsDirectory(serviceProvider.GetRequiredService<IConfiguration>().GetTestResultDirectory());

return junitReporter;
});
testApplicationBuilder.TestHost.AddTestHostApplicationLifetime(_ => junitReporter);
Expand Down Expand Up @@ -106,6 +112,10 @@ public static void AddTUnit(this ITestApplicationBuilder testApplicationBuilder)
// 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
htmlReporter.SetResultsDirectory(serviceProvider.GetRequiredService<IConfiguration>().GetTestResultDirectory());

return htmlReporter;
});
}
Expand Down
22 changes: 15 additions & 7 deletions TUnit.Engine/Reporters/Html/HtmlReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ internal sealed class HtmlReporter(IExtension extension) : IDataConsumer, IDataP
{
private string? _outputPath;
private IMessageBus? _messageBus;
private string _resultsDirectory = "TestResults";
private readonly ConcurrentDictionary<string, ConcurrentQueue<TestNodeUpdateMessage>> _updates = [];

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

public Task BeforeRunAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrEmpty(_outputPath))
{
_outputPath = GetDefaultOutputPath();
}

#if NET
_activityCollector = new ActivityCollector();
_activityCollector.Start();
Expand Down Expand Up @@ -117,6 +113,11 @@ public async Task OnTestSessionFinishingAsync(ITestSessionContext testSessionCon
return;
}

if (string.IsNullOrEmpty(_outputPath))
{
_outputPath = GetDefaultOutputPath();
}

var outputPath = _outputPath!;
// WriteFileAsync returns false if all retry attempts are exhausted (locked file, bad path, etc.).
// Artifact publishing is gated on a successful write — no file means no artifact.
Expand Down Expand Up @@ -178,6 +179,13 @@ internal void SetMessageBus(IMessageBus? messageBus)
_messageBus = messageBus;
}

// Called by the AddTestSessionLifetimeHandler factory at startup, before any session events fire,
// so _resultsDirectory is guaranteed to be set before OnTestSessionFinishingAsync is invoked.
internal void SetResultsDirectory(string path)
{
_resultsDirectory = path;
}

private ReportData BuildReportData()
{
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
Expand Down Expand Up @@ -527,13 +535,13 @@ private static (string Status, ReportExceptionData? Exception, string? SkipReaso
};
}

private static string GetDefaultOutputPath()
private string GetDefaultOutputPath()
{
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";
var sanitizedName = string.Concat(assemblyName.Split(Path.GetInvalidFileNameChars()));
var os = GetShortOsName();
var tfm = GetShortFrameworkName();
return Path.GetFullPath(Path.Combine("TestResults", $"{sanitizedName}-{os}-{tfm}-report.html"));
return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-{os}-{tfm}-report.html"));
}

private static string GetShortOsName()
Expand Down
32 changes: 20 additions & 12 deletions TUnit.Engine/Reporters/JUnitReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class JUnitReporter(IExtension extension) : IDataConsumer, ITestHostAppli
{
private string _outputPath = null!;
private bool _isEnabled;
private string _resultsDirectory = "TestResults";

public async Task<bool> IsEnabledAsync()
{
Expand All @@ -35,16 +36,6 @@ public async Task<bool> IsEnabledAsync()
return false;
}

// Determine output path (only if not already set via command-line argument)
if (string.IsNullOrEmpty(_outputPath))
{
var envPath = Environment.GetEnvironmentVariable(EnvironmentConstants.JUnitXmlOutputPath);

_outputPath = envPath is not null
? PathValidator.ValidateAndNormalizePath(envPath, nameof(EnvironmentConstants.JUnitXmlOutputPath))
: GetDefaultOutputPath();
}

_isEnabled = true;
return await extension.IsEnabledAsync();
}
Expand Down Expand Up @@ -97,6 +88,16 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellation)
return;
}

// Determine output path (only if not already set via command-line argument)
if (string.IsNullOrEmpty(_outputPath))
{
var envPath = Environment.GetEnvironmentVariable(EnvironmentConstants.JUnitXmlOutputPath);

_outputPath = envPath is not null
? PathValidator.ValidateAndNormalizePath(envPath, nameof(EnvironmentConstants.JUnitXmlOutputPath))
: GetDefaultOutputPath();
}

// Write to file with retry logic
await WriteXmlFileAsync(_outputPath, xmlContent, cancellation);
}
Expand All @@ -108,14 +109,21 @@ internal void SetOutputPath(string path)
_outputPath = PathValidator.ValidateAndNormalizePath(path, nameof(path));
}

private static string GetDefaultOutputPath()
// Called by the AddTestSessionLifetimeHandler factory at startup, before any session events fire,
// so _resultsDirectory is guaranteed to be set before AfterRunAsync is invoked.
internal void SetResultsDirectory(string path)
{
_resultsDirectory = path;
}

private string GetDefaultOutputPath()
{
var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "TestResults";

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

return Path.GetFullPath(Path.Combine("TestResults", $"{sanitizedName}-junit.xml"));
return Path.GetFullPath(Path.Combine(_resultsDirectory, $"{sanitizedName}-junit.xml"));
}

private static async Task WriteXmlFileAsync(string path, string content, CancellationToken cancellationToken)
Expand Down
Loading