File: ToolingTestBase.cs
Web Access
Project: src\src\Razor\src\Razor\test\Microsoft.AspNetCore.Razor.Test.Common.Tooling\Microsoft.AspNetCore.Razor.Test.Common.Tooling.csproj (Microsoft.AspNetCore.Razor.Test.Common.Tooling)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Test.Common.Logging;
using Microsoft.CodeAnalysis.Razor.Logging;
 
using Microsoft.VisualStudio.Threading;
using Xunit;
using Xunit.Abstractions;
using IAsyncDisposable = System.IAsyncDisposable;
 
namespace Microsoft.AspNetCore.Razor.Test.Common;
 
/// <summary>
///  Base class for all test classes that provides the following support:
///
///  <list type="bullet">
///   <item>A <see cref="Microsoft.VisualStudio.Threading.JoinableTaskFactory"/> that uses the xUnit
///   test thread as the main thread.</item>
///   <item>A <see cref="CancellationToken"/> that signals when the test has finished running
///   and xUnit disposes the test class.</item>
///   <item>An <see cref="ILoggerFactory"/> implementation that writes to an xUnit
///   <see cref="ITestOutputHelper"/>.</item>
///   <item>An easy way to register <see cref="IDisposable"/> objects that should be disposed
///   when the test completes.</item>
///   <item>An easy way to register <see cref="IAsyncDisposable"/> objects that should be disposed
///   when the test completes.</item>
///   <item>An implementation of <see cref="IAsyncLifetime"/> that test classes can override
///   to provide custom initialization and disposal for tests.</item>
///  </list>
/// </summary>
public abstract partial class ToolingTestBase : IAsyncLifetime
{
    static ToolingTestBase()
    {
#if NET472
        XunitDisposeHook.Initialize();
#endif
    }
 
    private readonly JoinableTaskCollection _joinableTaskCollection;
    private readonly CancellationTokenSource _disposalTokenSource;
    private List<IDisposable>? _disposables;
    private List<IAsyncDisposable>? _asyncDisposables;
 
    /// <summary>
    ///  A common context within which joinable tasks may be created and interact to avoid
    ///  deadlocks.
    /// </summary>
    protected JoinableTaskContext JoinableTaskContext { get; }
 
    /// <summary>
    ///  A factory for starting asynchronous tasks that can mitigate deadlocks when the
    ///  tasks require the Main thread of an application and the Main thread may itself
    ///  be blocking on the completion of a task.
    /// </summary>
    protected JoinableTaskFactory JoinableTaskFactory { get; }
 
    /// <summary>
    ///  A cancellation token that will signal when the currently running test completes.
    /// </summary>
    protected CancellationToken DisposalToken { get; }
 
    /// <summary>
    ///  An <see cref="ILoggerFactory"/> that creates <see cref="ILogger"/> instances that
    ///  write to xUnit's <see cref="ITestOutputHelper"/> for the currently running test.
    /// </summary>
    internal ILoggerFactory LoggerFactory { get; }
 
    /// <summary>
    /// An <see cref="ITestOutputHelper"/> that the currently running test can use to write
    /// though using <see cref="LoggerFactory"/> is probably preferred.
    /// </summary>
    internal ITestOutputHelper TestOutputHelper { get; }
 
    private ILogger? _logger;
 
    /// <summary>
    ///  An <see cref="ILogger"/> for the currently running test.
    /// </summary>
    private protected ILogger Logger => _logger ??= LoggerFactory.GetOrCreateLogger(GetType().Name);
 
    protected ToolingTestBase(ITestOutputHelper testOutput)
    {
        JoinableTaskContext = new();
        _joinableTaskCollection = JoinableTaskContext.CreateCollection();
        JoinableTaskFactory = JoinableTaskContext.CreateFactory(_joinableTaskCollection);
 
        _disposalTokenSource = new();
        DisposalToken = _disposalTokenSource.Token;
 
        TestOutputHelper = testOutput;
        LoggerFactory = new TestOutputLoggerFactory(testOutput);
 
        // Give this thread a name, so it's easier to find in the VS Threads window.
        Thread.CurrentThread.Name ??= "Main Thread";
    }
 
    Task IAsyncLifetime.InitializeAsync() => InitializeAsync();
 
    async Task IAsyncLifetime.DisposeAsync()
    {
        // First, call the protected DisposeAsync() to let test classes to run custom logic.
        await DisposeAsync();
 
        // Next, dispose any IAsyncDisposables that were registered by the current test.
        if (_asyncDisposables is { } asyncDisposables)
        {
            foreach (var asyncDisposable in asyncDisposables)
            {
                await asyncDisposable.DisposeAsync();
            }
        }
 
        // Next, dispose any IDisposables that were registered by the current test.
        if (_disposables is { } disposables)
        {
            foreach (var disposable in disposables)
            {
                disposable.Dispose();
            }
        }
 
        // Signal cancellation
        _disposalTokenSource.Cancel();
        try
        {
            // Wait for all joinable tasks to finish.
            await _joinableTaskCollection.JoinTillEmptyAsync();
        }
        catch (OperationCanceledException)
        {
            // This exception is expected because we signaled the cancellation token.
        }
        catch (AggregateException ex)
        {
            ex.Handle(x => x is OperationCanceledException);
        }
        finally
        {
            _disposalTokenSource.Dispose();
        }
 
        JoinableTaskContext.Dispose();
    }
 
    /// <summary>
    ///  Override to provide custom initialization logic for all tests in this test class.
    /// </summary>
    protected virtual Task InitializeAsync() => Task.CompletedTask;
 
    /// <summary>
    ///  Override to provide custom initialization logic for all tests in this test class.
    /// </summary>
    protected virtual Task DisposeAsync() => Task.CompletedTask;
 
    /// <summary>
    ///  Register an <see cref="IDisposable"/> instance to be disposed when the test completes.
    /// </summary>
    protected void AddDisposable(IDisposable disposable)
    {
        _disposables ??= [];
        _disposables.Add(disposable);
    }
 
    /// <summary>
    ///  Register an <see cref="IDisposable"/> instance to be disposed when the test completes.
    /// </summary>
    protected T AddDisposable<T>(T disposable)
        where T : IDisposable
    {
        AddDisposable((IDisposable)disposable);
        return disposable;
    }
 
    /// <summary>
    ///  Register a set of <see cref="IDisposable"/> instances to be disposed when the test completes.
    /// </summary>
    protected void AddDisposables(IEnumerable<IDisposable> disposables)
    {
        _disposables ??= [];
        _disposables.AddRange(disposables);
    }
 
    /// <summary>
    ///  Register a set of <see cref="IDisposable"/> instances to be disposed when the test completes.
    /// </summary>
    protected void AddDisposables(params IDisposable[] disposables)
        => AddDisposables((IEnumerable<IDisposable>)disposables);
 
    /// <summary>
    ///  Register an <see cref="IAsyncDisposable"/> instance to be disposed when the test completes.
    /// </summary>
    protected void AddDisposable(IAsyncDisposable disposable)
    {
        _asyncDisposables ??= [];
        _asyncDisposables.Add(disposable);
    }
 
    /// <summary>
    ///  Register a set of <see cref="IAsyncDisposable"/> instances to be disposed when the test completes.
    /// </summary>
    protected void AddDisposables(IEnumerable<IAsyncDisposable> disposables)
    {
        _asyncDisposables ??= [];
        _asyncDisposables.AddRange(disposables);
    }
 
    /// <summary>
    ///  Register a set of <see cref="IAsyncDisposable"/> instances to be disposed when the test completes.
    /// </summary>
    protected void AddDisposables(params IAsyncDisposable[] disposables)
        => AddDisposables((IEnumerable<IAsyncDisposable>)disposables);
}