File: Utils\OutputCollectorTests.cs
Web Access
Project: src\tests\Aspire.Cli.Tests\Aspire.Cli.Tests.csproj (Aspire.Cli.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.Cli.Utils;
 
namespace Aspire.Cli.Tests.Utils;
 
public class OutputCollectorTests
{
    [Fact]
    public async Task OutputCollector_ThreadSafety_MultipleThreadsAddingLines()
    {
        // Arrange
        var collector = new OutputCollector();
        const int threadCount = 10;
        const int linesPerThread = 100;
        var tasks = new Task[threadCount];
 
        // Act - Start multiple threads that add lines concurrently
        for (int i = 0; i < threadCount; i++)
        {
            int threadId = i;
            tasks[i] = Task.Run(() =>
            {
                for (int j = 0; j < linesPerThread; j++)
                {
                    if (j % 2 == 0)
                    {
                        collector.AppendOutput($"stdout-thread{threadId}-line{j}");
                    }
                    else
                    {
                        collector.AppendError($"stderr-thread{threadId}-line{j}");
                    }
                }
            });
        }
 
        await Task.WhenAll(tasks);
 
        // Assert - Should have all lines without any exceptions
        var lines = collector.GetLines().ToList();
        Assert.Equal(threadCount * linesPerThread, lines.Count);
 
        // Check that we have both stdout and stderr entries
        var stdoutLines = lines.Where(l => l.Stream == "stdout").ToList();
        var stderrLines = lines.Where(l => l.Stream == "stderr").ToList();
        
        Assert.Equal(threadCount * linesPerThread / 2, stdoutLines.Count);
        Assert.Equal(threadCount * linesPerThread / 2, stderrLines.Count);
    }
 
    [Fact]
    public void OutputCollector_GetLines_ReturnsSnapshotNotLiveReference()
    {
        // Arrange
        var collector = new OutputCollector();
        collector.AppendOutput("initial line");
 
        // Act
        var snapshot = collector.GetLines().ToList();
        collector.AppendOutput("added after snapshot");
 
        // Assert - Snapshot should not be affected by subsequent additions
        Assert.Single(snapshot);
        Assert.Equal("initial line", snapshot[0].Line);
        Assert.Equal("stdout", snapshot[0].Stream);
        
        // New call should include the additional line
        var newSnapshot = collector.GetLines().ToList();
        Assert.Equal(2, newSnapshot.Count);
    }
 
    [Fact]
    public async Task OutputCollector_ConcurrentReadWrite_ShouldNotCrash()
    {
        // Arrange
        var collector = new OutputCollector();
        var readerTask = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                _ = collector.GetLines().ToList();
                await Task.Delay(1);
            }
        });
 
        var writerTask = Task.Run(async () =>
        {
            for (int i = 0; i < 100; i++)
            {
                collector.AppendOutput($"line {i}");
                await Task.Delay(1);
            }
        });
 
        // Act & Assert - Should complete without exceptions
        await Task.WhenAll(readerTask, writerTask);
        
        // Verify final state
        var finalLines = collector.GetLines().ToList();
        Assert.Equal(100, finalLines.Count);
    }
}