File: ResourceFailureLoggingTests.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 Aspire.Hosting.Utils;
using Aspire.TestUtilities;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using TestConstants = Microsoft.AspNetCore.InternalTesting.TestConstants;
 
namespace Aspire.Hosting.Tests;
 
[Trait("Partition", "2")]
public class ExecutableResourceFailureLoggingTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    [QuarantinedTest("https://github.com/microsoft/aspire/issues/16189")]
    public async Task ExecutableExitsImmediately()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var executable = builder.AddExecutable("pwsh", "pwsh", "")
            .WithArgs("-Command", """
                Write-Host "Hello from Stdout"
                [Console]::Error.WriteLine("Hello from Stderr")
                """);
        AddFakeLogging(executable);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(executable.Resource.Name, KnownResourceStates.Finished, cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
        }
 
        var logLines = GetLogLines(logCollector);
 
        Assert.Contains(logLines, x => x.EndsWith("Hello from Stdout"));
        Assert.Contains(logLines, x => x.EndsWith("Hello from Stderr"));
    }
 
    [Fact]
    public async Task ExecutableDoesNotExist()
    {
        using var cts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.DefaultOrchestratorTestLongTimeout);
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var executable = builder.AddExecutable("exe", "does-not-exist", ".");
        AddFakeLogging(executable);
 
        FakeLogCollector logCollector;
        using (var app = builder.Build())
        {
            logCollector = app.Services.GetFakeLogCollector();
            await app.StartAsync(cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
            await app.ResourceNotifications.WaitForResourceAsync(executable.Resource.Name, KnownResourceStates.FailedToStart, cts.Token).DefaultTimeout(TestConstants.DefaultOrchestratorTestLongTimeout);
 
            var logLines = GetLogLines(logCollector);
            AssertSingleLogLine(
                logLines,
                x => x.Contains("[sys] Failed to start a process:") && x.Contains("executable file not found"),
                "process start failure");
            AssertSingleLogLine(
                logLines,
                x => x.Contains("[sys] The Executable failed to start:"),
                "executable failed-to-start status");
        }
    }
 
    private static void AddFakeLogging<T>(IResourceBuilder<T> builder)
        where T : IResource
    {
        var category = $"{builder.ApplicationBuilder.Environment.ApplicationName}.Resources.{builder.Resource.Name}";
        builder.ApplicationBuilder.Services.AddLogging(x => x.AddFakeLogging(y => y.FilteredCategories.Add(category)));
    }
 
    private static List<string> GetLogLines(FakeLogCollector logCollector)
    {
        return [.. logCollector.GetSnapshot()
                .Select(x => x.StructuredState?.SingleOrDefault(x => x.Key == "LineContent"))
                .Where(x => x is not null)
                .Select(x => x?.Value!)];
    }
 
    private static void AssertSingleLogLine(List<string> logLines, Func<string, bool> predicate, string expected)
    {
        var matches = logLines.Where(predicate).ToList();
        Assert.True(
            matches.Count == 1,
            $"Expected exactly one log line for {expected}, but found {matches.Count}.{Environment.NewLine}Captured resource logs:{Environment.NewLine}{FormatLogLines(logLines)}");
    }
 
    private static string FormatLogLines(List<string> logLines)
    {
        return logLines.Count == 0
            ? "<none>"
            : string.Join(Environment.NewLine, logLines.Select((line, index) => $"{index + 1}: {line}"));
    }
}