File: ResourceLoggerForwarderServiceTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Testing.Tests\Aspire.Hosting.Testing.Tests.csproj (Aspire.Hosting.Testing.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.Hosting.Tests.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Hosting.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Projects;
using Xunit;
using Xunit.Abstractions;
 
namespace Aspire.Hosting.Testing.Tests;
 
public class ResourceLoggerForwarderServiceTests(ITestOutputHelper output)
{
    [Fact]
    public async Task BackgroundServiceIsRegisteredInServiceProvider()
    {
        var appHost = await DistributedApplicationTestingBuilder.CreateAsync<TestingAppHost1_AppHost>();
        Assert.Contains(appHost.Services, sd =>
            sd.ServiceType == typeof(IHostedService)
            && sd.ImplementationType == typeof(ResourceLoggerForwarderService)
            && sd.Lifetime == ServiceLifetime.Singleton);
    }
 
    [Fact]
    public async Task ExecuteDoesNotThrowOperationCanceledWhenAppStoppingTokenSignaled()
    {
        var hostApplicationLifetime = new TestHostApplicationLifetime();
        var resourceLoggerService = new ResourceLoggerService();
        var resourceNotificationService = CreateResourceNotificationService(hostApplicationLifetime, resourceLoggerService);
        var hostEnvironment = new HostingEnvironment();
        var loggerFactory = new NullLoggerFactory();
        var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, hostEnvironment, loggerFactory);
 
        await resourceLogForwarder.StartAsync(hostApplicationLifetime.ApplicationStopping);
 
        Assert.NotNull(resourceLogForwarder.ExecuteTask);
        Assert.Equal(TaskStatus.WaitingForActivation, resourceLogForwarder.ExecuteTask.Status);
 
        // Signal the stopping token
        hostApplicationLifetime.StopApplication();
 
        await resourceLogForwarder.ExecuteTask;
    }
 
    [Fact]
    public async Task ResourceLogsAreForwardedToHostLogging()
    {
        var hostApplicationLifetime = new TestHostApplicationLifetime();
        var resourceLoggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService();
        var resourceNotificationService = CreateResourceNotificationService(hostApplicationLifetime, resourceLoggerService);
        var hostEnvironment = new HostingEnvironment { ApplicationName = "TestApp.AppHost" };
        var fakeLoggerProvider = new FakeLoggerProvider();
        var fakeLoggerFactory = new LoggerFactory([fakeLoggerProvider, new XunitLoggerProvider(output)]);
        var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, hostEnvironment, fakeLoggerFactory);
 
        var subscribedTcs = new TaskCompletionSource();
        var subscriberLoop = Task.Run(async () =>
        {
            await foreach (var sub in resourceLoggerService.WatchAnySubscribersAsync(hostApplicationLifetime.ApplicationStopping))
            {
                if (sub.AnySubscribers && sub.Name == "myresource")
                {
                    subscribedTcs.TrySetResult();
                }
            }
        });
 
        var expectedLogCountTcs = new TaskCompletionSource();
        var expectedLogCount = 6;
        var logCount = 0;
        resourceLogForwarder.OnResourceLog = resourceId =>
        {
            logCount++;
            if (logCount >= expectedLogCount)
            {
                expectedLogCountTcs.TrySetResult();
            }
        };
 
        await resourceLogForwarder.StartAsync(hostApplicationLifetime.ApplicationStopping);
 
        // Publish an update to the resource to kickstart the notification service loop
        var myresource = new CustomResource("myresource");
        await resourceNotificationService.PublishUpdateAsync(myresource, snapshot => snapshot with { State = "Running" });
 
        // Wait for the log stream to begin
        await subscribedTcs.Task.WaitAsync(TimeSpan.FromSeconds(15));
 
        // Log messages to the resource
        fakeLoggerProvider.Collector.Clear();
 
        var resourceLogger = resourceLoggerService.GetLogger(myresource);
        resourceLogger.LogTrace("Test trace message");
        resourceLogger.LogDebug("Test debug message");
        resourceLogger.LogInformation("Test information message");
        resourceLogger.LogWarning("Test warning message");
        resourceLogger.LogError("Test error message");
        resourceLogger.LogCritical("Test critical message");
 
        // Wait for the 6 log messages or timeout
        await expectedLogCountTcs.Task.WaitAsync(TimeSpan.FromSeconds(15));
 
        // Complete the resource log stream and wait for it to end
        resourceLoggerService.Complete(myresource);
        hostApplicationLifetime.StopApplication();
 
        // Get the logs from the fake logger
        var hostLogs = fakeLoggerProvider.Collector.GetSnapshot();
 
        // Line number is pre-pended to message
        // Category is derived from the application name and resource name
        // Logs sent at information level or lower are logged as information, otherwise they are logged as error
        Assert.Collection(hostLogs,
            log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("1: 2000-12-29T20:59:59.0000000Z Test trace message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
            log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("2: 2000-12-29T20:59:59.0000000Z Test debug message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
            log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("3: 2000-12-29T20:59:59.0000000Z Test information message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
            log => { Assert.Equal(LogLevel.Information, log.Level); Assert.Equal("4: 2000-12-29T20:59:59.0000000Z Test warning message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
            log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("5: 2000-12-29T20:59:59.0000000Z Test error message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); },
            log => { Assert.Equal(LogLevel.Error, log.Level); Assert.Equal("6: 2000-12-29T20:59:59.0000000Z Test critical message", log.Message); Assert.Equal("TestApp.AppHost.Resources.myresource", log.Category); });
    }
 
    private static ResourceNotificationService CreateResourceNotificationService(TestHostApplicationLifetime hostApplicationLifetime, ResourceLoggerService resourceLoggerService)
    {
        return new ResourceNotificationService(NullLogger<ResourceNotificationService>.Instance, hostApplicationLifetime, new ServiceCollection().BuildServiceProvider(), resourceLoggerService);
    }
 
    private sealed class CustomResource(string name) : Resource(name)
    {
 
    }
 
    private sealed class TestHostApplicationLifetime : IHostApplicationLifetime, IDisposable
    {
        private readonly CancellationTokenSource _startedCts = new();
        private readonly CancellationTokenSource _stoppingCts = new();
        private readonly CancellationTokenSource _stoppedCts = new();
 
        public TestHostApplicationLifetime(bool startStarted = true)
        {
            ApplicationStarted = _startedCts.Token;
            ApplicationStopping = _stoppingCts.Token;
            ApplicationStopped = _stoppedCts.Token;
 
            if (startStarted)
            {
                _startedCts.Cancel();
            }
        }
 
        public CancellationToken ApplicationStarted { get; }
        public CancellationToken ApplicationStopped { get; }
        public CancellationToken ApplicationStopping { get; }
 
        public void StartApplication()
        {
            _startedCts.Cancel();
        }
 
        public void StopApplication()
        {
            _stoppingCts.Cancel();
            _stoppedCts.Cancel();
        }
 
        public void Dispose()
        {
            _stoppingCts.Dispose();
            _stoppedCts.Dispose();
        }
    }
}