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
134 changes: 98 additions & 36 deletions TUnit.Engine/Services/HookCollectionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,20 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
{
var hooks = _beforeTestHooksCache.GetOrAdd(testClassType, type =>
{
var allHooks = new List<(int order, Func<TestContext, CancellationToken, Task> hook)>();
var hooksByType = new List<(Type type, List<(int order, Func<TestContext, CancellationToken, Task> hook)> hooks)>();

// Collect hooks for each type in the hierarchy
var currentType = type;
while (currentType != null)
{
if (Sources.BeforeTestHooks.TryGetValue(currentType, out var typeHooks))
var typeHooks = new List<(int order, Func<TestContext, CancellationToken, Task> hook)>();

if (Sources.BeforeTestHooks.TryGetValue(currentType, out var sourceHooks))
{
foreach (var hook in typeHooks)
foreach (var hook in sourceHooks)
{
var hookFunc = CreateInstanceHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}

Expand All @@ -42,18 +45,31 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
foreach (var hook in openTypeHooks)
{
var hookFunc = CreateInstanceHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}
}

if (typeHooks.Count > 0)
{
hooksByType.Add((currentType, typeHooks));
}

currentType = currentType.BaseType;
}

return allHooks
.OrderBy(h => h.order)
.Select(h => h.hook)
.ToList();
// For Before hooks: base class hooks run first
// Reverse the list since we collected from derived to base
hooksByType.Reverse();

var finalHooks = new List<Func<TestContext, CancellationToken, Task>>();
foreach (var (_, typeHooks) in hooksByType)
{
// Within each type level, sort by Order
finalHooks.AddRange(typeHooks.OrderBy(h => h.order).Select(h => h.hook));
}

return finalHooks;
});

return new ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>>(hooks);
Expand All @@ -63,17 +79,20 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
{
var hooks = _afterTestHooksCache.GetOrAdd(testClassType, type =>
{
var allHooks = new List<(int order, Func<TestContext, CancellationToken, Task> hook)>();
var hooksByType = new List<(Type type, List<(int order, Func<TestContext, CancellationToken, Task> hook)> hooks)>();

// Collect hooks for each type in the hierarchy
var currentType = type;
while (currentType != null)
{
if (Sources.AfterTestHooks.TryGetValue(currentType, out var typeHooks))
var typeHooks = new List<(int order, Func<TestContext, CancellationToken, Task> hook)>();

if (Sources.AfterTestHooks.TryGetValue(currentType, out var sourceHooks))
{
foreach (var hook in typeHooks)
foreach (var hook in sourceHooks)
{
var hookFunc = CreateInstanceHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}

Expand All @@ -86,18 +105,30 @@ public ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>> Coll
foreach (var hook in openTypeHooks)
{
var hookFunc = CreateInstanceHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}
}

if (typeHooks.Count > 0)
{
hooksByType.Add((currentType, typeHooks));
}

currentType = currentType.BaseType;
}

return allHooks
.OrderBy(h => h.order)
.Select(h => h.hook)
.ToList();
// For After hooks: derived class hooks run first
// No need to reverse since we collected from derived to base

var finalHooks = new List<Func<TestContext, CancellationToken, Task>>();
foreach (var (_, typeHooks) in hooksByType)
{
// Within each type level, sort by Order
finalHooks.AddRange(typeHooks.OrderBy(h => h.order).Select(h => h.hook));
}

return finalHooks;
});

return new ValueTask<IReadOnlyList<Func<TestContext, CancellationToken, Task>>>(hooks);
Expand Down Expand Up @@ -151,17 +182,20 @@ public ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>
{
var hooks = _beforeClassHooksCache.GetOrAdd(testClassType, type =>
{
var allHooks = new List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)>();
var hooksByType = new List<(Type type, List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)> hooks)>();

// Collect hooks for each type in the hierarchy
var currentType = type;
while (currentType != null)
{
if (Sources.BeforeClassHooks.TryGetValue(currentType, out var typeHooks))
var typeHooks = new List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)>();

if (Sources.BeforeClassHooks.TryGetValue(currentType, out var sourceHooks))
{
foreach (var hook in typeHooks)
foreach (var hook in sourceHooks)
{
var hookFunc = CreateClassHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}

Expand All @@ -174,18 +208,31 @@ public ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>
foreach (var hook in openTypeHooks)
{
var hookFunc = CreateClassHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}
}

if (typeHooks.Count > 0)
{
hooksByType.Add((currentType, typeHooks));
}

currentType = currentType.BaseType;
}

return allHooks
.OrderBy(h => h.order)
.Select(h => h.hook)
.ToList();
// For Before hooks: base class hooks run first
// Reverse the list since we collected from derived to base
hooksByType.Reverse();

var finalHooks = new List<Func<ClassHookContext, CancellationToken, Task>>();
foreach (var (_, typeHooks) in hooksByType)
{
// Within each type level, sort by Order
finalHooks.AddRange(typeHooks.OrderBy(h => h.order).Select(h => h.hook));
}

return finalHooks;
});

return new ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>(hooks);
Expand All @@ -195,17 +242,20 @@ public ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>
{
var hooks = _afterClassHooksCache.GetOrAdd(testClassType, type =>
{
var allHooks = new List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)>();
var hooksByType = new List<(Type type, List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)> hooks)>();

// Collect hooks for each type in the hierarchy
var currentType = type;
while (currentType != null)
{
if (Sources.AfterClassHooks.TryGetValue(currentType, out var typeHooks))
var typeHooks = new List<(int order, Func<ClassHookContext, CancellationToken, Task> hook)>();

if (Sources.AfterClassHooks.TryGetValue(currentType, out var sourceHooks))
{
foreach (var hook in typeHooks)
foreach (var hook in sourceHooks)
{
var hookFunc = CreateClassHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}

Expand All @@ -218,18 +268,30 @@ public ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>
foreach (var hook in openTypeHooks)
{
var hookFunc = CreateClassHookDelegate(hook);
allHooks.Add((hook.Order, hookFunc));
typeHooks.Add((hook.Order, hookFunc));
}
}
}

if (typeHooks.Count > 0)
{
hooksByType.Add((currentType, typeHooks));
}

currentType = currentType.BaseType;
}

return allHooks
.OrderBy(h => h.order)
.Select(h => h.hook)
.ToList();
// For After hooks: derived class hooks run first
// No need to reverse since we collected from derived to base

var finalHooks = new List<Func<ClassHookContext, CancellationToken, Task>>();
foreach (var (_, typeHooks) in hooksByType)
{
// Within each type level, sort by Order
finalHooks.AddRange(typeHooks.OrderBy(h => h.order).Select(h => h.hook));
}

return finalHooks;
});

return new ValueTask<IReadOnlyList<Func<ClassHookContext, CancellationToken, Task>>>(hooks);
Expand Down
145 changes: 145 additions & 0 deletions TUnit.TestProject/HookExecutionOrderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
using System.Collections.Generic;
using TUnit.Core;

namespace TUnit.TestProject;

public class HookExecutionOrderTest
{
private static readonly List<string> ExecutionOrder = new();

public class BaseTest
{
[Before(Test, Order = 10)] // High order, but should still run first due to hierarchy
public void BaseBeforeTest()
{
// Clear on the very first before hook to handle multiple test runs
if (ExecutionOrder.Count > 0 && ExecutionOrder[^1].StartsWith("BaseAfterTest"))
{
ExecutionOrder.Clear();
}
ExecutionOrder.Add("BaseBeforeTest");
}

[After(Test, Order = 2000)] // Use very high order to run after all other after hooks
public void VerifyExecutionOrder()
{
// Expected order:
// Before hooks (base to derived, with Order respected within each level):
// - Base: BaseBeforeTest2 (-5), then BaseBeforeTest (10)
// - Middle: MiddleBeforeTest2 (0), then MiddleBeforeTest (100)
// - Derived: DerivedBeforeTest (-1000), then DerivedBeforeTest2 (3)
// Test method
// After hooks (derived to base, with Order respected within each level):
// - Derived: DerivedAfterTest2 (-3), then DerivedAfterTest (1000)
// - Middle: MiddleAfterTest (-100), then MiddleAfterTest2 (0)
// - Base: BaseAfterTest (-10), then BaseAfterTest2 (5)

var expected = new List<string>
{
// Before hooks - base to derived
"BaseBeforeTest2", // Base level, Order = -5
"BaseBeforeTest", // Base level, Order = 10
"MiddleBeforeTest2", // Middle level, Order = 0
"MiddleBeforeTest", // Middle level, Order = 100
"DerivedBeforeTest", // Derived level, Order = -1000
"DerivedBeforeTest2", // Derived level, Order = 3

"TestMethod",

// After hooks - derived to base
"DerivedAfterTest2", // Derived level, Order = -3
"DerivedAfterTest", // Derived level, Order = 1000
"MiddleAfterTest", // Middle level, Order = -100
"MiddleAfterTest2", // Middle level, Order = 0
"BaseAfterTest", // Base level, Order = -10
"BaseAfterTest2" // Base level, Order = 5
};

for (var i = 0; i < expected.Count; i++)
{
if (i >= ExecutionOrder.Count || ExecutionOrder[i] != expected[i])
{
throw new Exception($"Hook execution order is incorrect at index {i}. Expected: {expected[i]}, Actual: {(i < ExecutionOrder.Count ? ExecutionOrder[i] : "missing")}. Full order - Expected: [{string.Join(", ", expected)}], Actual: [{string.Join(", ", ExecutionOrder)}]");
}
}
}

[Before(Test, Order = -5)] // Negative order, should run before BaseBeforeTest at same level
public void BaseBeforeTest2()
{
ExecutionOrder.Add("BaseBeforeTest2");
}

[After(Test, Order = -10)] // Negative order, but should still run last due to hierarchy
public void BaseAfterTest()
{
ExecutionOrder.Add("BaseAfterTest");
}

[After(Test, Order = 5)] // Higher order, should run after BaseAfterTest at same level
public void BaseAfterTest2()
{
ExecutionOrder.Add("BaseAfterTest2");
}
}

public class MiddleTest : BaseTest
{
[Before(Test, Order = 100)] // Very high order, but still runs after base
public void MiddleBeforeTest()
{
ExecutionOrder.Add("MiddleBeforeTest");
}

[Before(Test, Order = 0)] // Default order
public void MiddleBeforeTest2()
{
ExecutionOrder.Add("MiddleBeforeTest2");
}

[After(Test, Order = -100)] // Very low order, but still runs before base
public void MiddleAfterTest()
{
ExecutionOrder.Add("MiddleAfterTest");
}

[After(Test, Order = 0)] // Default order
public void MiddleAfterTest2()
{
ExecutionOrder.Add("MiddleAfterTest2");
}
}

public class DerivedTest : MiddleTest
{
[Before(Test, Order = -1000)] // Extremely low order, but still runs after middle
public void DerivedBeforeTest()
{
ExecutionOrder.Add("DerivedBeforeTest");
}

[Before(Test, Order = 3)] // Positive order
public void DerivedBeforeTest2()
{
ExecutionOrder.Add("DerivedBeforeTest2");
}

[After(Test, Order = 1000)] // Extremely high order, but still runs before middle
public void DerivedAfterTest()
{
ExecutionOrder.Add("DerivedAfterTest");
}

[After(Test, Order = -3)] // Negative order
public void DerivedAfterTest2()
{
ExecutionOrder.Add("DerivedAfterTest2");
}

[Test]
public void TestHookExecutionOrder()
{
ExecutionOrder.Add("TestMethod");
}
}
}
Loading
Loading