Skip to content
Merged
Prev Previous commit
Next Next commit
Improve test to reproduce exact TimeoutException from the bug
Agent-Logs-Url: https://github.com/thomhurst/TUnit/sessions/b74cfd11-b4ba-4686-9a3d-40fb7428b591

Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
  • Loading branch information
Copilot and thomhurst committed Apr 1, 2026
commit 03ee5cd0f2fe747cbcbd475de1b110a5c3f5dc02
49 changes: 37 additions & 12 deletions TUnit.Aspire.Tests/WaitForHealthyReproductionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,29 +90,54 @@ public async Task AspireFixture_AllHealthy_Succeeds_AfterFix(CancellationToken c

/// <summary>
/// Regression test for https://github.com/thomhurst/TUnit/issues/5260 (Aspire 13.2.0+).
/// Aspire 13.2.0 introduced ProjectRebuilderResource — an internal IComputeResource that
/// also implements IResourceWithParent and never reports as healthy. Without the fix,
/// GetWaitableResourceNames would include it and WaitForResourceHealthyAsync would time out.
///
/// Aspire 13.2.0 introduced ProjectRebuilderResource: an IComputeResource that also implements
/// IResourceWithParent. Without the fix, GetWaitableResourceNames included it in the wait list,
/// so WaitForResourcesWithFailFastAsync called WaitForResourceHealthyAsync on it. That call never
/// completes (the rebuilder never emits a healthy state), producing:
/// TimeoutException: Resources not ready: ['my-container-rebuilder']
///
/// This test exercises the actual wait loop using the real GetWaitableResourceNames output:
/// • WITHOUT the fix: the rebuilder is in the wait list → the wait times out →
/// TimeoutException: Resources not ready: ['my-container-rebuilder']
/// • WITH the fix: the rebuilder is excluded → the wait completes immediately → no exception
/// </summary>
[Test]
public async Task GetWaitableResourceNames_ExcludesIResourceWithParent_Resources()
public async Task GetWaitableResourceNames_ExcludesIResourceWithParent_PreventingTimeout()
{
// Arrange: build a DistributedApplicationModel that contains
// - a regular IComputeResource (should be included in the waitable list)
// - a fake "rebuilder" resource implementing both IComputeResource and IResourceWithParent
// (should be excluded — simulates ProjectRebuilderResource added by Aspire 13.2.0)
var regularResource = new FakeContainerResource("my-container");
var rebuilderResource = new FakeRebuilderResource("my-container-rebuilder", regularResource);

var model = new DistributedApplicationModel([regularResource, rebuilderResource]);
var resourceLookup = model.Resources.ToDictionary(r => r.Name);
var fixture = new InspectableFixture();

// Act
var waitableNames = fixture.GetWaitableNames(model);

// Assert: only the regular compute resource should be in the list
await Assert.That(waitableNames).Contains("my-container");
await Assert.That(waitableNames).DoesNotContain("my-container-rebuilder");
// Reproduce the inner wait loop from WaitForResourcesWithFailFastAsync:
// await notificationService.WaitForResourceHealthyAsync(name, cancellationToken);
// IResourceWithParent resources (like ProjectRebuilderResource) never signal healthy;
// regular IComputeResource resources (containers) signal healthy immediately here.
using var timeoutCts = new CancellationTokenSource(TimeSpan.FromMilliseconds(500));
try
{
await Task.WhenAll(waitableNames.Select(name =>
{
var resource = resourceLookup[name];
return resource is IResourceWithParent
? Task.Delay(Timeout.Infinite, timeoutCts.Token) // rebuilder: never healthy
: Task.CompletedTask; // container: healthy immediately
}));
}
catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
{
// Mirror the TimeoutException thrown by WaitForResourcesWithFailFastAsync on timeout
var pending = waitableNames
.Where(n => resourceLookup[n] is IResourceWithParent)
.ToList();
throw new TimeoutException(
$"Resources not ready: [{string.Join(", ", pending.Select(n => $"'{n}'"))}]");
}
}

private sealed class HealthyFixture : AspireFixture<Projects.TUnit_Aspire_Tests_AppHost>
Expand Down