Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions TUnit.Core/TestContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,11 @@ public ContextScope MakeCurrent()
/// <returns>The matching <see cref="TestContext"/>, or <c>null</c>.</returns>
public static TestContext? GetById(string id) => _testContextsById.GetValueOrDefault(id);

/// <summary>
/// Removes a test context from the static registry. Called when test execution completes.
/// </summary>
internal static void RemoveById(string id) => _testContextsById.TryRemove(id, out _);

/// <summary>
/// Gets the dictionary of test parameters indexed by parameter name.
/// </summary>
Expand Down
37 changes: 24 additions & 13 deletions TUnit.Engine/Models/ConstraintKeysCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,19 @@ public bool Equals(ConstraintKeysCollection? other)
return false;
}

return _constraintKeys.Intersect(other._constraintKeys).Any();
// Constraint key lists are typically 1-2 items; nested loop beats HashSet allocation
foreach (var key in _constraintKeys)
{
foreach (var otherKey in other._constraintKeys)
{
if (StringComparer.Ordinal.Equals(key, otherKey))
{
return true;
}
}
}

return false;
}

public int CompareTo(ConstraintKeysCollection? other)
Expand Down Expand Up @@ -62,7 +74,14 @@ public override bool Equals(object? obj)

public override int GetHashCode()
{
return 1;
var hash = 0;

foreach (var key in _constraintKeys)
{
hash ^= StringComparer.Ordinal.GetHashCode(key);
}

return hash;
}

public int CompareTo(object? obj)
Expand Down Expand Up @@ -93,23 +112,15 @@ public bool Equals(ConstraintKeysCollection? x, ConstraintKeysCollection? y)
return true;
}

if (x == null)
if (x == null || y == null)
{
return false;
}

if (y == null)
{
return false;
}

return x._constraintKeys.Intersect(y._constraintKeys).Any();
return x.Equals(y);
}

public int GetHashCode(ConstraintKeysCollection obj)
{
return 1;
}
public int GetHashCode(ConstraintKeysCollection obj) => obj.GetHashCode();
}

public static IEqualityComparer<ConstraintKeysCollection> ConstraintKeysCollectionComparer { get; } = new ConstraintKeysCollectionEqualityComparer();
Expand Down
94 changes: 53 additions & 41 deletions TUnit.Engine/Reporters/GitHubReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,16 @@ public async Task<bool> IsEnabledAsync()

public string Description => extension.Description;

private readonly ConcurrentDictionary<string, List<TestNodeUpdateMessage>> _updates = [];
private readonly ConcurrentDictionary<string, ConcurrentQueue<TestNodeUpdateMessage>> _updates = [];
private readonly ConcurrentDictionary<string, TestNodeUpdateMessage> _latestUpdates = [];

public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage) value;

_updates.GetOrAdd(testNodeUpdateMessage.TestNode.Uid.Value, []).Add(testNodeUpdateMessage);
var uid = testNodeUpdateMessage.TestNode.Uid.Value;
_updates.GetOrAdd(uid, static _ => []).Enqueue(testNodeUpdateMessage);
_latestUpdates[uid] = testNodeUpdateMessage;

return Task.CompletedTask;
}
Expand All @@ -104,42 +107,51 @@ public Task AfterRunAsync(int exitCode, CancellationToken cancellation)
?.FrameworkDisplayName
?? RuntimeInformation.FrameworkDescription;

var last = new Dictionary<string, TestNodeUpdateMessage>(_updates.Count);
foreach (var kvp in _updates)
var last = new Dictionary<string, TestNodeUpdateMessage>(_latestUpdates.Count);
foreach (var kvp in _latestUpdates)
{
if (kvp.Value.Count > 0)
{
last[kvp.Key] = kvp.Value[kvp.Value.Count - 1];
}
last[kvp.Key] = kvp.Value;
}

var passedCount = last.Count(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is PassedTestNodeStateProperty));
var passedCount = 0;
var failed = new List<KeyValuePair<string, TestNodeUpdateMessage>>();
var cancelled = new List<KeyValuePair<string, TestNodeUpdateMessage>>();
var timeout = new List<KeyValuePair<string, TestNodeUpdateMessage>>();
var skipped = new List<KeyValuePair<string, TestNodeUpdateMessage>>();
var inProgress = new List<KeyValuePair<string, TestNodeUpdateMessage>>();

var failed = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable()
.Any(p => p is FailedTestNodeStateProperty or ErrorTestNodeStateProperty)).ToArray();

#pragma warning disable CS0618 // CancelledTestNodeStateProperty is obsolete
var cancelled = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is CancelledTestNodeStateProperty)).ToArray();
foreach (var kvp in last)
{
var state = kvp.Value.TestNode.Properties.SingleOrDefault<TestNodeStateProperty>();
switch (state)
{
case PassedTestNodeStateProperty:
passedCount++;
break;
case FailedTestNodeStateProperty or ErrorTestNodeStateProperty:
failed.Add(kvp);
break;
case TimeoutTestNodeStateProperty:
timeout.Add(kvp);
break;
case SkippedTestNodeStateProperty:
skipped.Add(kvp);
break;
case InProgressTestNodeStateProperty:
inProgress.Add(kvp);
break;
#pragma warning disable CS0618
case CancelledTestNodeStateProperty:
#pragma warning restore CS0618

var timeout = last
.Where(x => x.Value.TestNode.Properties.AsEnumerable().Any(p => p is TimeoutTestNodeStateProperty))
.ToArray();

var skipped = last
.Where(x => x.Value.TestNode.Properties.AsEnumerable().Any(p => p is SkippedTestNodeStateProperty))
.ToArray();

var inProgress = last.Where(x =>
x.Value.TestNode.Properties.AsEnumerable().Any(p => p is InProgressTestNodeStateProperty)).ToArray();
cancelled.Add(kvp);
break;
}
}

_runStopwatch?.Stop();
var elapsed = _runStopwatch?.Elapsed;

var hasFailures = failed.Length > 0 || timeout.Length > 0 || cancelled.Length > 0;
var hasFailures = failed.Count > 0 || timeout.Count > 0 || cancelled.Count > 0;
var statusEmoji = hasFailures ? "\u274C" : "\u2705";

var stringBuilder = new StringBuilder();
Expand Down Expand Up @@ -172,29 +184,29 @@ public Task AfterRunAsync(int exitCode, CancellationToken cancellation)
{
var segments = new List<string> { $"\u2705 {passedCount} passed" };

if (failed.Length > 0)
if (failed.Count > 0)
{
segments.Add($"\u274C {failed.Length} failed");
segments.Add($"\u274C {failed.Count} failed");
}

if (skipped.Length > 0)
if (skipped.Count > 0)
{
segments.Add($"\u23ED\uFE0F {skipped.Length} skipped");
segments.Add($"\u23ED\uFE0F {skipped.Count} skipped");
}

if (timeout.Length > 0)
if (timeout.Count > 0)
{
segments.Add($"\u23F1\uFE0F {timeout.Length} timed out");
segments.Add($"\u23F1\uFE0F {timeout.Count} timed out");
}

if (cancelled.Length > 0)
if (cancelled.Count > 0)
{
segments.Add($"\uD83D\uDEAB {cancelled.Length} cancelled");
segments.Add($"\uD83D\uDEAB {cancelled.Count} cancelled");
}

if (inProgress.Length > 0)
if (inProgress.Count > 0)
{
segments.Add($"\u26A0\uFE0F {inProgress.Length} in progress");
segments.Add($"\u26A0\uFE0F {inProgress.Count} in progress");
}

stringBuilder.AppendLine(string.Join(" \u00B7 ", segments));
Expand Down Expand Up @@ -236,7 +248,7 @@ public Task AfterRunAsync(int exitCode, CancellationToken cancellation)
}
}

if (skipped.Length > 0)
if (skipped.Count > 0)
{
var skipGroups = skipped
.Select(x => x.Value.TestNode.Properties.AsEnumerable()
Expand All @@ -246,7 +258,7 @@ public Task AfterRunAsync(int exitCode, CancellationToken cancellation)

stringBuilder.AppendLine();
stringBuilder.AppendLine("<details>");
stringBuilder.AppendLine($"<summary>\u23ed\ufe0f {skipped.Length} skipped {(skipped.Length == 1 ? "test" : "tests")}</summary>");
stringBuilder.AppendLine($"<summary>\u23ed\ufe0f {skipped.Count} skipped {(skipped.Count == 1 ? "test" : "tests")}</summary>");
stringBuilder.AppendLine();
foreach (var group in skipGroups)
{
Expand Down
13 changes: 8 additions & 5 deletions TUnit.Engine/Reporters/JUnitReporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ public async Task<bool> IsEnabledAsync()

public string Description => extension.Description;

private readonly ConcurrentDictionary<string, List<TestNodeUpdateMessage>> _updates = [];
private readonly ConcurrentDictionary<string, ConcurrentQueue<TestNodeUpdateMessage>> _updates = [];
private readonly ConcurrentDictionary<string, TestNodeUpdateMessage> _latestUpdates = [];

public Task ConsumeAsync(IDataProducer dataProducer, IData value, CancellationToken cancellationToken)
{
var testNodeUpdateMessage = (TestNodeUpdateMessage)value;

_updates.GetOrAdd(testNodeUpdateMessage.TestNode.Uid.Value, []).Add(testNodeUpdateMessage);
var uid = testNodeUpdateMessage.TestNode.Uid.Value;
_updates.GetOrAdd(uid, static _ => []).Enqueue(testNodeUpdateMessage);
_latestUpdates[uid] = testNodeUpdateMessage;

return Task.CompletedTask;
}
Expand All @@ -74,10 +77,10 @@ public async Task AfterRunAsync(int exitCode, CancellationToken cancellation)
}

// Get the last update for each test
var lastUpdates = new List<TestNodeUpdateMessage>(_updates.Count);
foreach (var kvp in _updates.Where(kvp => kvp.Value.Count > 0))
var lastUpdates = new List<TestNodeUpdateMessage>(_latestUpdates.Count);
foreach (var kvp in _latestUpdates)
{
lastUpdates.Add(kvp.Value[kvp.Value.Count - 1]);
lastUpdates.Add(kvp.Value);
}

// Generate JUnit XML
Expand Down
13 changes: 7 additions & 6 deletions TUnit.Engine/Scheduling/TestScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,12 +176,13 @@ private async Task ExecuteGroupedTestsAsync(

foreach (var group in groupedTests.ParallelGroups)
{
var orderedTests = new List<AbstractExecutableTest>();
foreach (var kvp in group.Value.OrderBy(t => t.Key))
{
orderedTests.AddRange(kvp.Value);
}
var orderedTestsArray = orderedTests.ToArray();
var totalCount = 0;
foreach (var list in group.Value.Values) totalCount += list.Count;
var orderedTestsArray = new AbstractExecutableTest[totalCount];
var idx = 0;
foreach (var list in group.Value.Values)
foreach (var t in list)
orderedTestsArray[idx++] = t;

if (_logger.IsTraceEnabled)
await _logger.LogTraceAsync($"Starting parallel group '{group.Key}' with {orderedTestsArray.Length} orders").ConfigureAwait(false);
Expand Down
41 changes: 17 additions & 24 deletions TUnit.Engine/Services/EventReceiverOrchestrator.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Concurrent;
using System.Reflection;
using System.Runtime.CompilerServices;
using TUnit.Core;
using TUnit.Core.Data;
Expand All @@ -18,12 +19,12 @@ internal sealed class EventReceiverOrchestrator
private readonly TUnitFrameworkLogger _logger;

// Track which assemblies/classes/sessions have had their "first" event invoked
private ThreadSafeDictionary<string, Task> _firstTestInAssemblyTasks = new();
private ThreadSafeDictionary<Assembly, Task> _firstTestInAssemblyTasks = new();
private ThreadSafeDictionary<Type, Task> _firstTestInClassTasks = new();
private ThreadSafeDictionary<string, Task> _firstTestInSessionTasks = new();

// Track remaining test counts for "last" events
private readonly ConcurrentDictionary<string, Counter> _assemblyTestCounts = new();
private readonly ConcurrentDictionary<Assembly, Counter> _assemblyTestCounts = new();
private readonly ConcurrentDictionary<Type, Counter> _classTestCounts = new();

// Accessed from multiple threads via Interlocked to ensure atomic updates
Expand Down Expand Up @@ -355,9 +356,9 @@ public ValueTask InvokeFirstTestInAssemblyEventReceiversAsync(
return default;
}

var assemblyName = assemblyContext.Assembly.GetName().FullName ?? "";
var assembly = assemblyContext.Assembly;

var task = _firstTestInAssemblyTasks.GetOrAdd(assemblyName,
var task = _firstTestInAssemblyTasks.GetOrAdd(assembly,
static (_, args) => args.self.InvokeFirstTestInAssemblyEventReceiversCoreAsync(args.context, args.assemblyContext, args.cancellationToken),
(self: this, context, assemblyContext, cancellationToken));
return new ValueTask(task);
Expand Down Expand Up @@ -459,11 +460,11 @@ public ValueTask InvokeLastTestInAssemblyEventReceiversAsync(
return ValueTask.CompletedTask;
}

var assemblyName = assemblyContext.Assembly.GetName().FullName ?? "";
var assembly = assemblyContext.Assembly;

if (!_assemblyTestCounts.TryGetValue(assemblyName, out var assemblyCounter))
if (!_assemblyTestCounts.TryGetValue(assembly, out var assemblyCounter))
{
assemblyCounter = _assemblyTestCounts.GetOrAdd(assemblyName, static _ => new Counter());
assemblyCounter = _assemblyTestCounts.GetOrAdd(assembly, static _ => new Counter());
}

var assemblyCount = assemblyCounter.Decrement();
Expand Down Expand Up @@ -553,28 +554,20 @@ public void InitializeTestCounts(IEnumerable<TestContext> allTestContexts)
Interlocked.Exchange(ref _sessionTestCount, contexts.Count);

// Clear first-event tracking to ensure clean state for each test execution
_firstTestInAssemblyTasks = new ThreadSafeDictionary<string, Task>();
_firstTestInAssemblyTasks = new ThreadSafeDictionary<Assembly, Task>();
_firstTestInClassTasks = new ThreadSafeDictionary<Type, Task>();
_firstTestInSessionTasks = new ThreadSafeDictionary<string, Task>();

foreach (var group in contexts.GroupBy(c => c.ClassContext.AssemblyContext.Assembly.GetName().FullName))
// Initialize assembly and class counters in a single pass
foreach (var context in contexts)
{
if (!_assemblyTestCounts.TryGetValue(group.Key, out var counter))
{
counter = _assemblyTestCounts.GetOrAdd(group.Key, static _ => new Counter());
}

counter.Add(group.Count());
}

foreach (var group in contexts.GroupBy(c => c.ClassContext.ClassType))
{
if (!_classTestCounts.TryGetValue(group.Key, out var counter))
{
counter = _classTestCounts.GetOrAdd(group.Key, static _ => new Counter());
}
var assembly = context.ClassContext.AssemblyContext.Assembly;
var assemblyCounter = _assemblyTestCounts.GetOrAdd(assembly, static _ => new Counter());
assemblyCounter.Add(1);

counter.Add(group.Count());
var classType = context.ClassContext.ClassType;
var classCounter = _classTestCounts.GetOrAdd(classType, static _ => new Counter());
classCounter.Add(1);
}
}

Expand Down
Loading
Loading