From cc8387670568718f9ef49117fc1d221e47ea2ffc Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 01:36:22 +0100 Subject: [PATCH 01/12] Re-schedule tests --- TUnit.Core/BeforeTestContext.cs | 5 + TUnit.Core/EngineCancellationToken.cs | 12 +- .../Extensions/TestContextExtensions.cs | 2 +- TUnit.Core/ITUnitMessageBus.cs | 18 +-- TUnit.Core/ResettableLazy.cs | 5 + TUnit.Core/RunHelpers.cs | 61 ++++----- TUnit.Core/TUnit.Core.csproj | 3 + TUnit.Core/TestContext.cs | 21 +++- .../Extensions/TestContextExtensions.cs | 67 ++++++++++ TUnit.Engine/Extensions/TestExtensions.cs | 28 +---- .../Framework/TUnitServiceProvider.cs | 22 +++- TUnit.Engine/Framework/TUnitTestFramework.cs | 7 +- .../Hooks/TestDiscoveryHookOrchestrator.cs | 2 - .../Hooks/TestSessionHookOrchestrator.cs | 2 - TUnit.Engine/Services/Counter.cs | 48 ++++++++ TUnit.Engine/Services/HooksCollector.cs | 4 +- TUnit.Engine/Services/SingleTestExecutor.cs | 116 +++++++++++------- TUnit.Engine/Services/TestFilterService.cs | 1 - TUnit.Engine/Services/TestsConstructor.cs | 4 +- TUnit.Engine/Services/TestsExecutor.cs | 30 ++++- TUnit.Engine/TUnitMessageBus.cs | 46 ++++--- TUnit.Engine/TestRegistrar.cs | 2 +- .../DynamicallyRegisteredTests.cs | 60 +++++++++ 23 files changed, 403 insertions(+), 163 deletions(-) create mode 100644 TUnit.Engine/Extensions/TestContextExtensions.cs create mode 100644 TUnit.Engine/Services/Counter.cs create mode 100644 TUnit.TestProject/DynamicallyRegisteredTests.cs diff --git a/TUnit.Core/BeforeTestContext.cs b/TUnit.Core/BeforeTestContext.cs index 1de89f4534..aa5c41008d 100644 --- a/TUnit.Core/BeforeTestContext.cs +++ b/TUnit.Core/BeforeTestContext.cs @@ -23,4 +23,9 @@ public void SetHookExecutor(IHookExecutor hookExecutor) { _discoveredTest.HookExecutor = hookExecutor; } + + public void AddLinkedCancellationToken(CancellationToken cancellationToken) + { + _discoveredTest.TestContext.LinkedCancellationTokens.Add(cancellationToken); + } } \ No newline at end of file diff --git a/TUnit.Core/EngineCancellationToken.cs b/TUnit.Core/EngineCancellationToken.cs index 5d188e029c..53ea283450 100644 --- a/TUnit.Core/EngineCancellationToken.cs +++ b/TUnit.Core/EngineCancellationToken.cs @@ -1,13 +1,19 @@ namespace TUnit.Core; -public class EngineCancellationToken +public class EngineCancellationToken : IDisposable { internal CancellationTokenSource CancellationTokenSource { get; private set; } = new(); - - public CancellationToken Token => CancellationTokenSource.Token; + + public CancellationToken Token { get; private set; } internal void Initialise(CancellationToken cancellationToken) { CancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + Token = CancellationTokenSource.Token; + } + + public void Dispose() + { + CancellationTokenSource.Dispose(); } } \ No newline at end of file diff --git a/TUnit.Core/Extensions/TestContextExtensions.cs b/TUnit.Core/Extensions/TestContextExtensions.cs index 7277566e74..e013e6b212 100644 --- a/TUnit.Core/Extensions/TestContextExtensions.cs +++ b/TUnit.Core/Extensions/TestContextExtensions.cs @@ -11,7 +11,7 @@ public static TestContext[] GetTests(this TestContext context, string testName) public static TestContext[] GetTests(this TestContext context, string testName, Type[] parameterTypes) { - var tests = context.TestFinder.GetTestsByNameAndParameters( + var tests = context.GetService().GetTestsByNameAndParameters( testName: testName, methodParameterTypes: parameterTypes, classType: context.TestDetails.ClassType, diff --git a/TUnit.Core/ITUnitMessageBus.cs b/TUnit.Core/ITUnitMessageBus.cs index b8e6f6ac9a..1c644333a8 100644 --- a/TUnit.Core/ITUnitMessageBus.cs +++ b/TUnit.Core/ITUnitMessageBus.cs @@ -2,14 +2,14 @@ internal interface ITUnitMessageBus { - Task Discovered(TestContext testContext); - Task InProgress(TestContext testContext); - Task Passed(TestContext testContext, DateTimeOffset start); - Task Failed(TestContext testContext, Exception exception, DateTimeOffset start); - Task FailedInitialization(FailedInitializationTest failedInitializationTest); - Task Skipped(TestContext testContext, string reason); - Task Cancelled(TestContext testContext); + ValueTask Discovered(TestContext testContext); + ValueTask InProgress(TestContext testContext); + ValueTask Passed(TestContext testContext, DateTimeOffset start); + ValueTask Failed(TestContext testContext, Exception exception, DateTimeOffset start); + ValueTask FailedInitialization(FailedInitializationTest failedInitializationTest); + ValueTask Skipped(TestContext testContext, string reason); + ValueTask Cancelled(TestContext testContext); - Task SessionArtifact(Artifact artifact); - Task TestArtifact(TestContext testContext, Artifact artifact); + ValueTask SessionArtifact(Artifact artifact); + ValueTask TestArtifact(TestContext testContext, Artifact artifact); } \ No newline at end of file diff --git a/TUnit.Core/ResettableLazy.cs b/TUnit.Core/ResettableLazy.cs index 3802ad7e1f..5d9ca207e7 100644 --- a/TUnit.Core/ResettableLazy.cs +++ b/TUnit.Core/ResettableLazy.cs @@ -29,4 +29,9 @@ public async ValueTask DisposeAsync() disposable.Dispose(); } } + + public ResettableLazy Clone() + { + return new ResettableLazy(factory); + } } \ No newline at end of file diff --git a/TUnit.Core/RunHelpers.cs b/TUnit.Core/RunHelpers.cs index d16f65996d..667c7e1030 100644 --- a/TUnit.Core/RunHelpers.cs +++ b/TUnit.Core/RunHelpers.cs @@ -7,17 +7,38 @@ namespace TUnit.Core; internal static class RunHelpers { - internal static async Task RunWithTimeoutAsync(Func taskDelegate, TimeSpan? timeout, EngineCancellationToken engineCancellationToken) + internal static async Task RunWithTimeoutAsync(Func taskDelegate, TimeSpan? timeout, CancellationToken token) { - using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(engineCancellationToken.Token); + using var cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(token); var cancellationToken = cancellationTokenSource.Token; var taskCompletionSource = new TaskCompletionSource(); - var task = taskDelegate(cancellationToken); + await using var cancellationTokenRegistration = cancellationToken.Register(() => + { + if (token.IsCancellationRequested) + { + taskCompletionSource.TrySetException(new TestRunCanceledException()); + return; + } - _ = task.ContinueWith(async t => + if (timeout.HasValue) + { + taskCompletionSource.TrySetException(new TimeoutException(timeout.Value)); + } + else + { + taskCompletionSource.TrySetCanceled(cancellationToken); + } + }); + + if (timeout != null) + { + cancellationTokenSource.CancelAfter(timeout.Value); + } + + _ = taskDelegate(cancellationToken).ContinueWith(async t => { try { @@ -30,37 +51,7 @@ internal static async Task RunWithTimeoutAsync(Func tas } }, CancellationToken.None); - CancellationTokenRegistration? cancellationTokenRegistration = null; - if (cancellationToken.CanBeCanceled) - { - cancellationTokenRegistration = cancellationToken.Register(() => - { - if (engineCancellationToken.Token.IsCancellationRequested) - { - taskCompletionSource.TrySetException(new TestRunCanceledException()); - return; - } - - if (timeout.HasValue) - { - taskCompletionSource.TrySetException(new TimeoutException(timeout.Value)); - } - else - { - taskCompletionSource.TrySetCanceled(cancellationToken); - } - }); - } - - if (timeout != null) - { - cancellationTokenSource.CancelAfter(timeout.Value); - } - - await using (cancellationTokenRegistration) - { - await taskCompletionSource.Task; - } + await taskCompletionSource.Task; } [StackTraceHidden] diff --git a/TUnit.Core/TUnit.Core.csproj b/TUnit.Core/TUnit.Core.csproj index ef65daa61b..7ddfe64f74 100644 --- a/TUnit.Core/TUnit.Core.csproj +++ b/TUnit.Core/TUnit.Core.csproj @@ -18,4 +18,7 @@ + + + \ No newline at end of file diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index ee7e8d0d2c..6fd17a9f56 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -4,23 +4,29 @@ namespace TUnit.Core; public partial class TestContext : Context, IDisposable { - internal readonly IServiceProvider ServiceProvider; + private readonly IServiceProvider _serviceProvider; - internal ITestFinder TestFinder => (ITestFinder) ServiceProvider.GetService(typeof(ITestFinder))!; + internal T GetService() => (T) _serviceProvider.GetService(typeof(T))!; internal readonly TaskCompletionSource TaskCompletionSource = new(); internal readonly List Artifacts = []; + internal readonly List LinkedCancellationTokens = []; + internal readonly TestMetadata OriginalMetadata; + #if NET9_0_OR_GREATER public readonly Lock Lock = new(); #else public readonly object Lock = new(); #endif - internal TestContext(IServiceProvider serviceProvider, TestDetails testDetails, Dictionary objectBag) + internal bool ReportResult = true; + + internal TestContext(IServiceProvider serviceProvider, TestDetails testDetails, TestMetadata originalMetadata) { - ServiceProvider = serviceProvider; + _serviceProvider = serviceProvider; + OriginalMetadata = originalMetadata; TestDetails = testDetails; - ObjectBag = objectBag; + ObjectBag = originalMetadata.ObjectBag; } public DateTimeOffset? TestStart { get; internal set; } @@ -38,6 +44,11 @@ internal TestContext(IServiceProvider serviceProvider, TestDetails testDetails, public TestResult? Result { get; internal set; } internal DiscoveredTest InternalDiscoveredTest { get; set; } = null!; + + public void SuppressReportingResult() + { + ReportResult = false; + } public void AddArtifact(Artifact artifact) { diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs new file mode 100644 index 0000000000..883dba6d0b --- /dev/null +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -0,0 +1,67 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.Testing.Platform.Extensions.TestFramework; +using TUnit.Core; +using TUnit.Engine.Models; +using TUnit.Engine.Services; + +namespace TUnit.Engine.Extensions; + +public static class TestContextExtensions +{ + public static async Task ReregisterTestWithArguments<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TTestClass>( + this TestContext testContext, + object?[]? methodArguments, + Dictionary? objectBag = null, + Attribute[]? dataAttributes = null) + { + var testMetadata = (TestMetadata)testContext.OriginalMetadata; + + var newTestMetaData = testMetadata with + { + TestId = Guid.NewGuid().ToString(), + DataAttributes = dataAttributes ?? testContext.TestDetails.DataAttributes, + TestMethodArguments = methodArguments ?? [], + ObjectBag = objectBag ?? [], + ResettableClassFactory = testMetadata.ResettableClassFactory.Clone(), + TestMethodFactory = (@class, token) => + { + var hasTimeout = testContext.TestDetails.Timeout != null; + + var args = GetArgs(methodArguments, hasTimeout, token); + + return AsyncConvert.Convert( + testContext.TestDetails.MethodInfo.Invoke(@class, args)); + } + }; + + var newTest = testContext.GetService().ConstructTest(newTestMetaData); + + var startTime = DateTimeOffset.UtcNow; + + await testContext.GetService().RegisterInstance(newTest.TestContext, + onFailureToInitialize: exception => testContext.GetService().Failed(newTest.TestContext, exception, startTime)); + + _ = testContext.GetService().ExecuteAsync(new GroupedTests + { + AllValidTests = [newTest], + Parallel = new Queue([newTest]), + NotInParallel = new Queue(), + KeyedNotInParallel = [] + }, null, testContext.GetService()); + } + + private static object?[]? GetArgs(object?[]? methodArguments, bool hasTimeout, CancellationToken token) + { + if (!hasTimeout) + { + return methodArguments; + } + + if (methodArguments is null) + { + return [token]; + } + + return [..methodArguments, token]; + } +} \ No newline at end of file diff --git a/TUnit.Engine/Extensions/TestExtensions.cs b/TUnit.Engine/Extensions/TestExtensions.cs index fb6e3347ce..3170c73916 100644 --- a/TUnit.Engine/Extensions/TestExtensions.cs +++ b/TUnit.Engine/Extensions/TestExtensions.cs @@ -1,5 +1,4 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Testing.Extensions.TrxReport.Abstractions; +using Microsoft.Testing.Extensions.TrxReport.Abstractions; using Microsoft.Testing.Platform.Extensions.Messages; using TUnit.Core; using TUnit.Core.Helpers; @@ -86,29 +85,4 @@ public static TestNode WithProperty(this TestNode testNode, IProperty property) testNode.Properties.Add(property); return testNode; } - - internal static void ReRegisterTestWithArguments<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TTestClass>(this TestContext testContext, Func classFactory, object[] methodArguments) - { - // TODO: - // TestRegistrar.RegisterTest(new TestMetadata - // { - // TestId = Guid.NewGuid().ToString(), - // AttributeTypes = [], - // ClassConstructor = null, - // CurrentRepeatAttempt = 0, - // DataAttributes = [], - // MethodInfo = testContext.TestDetails.MethodInfo, - // ResettableClassFactory = new ResettableLazy(classFactory), - // TestClassArguments = [], - // TestMethodArguments = methodArguments, - // ObjectBag = [], - // ParallelLimit = testContext.TestDetails.ParallelLimit, - // RepeatLimit = 0, - // TestExecutor = testContext.InternalDiscoveredTest.TestExecutor, - // TestClassProperties = [], - // TestFilePath = testContext.TestDetails.TestFilePath, - // TestLineNumber = testContext.TestDetails.TestLineNumber, - // TestMethodFactory = (@class, token) => AsyncConvert.Convert(testContext.TestDetails.MethodInfo.Invoke(@class, [..methodArguments, token])) - // }); - } } \ No newline at end of file diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index bfeeb2554b..4ff58d4819 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -44,13 +44,15 @@ public TUnitServiceProvider(IExtension extension, IMessageBus messageBus, IServiceProvider frameworkServiceProvider) { + Register(context); + EngineCancellationToken = Register(new EngineCancellationToken()); - LoggerFactory = Register(frameworkServiceProvider.GetLoggerFactory()); + LoggerFactory = frameworkServiceProvider.GetLoggerFactory(); - OutputDevice = Register(frameworkServiceProvider.GetOutputDevice()); + OutputDevice = frameworkServiceProvider.GetOutputDevice(); - CommandLineOptions = Register(frameworkServiceProvider.GetCommandLineOptions()); + CommandLineOptions = frameworkServiceProvider.GetCommandLineOptions(); Logger = Register(new TUnitFrameworkLogger(extension, OutputDevice, LoggerFactory.CreateLogger())); @@ -89,25 +91,33 @@ public TUnitServiceProvider(IExtension extension, TestFinder = Register(new TestsFinder(TestDiscoverer)); Register(TestFinder); - var disposer = Register(new Disposer(Logger)); + Disposer = Register(new Disposer(Logger)); + var cancellationTokenSource = Register(EngineCancellationToken.CancellationTokenSource); var testInvoker = Register(new TestInvoker(testHookOrchestrator)); var explicitFilterService = Register(new ExplicitFilterService()); var parallelLimitProvider = Register(new ParallelLimitProvider()); var hookMessagePublisher = Register(new HookMessagePublisher(extension, messageBus)); - var singleTestExecutor = Register(new SingleTestExecutor(extension, disposer, cancellationTokenSource, testInvoker, + var singleTestExecutor = Register(new SingleTestExecutor(extension, Disposer, cancellationTokenSource, testInvoker, explicitFilterService, parallelLimitProvider, AssemblyHookOrchestrator, classHookOrchestrator, testHookOrchestrator, TestFinder, TUnitMessageBus, Logger, EngineCancellationToken)); TestsExecutor = Register(new TestsExecutor(singleTestExecutor, Logger, CommandLineOptions, EngineCancellationToken)); OnEndExecutor = Register(new OnEndExecutor(CommandLineOptions, Logger)); } - + + public Disposer Disposer { get; } + public async ValueTask DisposeAsync() { await StandardOutConsoleInterceptor.DisposeAsync(); await StandardErrorConsoleInterceptor.DisposeAsync(); + + foreach (var servicesValue in _services.Values) + { + await Disposer.DisposeAsync(servicesValue); + } } private T Register(T t) diff --git a/TUnit.Engine/Framework/TUnitTestFramework.cs b/TUnit.Engine/Framework/TUnitTestFramework.cs index f1fc95ce77..456c7972df 100644 --- a/TUnit.Engine/Framework/TUnitTestFramework.cs +++ b/TUnit.Engine/Framework/TUnitTestFramework.cs @@ -6,7 +6,6 @@ using Microsoft.Testing.Platform.Requests; using TUnit.Core; using TUnit.Core.Logging; -using TUnit.Engine.Hooks; namespace TUnit.Engine.Framework; @@ -94,6 +93,9 @@ public async Task ExecuteRequestAsync(ExecuteRequestContext context) await serviceProvider.TestsExecutor.ExecuteAsync(filteredTests, runTestExecutionRequest.Filter, context); + // Tests could reschedule separate invocations - This allows us to wait for all invocations + await serviceProvider.TestsExecutor.WaitForFinishAsync(); + await serviceProvider.TestSessionHookOrchestrator.ExecuteAfterHooks(); foreach (var artifact in testSessionContext.Artifacts) @@ -157,7 +159,6 @@ public async Task CloseTestSessionAsync(CloseTestSession public Type[] DataTypesProduced { get; } = [ - typeof(TestNodeUpdateMessage), - typeof(SessionFileArtifact) + typeof(TestNodeUpdateMessage) ]; } \ No newline at end of file diff --git a/TUnit.Engine/Hooks/TestDiscoveryHookOrchestrator.cs b/TUnit.Engine/Hooks/TestDiscoveryHookOrchestrator.cs index 5bc959caa1..50cbebd1f9 100644 --- a/TUnit.Engine/Hooks/TestDiscoveryHookOrchestrator.cs +++ b/TUnit.Engine/Hooks/TestDiscoveryHookOrchestrator.cs @@ -1,6 +1,4 @@ using TUnit.Core; -using TUnit.Core.Data; -using TUnit.Core.Extensions; using TUnit.Engine.Helpers; using TUnit.Engine.Services; diff --git a/TUnit.Engine/Hooks/TestSessionHookOrchestrator.cs b/TUnit.Engine/Hooks/TestSessionHookOrchestrator.cs index c41a6ffab8..fc2ffa4f5c 100644 --- a/TUnit.Engine/Hooks/TestSessionHookOrchestrator.cs +++ b/TUnit.Engine/Hooks/TestSessionHookOrchestrator.cs @@ -1,6 +1,4 @@ using TUnit.Core; -using TUnit.Core.Data; -using TUnit.Core.Extensions; using TUnit.Engine.Helpers; using TUnit.Engine.Services; diff --git a/TUnit.Engine/Services/Counter.cs b/TUnit.Engine/Services/Counter.cs new file mode 100644 index 0000000000..a87956b007 --- /dev/null +++ b/TUnit.Engine/Services/Counter.cs @@ -0,0 +1,48 @@ +using System.Diagnostics; + +namespace TUnit.Engine.Services; + +[DebuggerDisplay("Count = {CurrentCount}")] +public class Counter +{ +#if NET9_0_OR_GREATER + private readonly Lock _locker = new(); +#else + private readonly object _locker = new(); +#endif + + private int _count; + + public int Increment() + { + lock (_locker) + { + _count++; + OnCountChanged?.Invoke(this, _count); + return _count; + } + } + + public int Decrement() + { + lock (_locker) + { + _count--; + OnCountChanged?.Invoke(this, _count); + return _count; + } + } + + public int CurrentCount + { + get + { + lock (_locker) + { + return _count; + } + } + } + + public EventHandler? OnCountChanged; +} \ No newline at end of file diff --git a/TUnit.Engine/Services/HooksCollector.cs b/TUnit.Engine/Services/HooksCollector.cs index 4145d45b51..dfc8250272 100644 --- a/TUnit.Engine/Services/HooksCollector.cs +++ b/TUnit.Engine/Services/HooksCollector.cs @@ -1,6 +1,4 @@ -using System.Collections.Concurrent; -using System.Reflection; -using EnumerableAsyncProcessor.Extensions; +using System.Reflection; using TUnit.Core; using TUnit.Core.Data; diff --git a/TUnit.Engine/Services/SingleTestExecutor.cs b/TUnit.Engine/Services/SingleTestExecutor.cs index 88d140182e..11432943f1 100644 --- a/TUnit.Engine/Services/SingleTestExecutor.cs +++ b/TUnit.Engine/Services/SingleTestExecutor.cs @@ -1,5 +1,4 @@ -using Microsoft.Testing.Extensions.TrxReport.Abstractions; -using Microsoft.Testing.Platform.Extensions; +using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.Messages; using Microsoft.Testing.Platform.Extensions.TestFramework; using Microsoft.Testing.Platform.Requests; @@ -10,12 +9,10 @@ using TUnit.Core.Helpers; using TUnit.Core.Interfaces; using TUnit.Core.Logging; -using TUnit.Engine.Extensions; using TUnit.Engine.Helpers; using TUnit.Engine.Hooks; using TUnit.Engine.Logging; -using TUnit.Engine.Models; -using TimeoutException = TUnit.Core.Exceptions.TimeoutException; + #pragma warning disable TPEXP namespace TUnit.Engine.Services; @@ -168,13 +165,45 @@ private async Task ExecuteTestInternalAsync(DiscoveredTest test, ITestExecutionF private async Task ExecuteTest(DiscoveredTest test, ExecuteRequestContext context, TestContext testContext, List cleanUpExceptions) { + var start = DateTimeOffset.Now; + try { await ExecuteBeforeHooks(test, context, testContext); TestContext.Current = testContext; - + await ExecuteWithRetries(test); + + var timingProperty = GetTimingProperty(testContext, start); + + testContext.Result = new TestResult + { + Duration = timingProperty.GlobalTiming.Duration, + Start = timingProperty.GlobalTiming.StartTime, + End = timingProperty.GlobalTiming.EndTime, + ComputerName = Environment.MachineName, + Exception = null, + Status = Status.Passed, + Output = $"{testContext.GetErrorOutput()}{Environment.NewLine}{testContext.GetStandardOutput()}" + }; + } + catch (Exception e) + { + var timingProperty = GetTimingProperty(testContext, start); + + testContext.Result = new TestResult + { + Duration = timingProperty.GlobalTiming.Duration, + Start = timingProperty.GlobalTiming.StartTime, + End = timingProperty.GlobalTiming.EndTime, + ComputerName = Environment.MachineName, + Exception = e, + Status = Status.Failed, + Output = $"{testContext.GetErrorOutput()}{Environment.NewLine}{testContext.GetStandardOutput()}" + }; + + throw; } finally { @@ -227,28 +256,23 @@ private async Task ExecuteBeforeHooks(DiscoveredTest test, ExecuteRequestContext private async Task ExecuteAfterHooks(DiscoveredTest test, ExecuteRequestContext context, TestContext testContext, List cleanUpExceptions) { - try - { - await classHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, + await classHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, test.TestContext.TestDetails.ClassType, cleanUpExceptions); - await assemblyHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, - test.TestContext.TestDetails.ClassType.Assembly, cleanUpExceptions); + await assemblyHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, + test.TestContext.TestDetails.ClassType.Assembly, cleanUpExceptions); - if (InstanceTracker.IsLastTest()) + if (InstanceTracker.IsLastTest()) + { + foreach (var testEndEventsObject in testContext.GetLastTestInTestSessionEventObjects()) { - foreach (var testEndEventsObject in testContext.GetLastTestInTestSessionEventObjects()) - { - await RunHelpers.RunValueTaskSafelyAsync( - () => testEndEventsObject.IfLastTestInTestSession(TestSessionContext.Current!, testContext), - cleanUpExceptions); - } + await RunHelpers.RunValueTaskSafelyAsync( + () => testEndEventsObject.IfLastTestInTestSession(TestSessionContext.Current!, testContext), + cleanUpExceptions); } } - catch - { - // Ignored - Will be counted as its own test failure - We don't need to bind it to this test - } + + ExceptionsHelper.ThrowIfAny(cleanUpExceptions); } private async ValueTask DisposeTest(TestContext testContext, List cleanUpExceptions) @@ -292,21 +316,6 @@ private static TimingProperty GetTimingProperty(TestContext testContext, DateTim } } - private static IProperty GetFailureStateProperty(TestContext testContext, Exception e, TimeSpan duration) - { - if (testContext.TestDetails.Timeout.HasValue - && e is TaskCanceledException or OperationCanceledException or TimeoutException - && duration >= testContext.TestDetails.Timeout.Value) - { - return new TimeoutTestNodeStateProperty(e) - { - Timeout = testContext.TestDetails.Timeout, - }; - } - - return new FailedTestNodeStateProperty(e); - } - private async ValueTask Dispose(TestContext testContext) { await disposer.DisposeAsync(testContext); @@ -380,8 +389,31 @@ private async ValueTask ExecuteCore(DiscoveredTest discoveredTest) { throw new SkipTestException("The test has been cancelled..."); } - - await ExecuteTestMethodWithTimeout(discoveredTest); + + var linkedTokenSource = CreateLinkedToken(discoveredTest.TestContext, engineCancellationToken.CancellationTokenSource); + + try + { + await ExecuteTestMethodWithTimeout(discoveredTest, linkedTokenSource.Token); + } + finally + { + if (linkedTokenSource != engineCancellationToken.CancellationTokenSource) + { + linkedTokenSource.Dispose(); + } + } + } + + private static CancellationTokenSource CreateLinkedToken(TestContext testContext, + CancellationTokenSource cancellationTokenSource) + { + if (testContext.LinkedCancellationTokens.Count == 0) + { + return cancellationTokenSource; + } + + return CancellationTokenSource.CreateLinkedTokenSource([cancellationTokenSource.Token, ..testContext.LinkedCancellationTokens.ToArray()]); } private async ValueTask WaitForDependsOnTests(DiscoveredTest testContext, ITestExecutionFilter? filter, @@ -436,17 +468,17 @@ private async ValueTask WaitForDependsOnTests(DiscoveredTest testContext, ITestE } } - private async Task ExecuteTestMethodWithTimeout(DiscoveredTest discoveredTest) + private async Task ExecuteTestMethodWithTimeout(DiscoveredTest discoveredTest, CancellationToken cancellationToken) { var testDetails = discoveredTest.TestDetails; if (testDetails.Timeout == null || testDetails.Timeout.Value == default) { - await RunTest(discoveredTest, engineCancellationToken.Token); + await RunTest(discoveredTest, cancellationToken); return; } - await RunHelpers.RunWithTimeoutAsync(token => RunTest(discoveredTest, token), testDetails.Timeout, engineCancellationToken); + await RunHelpers.RunWithTimeoutAsync(token => RunTest(discoveredTest, token), testDetails.Timeout, cancellationToken); } diff --git a/TUnit.Engine/Services/TestFilterService.cs b/TUnit.Engine/Services/TestFilterService.cs index 170f87c1ec..ccf5f01a20 100644 --- a/TUnit.Engine/Services/TestFilterService.cs +++ b/TUnit.Engine/Services/TestFilterService.cs @@ -3,7 +3,6 @@ using Microsoft.Testing.Platform.Requests; using TUnit.Core; using TUnit.Core.Exceptions; -using TUnit.Engine.Extensions; namespace TUnit.Engine.Services; diff --git a/TUnit.Engine/Services/TestsConstructor.cs b/TUnit.Engine/Services/TestsConstructor.cs index 5326ad0947..581706107e 100644 --- a/TUnit.Engine/Services/TestsConstructor.cs +++ b/TUnit.Engine/Services/TestsConstructor.cs @@ -14,11 +14,11 @@ public IEnumerable GetTests() return testMetadatas.Select(ConstructTest); } - private DiscoveredTest ConstructTest(TestMetadata testMetadata) + public DiscoveredTest ConstructTest(TestMetadata testMetadata) { var testDetails = testMetadata.BuildTestDetails(); - var testContext = new TestContext(serviceProvider, testDetails, testMetadata.ObjectBag); + var testContext = new TestContext(serviceProvider, testDetails, testMetadata); RunOnTestDiscoveryAttributeHooks([..testDetails.DataAttributes, ..testDetails.Attributes], testContext); diff --git a/TUnit.Engine/Services/TestsExecutor.cs b/TUnit.Engine/Services/TestsExecutor.cs index a0bd73c5cb..f6bb92c316 100644 --- a/TUnit.Engine/Services/TestsExecutor.cs +++ b/TUnit.Engine/Services/TestsExecutor.cs @@ -19,6 +19,9 @@ internal class TestsExecutor private readonly TUnitFrameworkLogger _logger; private readonly ICommandLineOptions _commandLineOptions; private readonly EngineCancellationToken _engineCancellationToken; + + private readonly Counter _executionCounter = new(); + private readonly TaskCompletionSource _onFinished = new(); private readonly ConcurrentDictionary _notInParallelKeyedLocks = new(); #if NET9_0_OR_GREATER @@ -40,16 +43,37 @@ public TestsExecutor(SingleTestExecutor singleTestExecutor, _engineCancellationToken = engineCancellationToken; _maximumParallelTests = GetParallelTestsLimit(); + + _executionCounter.OnCountChanged += (sender, count) => + { + if (count == 0) + { + _onFinished.TrySetResult(); + } + }; } public async Task ExecuteAsync(GroupedTests tests, ITestExecutionFilter? filter, ExecuteRequestContext context) { - await ProcessParallelTests(tests.Parallel, filter, context); + await using var _ = _engineCancellationToken.Token.Register(() => _onFinished.TrySetCanceled()); + + try + { + _executionCounter.Increment(); + + await ProcessParallelTests(tests.Parallel, filter, context); - await ProcessKeyedNotInParallelTests(tests.KeyedNotInParallel, filter, context); + await ProcessKeyedNotInParallelTests(tests.KeyedNotInParallel, filter, context); - await ProcessNotInParallelTests(tests.NotInParallel, filter, context); + await ProcessNotInParallelTests(tests.NotInParallel, filter, context); + } + finally + { + _executionCounter.Decrement(); + } } + + public Task WaitForFinishAsync() => _onFinished.Task; private async Task ProcessNotInParallelTests(Queue testsNotInParallel, ITestExecutionFilter? filter, ExecuteRequestContext context) { diff --git a/TUnit.Engine/TUnitMessageBus.cs b/TUnit.Engine/TUnitMessageBus.cs index b8f6e23b4b..31ce1e1556 100644 --- a/TUnit.Engine/TUnitMessageBus.cs +++ b/TUnit.Engine/TUnitMessageBus.cs @@ -13,27 +13,32 @@ public class TUnitMessageBus(IExtension extension, ExecuteRequestContext context { private readonly SessionUid _sessionSessionUid = context.Request.Session.SessionUid; - public Task Discovered(TestContext testContext) + public async ValueTask Discovered(TestContext testContext) { - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(DiscoveredTestNodeStateProperty.CachedInstance) )); } - public Task InProgress(TestContext testContext) + public async ValueTask InProgress(TestContext testContext) { - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(InProgressTestNodeStateProperty.CachedInstance) )); } - public Task Passed(TestContext testContext, DateTimeOffset start) + public async ValueTask Passed(TestContext testContext, DateTimeOffset start) { - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + if (!testContext.ReportResult) + { + return; + } + + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(PassedTestNodeStateProperty.CachedInstance) @@ -43,14 +48,19 @@ public Task Passed(TestContext testContext, DateTimeOffset start) )); } - public Task Failed(TestContext testContext, Exception exception, DateTimeOffset start) + public async ValueTask Failed(TestContext testContext, Exception exception, DateTimeOffset start) { + if (!testContext.ReportResult) + { + return; + } + var timingProperty = GetTimingProperty(testContext, start); var updateType = GetFailureStateProperty(testContext, exception, timingProperty.GlobalTiming.Duration); - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(updateType) @@ -61,11 +71,11 @@ public Task Failed(TestContext testContext, Exception exception, DateTimeOffset )); } - public Task FailedInitialization(FailedInitializationTest failedInitializationTest) + public async ValueTask FailedInitialization(FailedInitializationTest failedInitializationTest) { var testClass = failedInitializationTest.TestClass; - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: new TestNode { @@ -93,9 +103,9 @@ public Task FailedInitialization(FailedInitializationTest failedInitializationTe )); } - public Task Skipped(TestContext testContext, string reason) + public async ValueTask Skipped(TestContext testContext, string reason) { - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(new SkippedTestNodeStateProperty(reason)) @@ -104,18 +114,18 @@ public Task Skipped(TestContext testContext, string reason) )); } - public Task Cancelled(TestContext testContext) + public async ValueTask Cancelled(TestContext testContext) { - return context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( + await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage( sessionUid: _sessionSessionUid, testNode: testContext.ToTestNode() .WithProperty(new CancelledTestNodeStateProperty()) )); } - public Task SessionArtifact(Artifact artifact) + public async ValueTask SessionArtifact(Artifact artifact) { - return context.MessageBus.PublishAsync(this, + await context.MessageBus.PublishAsync(this, new SessionFileArtifact( context.Request.Session.SessionUid, artifact.File, @@ -125,9 +135,9 @@ public Task SessionArtifact(Artifact artifact) ); } - public Task TestArtifact(TestContext testContext, Artifact artifact) + public async ValueTask TestArtifact(TestContext testContext, Artifact artifact) { - return context.MessageBus.PublishAsync(this, + await context.MessageBus.PublishAsync(this, new TestNodeFileArtifact( context.Request.Session.SessionUid, testContext.ToTestNode(), diff --git a/TUnit.Engine/TestRegistrar.cs b/TUnit.Engine/TestRegistrar.cs index 7e57e9a392..8496567ce3 100644 --- a/TUnit.Engine/TestRegistrar.cs +++ b/TUnit.Engine/TestRegistrar.cs @@ -7,7 +7,7 @@ namespace TUnit.Engine; internal class TestRegistrar(AssemblyHookOrchestrator assemblyHookOrchestrator, ClassHookOrchestrator classHookOrchestrator) { - internal async Task RegisterInstance(TestContext testContext, Func onFailureToInitialize) + internal async Task RegisterInstance(TestContext testContext, Func onFailureToInitialize) { try { diff --git a/TUnit.TestProject/DynamicallyRegisteredTests.cs b/TUnit.TestProject/DynamicallyRegisteredTests.cs new file mode 100644 index 0000000000..a093a00da7 --- /dev/null +++ b/TUnit.TestProject/DynamicallyRegisteredTests.cs @@ -0,0 +1,60 @@ +using TUnit.Core.Enums; +using TUnit.Core.Interfaces; +using TUnit.Engine.Extensions; + +namespace TUnit.TestProject; + +public class DynamicallyRegisteredTests +{ + [Test] + [DynamicDataGenerator] + public void MyTest(int value) + { + throw new Exception($@"Value {value} !"); + } +} + +public class DynamicDataGenerator : DataSourceGeneratorAttribute, ITestStartEvent, ITestEndEvent +{ + private static int _count; + + private readonly CancellationTokenSource _cancellationTokenSource = new(); + + public override IEnumerable GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) + { + yield return Random.Shared.Next(); + } + + public ValueTask OnTestStart(BeforeTestContext beforeTestContext) + { + if (!beforeTestContext.TestContext.ObjectBag.ContainsKey("DynamicDataGeneratorRetry")) + { + beforeTestContext.AddLinkedCancellationToken(_cancellationTokenSource.Token); + } + + return ValueTask.CompletedTask; + } + + public async ValueTask OnTestEnd(TestContext testContext) + { + if (testContext.Result?.Status == Status.Failed) + { + await _cancellationTokenSource.CancelAsync(); + + // We need a condition to end execution at some point otherwise we could go forever recursively + if (_count++ > 5) + { + throw new Exception(); + return; + } + + await testContext.ReregisterTestWithArguments(methodArguments: [Random.Shared.Next()], + objectBag: new() + { + ["DynamicDataGeneratorRetry"] = true + }, + dataAttributes: [this] + ); + } + } +} \ No newline at end of file From 4277401a2575b81a7362495a97965bdb6b42f699 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 01:46:07 +0100 Subject: [PATCH 02/12] ExceptionDispatchInfo --- TUnit.Core/AsyncConvert.cs | 8 +++----- TUnit.Engine/Extensions/TestContextExtensions.cs | 16 ++++++++++++---- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/TUnit.Core/AsyncConvert.cs b/TUnit.Core/AsyncConvert.cs index b7053411cb..dc7d3ff2d3 100644 --- a/TUnit.Core/AsyncConvert.cs +++ b/TUnit.Core/AsyncConvert.cs @@ -21,18 +21,16 @@ public static async Task Convert(Func action) await action(); } - public static Task Convert(object? invoke) + public static async ValueTask Convert(object? invoke) { if (invoke is Task task) { - return task; + await task; } if (invoke is ValueTask valueTask) { - return valueTask.AsTask(); + await valueTask.AsTask(); } - - return Task.CompletedTask; } } \ No newline at end of file diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index 883dba6d0b..96d7fb2255 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -1,4 +1,6 @@ using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using System.Runtime.ExceptionServices; using Microsoft.Testing.Platform.Extensions.TestFramework; using TUnit.Core; using TUnit.Engine.Models; @@ -23,14 +25,20 @@ public static class TestContextExtensions TestMethodArguments = methodArguments ?? [], ObjectBag = objectBag ?? [], ResettableClassFactory = testMetadata.ResettableClassFactory.Clone(), - TestMethodFactory = (@class, token) => + TestMethodFactory = async (@class, token) => { var hasTimeout = testContext.TestDetails.Timeout != null; var args = GetArgs(methodArguments, hasTimeout, token); - - return AsyncConvert.Convert( - testContext.TestDetails.MethodInfo.Invoke(@class, args)); + + try + { + await AsyncConvert.Convert(testContext.TestDetails.MethodInfo.Invoke(@class, args)); + } + catch (TargetInvocationException e) + { + ExceptionDispatchInfo.Throw(e.InnerException ?? e); + } } }; From 7fcf343a6813ceabeba33dd80de49ed3056a03cf Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 02:07:13 +0100 Subject: [PATCH 03/12] Re-register tests --- TUnit.Core/InstanceTracker.cs | 91 ------------------- .../Extensions/TestContextExtensions.cs | 7 +- .../Framework/TUnitServiceProvider.cs | 10 +- .../Hooks/AssemblyHookOrchestrator.cs | 4 +- TUnit.Engine/Hooks/ClassHookOrchestrator.cs | 4 +- TUnit.Engine/InstanceTracker.cs | 66 ++++++++++++++ TUnit.Engine/Services/SingleTestExecutor.cs | 3 +- TUnit.Engine/TestRegistrar.cs | 4 +- .../DynamicallyRegisteredTests.cs | 24 +++-- 9 files changed, 100 insertions(+), 113 deletions(-) delete mode 100644 TUnit.Core/InstanceTracker.cs create mode 100644 TUnit.Engine/InstanceTracker.cs diff --git a/TUnit.Core/InstanceTracker.cs b/TUnit.Core/InstanceTracker.cs deleted file mode 100644 index ed11dd627a..0000000000 --- a/TUnit.Core/InstanceTracker.cs +++ /dev/null @@ -1,91 +0,0 @@ -using System.Collections.Concurrent; -using System.Reflection; - -namespace TUnit.Core; - -internal static class InstanceTracker -{ -#if NET9_0_OR_GREATER - private static readonly Lock PerClassTypeLock = new(); -#else - private static readonly object PerClassTypeLock = new(); -#endif - private static readonly ConcurrentDictionary PerClassType = new(); - -#if NET9_0_OR_GREATER - private static readonly Lock PerAssemblyLock = new(); -#else - private static readonly object PerAssemblyLock = new(); -#endif - private static readonly ConcurrentDictionary PerAssembly = new(); - - private static int TotalInstances; - - public static void Register(Type classType) - { - foreach (var type in GetTypesIncludingBase(classType)) - { - var count = PerClassType.GetOrAdd(type, _ => 0); - PerClassType[type] = count + 1; - } - - lock (PerAssemblyLock) - { - var assembly = classType.Assembly; - var count = PerAssembly.GetOrAdd(assembly, _ => 0); - PerAssembly[assembly] = count + 1; - } - - TotalInstances++; - } - - public static bool IsLastTestForType(Type type) - { - lock (PerClassTypeLock) - { - var count = PerClassType[type]; - var newCount = count - 1; - PerClassType[type] = newCount; - - if (newCount < 0) - { - throw new Exception($"Remaining tests has gone below 0 for Type {type}"); - } - - return newCount == 0; - } - } - - public static bool IsLastTestForAssembly(Assembly assembly) - { - lock (PerAssemblyLock) - { - var count = PerAssembly[assembly]; - var newCount = count - 1; - PerAssembly[assembly] = newCount; - - if (newCount < 0) - { - throw new Exception("Remaining tests has gone below 0"); - } - - return newCount == 0; - } - } - - private static IEnumerable GetTypesIncludingBase(Type testClassType) - { - var type = testClassType; - - while (type != null && type != typeof(object)) - { - yield return type; - type = type.BaseType; - } - } - - public static bool IsLastTest() - { - return Interlocked.Decrement(ref TotalInstances) == 0; - } -} \ No newline at end of file diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index 96d7fb2255..791182164e 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -10,18 +10,17 @@ namespace TUnit.Engine.Extensions; public static class TestContextExtensions { + [Experimental("WIP")] public static async Task ReregisterTestWithArguments<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TTestClass>( this TestContext testContext, object?[]? methodArguments, - Dictionary? objectBag = null, - Attribute[]? dataAttributes = null) + Dictionary? objectBag = null) { - var testMetadata = (TestMetadata)testContext.OriginalMetadata; + var testMetadata = (TestMetadata) testContext.OriginalMetadata; var newTestMetaData = testMetadata with { TestId = Guid.NewGuid().ToString(), - DataAttributes = dataAttributes ?? testContext.TestDetails.DataAttributes, TestMethodArguments = methodArguments ?? [], ObjectBag = objectBag ?? [], ResettableClassFactory = testMetadata.ResettableClassFactory.Clone(), diff --git a/TUnit.Engine/Framework/TUnitServiceProvider.cs b/TUnit.Engine/Framework/TUnitServiceProvider.cs index 4ff58d4819..cbe60b6bdd 100644 --- a/TUnit.Engine/Framework/TUnitServiceProvider.cs +++ b/TUnit.Engine/Framework/TUnitServiceProvider.cs @@ -67,6 +67,8 @@ public TUnitServiceProvider(IExtension extension, var stringFilter = FilterParser.GetTestFilter(context); TUnitMessageBus = Register(new TUnitMessageBus(extension, context)); + + var instanceTracker = Register(new InstanceTracker()); var hooksCollector = Register(new HooksCollector()); @@ -76,16 +78,16 @@ public TUnitServiceProvider(IExtension extension, TestGrouper = Register(new TestGrouper()); - AssemblyHookOrchestrator = Register(new AssemblyHookOrchestrator(hooksCollector)); + AssemblyHookOrchestrator = Register(new AssemblyHookOrchestrator(instanceTracker, hooksCollector)); TestDiscoveryHookOrchestrator = Register(new TestDiscoveryHookOrchestrator(hooksCollector, stringFilter)); TestSessionHookOrchestrator = Register(new TestSessionHookOrchestrator(hooksCollector, AssemblyHookOrchestrator, stringFilter)); - var classHookOrchestrator = Register(new ClassHookOrchestrator(hooksCollector)); + var classHookOrchestrator = Register(new ClassHookOrchestrator(instanceTracker, hooksCollector)); var testHookOrchestrator = Register(new TestHookOrchestrator(hooksCollector)); - var testRegistrar = Register(new TestRegistrar(AssemblyHookOrchestrator, classHookOrchestrator)); + var testRegistrar = Register(new TestRegistrar(instanceTracker, AssemblyHookOrchestrator, classHookOrchestrator)); TestDiscoverer = Register(new TUnitTestDiscoverer(hooksCollector, testsLoader, testFilterService, TestGrouper, testRegistrar, TestDiscoveryHookOrchestrator, TUnitMessageBus, LoggerFactory, extension)); TestFinder = Register(new TestsFinder(TestDiscoverer)); @@ -99,7 +101,7 @@ public TUnitServiceProvider(IExtension extension, var parallelLimitProvider = Register(new ParallelLimitProvider()); var hookMessagePublisher = Register(new HookMessagePublisher(extension, messageBus)); - var singleTestExecutor = Register(new SingleTestExecutor(extension, Disposer, cancellationTokenSource, testInvoker, + var singleTestExecutor = Register(new SingleTestExecutor(extension, Disposer, cancellationTokenSource, instanceTracker, testInvoker, explicitFilterService, parallelLimitProvider, AssemblyHookOrchestrator, classHookOrchestrator, testHookOrchestrator, TestFinder, TUnitMessageBus, Logger, EngineCancellationToken)); TestsExecutor = Register(new TestsExecutor(singleTestExecutor, Logger, CommandLineOptions, EngineCancellationToken)); diff --git a/TUnit.Engine/Hooks/AssemblyHookOrchestrator.cs b/TUnit.Engine/Hooks/AssemblyHookOrchestrator.cs index 60516a8222..cd84647506 100644 --- a/TUnit.Engine/Hooks/AssemblyHookOrchestrator.cs +++ b/TUnit.Engine/Hooks/AssemblyHookOrchestrator.cs @@ -10,7 +10,7 @@ namespace TUnit.Engine.Hooks; #if !DEBUG [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] #endif -internal class AssemblyHookOrchestrator(HooksCollector hooksCollector) +internal class AssemblyHookOrchestrator(InstanceTracker instanceTracker, HooksCollector hooksCollector) { private readonly ConcurrentDictionary _assemblyHookContexts = new(); @@ -47,7 +47,7 @@ public async Task ExecuteCleanUpsIfLastInstance( List cleanUpExceptions ) { - if (!InstanceTracker.IsLastTestForAssembly(assembly)) + if (!instanceTracker.IsLastTestForAssembly(assembly)) { // Only run one time clean downs when no instances are left! return; diff --git a/TUnit.Engine/Hooks/ClassHookOrchestrator.cs b/TUnit.Engine/Hooks/ClassHookOrchestrator.cs index 6b691e5540..766f505ebb 100644 --- a/TUnit.Engine/Hooks/ClassHookOrchestrator.cs +++ b/TUnit.Engine/Hooks/ClassHookOrchestrator.cs @@ -9,7 +9,7 @@ namespace TUnit.Engine.Hooks; #if !DEBUG [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] #endif -internal class ClassHookOrchestrator(HooksCollector hooksCollector) +internal class ClassHookOrchestrator(InstanceTracker instanceTracker, HooksCollector hooksCollector) { private readonly ConcurrentDictionary _classHookContexts = new(); @@ -52,7 +52,7 @@ public async Task ExecuteCleanUpsIfLastInstance( List cleanUpExceptions ) { - if (!InstanceTracker.IsLastTestForType(testClassType)) + if (!instanceTracker.IsLastTestForType(testClassType)) { // Only run one time clean downs when no instances are left! return; diff --git a/TUnit.Engine/InstanceTracker.cs b/TUnit.Engine/InstanceTracker.cs new file mode 100644 index 0000000000..eaedf911b1 --- /dev/null +++ b/TUnit.Engine/InstanceTracker.cs @@ -0,0 +1,66 @@ +using System.Collections.Concurrent; +using System.Reflection; +using TUnit.Engine.Services; + +namespace TUnit.Engine; + +internal class InstanceTracker +{ + private readonly ConcurrentDictionary _perClassType = new(); + + private readonly ConcurrentDictionary _perAssembly = new(); + + private readonly Counter _totalInstances = new(); + + public void Register(Type classType) + { + foreach (var type in GetTypesIncludingBase(classType)) + { + _perClassType.GetOrAdd(type, _ => new Counter()).Increment(); + } + + _perAssembly.GetOrAdd(classType.Assembly, _ => new Counter()).Increment(); + + _totalInstances.Increment(); + } + + public bool IsLastTestForType(Type type) + { + var count = _perClassType[type].Decrement(); + + if (count < 0) + { + throw new Exception($"Remaining tests has gone below 0 for Type {type}"); + } + + return count == 0; + } + + public bool IsLastTestForAssembly(Assembly assembly) + { + var count = _perAssembly[assembly].Decrement(); + + if (count < 0) + { + throw new Exception("Remaining tests has gone below 0"); + } + + return count == 0; + } + + public bool IsLastTest() + { + return _totalInstances.Decrement() == 0; + } + + private static IEnumerable GetTypesIncludingBase(Type testClassType) + { + var type = testClassType; + + while (type != null && type != typeof(object)) + { + yield return type; + type = type.BaseType; + } + } +} \ No newline at end of file diff --git a/TUnit.Engine/Services/SingleTestExecutor.cs b/TUnit.Engine/Services/SingleTestExecutor.cs index 11432943f1..08a88cc3c8 100644 --- a/TUnit.Engine/Services/SingleTestExecutor.cs +++ b/TUnit.Engine/Services/SingleTestExecutor.cs @@ -21,6 +21,7 @@ internal class SingleTestExecutor( IExtension extension, Disposer disposer, CancellationTokenSource cancellationTokenSource, + InstanceTracker instanceTracker, TestInvoker testInvoker, ExplicitFilterService explicitFilterService, ParallelLimitProvider parallelLimitProvider, @@ -262,7 +263,7 @@ await classHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, await assemblyHookOrchestrator.ExecuteCleanUpsIfLastInstance(testContext, test.TestContext.TestDetails.ClassType.Assembly, cleanUpExceptions); - if (InstanceTracker.IsLastTest()) + if (instanceTracker.IsLastTest()) { foreach (var testEndEventsObject in testContext.GetLastTestInTestSessionEventObjects()) { diff --git a/TUnit.Engine/TestRegistrar.cs b/TUnit.Engine/TestRegistrar.cs index 8496567ce3..3475828e7f 100644 --- a/TUnit.Engine/TestRegistrar.cs +++ b/TUnit.Engine/TestRegistrar.cs @@ -5,7 +5,7 @@ namespace TUnit.Engine; -internal class TestRegistrar(AssemblyHookOrchestrator assemblyHookOrchestrator, ClassHookOrchestrator classHookOrchestrator) +internal class TestRegistrar(InstanceTracker instanceTracker, AssemblyHookOrchestrator assemblyHookOrchestrator, ClassHookOrchestrator classHookOrchestrator) { internal async Task RegisterInstance(TestContext testContext, Func onFailureToInitialize) { @@ -15,7 +15,7 @@ internal async Task RegisterInstance(TestContext testContext, Func GenerateDataSources(DataGeneratorMetadata dataG public ValueTask OnTestStart(BeforeTestContext beforeTestContext) { - if (!beforeTestContext.TestContext.ObjectBag.ContainsKey("DynamicDataGeneratorRetry")) + if (!IsReregisteredTest(beforeTestContext.TestContext)) { beforeTestContext.AddLinkedCancellationToken(_cancellationTokenSource.Token); } @@ -35,6 +36,7 @@ public ValueTask OnTestStart(BeforeTestContext beforeTestContext) return ValueTask.CompletedTask; } + [Experimental("WIP")] public async ValueTask OnTestEnd(TestContext testContext) { if (testContext.Result?.Status == Status.Failed) @@ -42,19 +44,27 @@ public async ValueTask OnTestEnd(TestContext testContext) await _cancellationTokenSource.CancelAsync(); // We need a condition to end execution at some point otherwise we could go forever recursively - if (_count++ > 5) + if (Interlocked.Increment(ref _count) > 5) { throw new Exception(); - return; + } + + if (IsReregisteredTest(testContext)) + { + // Optional to reduce noise + // testContext.SuppressReportingResult(); } await testContext.ReregisterTestWithArguments(methodArguments: [Random.Shared.Next()], objectBag: new() { ["DynamicDataGeneratorRetry"] = true - }, - dataAttributes: [this] - ); + }); } } + + private static bool IsReregisteredTest(TestContext testContext) + { + return testContext.ObjectBag.ContainsKey("DynamicDataGeneratorRetry"); + } } \ No newline at end of file From da423454529fc23acb957844886066646ee1a0c6 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:34:28 +0100 Subject: [PATCH 04/12] Update TUnit.Core/AsyncConvert.cs Co-authored-by: campersau --- TUnit.Core/AsyncConvert.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Core/AsyncConvert.cs b/TUnit.Core/AsyncConvert.cs index dc7d3ff2d3..dfa242f6a5 100644 --- a/TUnit.Core/AsyncConvert.cs +++ b/TUnit.Core/AsyncConvert.cs @@ -30,7 +30,7 @@ public static async ValueTask Convert(object? invoke) if (invoke is ValueTask valueTask) { - await valueTask.AsTask(); + await valueTask; } } } \ No newline at end of file From 4e54e52ea25aed24f9905c6bd2abb12f62dabba7 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:58:18 +0100 Subject: [PATCH 05/12] ReregisterTestWithArguments remove generic argument --- TUnit.Core/TestMetadata.cs | 11 +++++ .../Extensions/TestContextExtensions.cs | 43 +++++++++---------- .../DynamicallyRegisteredTests.cs | 2 +- 3 files changed, 33 insertions(+), 23 deletions(-) diff --git a/TUnit.Core/TestMetadata.cs b/TUnit.Core/TestMetadata.cs index f0351d805c..85cbe10e5a 100644 --- a/TUnit.Core/TestMetadata.cs +++ b/TUnit.Core/TestMetadata.cs @@ -73,6 +73,15 @@ internal override DiscoveredTest BuildDiscoveredTest(TestContext testContext) ClassConstructor = ClassConstructor }; } + + public override TestMetadata CloneWithNewMethodFactory(Func testMethodFactory) + { + return this with + { + TestMethodFactory = (@class, token) => testMethodFactory.Invoke(@class, token), + ResettableClassFactory = ResettableClassFactory.Clone() + }; + } } public abstract record TestMetadata @@ -105,4 +114,6 @@ public abstract record TestMetadata public abstract TestDetails BuildTestDetails(); internal abstract DiscoveredTest BuildDiscoveredTest(TestContext testContext); + + public abstract TestMetadata CloneWithNewMethodFactory(Func testMethodFactory); } \ No newline at end of file diff --git a/TUnit.Engine/Extensions/TestContextExtensions.cs b/TUnit.Engine/Extensions/TestContextExtensions.cs index 791182164e..f43fe91410 100644 --- a/TUnit.Engine/Extensions/TestContextExtensions.cs +++ b/TUnit.Engine/Extensions/TestContextExtensions.cs @@ -11,35 +11,34 @@ namespace TUnit.Engine.Extensions; public static class TestContextExtensions { [Experimental("WIP")] - public static async Task ReregisterTestWithArguments<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TTestClass>( + public static async Task ReregisterTestWithArguments( this TestContext testContext, object?[]? methodArguments, Dictionary? objectBag = null) { - var testMetadata = (TestMetadata) testContext.OriginalMetadata; - - var newTestMetaData = testMetadata with - { - TestId = Guid.NewGuid().ToString(), - TestMethodArguments = methodArguments ?? [], - ObjectBag = objectBag ?? [], - ResettableClassFactory = testMetadata.ResettableClassFactory.Clone(), - TestMethodFactory = async (@class, token) => - { - var hasTimeout = testContext.TestDetails.Timeout != null; - - var args = GetArgs(methodArguments, hasTimeout, token); + var testMetadata = testContext.OriginalMetadata; - try - { - await AsyncConvert.Convert(testContext.TestDetails.MethodInfo.Invoke(@class, args)); - } - catch (TargetInvocationException e) + var newTestMetaData = testMetadata.CloneWithNewMethodFactory(async (@class, token) => { - ExceptionDispatchInfo.Throw(e.InnerException ?? e); + var hasTimeout = testContext.TestDetails.Timeout != null; + + var args = GetArgs(methodArguments, hasTimeout, token); + + try + { + await AsyncConvert.Convert(testContext.TestDetails.MethodInfo.Invoke(@class, args)); + } + catch (TargetInvocationException e) + { + ExceptionDispatchInfo.Throw(e.InnerException ?? e); + } } - } - }; + ) with + { + TestId = Guid.NewGuid().ToString(), + TestMethodArguments = methodArguments ?? [], + ObjectBag = objectBag ?? [], + }; var newTest = testContext.GetService().ConstructTest(newTestMetaData); diff --git a/TUnit.TestProject/DynamicallyRegisteredTests.cs b/TUnit.TestProject/DynamicallyRegisteredTests.cs index 3b3fd891cc..6b5e09b194 100644 --- a/TUnit.TestProject/DynamicallyRegisteredTests.cs +++ b/TUnit.TestProject/DynamicallyRegisteredTests.cs @@ -55,7 +55,7 @@ public async ValueTask OnTestEnd(TestContext testContext) // testContext.SuppressReportingResult(); } - await testContext.ReregisterTestWithArguments(methodArguments: [Random.Shared.Next()], + await testContext.ReregisterTestWithArguments(methodArguments: [Random.Shared.Next()], objectBag: new() { ["DynamicDataGeneratorRetry"] = true From b0e50e22178b028be15091429f075cc150952764 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 10:59:42 +0100 Subject: [PATCH 06/12] Remove csproj entry --- TUnit.Core/TUnit.Core.csproj | 3 --- 1 file changed, 3 deletions(-) diff --git a/TUnit.Core/TUnit.Core.csproj b/TUnit.Core/TUnit.Core.csproj index 7ddfe64f74..ef65daa61b 100644 --- a/TUnit.Core/TUnit.Core.csproj +++ b/TUnit.Core/TUnit.Core.csproj @@ -18,7 +18,4 @@ - - - \ No newline at end of file From 263be341e76643d1ccb0aad3fd2ed3bd410c9427 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 11:11:05 +0100 Subject: [PATCH 07/12] Simplify try finally block and fix tests --- TUnit.Core/TestMetadata.cs | 2 +- TUnit.Engine/Services/SingleTestExecutor.cs | 19 ++----------------- TUnit.UnitTests/TestExtensionsTests.cs | 19 ++++++++++++++++--- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/TUnit.Core/TestMetadata.cs b/TUnit.Core/TestMetadata.cs index 85cbe10e5a..4d1c99514c 100644 --- a/TUnit.Core/TestMetadata.cs +++ b/TUnit.Core/TestMetadata.cs @@ -78,7 +78,7 @@ public override TestMetadata CloneWithNewMethodFactory(Func testMethodFactory.Invoke(@class, token), + TestMethodFactory = (@class, token) => testMethodFactory.Invoke(@class!, token), ResettableClassFactory = ResettableClassFactory.Clone() }; } diff --git a/TUnit.Engine/Services/SingleTestExecutor.cs b/TUnit.Engine/Services/SingleTestExecutor.cs index 08a88cc3c8..06b571d008 100644 --- a/TUnit.Engine/Services/SingleTestExecutor.cs +++ b/TUnit.Engine/Services/SingleTestExecutor.cs @@ -391,29 +391,14 @@ private async ValueTask ExecuteCore(DiscoveredTest discoveredTest) throw new SkipTestException("The test has been cancelled..."); } - var linkedTokenSource = CreateLinkedToken(discoveredTest.TestContext, engineCancellationToken.CancellationTokenSource); + using var linkedTokenSource = CreateLinkedToken(discoveredTest.TestContext, engineCancellationToken.CancellationTokenSource); - try - { - await ExecuteTestMethodWithTimeout(discoveredTest, linkedTokenSource.Token); - } - finally - { - if (linkedTokenSource != engineCancellationToken.CancellationTokenSource) - { - linkedTokenSource.Dispose(); - } - } + await ExecuteTestMethodWithTimeout(discoveredTest, linkedTokenSource.Token); } private static CancellationTokenSource CreateLinkedToken(TestContext testContext, CancellationTokenSource cancellationTokenSource) { - if (testContext.LinkedCancellationTokens.Count == 0) - { - return cancellationTokenSource; - } - return CancellationTokenSource.CreateLinkedTokenSource([cancellationTokenSource.Token, ..testContext.LinkedCancellationTokens.ToArray()]); } diff --git a/TUnit.UnitTests/TestExtensionsTests.cs b/TUnit.UnitTests/TestExtensionsTests.cs index 120fb6ff14..3c5dbd1ca7 100644 --- a/TUnit.UnitTests/TestExtensionsTests.cs +++ b/TUnit.UnitTests/TestExtensionsTests.cs @@ -24,13 +24,13 @@ public void TopLevelClass() .With(x => x.TestClassArguments, []) .Create(); - var context = new TestContext(null!, testDetails, []); + var context = new TestContext(null!, testDetails, CreateDummyMetadata()); var name = context.GetClassTypeName(); Assert.That(name, Is.EqualTo("TestExtensionsTests")); } - + [Test] public void NestedClass() { @@ -46,12 +46,25 @@ public void NestedClass() .With(x => x.TestClassArguments, []) .Create(); - var context = new TestContext(null!, testDetails, []); + var context = new TestContext(null!, testDetails, CreateDummyMetadata()); var name = context.GetClassTypeName(); Assert.That(name, Is.EqualTo("TestExtensionsTests+InnerClass")); } + private TestMetadata CreateDummyMetadata() + { + return _fixture.Build>() + .Without(x => x.MethodInfo) + .Without(x => x.ResettableClassFactory) + .Without(x => x.ParallelLimit) + .Without(x => x.ClassConstructor) + .Without(x => x.TestExecutor) + .With(x => x.AttributeTypes, []) + .With(x => x.DataAttributes, []) + .Create(); + } + public class InnerClass; } \ No newline at end of file From 49a4e17ab492cdd92cdfd704f59cbdb4b3e7873b Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 15:56:40 +0100 Subject: [PATCH 08/12] Update ClassDataSource overloads to support mixed shared types and keys --- .../TestData/ClassDataSourceAttribute.cs | 17 +- .../TestData/ClassDataSourceAttribute_2.cs | 83 ++++---- .../TestData/ClassDataSourceAttribute_3.cs | 134 ++++++------ .../TestData/ClassDataSourceAttribute_4.cs | 168 +++++++-------- .../TestData/ClassDataSourceAttribute_5.cs | 201 +++++++++--------- .../Attributes/TestData/ClassDataSources.cs | 35 ++- .../MultipleClassDataSourceDrivenTests.cs | 11 +- .../docs/tutorial-basics/class-data-source.md | 21 ++ 8 files changed, 354 insertions(+), 316 deletions(-) diff --git a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs index b8b8e51888..d2c49f4dd5 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute.cs @@ -14,20 +14,10 @@ namespace TUnit.Core; public override IEnumerable GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) { _dataGeneratorMetadata = dataGeneratorMetadata; - - var t = Shared switch - { - SharedType.None => new T(), - SharedType.Globally => TestDataContainer.GetGlobalInstance(() => new T()), - SharedType.ForClass => TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T()), - SharedType.Keyed => TestDataContainer.GetInstanceForKey(Key, () => new T()), - SharedType.ForAssembly => TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T()), - _ => throw new ArgumentOutOfRangeException() - }; - _item = t; + _item = ClassDataSources.Get(Shared, dataGeneratorMetadata.TestClassType, Key); - yield return t; + yield return _item; } public async ValueTask OnTestRegistered(TestContext testContext) @@ -52,8 +42,7 @@ public ValueTask OnTestStart(BeforeTestContext beforeTestContext) public async ValueTask OnTestEnd(TestContext testContext) { - await ClassDataSources.OnTestEnd(Shared, - Key, _item); + await ClassDataSources.OnTestEnd(Shared, Key, _item); } public async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext) diff --git a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_2.cs b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_2.cs index c83945a3dd..5d00435bdd 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_2.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_2.cs @@ -8,47 +8,49 @@ public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(Dynamic where T1 : new() where T2 : new() { - private T1? _item1; - private T2? _item2; private DataGeneratorMetadata? _dataGeneratorMetadata; - public SharedType Shared { get; set; } = SharedType.None; - public string Key { get; set; } = string.Empty; + public SharedType[] Shared { get; set; } = [SharedType.None, SharedType.None, SharedType.None, SharedType.None, SharedType.None]; + public string[] Keys { get; set; } = [string.Empty, string.Empty, string.Empty, string.Empty, string.Empty]; + + private + ( + (T1 T, SharedType SharedType, string Key), + (T2 T, SharedType SharedType, string Key) + ) _itemsWithMetadata; + public override IEnumerable<(T1, T2)> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) { _dataGeneratorMetadata = dataGeneratorMetadata; - var t = Shared switch - { - SharedType.None => (new T1(), new T2()), - SharedType.Globally => (TestDataContainer.GetGlobalInstance(() => new T1()), TestDataContainer.GetGlobalInstance(() => new T2())), - SharedType.ForClass => (TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T1()), TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T2())), - SharedType.Keyed => (TestDataContainer.GetInstanceForKey(Key, () => new T1()), TestDataContainer.GetInstanceForKey(Key, () => new T2())), - SharedType.ForAssembly => (TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T1()), TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T2())), - _ => throw new ArgumentOutOfRangeException() - }; - - _item1 = t.Item1; - _item2 = t.Item2; + _itemsWithMetadata = + ( + ClassDataSources.GetItemForIndex(0, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(1, dataGeneratorMetadata.TestClassType, Shared, Keys) + ); - yield return t; + yield return + ( + _itemsWithMetadata.Item1.T, + _itemsWithMetadata.Item2.T + ); } public async ValueTask OnTestRegistered(TestContext testContext) { - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); } public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) @@ -56,33 +58,40 @@ public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.Key); } public async ValueTask OnTestEnd(TestContext testContext) { - await ClassDataSources.OnTestEnd(Shared, Key, _item1); - await ClassDataSources.OnTestEnd(Shared, Key, _item2); + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); } public async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item2.SharedType); } public async ValueTask IfLastTestInAssembly(AssemblyHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInAssembly(Shared); - await ClassDataSources.IfLastTestInAssembly(Shared); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item2.SharedType); } } \ No newline at end of file diff --git a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_3.cs b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_3.cs index 0f668dcddd..5045c9e06b 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_3.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_3.cs @@ -9,76 +9,59 @@ public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(Dynamic where T2 : new() where T3 : new() { - private T1? _item1; - private T2? _item2; - private T3? _item3; private DataGeneratorMetadata? _dataGeneratorMetadata; - public SharedType Shared { get; set; } = SharedType.None; - public string Key { get; set; } = string.Empty; + public SharedType[] Shared { get; set; } = [SharedType.None, SharedType.None, SharedType.None, SharedType.None, SharedType.None]; + public string[] Keys { get; set; } = [string.Empty, string.Empty, string.Empty, string.Empty, string.Empty]; + + private + ( + (T1 T, SharedType SharedType, string Key), + (T2 T, SharedType SharedType, string Key), + (T3 T, SharedType SharedType, string Key) + ) _itemsWithMetadata; + public override IEnumerable<(T1, T2, T3)> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) { _dataGeneratorMetadata = dataGeneratorMetadata; - var t = Shared switch - { - SharedType.None => ( - new T1(), - new T2(), - new T3() - ), - SharedType.Globally => ( - TestDataContainer.GetGlobalInstance(() => new T1()), - TestDataContainer.GetGlobalInstance(() => new T2()), - TestDataContainer.GetGlobalInstance(() => new T3()) - ), - SharedType.ForClass => ( - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T1()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T2()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T3()) - ), - SharedType.Keyed => ( - TestDataContainer.GetInstanceForKey(Key, () => new T1()), - TestDataContainer.GetInstanceForKey(Key, () => new T2()), - TestDataContainer.GetInstanceForKey(Key, () => new T3()) - ), - SharedType.ForAssembly => ( - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T1()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T2()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T3()) - ), - _ => throw new ArgumentOutOfRangeException() - }; - - _item1 = t.Item1; - _item2 = t.Item2; - _item3 = t.Item3; - - yield return t; + _itemsWithMetadata = + ( + ClassDataSources.GetItemForIndex(0, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(1, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(2, dataGeneratorMetadata.TestClassType, Shared, Keys) + ); + + yield return + ( + _itemsWithMetadata.Item1.T, + _itemsWithMetadata.Item2.T, + _itemsWithMetadata.Item3.T + ); } public async ValueTask OnTestRegistered(TestContext testContext) { - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); } public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) @@ -86,43 +69,54 @@ public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.Key); } public async ValueTask OnTestEnd(TestContext testContext) { - await ClassDataSources.OnTestEnd(Shared, Key, _item1); - await ClassDataSources.OnTestEnd(Shared, Key, _item2); - await ClassDataSources.OnTestEnd(Shared, Key, _item3); + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); } public async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item3.SharedType); } public async ValueTask IfLastTestInAssembly(AssemblyHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInAssembly(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item3.SharedType); } } \ No newline at end of file diff --git a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_4.cs b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_4.cs index 929d59f0eb..8d35750069 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_4.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_4.cs @@ -10,90 +10,69 @@ public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(Dynamic where T3 : new() where T4 : new() { - private T1? _item1; - private T2? _item2; - private T3? _item3; - private T4? _item4; private DataGeneratorMetadata? _dataGeneratorMetadata; - public SharedType Shared { get; set; } = SharedType.None; - public string Key { get; set; } = string.Empty; + public SharedType[] Shared { get; set; } = [SharedType.None, SharedType.None, SharedType.None, SharedType.None, SharedType.None]; + public string[] Keys { get; set; } = [string.Empty, string.Empty, string.Empty, string.Empty, string.Empty]; + + private + ( + (T1 T, SharedType SharedType, string Key), + (T2 T, SharedType SharedType, string Key), + (T3 T, SharedType SharedType, string Key), + (T4 T, SharedType SharedType, string Key) + ) _itemsWithMetadata; + public override IEnumerable<(T1, T2, T3, T4)> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) { _dataGeneratorMetadata = dataGeneratorMetadata; - var t = Shared switch - { - SharedType.None => ( - new T1(), - new T2(), - new T3(), - new T4() - ), - SharedType.Globally => ( - TestDataContainer.GetGlobalInstance(() => new T1()), - TestDataContainer.GetGlobalInstance(() => new T2()), - TestDataContainer.GetGlobalInstance(() => new T3()), - TestDataContainer.GetGlobalInstance(() => new T4()) - ), - SharedType.ForClass => ( - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T1()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T2()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T3()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T4()) - ), - SharedType.Keyed => ( - TestDataContainer.GetInstanceForKey(Key, () => new T1()), - TestDataContainer.GetInstanceForKey(Key, () => new T2()), - TestDataContainer.GetInstanceForKey(Key, () => new T3()), - TestDataContainer.GetInstanceForKey(Key, () => new T4()) - ), - SharedType.ForAssembly => ( - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T1()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T2()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T3()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T4()) - ), - _ => throw new ArgumentOutOfRangeException() - }; - - _item1 = t.Item1; - _item2 = t.Item2; - _item3 = t.Item3; - _item4 = t.Item4; - - yield return t; + _itemsWithMetadata = + ( + ClassDataSources.GetItemForIndex(0, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(1, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(2, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(3, dataGeneratorMetadata.TestClassType, Shared, Keys) + ); + + yield return + ( + _itemsWithMetadata.Item1.T, + _itemsWithMetadata.Item2.T, + _itemsWithMetadata.Item3.T, + _itemsWithMetadata.Item4.T + ); } public async ValueTask OnTestRegistered(TestContext testContext) { - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item4); + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.T); } public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) @@ -101,53 +80,68 @@ public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item4); + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.Key); } public async ValueTask OnTestEnd(TestContext testContext) { - await ClassDataSources.OnTestEnd(Shared, Key, _item1); - await ClassDataSources.OnTestEnd(Shared, Key, _item2); - await ClassDataSources.OnTestEnd(Shared, Key, _item3); - await ClassDataSources.OnTestEnd(Shared, Key, _item4); + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.T); } public async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item3.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item4.SharedType); } public async ValueTask IfLastTestInAssembly(AssemblyHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInAssembly(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item3.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item4.SharedType); } } \ No newline at end of file diff --git a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_5.cs b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_5.cs index 38145499cf..6df2744e44 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_5.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSourceAttribute_5.cs @@ -11,104 +11,78 @@ public sealed class ClassDataSourceAttribute<[DynamicallyAccessedMembers(Dynamic where T4 : new() where T5 : new() { - private T1? _item1; - private T2? _item2; - private T3? _item3; - private T4? _item4; - private T5? _item5; private DataGeneratorMetadata? _dataGeneratorMetadata; - public SharedType Shared { get; set; } = SharedType.None; - public string Key { get; set; } = string.Empty; + public SharedType[] Shared { get; set; } = [SharedType.None, SharedType.None, SharedType.None, SharedType.None, SharedType.None]; + public string[] Keys { get; set; } = [string.Empty, string.Empty, string.Empty, string.Empty, string.Empty]; + + private + ( + (T1 T, SharedType SharedType, string Key), + (T2 T, SharedType SharedType, string Key), + (T3 T, SharedType SharedType, string Key), + (T4 T, SharedType SharedType, string Key), + (T5 T, SharedType SharedType, string Key) + ) _itemsWithMetadata; + public override IEnumerable<(T1, T2, T3, T4, T5)> GenerateDataSources(DataGeneratorMetadata dataGeneratorMetadata) { _dataGeneratorMetadata = dataGeneratorMetadata; - var t = Shared switch - { - SharedType.None => ( - new T1(), - new T2(), - new T3(), - new T4(), - new T5() - ), - SharedType.Globally => ( - TestDataContainer.GetGlobalInstance(() => new T1()), - TestDataContainer.GetGlobalInstance(() => new T2()), - TestDataContainer.GetGlobalInstance(() => new T3()), - TestDataContainer.GetGlobalInstance(() => new T4()), - TestDataContainer.GetGlobalInstance(() => new T5()) - ), - SharedType.ForClass => ( - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T1()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T2()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T3()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T4()), - TestDataContainer.GetInstanceForType(dataGeneratorMetadata.TestClassType, () => new T5()) - ), - SharedType.Keyed => ( - TestDataContainer.GetInstanceForKey(Key, () => new T1()), - TestDataContainer.GetInstanceForKey(Key, () => new T2()), - TestDataContainer.GetInstanceForKey(Key, () => new T3()), - TestDataContainer.GetInstanceForKey(Key, () => new T4()), - TestDataContainer.GetInstanceForKey(Key, () => new T5()) - ), - SharedType.ForAssembly => ( - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T1()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T2()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T3()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T4()), - TestDataContainer.GetInstanceForAssembly(dataGeneratorMetadata.TestClassType.Assembly, () => new T5()) - ), - _ => throw new ArgumentOutOfRangeException() - }; - - _item1 = t.Item1; - _item2 = t.Item2; - _item3 = t.Item3; - _item4 = t.Item4; - _item5 = t.Item5; - - yield return t; + _itemsWithMetadata = ( + ClassDataSources.GetItemForIndex(0, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(1, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(2, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(3, dataGeneratorMetadata.TestClassType, Shared, Keys), + ClassDataSources.GetItemForIndex(4, dataGeneratorMetadata.TestClassType, Shared, Keys) + ); + + yield return + ( + _itemsWithMetadata.Item1.T, + _itemsWithMetadata.Item2.T, + _itemsWithMetadata.Item3.T, + _itemsWithMetadata.Item4.T, + _itemsWithMetadata.Item5.T + ); } public async ValueTask OnTestRegistered(TestContext testContext) { - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item4); + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.T); - await ClassDataSources.OnTestRegistered( + await ClassDataSources.OnTestRegistered( testContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item5); + _itemsWithMetadata.Item5.SharedType, + _itemsWithMetadata.Item5.Key, + _itemsWithMetadata.Item5.T); } public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) @@ -116,63 +90,82 @@ public async ValueTask OnTestStart(BeforeTestContext beforeTestContext) await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item1); + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item2); + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item3); + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item4); + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.Key); await ClassDataSources.OnTestStart( beforeTestContext, _dataGeneratorMetadata?.PropertyInfo?.GetAccessors()[0].IsStatic == true, - Shared, - Key, - _item5); + _itemsWithMetadata.Item5.SharedType, + _itemsWithMetadata.Item5.Key, + _itemsWithMetadata.Item5.Key); } public async ValueTask OnTestEnd(TestContext testContext) { - await ClassDataSources.OnTestEnd(Shared, Key, _item1); - await ClassDataSources.OnTestEnd(Shared, Key, _item2); - await ClassDataSources.OnTestEnd(Shared, Key, _item3); - await ClassDataSources.OnTestEnd(Shared, Key, _item4); - await ClassDataSources.OnTestEnd(Shared, Key, _item5); + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item1.SharedType, + _itemsWithMetadata.Item1.Key, + _itemsWithMetadata.Item1.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item2.SharedType, + _itemsWithMetadata.Item2.Key, + _itemsWithMetadata.Item2.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item3.SharedType, + _itemsWithMetadata.Item3.Key, + _itemsWithMetadata.Item3.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item4.SharedType, + _itemsWithMetadata.Item4.Key, + _itemsWithMetadata.Item4.T); + + await ClassDataSources.OnTestEnd( + _itemsWithMetadata.Item5.SharedType, + _itemsWithMetadata.Item5.Key, + _itemsWithMetadata.Item5.T); } public async ValueTask IfLastTestInClass(ClassHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item3.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item4.SharedType); + await ClassDataSources.IfLastTestInClass(_itemsWithMetadata.Item5.SharedType); } public async ValueTask IfLastTestInAssembly(AssemblyHookContext context, TestContext testContext) { - await ClassDataSources.IfLastTestInAssembly(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); - await ClassDataSources.IfLastTestInClass(Shared); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item1.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item2.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item3.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item4.SharedType); + await ClassDataSources.IfLastTestInAssembly(_itemsWithMetadata.Item5.SharedType); } } \ No newline at end of file diff --git a/TUnit.Core/Attributes/TestData/ClassDataSources.cs b/TUnit.Core/Attributes/TestData/ClassDataSources.cs index e7f95e2956..ac48be416f 100644 --- a/TUnit.Core/Attributes/TestData/ClassDataSources.cs +++ b/TUnit.Core/Attributes/TestData/ClassDataSources.cs @@ -5,13 +5,46 @@ namespace TUnit.Core; -internal class ClassDataSources +internal static class ClassDataSources { public static GetOnlyDictionary GlobalInitializers = new(); public static readonly GetOnlyDictionary> TestClassTypeInitializers = new(); public static readonly GetOnlyDictionary> AssemblyInitializers = new(); public static readonly GetOnlyDictionary> KeyedInitializers = new(); + public static (T, SharedType, string) GetItemForIndex(int index, Type testClassType, SharedType[] sharedTypes, string[] keys) where T : new() + { + var shared = sharedTypes.ElementAtOrDefault(index); + var key = shared == SharedType.Keyed ? GetKey(index, sharedTypes, keys) : string.Empty; + + return + ( + Get(shared, testClassType, key), + shared, + key + ); + } + + private static string GetKey(int index, SharedType[] sharedTypes, string[] keys) + { + var keyedIndex = sharedTypes.Take(index + 1).Count(x => x == SharedType.Keyed) - 1; + + return keys.ElementAtOrDefault(keyedIndex) ?? throw new ArgumentException($"Key at index {keyedIndex} not found"); + } + + public static T Get(SharedType sharedType, Type testClassType, string key) where T : new() + { + return sharedType switch + { + SharedType.None => new T(), + SharedType.Globally => TestDataContainer.GetGlobalInstance(() => new T()), + SharedType.ForClass => TestDataContainer.GetInstanceForType(testClassType, () => new T()), + SharedType.Keyed => TestDataContainer.GetInstanceForKey(key, () => new T()), + SharedType.ForAssembly => TestDataContainer.GetInstanceForAssembly(testClassType.Assembly, () => new T()), + _ => throw new ArgumentOutOfRangeException() + }; + } + public static Task InitializeObject(object? item) { if (item is IAsyncInitializer asyncInitializer) diff --git a/TUnit.TestProject/MultipleClassDataSourceDrivenTests.cs b/TUnit.TestProject/MultipleClassDataSourceDrivenTests.cs index 0e08201698..23867d6b3c 100644 --- a/TUnit.TestProject/MultipleClassDataSourceDrivenTests.cs +++ b/TUnit.TestProject/MultipleClassDataSourceDrivenTests.cs @@ -1,10 +1,15 @@ -using TUnit.TestProject.Dummy; #pragma warning disable CS9113 // Parameter is unread. namespace TUnit.TestProject; -[ClassDataSource] -public class MultipleClassDataSourceDrivenTests(MultipleClassDataSourceDrivenTests.Inject1 inject1, MultipleClassDataSourceDrivenTests.Inject2 inject2, MultipleClassDataSourceDrivenTests.Inject3 inject3, MultipleClassDataSourceDrivenTests.Inject4 inject4, MultipleClassDataSourceDrivenTests.Inject5 inject5) +[ClassDataSource(Shared = [SharedType.None, SharedType.None, SharedType.None, SharedType.None, SharedType.None])] +public class MultipleClassDataSourceDrivenTests( + MultipleClassDataSourceDrivenTests.Inject1 inject1, + MultipleClassDataSourceDrivenTests.Inject2 inject2, + MultipleClassDataSourceDrivenTests.Inject3 inject3, + MultipleClassDataSourceDrivenTests.Inject4 inject4, + MultipleClassDataSourceDrivenTests.Inject5 inject5 + ) { [Test] public void Test1() diff --git a/docs/docs/tutorial-basics/class-data-source.md b/docs/docs/tutorial-basics/class-data-source.md index 0167fd0c73..610157d903 100644 --- a/docs/docs/tutorial-basics/class-data-source.md +++ b/docs/docs/tutorial-basics/class-data-source.md @@ -32,6 +32,8 @@ The instance is shared for every test that also has this setting, and also uses ## Initialization and TearDown If you need to do some initialization or teardown for when this object is created/disposed, simply implement the `IAsyncInitializer` and/or `IAsyncDisposable` interfaces +# Example + ```csharp public class MyTestClass { @@ -56,4 +58,23 @@ public class MyTestClass } } } +``` + +# Class Data Source Overloads + +If you are using an overload that supports injecting multiple classes at once (e.g. `ClassDataSource`) then you should specify multiple SharedTypes in an array and keys where applicable. + +E.g. + +```csharp +[Test] + [ClassDataSource + ( + Shared = [SharedType.Globally, SharedType.Keyed, SharedType.ForClass, SharedType.Keyed, SharedType.None], + Keys = [ "Value2Key", "Value4Key" ] + )] + public class MyType(Value1 value1, Value2 value2, Value3 value3, Value4 value4, Value5 value5) + { + + } ``` \ No newline at end of file From 45a0caadbbdbcb2e8ff4821cb9526d7d4a2a1c84 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:17:18 +0100 Subject: [PATCH 09/12] Tidy usings --- TUnit.Core/TestContext.cs | 4 +--- TUnit.Engine/Services/SingleTestExecutor.cs | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/TUnit.Core/TestContext.cs b/TUnit.Core/TestContext.cs index 6fd17a9f56..25d162ab3f 100644 --- a/TUnit.Core/TestContext.cs +++ b/TUnit.Core/TestContext.cs @@ -1,6 +1,4 @@ -using TUnit.Core.Interfaces; - -namespace TUnit.Core; +namespace TUnit.Core; public partial class TestContext : Context, IDisposable { diff --git a/TUnit.Engine/Services/SingleTestExecutor.cs b/TUnit.Engine/Services/SingleTestExecutor.cs index 079ceb6510..06b571d008 100644 --- a/TUnit.Engine/Services/SingleTestExecutor.cs +++ b/TUnit.Engine/Services/SingleTestExecutor.cs @@ -12,7 +12,6 @@ using TUnit.Engine.Helpers; using TUnit.Engine.Hooks; using TUnit.Engine.Logging; -using TimeoutException = TUnit.Core.Exceptions.TimeoutException; #pragma warning disable TPEXP From ddaa17e177818925a23d174a1505fcb3e7d13537 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sat, 26 Oct 2024 17:21:06 +0100 Subject: [PATCH 10/12] fix --- TUnit.Core/ResettableLazy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TUnit.Core/ResettableLazy.cs b/TUnit.Core/ResettableLazy.cs index 6f653aedba..1edbfe9e47 100644 --- a/TUnit.Core/ResettableLazy.cs +++ b/TUnit.Core/ResettableLazy.cs @@ -75,6 +75,6 @@ protected static async ValueTask DisposeAsync(object? obj) public ResettableLazy Clone() { - return new ResettableLazy(factory); + return new ResettableLazy(_factory); } } \ No newline at end of file From c8f4e41ea9d3cd8a45feef1ef1b67171f4f4ed2d Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:21:25 +0000 Subject: [PATCH 11/12] Fix test --- TUnit.UnitTests/TestExtensionsTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/TUnit.UnitTests/TestExtensionsTests.cs b/TUnit.UnitTests/TestExtensionsTests.cs index 3c5dbd1ca7..440d215c3e 100644 --- a/TUnit.UnitTests/TestExtensionsTests.cs +++ b/TUnit.UnitTests/TestExtensionsTests.cs @@ -59,7 +59,6 @@ private TestMetadata CreateDummyMetadata() .Without(x => x.MethodInfo) .Without(x => x.ResettableClassFactory) .Without(x => x.ParallelLimit) - .Without(x => x.ClassConstructor) .Without(x => x.TestExecutor) .With(x => x.AttributeTypes, []) .With(x => x.DataAttributes, []) From 208bcbe2729f84f86a2df4a11eee8285dd059686 Mon Sep 17 00:00:00 2001 From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com> Date: Sun, 27 Oct 2024 16:33:23 +0000 Subject: [PATCH 12/12] Tidy usings --- .../MultipleClassDataSourceDrivenTests.cs | 1 - .../CodeGenerators/TestHooksGenerator.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.cs b/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.cs index fcf32e0c3d..91306cb3f2 100644 --- a/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.cs +++ b/TUnit.Core.SourceGenerator.Tests/MultipleClassDataSourceDrivenTests.cs @@ -1,6 +1,5 @@ using TUnit.Assertions.Extensions; using TUnit.Core.SourceGenerator.CodeGenerators; -using TUnit.Core.SourceGenerator.Tests.Options; namespace TUnit.Core.SourceGenerator.Tests; diff --git a/TUnit.Core.SourceGenerator/CodeGenerators/TestHooksGenerator.cs b/TUnit.Core.SourceGenerator/CodeGenerators/TestHooksGenerator.cs index 249215e14b..c6917bfdc6 100644 --- a/TUnit.Core.SourceGenerator/CodeGenerators/TestHooksGenerator.cs +++ b/TUnit.Core.SourceGenerator/CodeGenerators/TestHooksGenerator.cs @@ -1,5 +1,4 @@ -using System.Collections.Immutable; -using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using TUnit.Core.SourceGenerator.CodeGenerators.Helpers; using TUnit.Core.SourceGenerator.CodeGenerators.Writers.Hooks;