Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Fix timeout for ProjectRebuilderResource in Aspire 13.2.0+ by filteri…
…ng IResourceWithParent resources

Agent-Logs-Url: https://github.com/thomhurst/TUnit/sessions/ee4f5b19-6216-4842-be89-1a193cb86be7

Co-authored-by: thomhurst <30480171+thomhurst@users.noreply.github.com>
  • Loading branch information
Copilot and thomhurst committed Apr 1, 2026
commit 944d0a01d87a6024a6e04ef68a6deaa6e2f17bb1
55 changes: 55 additions & 0 deletions TUnit.Aspire.Tests/WaitForHealthyReproductionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,63 @@ 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.
/// </summary>
[Test]
public async Task GetWaitableResourceNames_ExcludesIResourceWithParent_Resources()
{
// 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 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");
}

private sealed class HealthyFixture : AspireFixture<Projects.TUnit_Aspire_Tests_AppHost>
{
protected override TimeSpan ResourceTimeout => TimeSpan.FromSeconds(60);
}

/// <summary>
/// Exposes <see cref="AspireFixture{TAppHost}.GetWaitableResourceNames"/> for unit testing.
/// </summary>
private sealed class InspectableFixture : AspireFixture<Projects.TUnit_Aspire_Tests_AppHost>
{
public List<string> GetWaitableNames(DistributedApplicationModel model)
=> GetWaitableResourceNames(model);
}

/// <summary>A plain IComputeResource with no parent.</summary>
private sealed class FakeContainerResource(string name) : IComputeResource
{
public string Name => name;
public ResourceAnnotationCollection Annotations { get; } = new ResourceAnnotationCollection();
}

/// <summary>
/// Simulates ProjectRebuilderResource from Aspire 13.2.0:
/// an IComputeResource that also implements IResourceWithParent.
/// </summary>
private sealed class FakeRebuilderResource(string name, IResource parent)
: IComputeResource, IResourceWithParent
{
public string Name => name;
public ResourceAnnotationCollection Annotations { get; } = new ResourceAnnotationCollection();
public IResource Parent => parent;
}
}
9 changes: 6 additions & 3 deletions TUnit.Aspire/AspireFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -525,14 +525,17 @@ private async Task<string> CollectResourceLogsAsync(

// Opt-in: only wait for IComputeResource (containers, projects, executables).
// Non-compute resources (parameters, connection strings) never report healthy and would hang.
private List<string> GetWaitableResourceNames(DistributedApplicationModel model)
// Also skip resources that implement IResourceWithParent — these are internally-managed child
// resources such as ProjectRebuilderResource (introduced in Aspire 13.2.0) that are orchestrated
// by Aspire itself and are not user-visible resources that need to be awaited.
protected virtual List<string> GetWaitableResourceNames(DistributedApplicationModel model)
{
var waitable = new List<string>();
List<string>? skipped = null;

foreach (var r in model.Resources)
{
if (r is not IComputeResource)
if (r is not IComputeResource || r is IResourceWithParent)
{
skipped ??= [];
skipped.Add(r.Name);
Expand All @@ -545,7 +548,7 @@ private List<string> GetWaitableResourceNames(DistributedApplicationModel model)

if (skipped is { Count: > 0 })
{
LogProgress($"Skipping {skipped.Count} non-compute resource(s): [{string.Join(", ", skipped)}]");
LogProgress($"Skipping {skipped.Count} non-waitable resource(s) (non-compute or child resources): [{string.Join(", ", skipped)}]");
}

return waitable;
Expand Down