File: Backchannel\BackchannelLoggerProviderTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Tests\Aspire.Hosting.Tests.csproj (Aspire.Hosting.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.Extensions.Logging;
 
namespace Aspire.Hosting.Backchannel;
 
public class BackchannelLoggerProviderTests
{
    [Fact]
    public void Subscribe_ReturnsBufferedEntries()
    {
        using var provider = new BackchannelLoggerProvider();
 
        var logger = provider.CreateLogger("TestCategory");
        logger.LogInformation("Message 1");
        logger.LogWarning("Message 2");
        logger.LogError("Message 3");
 
        var (snapshot, subscriberId, _) = provider.Subscribe();
        provider.Unsubscribe(subscriberId);
 
        Assert.Equal(3, snapshot.Count);
        Assert.Equal("Message 1", snapshot[0].Message);
        Assert.Equal("Message 2", snapshot[1].Message);
        Assert.Equal("Message 3", snapshot[2].Message);
        Assert.Equal("TestCategory", snapshot[0].CategoryName);
        Assert.Equal(LogLevel.Information, snapshot[0].LogLevel);
        Assert.Equal(LogLevel.Warning, snapshot[1].LogLevel);
        Assert.Equal(LogLevel.Error, snapshot[2].LogLevel);
    }
 
    [Fact]
    public void ReplayBuffer_EvictsOldestWhenFull()
    {
        using var provider = new BackchannelLoggerProvider();
 
        var logger = provider.CreateLogger("TestCategory");
 
        // Write 1001 entries — the first should be evicted
        for (var i = 0; i < 1001; i++)
        {
            logger.LogInformation("Message {Index}", i);
        }
 
        var (snapshot, subscriberId, _) = provider.Subscribe();
        provider.Unsubscribe(subscriberId);
 
        Assert.Equal(1000, snapshot.Count);
        // First entry should be "Message 1" (index 0 was evicted)
        Assert.Equal("Message 1", snapshot[0].Message);
        Assert.Equal("Message 1000", snapshot[999].Message);
    }
 
    [Fact]
    public void Subscribe_ReturnsIndependentSnapshot()
    {
        using var provider = new BackchannelLoggerProvider();
 
        var logger = provider.CreateLogger("TestCategory");
        logger.LogInformation("Before snapshot");
 
        var (snapshot1, sub1, _) = provider.Subscribe();
        provider.Unsubscribe(sub1);
 
        logger.LogInformation("After snapshot");
 
        var (snapshot2, sub2, _) = provider.Subscribe();
        provider.Unsubscribe(sub2);
 
        // First snapshot should not be affected by subsequent writes
        Assert.Single(snapshot1);
        Assert.Equal(2, snapshot2.Count);
    }
 
    [Fact]
    public async Task ConcurrentSubscribers_ReceiveSameEntries()
    {
        using var provider = new BackchannelLoggerProvider();
 
        var logger = provider.CreateLogger("TestCategory");
        logger.LogInformation("Historical");
 
        // Two subscribers connect concurrently
        var (snapshot1, sub1, channel1) = provider.Subscribe();
        var (snapshot2, sub2, channel2) = provider.Subscribe();
 
        // Both see the historical entry
        Assert.Single(snapshot1);
        Assert.Single(snapshot2);
 
        // New entries arrive after both subscribe
        logger.LogInformation("Live 1");
        logger.LogInformation("Live 2");
 
        // Both subscribers receive both live entries
        Assert.True(channel1.Reader.TryRead(out var entry1a));
        Assert.Equal("Live 1", entry1a!.Message);
        Assert.True(channel1.Reader.TryRead(out var entry1b));
        Assert.Equal("Live 2", entry1b!.Message);
 
        Assert.True(channel2.Reader.TryRead(out var entry2a));
        Assert.Equal("Live 1", entry2a!.Message);
        Assert.True(channel2.Reader.TryRead(out var entry2b));
        Assert.Equal("Live 2", entry2b!.Message);
 
        provider.Unsubscribe(sub1);
        provider.Unsubscribe(sub2);
 
        // Channels are completed after unsubscribe
        await channel1.Reader.Completion;
        await channel2.Reader.Completion;
    }
}