-
-
Notifications
You must be signed in to change notification settings - Fork 128
Expand file tree
/
Copy pathHookTimeoutHelper.cs
More file actions
169 lines (149 loc) · 6.58 KB
/
Copy pathHookTimeoutHelper.cs
File metadata and controls
169 lines (149 loc) · 6.58 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
using System.Diagnostics.CodeAnalysis;
using TUnit.Core;
using TUnit.Core.Hooks;
using TUnit.Core.Interfaces;
namespace TUnit.Engine.Helpers;
/// <summary>
/// Helper class for executing hooks with timeout enforcement
/// </summary>
internal static class HookTimeoutHelper
{
/// <summary>
/// Creates a timeout-aware action wrapper for a hook
/// </summary>
public static Task CreateTimeoutHookAction<T>(
StaticHookMethod<T> hook,
T context,
CancellationToken cancellationToken)
{
// CENTRAL POINT: At execution time, check if we should use a custom hook executor
// This happens AFTER OnTestRegistered, so CustomHookExecutor will be set if the user called SetHookExecutor
var timeout = hook.Timeout;
if (timeout == null)
{
// No timeout specified, execute with potential custom executor
return ExecuteHookWithPotentialCustomExecutor(hook, context, cancellationToken).AsTask();
}
var timeoutMs = (int)timeout.Value.TotalMilliseconds;
return CreateTimeoutHookActionAsync(hook, context, timeoutMs, cancellationToken);
static async Task CreateTimeoutHookActionAsync(
StaticHookMethod<T> hook,
T context,
int timeoutMs,
CancellationToken cancellationToken)
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
try
{
await ExecuteHookWithPotentialCustomExecutor(hook, context, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
var baseMessage = $"Hook '{hook.Name}' exceeded timeout of {timeoutMs}ms";
throw new TimeoutException(TimeoutDiagnostics.BuildTimeoutDiagnosticsMessage(baseMessage, executionTask: null));
}
}
}
/// <summary>
/// Executes a hook, using a custom executor if one is set on the TestContext
/// </summary>
private static ValueTask ExecuteHookWithPotentialCustomExecutor<T>(StaticHookMethod<T> hook, T context, CancellationToken cancellationToken)
{
// Check if this is a TestContext with a custom hook executor
if (context is TestContext testContext && testContext.CustomHookExecutor != null)
{
// BYPASS the hook's default executor and call the custom executor directly with the hook's body
var customExecutor = testContext.CustomHookExecutor;
// Determine which executor method to call based on hook type
if (hook is BeforeTestHookMethod || hook is InstanceHookMethod)
{
return ExecuteBeforeTestHook(hook, context, cancellationToken, customExecutor, testContext);
}
else if (hook is AfterTestHookMethod)
{
return ExecuteAfterTestHook(hook, context, cancellationToken, customExecutor, testContext);
}
}
// No custom executor, use the hook's default executor
return hook.ExecuteAsync(context, cancellationToken);
}
private static ValueTask ExecuteBeforeTestHook<T>(StaticHookMethod<T> hook, [DisallowNull] T context,
CancellationToken cancellationToken, IHookExecutor customExecutor, TestContext testContext) =>
customExecutor.ExecuteBeforeTestHook(
hook.MethodInfo,
testContext,
() => hook.Body!.Invoke(context, cancellationToken)
);
private static ValueTask ExecuteAfterTestHook<T>(StaticHookMethod<T> hook, [DisallowNull] T context,
CancellationToken cancellationToken, IHookExecutor customExecutor, TestContext testContext) =>
customExecutor.ExecuteAfterTestHook(
hook.MethodInfo,
testContext,
() => hook.Body!.Invoke(context, cancellationToken)
);
/// <summary>
/// Creates a timeout-aware action wrapper for a hook delegate
/// </summary>
public static Func<Task> CreateTimeoutHookAction<T>(
Func<T, CancellationToken, Task> hookDelegate,
T context,
TimeSpan? timeout,
string hookName,
CancellationToken cancellationToken)
{
if (timeout == null)
{
// No timeout specified, execute normally
return async () => await hookDelegate(context, cancellationToken);
}
var timeoutMs = (int)timeout.Value.TotalMilliseconds;
return async () =>
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
try
{
await hookDelegate(context, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
var baseMessage = $"Hook '{hookName}' exceeded timeout of {timeoutMs}ms";
throw new TimeoutException(TimeoutDiagnostics.BuildTimeoutDiagnosticsMessage(baseMessage, executionTask: null));
}
};
}
/// <summary>
/// Creates a timeout-aware action wrapper for a hook delegate that returns ValueTask
/// This overload is used for instance hooks (InstanceHookMethod)
/// Custom executor handling for instance hooks is done in HookDelegateBuilder.CreateInstanceHookDelegateAsync
/// </summary>
public static Func<Task> CreateTimeoutHookAction<T>(
Func<T, CancellationToken, ValueTask> hookDelegate,
T context,
TimeSpan? timeout,
string hookName,
CancellationToken cancellationToken)
{
if (timeout == null)
{
// No timeout specified, execute normally
return async () => await hookDelegate(context, cancellationToken);
}
var timeoutMs = (int)timeout.Value.TotalMilliseconds;
return async () =>
{
using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
cts.CancelAfter(timeoutMs);
try
{
await hookDelegate(context, cts.Token);
}
catch (OperationCanceledException) when (cts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
{
var baseMessage = $"Hook '{hookName}' exceeded timeout of {timeoutMs}ms";
throw new TimeoutException(TimeoutDiagnostics.BuildTimeoutDiagnosticsMessage(baseMessage, executionTask: null));
}
};
}
}