Skip to content

Commit a41aa9c

Browse files
authored
Experimental: Register dynamic tests at runtime from within other tests (#2186)
* Basic structure * Register dynamic tests at runtime from within other tests * Update Public API * Update snaps * Docs
1 parent c18ed00 commit a41aa9c

19 files changed

Lines changed: 330 additions & 58 deletions
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using TUnit.Core.Interfaces;
2+
3+
namespace TUnit.Core;
4+
5+
public class RunOnDiscoveryAttribute : TUnitAttribute, ITestDiscoveryEventReceiver
6+
{
7+
public int Order => 0;
8+
9+
public void OnTestDiscovery(DiscoveredTestContext discoveredTestContext)
10+
{
11+
discoveredTestContext.RunOnTestDiscovery = true;
12+
}
13+
}

TUnit.Core/DiscoveredTestContext.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ public void AddProperty(string key, string value)
2121
{
2222
TestContext.TestDetails.InternalCustomProperties.Add(key, value);
2323
}
24-
24+
2525
public void AddCategory(string category)
2626
{
2727
TestContext.TestDetails.MutableCategories.Add(category);
2828
}
29-
29+
3030
public void SetDisplayName(string displayName)
3131
{
3232
TestContext.TestDetails.DisplayName = displayName;
3333
}
34-
34+
3535
public void AddArgumentDisplayFormatter(ArgumentDisplayFormatter formatter)
3636
{
3737
TestContext.ArgumentDisplayFormatters.Add(formatter);
@@ -41,15 +41,21 @@ public void SetParallelConstraint(IParallelConstraint parallelConstraint)
4141
{
4242
TestContext.TestDetails.ParallelConstraint = parallelConstraint;
4343
}
44-
44+
4545
public void SetRetryCount(int times)
4646
{
4747
SetRetryCount(times, (_, _, _) => Task.FromResult(true));
4848
}
49-
49+
5050
public void SetRetryCount(int times, Func<TestContext, Exception, int, Task<bool>> shouldRetry)
5151
{
5252
TestContext.TestDetails.RetryLimit = times;
5353
TestContext.TestDetails.RetryLogic = shouldRetry;
5454
}
55+
56+
public bool RunOnTestDiscovery
57+
{
58+
get => TestContext.RunOnTestDiscovery;
59+
set => TestContext.RunOnTestDiscovery = value;
60+
}
5561
}

TUnit.Core/DynamicTestBuilderContext.cs

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,21 @@
22

33
namespace TUnit.Core;
44

5-
public class DynamicTestBuilderContext(string filePath, int lineNumber)
5+
public class DynamicTestBuilderContext
66
{
7+
private readonly string _filePath;
8+
private readonly int _lineNumber;
9+
10+
public DynamicTestBuilderContext(string filePath, int lineNumber)
11+
{
12+
_filePath = filePath;
13+
_lineNumber = lineNumber;
14+
}
15+
16+
public DynamicTestBuilderContext(TestContext testContext) : this(testContext.TestDetails.TestFilePath, testContext.TestDetails.TestLineNumber)
17+
{
18+
}
19+
720
public List<DynamicTest> Tests { get; } = [];
821

922
public void AddTest<
@@ -12,10 +25,21 @@ public void AddTest<
1225
| DynamicallyAccessedMemberTypes.PublicProperties)]
1326
TClass>(DynamicTest<TClass> dynamicTest) where TClass : class
1427
{
15-
Tests.Add(dynamicTest with
28+
var testToRegister = dynamicTest with
1629
{
17-
TestFilePath = filePath,
18-
TestLineNumber = lineNumber
19-
});
30+
TestFilePath = _filePath,
31+
TestLineNumber = _lineNumber
32+
};
33+
34+
Tests.Add(testToRegister);
35+
}
36+
37+
public async Task AddTestAtRuntime<
38+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
39+
| DynamicallyAccessedMemberTypes.PublicMethods
40+
| DynamicallyAccessedMemberTypes.PublicProperties)]
41+
TClass>(TestContext testContext, DynamicTest<TClass> dynamicTest) where TClass : class
42+
{
43+
await testContext.GetService<IDynamicTestRegistrar>().Register(dynamicTest);
2044
}
2145
}

TUnit.Core/Extensions/TestContextExtensions.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using TUnit.Core.Helpers;
1+
using System.Diagnostics.CodeAnalysis;
2+
using TUnit.Core.Helpers;
23
using TUnit.Core.Interfaces;
34

45
namespace TUnit.Core.Extensions;
@@ -68,6 +69,16 @@ public static string GetClassTypeName(this TestContext testContext)
6869
$"{classTypeName}({string.Join(", ", testDetails.TestClassArguments.Select(x => ArgumentFormatter.GetConstantValue(testContext, x)))})";
6970
}
7071

72+
[Experimental("WIP")]
73+
public static Task AddDynamicTest<
74+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
75+
| DynamicallyAccessedMemberTypes.PublicMethods
76+
| DynamicallyAccessedMemberTypes.PublicProperties)]
77+
T>(this TestContext testContext, DynamicTest<T> dynamicTest) where T : class
78+
{
79+
return new DynamicTestBuilderContext(testContext).AddTestAtRuntime(testContext, dynamicTest);
80+
}
81+
7182
/// <summary>
7283
/// Gets the test display name for the test context.
7384
/// </summary>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
3+
namespace TUnit.Core;
4+
5+
public interface IDynamicTestRegistrar
6+
{
7+
Task Register<
8+
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors
9+
| DynamicallyAccessedMemberTypes.PublicMethods
10+
| DynamicallyAccessedMemberTypes.PublicProperties)]
11+
TClass>(DynamicTest<TClass> dynamicTest) where TClass : class;
12+
}

TUnit.Core/TestContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,4 +130,6 @@ public void AddArtifact(Artifact artifact)
130130
/// Gets or sets the event objects.
131131
/// </summary>
132132
internal object?[]? EventObjects { get; set; }
133+
134+
internal bool RunOnTestDiscovery { get; set; }
133135
}

TUnit.Engine.Tests/DynamicTests.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Shouldly;
2+
3+
namespace TUnit.Engine.Tests;
4+
5+
public class DynamicTests : InvokableTestBase
6+
{
7+
[Test]
8+
public async Task Test()
9+
{
10+
await RunTestsWithFilter(
11+
"/*/*DynamicTests/*/*",
12+
[
13+
result => result.ResultSummary.Outcome.ShouldBe("Completed"),
14+
result => result.ResultSummary.Counters.Total.ShouldBe(70),
15+
result => result.ResultSummary.Counters.Passed.ShouldBe(70),
16+
result => result.ResultSummary.Counters.Failed.ShouldBe(0),
17+
result => result.ResultSummary.Counters.NotExecuted.ShouldBe(0)
18+
]);
19+
}
20+
}

TUnit.Engine/Extensions/TestContextExtensions.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public static async Task ReregisterTestWithArguments(
2424
object?[]? methodArguments,
2525
Dictionary<string, object?>? objectBag = null)
2626
{
27+
// TODO: Rework to use DynamicTestRegistrar
28+
2729
var testMetadata = testContext.OriginalMetadata;
2830

2931
var testBuilderContext = new TestBuilderContext();
@@ -76,7 +78,7 @@ await testContext.GetService<TestRegistrar>().RegisterInstance(newTest,
7678
NotInParallel = new PriorityQueue<DiscoveredTest, int>(),
7779
KeyedNotInParallel = new Dictionary<ConstraintKeysCollection, PriorityQueue<DiscoveredTest, int>>(),
7880
ParallelGroups = new ConcurrentDictionary<ParallelGroupConstraint, List<DiscoveredTest>>()
79-
}, null, testContext.GetService<ExecuteRequestContext>());
81+
}, null, testContext.GetService<EngineCancellationToken>().CancellationTokenSource.Token);
8082
}
8183

8284
internal static void SetResult(this TestContext testContext, Exception? exception)

TUnit.Engine/Framework/TUnitServiceProvider.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -91,26 +91,32 @@ public TUnitServiceProvider(IExtension extension,
9191
var testHookOrchestrator = Register(new TestHookOrchestrator(HooksCollector, Logger));
9292

9393
var testRegistrar = Register(new TestRegistrar(instanceTracker, AssemblyHookOrchestrator, classHookOrchestrator));
94-
TestDiscoverer = Register(new TUnitTestDiscoverer(testsConstructor, testFilterService, TestGrouper, testRegistrar, TUnitMessageBus, Logger, extension));
95-
96-
TestFinder = Register(new TestsFinder(TestDiscoverer));
97-
Register<ITestFinder>(TestFinder);
9894

9995
Disposer = Register(new Disposer(Logger));
10096

10197
var testInvoker = Register(new TestInvoker(testHookOrchestrator, Logger, Disposer));
10298
var parallelLimitProvider = Register(new ParallelLimitLockProvider());
10399

104-
// TODO
105-
Register(new HookMessagePublisher(extension, messageBus));
106-
107100
var singleTestExecutor = Register(new SingleTestExecutor(extension, instanceTracker, testInvoker, parallelLimitProvider, AssemblyHookOrchestrator, classHookOrchestrator, TUnitMessageBus, Logger, EngineCancellationToken, testRegistrar));
108101

109102
TestsExecutor = Register(new TestsExecutor(singleTestExecutor, Logger, CommandLineOptions, EngineCancellationToken, AssemblyHookOrchestrator, classHookOrchestrator));
110103

104+
TestDiscoverer = Register(new TUnitTestDiscoverer(testsConstructor, testFilterService, TestGrouper, testRegistrar, TUnitMessageBus, Logger, TestsExecutor, extension));
105+
106+
DynamicTestRegistrar = Register<IDynamicTestRegistrar>(new DynamicTestRegistrar(testsConstructor, testRegistrar,
107+
TestGrouper, TUnitMessageBus, TestsExecutor, EngineCancellationToken));
108+
109+
TestFinder = Register(new TestsFinder(TestDiscoverer));
110+
Register<ITestFinder>(TestFinder);
111+
112+
// TODO
113+
Register(new HookMessagePublisher(extension, messageBus));
114+
111115
OnEndExecutor = Register(new OnEndExecutor(CommandLineOptions, Logger));
112116
}
113-
117+
118+
public IDynamicTestRegistrar DynamicTestRegistrar { get; }
119+
114120
public Disposer Disposer { get; }
115121

116122
public async ValueTask DisposeAsync()

TUnit.Engine/Framework/TUnitTestFramework.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,11 +119,13 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context)
119119
TestFilter = stringFilter,
120120
Id = runTestExecutionRequest.Session.SessionUid.Value
121121
};
122+
123+
TestSessionContext.Current = testSessionContext;
122124

123125
ExecutionContextHelper.RestoreContext(await serviceProvider.TestSessionHookOrchestrator.RunBeforeTestSession(context));
124126

125127
await serviceProvider.TestsExecutor.ExecuteAsync(filteredTests, runTestExecutionRequest.Filter,
126-
context);
128+
context.CancellationToken);
127129

128130
// Tests could reschedule separate invocations - This allows us to wait for all invocations
129131
await serviceProvider.TestsExecutor.WaitForFinishAsync();

0 commit comments

Comments
 (0)