File: LoggedTest\LoggedTestBase.cs
Web Access
Project: src\src\Testing\src\Microsoft.AspNetCore.InternalTesting.csproj (Microsoft.AspNetCore.InternalTesting)
// 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.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.ExceptionServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Xunit.Abstractions;
 
namespace Microsoft.AspNetCore.InternalTesting;
 
public abstract class LoggedTestBase : ITestMethodLifecycle, IDisposable
{
    private ExceptionDispatchInfo _initializationException;
 
    private IDisposable _testLog;
 
    // Obsolete but keeping for back compat
    public LoggedTestBase(ITestOutputHelper output = null)
    {
        TestOutputHelper = output;
    }
 
    protected TestContext Context { get; private set; }
 
    // Internal for testing
    internal string ResolvedTestClassName { get; set; }
 
    public string ResolvedLogOutputDirectory { get; set; }
 
    public string ResolvedTestMethodName { get; set; }
 
    public Microsoft.Extensions.Logging.ILogger Logger { get; set; }
 
    public ILoggerFactory LoggerFactory { get; set; }
 
    public ITestOutputHelper TestOutputHelper { get; set; }
 
    public void AddTestLogging(IServiceCollection services) => services.AddSingleton(LoggerFactory);
 
    // For back compat
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public IDisposable StartLog(out ILoggerFactory loggerFactory, [CallerMemberName] string testName = null) => StartLog(out loggerFactory, LogLevel.Debug, testName);
 
    // For back compat
    [SuppressMessage("ApiDesign", "RS0026:Do not add multiple public overloads with optional parameters", Justification = "Required to maintain compatibility")]
    public IDisposable StartLog(out ILoggerFactory loggerFactory, LogLevel minLogLevel, [CallerMemberName] string testName = null)
    {
        return AssemblyTestLog.ForAssembly(GetType().GetTypeInfo().Assembly).StartTestLog(TestOutputHelper, GetType().FullName, out loggerFactory, minLogLevel, testName);
    }
 
    protected virtual void Initialize(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
    {
        try
        {
            TestOutputHelper = testOutputHelper;
 
            var classType = GetType();
            var logLevelAttribute = methodInfo.GetCustomAttribute<LogLevelAttribute>()
                                    ?? methodInfo.DeclaringType.GetCustomAttribute<LogLevelAttribute>()
                                    ?? methodInfo.DeclaringType.Assembly.GetCustomAttribute<LogLevelAttribute>();
 
            // internal for testing
            ResolvedTestClassName = context.FileOutput.TestClassName;
 
            _testLog = AssemblyTestLog
                .ForAssembly(classType.GetTypeInfo().Assembly)
                .StartTestLog(
                    TestOutputHelper,
                    context.FileOutput.TestClassName,
                    out var loggerFactory,
                    logLevelAttribute?.LogLevel ?? LogLevel.Debug,
                    out var resolvedTestName,
                    out var logDirectory,
                    context.FileOutput.TestName);
 
            ResolvedLogOutputDirectory = logDirectory;
            ResolvedTestMethodName = resolvedTestName;
 
            LoggerFactory = loggerFactory;
            Logger = loggerFactory.CreateLogger(classType);
        }
        catch (Exception e)
        {
            _initializationException = ExceptionDispatchInfo.Capture(e);
        }
    }
 
    public virtual Task InitializeAsync(TestContext context, MethodInfo methodInfo, object[] testMethodArguments, ITestOutputHelper testOutputHelper)
    {
        Initialize(context, methodInfo, testMethodArguments, testOutputHelper);
        return InitializeCoreAsync(context);
    }
 
    protected virtual Task InitializeCoreAsync(TestContext context) => Task.CompletedTask;
 
    public virtual void Dispose()
    {
        if (_testLog == null)
        {
            // It seems like sometimes the MSBuild goop that adds the test framework can end up in a bad state and not actually add it
            // Not sure yet why that happens but the exception isn't clear so I'm adding this error so we can detect it better.
            // -anurse
            throw new InvalidOperationException("LoggedTest base class was used but nothing initialized it! The test framework may not be enabled. Try cleaning your 'obj' directory.");
        }
 
        _initializationException?.Throw();
        _testLog.Dispose();
    }
 
    Task ITestMethodLifecycle.OnTestStartAsync(TestContext context, CancellationToken cancellationToken)
    {
        Context = context;
        return InitializeAsync(context, context.TestMethod, context.MethodArguments, context.Output);
    }
 
    Task ITestMethodLifecycle.OnTestEndAsync(TestContext context, Exception exception, CancellationToken cancellationToken)
    {
        if (exception is not null)
        {
            Logger.LogError(exception, "Test threw an exception.");
        }
        return Task.CompletedTask;
    }
}