File: HttpConnectionManagerTests.cs
Web Access
Project: src\src\Servers\Kestrel\test\InMemory.FunctionalTests\InMemory.FunctionalTests.csproj (InMemory.FunctionalTests)
// 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;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure;
using Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests.TestTransport;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using Moq;
using Xunit;
 
namespace Microsoft.AspNetCore.Server.Kestrel.InMemory.FunctionalTests;
 
public class HttpConnectionManagerTests : LoggedTest
{
    // This test causes MemoryPoolBlocks to be finalized which in turn causes an assert failure in debug builds.
#if !DEBUG
    [ConditionalFact]
    [NoDebuggerCondition]
    public async Task CriticalErrorLoggedIfApplicationDoesntComplete()
    {
        ////////////////////////////////////////////////////////////////////////////////////////
        // WARNING: This test will fail under a debugger because Task.s_currentActiveTasks    //
        //          roots HttpConnection.                                                     //
        ////////////////////////////////////////////////////////////////////////////////////////
 
        var logWh = new SemaphoreSlim(0);
        var appStartedWh = new SemaphoreSlim(0);
 
        var factory = new LoggerFactory();
 
        // Use a custom logger for callback instead of TestSink because TestSink keeps references
        // to types when logging, prevents garbage collection, and makes the test fail.
        factory.AddProvider(new CallbackLoggerProvider(eventId =>
        {
            if (eventId.Name == "ApplicationNeverCompleted")
            {
                Logger.LogInformation("Releasing ApplicationNeverCompleted log wait handle.");
                logWh.Release();
            }
        }));
 
        var testContext = new TestServiceContext(factory);
        testContext.InitializeHeartbeat();
 
        await using (var server = new TestServer(context =>
        {
            appStartedWh.Release();
            var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
            return tcs.Task;
        },
        testContext))
        {
            using (var connection = server.CreateConnection())
            {
                await connection.SendEmptyGet();
 
                Assert.True(await appStartedWh.WaitAsync(TestConstants.DefaultTimeout));
 
                // Close connection without waiting for a response
            }
 
            var logWaitAttempts = 0;
 
            for (; !await logWh.WaitAsync(TimeSpan.FromSeconds(1)) && logWaitAttempts < 30; logWaitAttempts++)
            {
                GC.Collect();
                GC.WaitForPendingFinalizers();
            }
 
            Assert.True(logWaitAttempts < 10);
        }
    }
 
    private class CallbackLoggerProvider : ILoggerProvider
    {
        private readonly Action<EventId> _logAction;
 
        public CallbackLoggerProvider(Action<EventId> logAction)
        {
            _logAction = logAction;
        }
 
        public ILogger CreateLogger(string categoryName) => new CallbackLogger(_logAction);
 
        public void Dispose()
        {
        }
 
        private class CallbackLogger : ILogger
        {
            private readonly Action<EventId> _logAction;
 
            public CallbackLogger(Action<EventId> logAction)
            {
                _logAction = logAction;
            }
 
            public IDisposable BeginScope<TState>(TState state) => null;
            public bool IsEnabled(LogLevel logLevel) => true;
 
            public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
            {
                _logAction(eventId);
            }
        }
    }
#endif
 
    private class NoDebuggerConditionAttribute : Attribute, ITestCondition
    {
        public bool IsMet => !Debugger.IsAttached;
        public string SkipReason => "A debugger is attached.";
    }
}