/// <summary> /// Creates the test class (if necessary), and invokes the test method. /// </summary> /// <param name="ctxt">The invoker context</param> /// <returns>Returns the time (in seconds) spent creating the test class, running /// the test, and disposing of the test class.</returns> protected async ValueTask <decimal> RunAsync(TContext ctxt) { await ctxt.InitializeAsync(); try { return(await ctxt.Aggregator.RunAsync(async() => { if (ctxt.CancellationTokenSource.IsCancellationRequested) { return 0m; } SetTestContext(ctxt, TestEngineStatus.Initializing); object?testClassInstance = null; var elapsedTime = ExecutionTimer.Measure(() => { testClassInstance = CreateTestClass(ctxt); }); var asyncDisposable = testClassInstance as IAsyncDisposable; var disposable = testClassInstance as IDisposable; var testAssemblyUniqueID = ctxt.Test.TestCase.TestCollection.TestAssembly.UniqueID; var testCollectionUniqueID = ctxt.Test.TestCase.TestCollection.UniqueID; var testClassUniqueID = ctxt.Test.TestCase.TestClass?.UniqueID; var testMethodUniqueID = ctxt.Test.TestCase.TestMethod?.UniqueID; var testCaseUniqueID = ctxt.Test.TestCase.UniqueID; var testUniqueID = ctxt.Test.UniqueID; try { if (testClassInstance is IAsyncLifetime asyncLifetime) { elapsedTime += await ExecutionTimer.MeasureAsync(asyncLifetime.InitializeAsync); } try { if (!ctxt.CancellationTokenSource.IsCancellationRequested) { elapsedTime += await ExecutionTimer.MeasureAsync(() => BeforeTestMethodInvokedAsync(ctxt)); SetTestContext(ctxt, TestEngineStatus.Running); if (!ctxt.CancellationTokenSource.IsCancellationRequested && !ctxt.Aggregator.HasExceptions) { await InvokeTestMethodAsync(ctxt, testClassInstance); } SetTestContext(ctxt, TestEngineStatus.CleaningUp, TestState.FromException((decimal)elapsedTime.TotalSeconds, ctxt.Aggregator.ToException())); elapsedTime += await ExecutionTimer.MeasureAsync(() => AfterTestMethodInvokedAsync(ctxt)); } } finally { if (asyncDisposable != null || disposable != null) { var testClassDisposeStarting = new _TestClassDisposeStarting { AssemblyUniqueID = testAssemblyUniqueID, TestCaseUniqueID = testCaseUniqueID, TestClassUniqueID = testClassUniqueID, TestCollectionUniqueID = testCollectionUniqueID, TestMethodUniqueID = testMethodUniqueID, TestUniqueID = testUniqueID }; if (!ctxt.MessageBus.QueueMessage(testClassDisposeStarting)) { ctxt.CancellationTokenSource.Cancel(); } } if (asyncDisposable != null) { elapsedTime += await ExecutionTimer.MeasureAsync(() => ctxt.Aggregator.RunAsync(asyncDisposable.DisposeAsync)); } } } finally { if (disposable != null) { elapsedTime += ExecutionTimer.Measure(() => ctxt.Aggregator.Run(disposable.Dispose)); } if (asyncDisposable != null || disposable != null) { var testClassDisposeFinished = new _TestClassDisposeFinished { AssemblyUniqueID = testAssemblyUniqueID, TestCaseUniqueID = testCaseUniqueID, TestClassUniqueID = testClassUniqueID, TestCollectionUniqueID = testCollectionUniqueID, TestMethodUniqueID = testMethodUniqueID, TestUniqueID = testUniqueID }; if (!ctxt.MessageBus.QueueMessage(testClassDisposeFinished)) { ctxt.CancellationTokenSource.Cancel(); } } } return (decimal)elapsedTime.TotalSeconds; }, 0m)); } finally { await ctxt.DisposeAsync(); } }
/// <summary> /// Invokes the test method on the given test class instance. This method sets up support for "async void" /// test methods, ensures that the test method has the correct number of arguments, then calls <see cref="CallTestMethod"/> /// to do the actual method invocation. It ensure that any async test method is fully completed before returning, and /// returns the measured clock time that the invocation took. This method should NEVER throw; any exceptions should be /// placed into the aggregator in <paramref name="ctxt"/>. /// </summary> /// <param name="ctxt">The context that describes the current test</param> /// <param name="testClassInstance">The test class instance</param> /// <returns>Returns the amount of time the test took to run, in seconds</returns> protected virtual async ValueTask <decimal> InvokeTestMethodAsync( TContext ctxt, object?testClassInstance) { var oldSyncContext = default(SynchronizationContext); var asyncSyncContext = default(AsyncTestSyncContext); try { if (AsyncUtility.IsAsyncVoid(ctxt.TestMethod)) { oldSyncContext = SynchronizationContext.Current; asyncSyncContext = new AsyncTestSyncContext(oldSyncContext); SetSynchronizationContext(asyncSyncContext); } var elapsed = await ExecutionTimer.MeasureAsync( () => ctxt.Aggregator.RunAsync( async() => { var parameterCount = ctxt.TestMethod.GetParameters().Length; var valueCount = ctxt.TestMethodArguments == null ? 0 : ctxt.TestMethodArguments.Length; if (parameterCount != valueCount) { ctxt.Aggregator.Add( new InvalidOperationException( $"The test method expected {parameterCount} parameter value{(parameterCount == 1 ? "" : "s")}, but {valueCount} parameter value{(valueCount == 1 ? "" : "s")} {(valueCount == 1 ? "was" : "were")} provided." ) ); } else { var result = CallTestMethod(ctxt, testClassInstance); var valueTask = AsyncUtility.TryConvertToValueTask(result); if (valueTask.HasValue) { await valueTask.Value; } else if (asyncSyncContext != null) { var ex = await asyncSyncContext.WaitForCompletionAsync(); if (ex != null) { ctxt.Aggregator.Add(ex); } } } } ) ); return((decimal)elapsed.TotalSeconds); } finally { if (asyncSyncContext != null) { SetSynchronizationContext(oldSyncContext); } } }