File: WaitForTests.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.Components.Common.Tests;
using Aspire.Dashboard.Model;
using Aspire.Hosting.Utils;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Xunit;
using Xunit.Abstractions;
 
namespace Aspire.Hosting.Tests;
 
public class WaitForTests(ITestOutputHelper testOutputHelper)
{
    [Fact]
    [RequiresDocker]
    public async Task ResourceThatFailsToStartDueToExceptionDoesNotCauseStartAsyncToThrow()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
        var throwingResource = builder.AddContainer("throwingresource", "doesnotmatter")
                              .WithEnvironment(ctx => throw new InvalidOperationException("BOOM!"));
        var dependingContainerResource = builder.AddContainer("dependingcontainerresource", "doesnotmatter")
                                       .WaitFor(throwingResource);
        var dependingExecutableResource = builder.AddExecutable("dependingexecutableresource", "doesnotmatter", "alsodoesntmatter")
                                       .WaitFor(throwingResource);
 
        var abortCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        using var app = builder.Build();
        await app.StartAsync(abortCts.Token);
 
        await app.ResourceNotifications.WaitForResourceAsync(throwingResource.Resource.Name, KnownResourceStates.FailedToStart, abortCts.Token);
        await app.ResourceNotifications.WaitForResourceAsync(dependingContainerResource.Resource.Name, KnownResourceStates.FailedToStart, abortCts.Token);
        await app.ResourceNotifications.WaitForResourceAsync(dependingExecutableResource.Resource.Name, KnownResourceStates.FailedToStart, abortCts.Token);
 
        await app.StopAsync(abortCts.Token);
    }
 
    [Fact]
    public void ResourceCannotWaitForItself()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var resource = builder.AddResource(new CustomResource("test"));
 
        var waitForEx = Assert.Throws<DistributedApplicationException>(() =>
        {
            resource.WaitFor(resource);
        });
 
        Assert.Equal("The 'test' resource cannot wait for itself.", waitForEx.Message);
 
        var waitForCompletionEx = Assert.Throws<DistributedApplicationException>(() =>
        {
            resource.WaitForCompletion(resource);
        });
 
        Assert.Equal("The 'test' resource cannot wait for itself.", waitForCompletionEx.Message);
    }
 
    [Fact]
    public void ResourceCannotWaitForItsParent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var parentResourceBuilder = builder.AddResource(new CustomResource("parent"));
        var childResourceBuilder = builder.AddResource(new CustomChildResource("child", parentResourceBuilder.Resource));
 
        var waitForEx = Assert.Throws<DistributedApplicationException>(() =>
        {
            childResourceBuilder.WaitFor(parentResourceBuilder);
        });
 
        Assert.Equal("The 'child' resource cannot wait for its parent 'parent'.", waitForEx.Message);
 
        var waitForCompletionEx = Assert.Throws<DistributedApplicationException>(() =>
        {
            childResourceBuilder.WaitForCompletion(parentResourceBuilder);
        });
 
        Assert.Equal("The 'child' resource cannot wait for its parent 'parent'.", waitForCompletionEx.Message);
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitingForParameterResourceCompletesImmediately()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        builder.Configuration["ConnectionStrings:cs"] = "cs-value";
 
        // This test waits for a parameter, a connection string, and a custom resource.
        // The only thing being waited on should be the custom resource.
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var cs = builder.AddConnectionString("cs");
        var param = builder.AddParameter("param", "value");
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WaitFor(cs)
                           .WaitFor(param)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        using var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        using var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        // Notice we don't need to move the parameter or connection string to a running state
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitingForConnectionStringResourceWaitsForReferencedResources()
    {
        using var builder = TestDistributedApplicationBuilder.Create(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var cs = builder.AddConnectionString("cs", ReferenceExpression.Create($"{dependency};Timeout=100"));
 
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WaitFor(cs);
 
        using var app = builder.Build();
 
        using var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        using var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task EnsureDependentResourceMovesIntoWaitingState()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Running state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow a long timeout just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can swap
        // the dependency into a running state which will unblock startup and
        // we can continue executing.
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        await startTask;
 
        await app.StopAsync();
    }
 
    // Add a test that verifies the wait for behavior when the dependency is in varying states
    // and the dependent resource is waiting for the dependency.
    // Use a theory to test the different states and expected behavior.
 
    [Theory]
    [InlineData(nameof(KnownResourceStates.Exited))]
    [InlineData(nameof(KnownResourceStates.FailedToStart))]
    [InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
    [InlineData(nameof(KnownResourceStates.Finished))]
    [RequiresDocker]
    public async Task WaitForBehaviorStopOnResourceUnavailable(string status)
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency, WaitBehavior.StopOnResourceUnavailable);
 
        using var app = builder.Build();
 
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = status
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, waitingStateCts.Token);
 
        await startTask;
    }
 
   [Fact]
   [RequiresDocker]
   public async Task WhenWaitBehaviorIsStopOnResourceUnavailableWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
   {
      using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
      var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
      var dependency = builder.AddContainer("redis", "redis");
 
      dependency.WaitFor(failToStart, WaitBehavior.StopOnResourceUnavailable);
 
      using var app = builder.Build();
      await app.StartAsync();
 
      var ex = await Assert.ThrowsAsync<DistributedApplicationException>(async () => {
        await app.ResourceNotifications.WaitForResourceHealthyAsync(
            dependency.Resource.Name,
            WaitBehavior.StopOnResourceUnavailable
            ).WaitAsync(TimeSpan.FromSeconds(15));
      });
 
      Assert.Equal("Stopped waiting for resource 'redis' to become healthy because it failed to start.", ex.Message);
   }
 
   [Fact]
   [RequiresDocker]
   public async Task WhenWaitBehaviorIsWaitOnResourceUnavailableWaitForResourceHealthyAsyncShouldThrowWhenResourceFailsToStart()
   {
      using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
      var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
      var dependency = builder.AddContainer("redis", "redis");
 
      dependency.WaitFor(failToStart, WaitBehavior.StopOnResourceUnavailable);
 
      using var app = builder.Build();
      await app.StartAsync();
 
      var ex = await Assert.ThrowsAsync<TimeoutException>(async () => {
        await app.ResourceNotifications.WaitForResourceHealthyAsync(
            dependency.Resource.Name,
            WaitBehavior.WaitOnResourceUnavailable
            ).WaitAsync(TimeSpan.FromSeconds(15));
      });
 
      Assert.Equal("The operation has timed out.", ex.Message);
   }
 
    [Theory]
    [RequiresDocker]
    [InlineData(WaitBehavior.WaitOnResourceUnavailable, typeof(TimeoutException), "The operation has timed out.")]
    [InlineData(WaitBehavior.StopOnResourceUnavailable, typeof(DistributedApplicationException), "Stopped waiting for resource 'redis' to become healthy because it failed to start.")]
    public async Task WhenWaitBehaviorIsMissingWaitForResourceHealthyAsyncShouldUseDefaultWaitBehavior(WaitBehavior defaultWaitBehavior, Type exceptionType, string exceptionMessage)
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        builder.Services.Configure<ResourceNotificationServiceOptions>(o =>
        {
            o.DefaultWaitBehavior = defaultWaitBehavior;
        });
 
        var failToStart = builder.AddExecutable("failToStart", "does-not-exist", ".");
        var dependency = builder.AddContainer("redis", "redis");
 
        dependency.WaitFor(failToStart, WaitBehavior.StopOnResourceUnavailable);
 
        using var app = builder.Build();
        await app.StartAsync();
 
        var ex = await Assert.ThrowsAsync(exceptionType, async () => {
            await app.ResourceNotifications.WaitForResourceHealthyAsync(dependency.Resource.Name)
                .WaitAsync(TimeSpan.FromSeconds(15));
        });
 
        Assert.Equal(exceptionMessage, ex.Message);
    }
 
    [Theory]
    [InlineData(nameof(KnownResourceStates.Exited))]
    [InlineData(nameof(KnownResourceStates.FailedToStart))]
    [InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
    [InlineData(nameof(KnownResourceStates.Finished))]
    [RequiresDocker]
    public async Task WaitForBehaviorStopOnDependencyIsDefaultWithNoDashboardFailure(string status)
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = status
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, waitingStateCts.Token);
 
        await startTask;
    }
 
    [Theory]
    [InlineData(nameof(KnownResourceStates.Exited))]
    [InlineData(nameof(KnownResourceStates.FailedToStart))]
    [InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
    [InlineData(nameof(KnownResourceStates.Finished))]
    [RequiresDocker]
    public async Task WaitForBehaviorWaitOnResourceUnavailable(string status)
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency, WaitBehavior.WaitOnResourceUnavailable);
 
        using var app = builder.Build();
 
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = status
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Fake a restart of the dependency
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, waitingStateCts.Token);
 
        await startTask;
    }
 
    [Theory]
    [InlineData(nameof(KnownResourceStates.Exited))]
    [InlineData(nameof(KnownResourceStates.FailedToStart))]
    [InlineData(nameof(KnownResourceStates.RuntimeUnhealthy))]
    [InlineData(nameof(KnownResourceStates.Finished))]
    [RequiresDocker]
    public async Task WaitForBehaviorWaitOnResourceUnavailableViaOptions(string status)
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        builder.Services.Configure<ResourceNotificationServiceOptions>(o =>
        {
            o.DefaultWaitBehavior = WaitBehavior.WaitOnResourceUnavailable;
        });
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = status
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Fake a restart of the dependency
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, waitingStateCts.Token);
 
        await startTask;
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitForCompletionWaitsForTerminalStateOfDependencyResource()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitForCompletion(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can swap
        // the dependency into a running state which will unblock startup and
        // we can continue executing.
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Finished,
            ExitCode = 0
        });
 
        // This time we want to wait for Nginx to move into a Running state to verify that
        // it successfully started after we moved the dependency resource into the Finished, but
        // we need to give it more time since we have to download the image in CI.
        var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, runningStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitForThrowsIfResourceMovesToTerminalStateBeforeRunning()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can swap
        // the dependency into a running state which will unblock startup and
        // we can continue executing.
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Finished,
            ExitCode = 0
        });
 
        // This time we want to wait for Nginx to move into a Running state to verify that
        // it successfully started after we moved the dependency resource into the Finished, but
        // we need to give it more time since we have to download the image in CI.
        var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitForObservedResultOfResourceReadyEvent()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        builder.Services.AddLogging(b =>
        {
            b.AddFakeLogging();
        });
 
        var resourceReadyTcs = new TaskCompletionSource();
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        builder.Eventing.Subscribe<ResourceReadyEvent>(dependency.Resource, (e, ct) => resourceReadyTcs.Task);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, "Waiting", waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can swap
        // the dependency into a running state which will unblock startup and
        // we can continue executing.
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Running
        });
 
        resourceReadyTcs.SetException(new InvalidOperationException("The resource ready event failed!"));
 
        // This time we want to wait for Nginx to move into a Running state to verify that
        // it successfully started after we moved the dependency resource into the Finished, but
        // we need to give it more time since we have to download the image in CI.
        var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token);
 
        await startTask;
 
        var collector = app.Services.GetFakeLogCollector();
        var logs = collector.GetSnapshot();
 
        // Just looking for a common message in Docker build output.
        Assert.Contains(logs, log => log.Message.Contains("The resource ready event failed!"));
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task EnsureDependencyResourceThatReturnsNonMatchingExitCodeResultsInDependentResourceFailingToStart()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitForCompletion(dependency, exitCode: 2);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can swap
        // the dependency into a finished state which will unblock startup and
        // we can continue executing.
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Finished,
            ExitCode = 3 // Exit code does not match expected exit code above intentionally.
        });
 
        // This time we want to wait for Nginx to move into a FailedToStart state to verify that
        // it didn't start if the dependency resource didn't finish with the correct exit code.
        var runningStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.FailedToStart, runningStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task DependencyWithGreaterThan1ReplicaAnnotationWaitsForAll()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"))
                                .WithAnnotation(new ReplicaAnnotation(2))
                                .WithAnnotation(new DcpInstancesAnnotation([
                                    new("test0", "", 0), new("test1", "", 1)
                                ]));
 
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitFor(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Publish the first replica as finished
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, "test0", s => s with
        {
            State = KnownResourceStates.Running,
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Publish the second replica as finished
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, "test1", s => s with
        {
            State = KnownResourceStates.Running,
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, waitingStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task DependencyWithGreaterThan1ReplicaAnnotationWaitsForAllToComplete()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"))
                                .WithAnnotation(new ReplicaAnnotation(2))
                                .WithAnnotation(new DcpInstancesAnnotation([
                                    new("test0", "", 0), new("test1", "", 1)
                                ]));
 
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitForCompletion(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Publish the first replica as finished
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, "test0", s => s with
        {
            State = KnownResourceStates.Finished,
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Publish the second replica as finished
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, "test1", s => s with
        {
            State = KnownResourceStates.Finished,
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, waitingStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    [RequiresDocker]
    public async Task WaitForCompletionSucceedsIfDependentResourceEntersTerminalStateWithoutAnExitCode()
    {
        using var builder = TestDistributedApplicationBuilder.Create().WithTestAndResourceLogging(testOutputHelper);
 
        var dependency = builder.AddResource(new CustomResource("test"));
 
        var nginx = builder.AddContainer("nginx", "mcr.microsoft.com/cbl-mariner/base/nginx", "1.22")
                           .WithReference(dependency)
                           .WaitForCompletion(dependency);
 
        using var app = builder.Build();
 
        // StartAsync will currently block until the dependency resource moves
        // into a Finished state, so rather than awaiting it we'll hold onto the
        // task so we can inspect the state of the Nginx resource which should
        // be in a waiting state if everything is working correctly.
        var startupCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
        var startTask = app.StartAsync(startupCts.Token);
 
        // We don't want to wait forever for Nginx to move into a waiting state,
        // it should be super quick, but we'll allow 60 seconds just in case the
        // CI machine is chugging (also useful when collecting code coverage).
        var waitingStateCts = AsyncTestHelpers.CreateDefaultTimeoutTokenSource(TestConstants.LongTimeoutDuration);
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Waiting, waitingStateCts.Token);
 
        // Now that we know we successfully entered the Waiting state, we can end the dependency
        await app.ResourceNotifications.PublishUpdateAsync(dependency.Resource, s => s with
        {
            State = KnownResourceStates.Finished
        });
 
        await app.ResourceNotifications.WaitForResourceAsync(nginx.Resource.Name, KnownResourceStates.Running, waitingStateCts.Token);
 
        await startTask;
 
        await app.StopAsync();
    }
 
    [Fact]
    public void WaitForOnChildResourceAddsWaitAnnotationPointingToParent()
    {
        using var builder = TestDistributedApplicationBuilder.Create();
        var parentResource = builder.AddResource(new CustomResource("parent"));
        var childResource = builder.AddResource(new CustomChildResource("child", parentResource.Resource));
        var containerResource = builder.AddContainer("container", "image", "tag")
                                       .WaitFor(childResource);
 
        Assert.True(containerResource.Resource.TryGetAnnotationsOfType<WaitAnnotation>(out var waitAnnotations));
        Assert.Collection(
            waitAnnotations,
            a => Assert.Equal(a.Resource, parentResource.Resource),
            a => Assert.Equal(a.Resource, childResource.Resource)
            );
 
        Assert.True(containerResource.Resource.TryGetAnnotationsOfType<ResourceRelationshipAnnotation>(out var relationshipAnnotations));
        var relationshipAnnotation = Assert.Single(relationshipAnnotations);
 
        Assert.Equal(childResource.Resource, relationshipAnnotation.Resource);
        Assert.Equal(KnownRelationshipTypes.WaitFor, relationshipAnnotation.Type);
    }
 
    private sealed class CustomChildResource(string name, CustomResource parent) : Resource(name), IResourceWithParent<CustomResource>, IResourceWithWaitSupport
    {
        public CustomResource Parent => parent;
    }
 
    private sealed class CustomResource(string name) : Resource(name), IResourceWithConnectionString, IResourceWithWaitSupport
    {
        public ReferenceExpression ConnectionStringExpression => ReferenceExpression.Create($"foo");
    }
}