File: Backchannel\ResourceWaitServiceTests.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 System.Collections.Concurrent;
using Aspire.Cli.Backchannel;
using Microsoft.Extensions.Logging.Abstractions;
using Aspire.Cli.Tests.TestServices;
 
namespace Aspire.Cli.Tests.Backchannel;
 
public class ResourceWaitServiceTests
{
    [Theory]
    [InlineData("healthy")]
    [InlineData("up")]
    [InlineData("down")]
    public async Task WaitAsync_MapsTargetsAndSuccessfulResponse(string expectedStatus)
    {
        var target = expectedStatus switch
        {
            "healthy" => ResourceWaitTarget.Healthy,
            "up" => ResourceWaitTarget.Up,
            "down" => ResourceWaitTarget.Down,
            _ => throw new ArgumentOutOfRangeException(nameof(expectedStatus))
        };
        string? actualResourceName = null;
        string? actualStatus = null;
        int? actualTimeoutSeconds = null;
        var backchannel = new TestAppHostAuxiliaryBackchannel
        {
            WaitForResourceHandler = (resourceName, status, timeoutSeconds, _) =>
            {
                actualResourceName = resourceName;
                actualStatus = status;
                actualTimeoutSeconds = timeoutSeconds;
                return Task.FromResult(new WaitForResourceResponse
                {
                    Success = true,
                    State = "Running"
                });
            }
        };
        var service = new ResourceWaitService(TimeProvider.System, NullLogger<ResourceWaitService>.Instance);
 
        var result = await service.WaitAsync(
            backchannel,
            "api",
            target,
            timeoutSeconds: 30,
            TestContext.Current.CancellationToken);
 
        Assert.Equal("api", actualResourceName);
        Assert.Equal(expectedStatus, actualStatus);
        Assert.Equal(30, actualTimeoutSeconds);
        Assert.Equal(ResourceWaitOutcome.Success, result.Outcome);
        Assert.Equal("Running", result.State);
        Assert.False(result.ResourceNotFound);
        Assert.Null(result.ErrorMessage);
    }
 
    [Fact]
    public async Task WaitAsync_TreatsFailedToStartAsFailureForDownTarget()
    {
        var backchannel = new TestAppHostAuxiliaryBackchannel
        {
            WaitForResourceHandler = (_, _, _, _) => Task.FromResult(new WaitForResourceResponse
            {
                Success = true,
                State = "FailedToStart"
            })
        };
        var service = new ResourceWaitService(TimeProvider.System, NullLogger<ResourceWaitService>.Instance);
 
        var result = await service.WaitAsync(
            backchannel,
            "api",
            ResourceWaitTarget.Down,
            timeoutSeconds: 30,
            TestContext.Current.CancellationToken);
 
        Assert.Equal(ResourceWaitOutcome.Failure, result.Outcome);
        Assert.Equal("FailedToStart", result.State);
    }
 
    [Theory]
    [InlineData(true, false, "Failure")]
    [InlineData(false, true, "Timeout")]
    [InlineData(false, false, "Failure")]
    public async Task WaitAsync_MapsUnsuccessfulResponses(
        bool resourceNotFound,
        bool timedOut,
        string expectedOutcomeName)
    {
        var backchannel = new TestAppHostAuxiliaryBackchannel
        {
            WaitForResourceHandler = (_, _, _, _) => Task.FromResult(new WaitForResourceResponse
            {
                Success = false,
                State = "Waiting",
                ResourceNotFound = resourceNotFound,
                TimedOut = timedOut,
                ErrorMessage = "Wait failed."
            })
        };
        var service = new ResourceWaitService(TimeProvider.System, NullLogger<ResourceWaitService>.Instance);
 
        var result = await service.WaitAsync(
            backchannel,
            "api",
            ResourceWaitTarget.Healthy,
            timeoutSeconds: 30,
            TestContext.Current.CancellationToken);
 
        Assert.Equal(Enum.Parse<ResourceWaitOutcome>(expectedOutcomeName), result.Outcome);
        Assert.Equal("Waiting", result.State);
        Assert.Equal(resourceNotFound, result.ResourceNotFound);
        Assert.Equal("Wait failed.", result.ErrorMessage);
    }
 
    [Theory]
    [InlineData(-300)]
    [InlineData(300)]
    public async Task WaitForResourcesAsync_UtcClockChangesDoNotChangeTheSharedBudget(int clockChangeSeconds)
    {
        var timeProvider = new AdjustableTimeProvider();
        var timeouts = new ConcurrentQueue<int>();
        var backchannel = new TestAppHostAuxiliaryBackchannel
        {
            WaitForResourceHandler = (resourceName, _, timeoutSeconds, _) =>
            {
                timeouts.Enqueue(timeoutSeconds);
                if (resourceName == "api")
                {
                    // Change UTC during dispatch without adding that jump to elapsed time.
                    timeProvider.Elapsed += TimeSpan.FromSeconds(1);
                    timeProvider.UtcNow += TimeSpan.FromSeconds(1 + clockChangeSeconds);
                }
 
                return Task.FromResult(new WaitForResourceResponse { Success = true, State = "Running" });
            }
        };
        var service = new ResourceWaitService(timeProvider, NullLogger<ResourceWaitService>.Instance);
 
        var results = await service.WaitForResourcesAsync(
            backchannel, ["api", "worker"], ResourceWaitTarget.Healthy, 30, TestContext.Current.CancellationToken);
 
        Assert.Equal([30, 29], timeouts);
        Assert.Collection(results,
            result =>
            {
                Assert.Equal(ResourceWaitOutcome.Success, result.Outcome);
                Assert.Equal(TimeSpan.FromSeconds(1), result.Elapsed);
            },
            result =>
            {
                Assert.Equal(ResourceWaitOutcome.Success, result.Outcome);
                Assert.Equal(TimeSpan.Zero, result.Elapsed);
            });
    }
 
    [Theory]
    [InlineData(250, 30)]
    [InlineData(1250, 29)]
    [InlineData(30000, 0)]
    [InlineData(31000, 0)]
    public async Task WaitForResourcesAsync_RoundsRemainingBudgetAndSkipsExpiredRequests(int elapsedMilliseconds, int expectedRemainingSeconds)
    {
        var timeProvider = new AdjustableTimeProvider();
        var timeouts = new ConcurrentQueue<int>();
        var backchannel = new TestAppHostAuxiliaryBackchannel
        {
            WaitForResourceHandler = (resourceName, _, timeoutSeconds, _) =>
            {
                timeouts.Enqueue(timeoutSeconds);
                if (resourceName == "api")
                {
                    var elapsed = TimeSpan.FromMilliseconds(elapsedMilliseconds);
                    timeProvider.Elapsed += elapsed;
                    timeProvider.UtcNow += elapsed;
                }
 
                return Task.FromResult(new WaitForResourceResponse { Success = true, State = "Running" });
            }
        };
        var service = new ResourceWaitService(timeProvider, NullLogger<ResourceWaitService>.Instance);
 
        var results = await service.WaitForResourcesAsync(
            backchannel, ["api", "worker"], ResourceWaitTarget.Healthy, 30, TestContext.Current.CancellationToken);
 
        int[] expectedTimeouts = expectedRemainingSeconds == 0 ? [30] : [30, expectedRemainingSeconds];
        Assert.Equal(expectedTimeouts, timeouts);
        Assert.Collection(results,
            result => Assert.Equal(ResourceWaitOutcome.Success, result.Outcome),
            result =>
            {
                Assert.Equal(expectedRemainingSeconds == 0 ? ResourceWaitOutcome.Timeout : ResourceWaitOutcome.Success, result.Outcome);
                Assert.Equal(TimeSpan.Zero, result.Elapsed);
            });
    }
}