Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
</GlobalPackageReference>
</ItemGroup>
<ItemGroup>
<PackageVersion Include="Aspire.Hosting.AppHost" Version="13.2.0" />
<PackageVersion Include="Aspire.Hosting.Testing" Version="13.2.0" />
<PackageVersion Include="AutoFixture" Version="4.18.1" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
Expand Down
6 changes: 6 additions & 0 deletions TUnit.Aspire.Tests.AppHost/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var builder = DistributedApplication.CreateBuilder(args);

builder.AddParameter("my-secret", secret: true);
builder.AddContainer("nginx-no-healthcheck", "nginx");

builder.Build().Run();
14 changes: 14 additions & 0 deletions TUnit.Aspire.Tests.AppHost/TUnit.Aspire.Tests.AppHost.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<Project Sdk="Aspire.AppHost.Sdk/13.2.0">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsAspireHost>true</IsAspireHost>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
</ItemGroup>

</Project>
24 changes: 24 additions & 0 deletions TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">

<Import Project="..\TUnit.Engine\TUnit.Engine.props" />

<PropertyGroup>
<TargetFrameworks>net10.0</TargetFrameworks>
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\TUnit.Aspire\TUnit.Aspire.csproj" />
<ProjectReference Include="..\TUnit.Engine\TUnit.Engine.csproj" />
<ProjectReference Include="..\TUnit.Assertions\TUnit.Assertions.csproj" />
<ProjectReference Include="..\TUnit.Aspire.Tests.AppHost\TUnit.Aspire.Tests.AppHost.csproj" />
<ProjectReference Include="..\TUnit.Core.SourceGenerator\TUnit.Core.SourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\TUnit.Analyzers\TUnit.Analyzers.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Aspire.Hosting.Testing" />
<PackageReference Include="Microsoft.Testing.Platform.MSBuild" />
</ItemGroup>

</Project>
95 changes: 95 additions & 0 deletions TUnit.Aspire.Tests/WaitForHealthyReproductionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Testing;
using Microsoft.Extensions.DependencyInjection;
using TUnit.Assertions;
using TUnit.Assertions.Extensions;
using TUnit.Core;

namespace TUnit.Aspire.Tests;

/// <summary>
/// Reproduction and fix verification tests for https://github.com/thomhurst/TUnit/issues/5260
/// </summary>
public class WaitForHealthyReproductionTests
{
[Test]
[Category("Docker")]
public async Task ParameterResource_DoesNotImplement_IResourceWithoutLifetime_InAspire13_2(CancellationToken ct)
{
await using var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.TUnit_Aspire_Tests_AppHost>();
await using var app = await builder.BuildAsync();

var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var paramResource = model.Resources.First(r => r.Name == "my-secret");

await Assert.That(paramResource is IResourceWithoutLifetime).IsFalse();
}

[Test]
[Category("Docker")]
public async Task ParameterResource_IsNot_IComputeResource(CancellationToken ct)
{
await using var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.TUnit_Aspire_Tests_AppHost>();
await using var app = await builder.BuildAsync();

var model = app.Services.GetRequiredService<DistributedApplicationModel>();
var paramResource = model.Resources.First(r => r.Name == "my-secret");
var containerResource = model.Resources.First(r => r.Name == "nginx-no-healthcheck");

await Assert.That(paramResource is IComputeResource).IsFalse();
await Assert.That(containerResource is IComputeResource).IsTrue();
}

[Test]
[Category("Docker")]
public async Task WaitForResourceHealthyAsync_OnParameterResource_Hangs(CancellationToken ct)
{
await using var builder = await DistributedApplicationTestingBuilder.CreateAsync<Projects.TUnit_Aspire_Tests_AppHost>();
await using var app = await builder.BuildAsync();
await app.StartAsync(ct);

var notificationService = app.Services.GetRequiredService<ResourceNotificationService>();

using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
cts.CancelAfter(TimeSpan.FromSeconds(15));

var timedOut = false;
try
{
await notificationService.WaitForResourceHealthyAsync("my-secret", cts.Token);
}
catch (OperationCanceledException)
{
timedOut = true;
}
catch (InvalidOperationException)
{
// Aspire throws InvalidOperationException for resources that can never become healthy
timedOut = true;
}

await Assert.That(timedOut).IsTrue();
}

[Test]
[Category("Docker")]
public async Task AspireFixture_AllHealthy_Succeeds_AfterFix(CancellationToken ct)
{
var fixture = new HealthyFixture();

try
{
await fixture.InitializeAsync();
}
finally
{
await fixture.DisposeAsync();
}
}

private sealed class HealthyFixture : AspireFixture<Projects.TUnit_Aspire_Tests_AppHost>
{
protected override TimeSpan ResourceTimeout => TimeSpan.FromSeconds(60);
}
}
6 changes: 4 additions & 2 deletions TUnit.Aspire/AspireFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -523,14 +523,16 @@ private async Task<string> CollectResourceLogsAsync(
return string.Join(Environment.NewLine, lines);
}

// 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)
{
var waitable = new List<string>();
List<string>? skipped = null;

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

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

return waitable;
Expand Down
2 changes: 2 additions & 0 deletions TUnit.CI.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@
<Project Path="TUnit.RpcTests/TUnit.RpcTests.csproj" />
<Project Path="TUnit.Templates.Tests/TUnit.Templates.Tests.csproj" />
<Project Path="TUnit.UnitTests/TUnit.UnitTests.csproj" />
<Project Path="TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj" />
<Project Path="TUnit.Aspire.Tests.AppHost/TUnit.Aspire.Tests.AppHost.csproj" />
</Folder>

<!-- Build infrastructure -->
Expand Down
43 changes: 43 additions & 0 deletions TUnit.Pipeline/Modules/RunAspireTestsModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using ModularPipelines.Attributes;
using ModularPipelines.Context;
using ModularPipelines.DotNet.Extensions;
using ModularPipelines.DotNet.Options;
using ModularPipelines.Extensions;
using ModularPipelines.Git.Extensions;
using ModularPipelines.Models;
using ModularPipelines.Modules;
using ModularPipelines.Options;

namespace TUnit.Pipeline.Modules;

[NotInParallel("NetworkTests"), RunOnLinuxOnly, RunOnWindowsOnly]
public class RunAspireTestsModule : Module<CommandResult>
{
protected override async Task<CommandResult?> ExecuteAsync(IModuleContext context, CancellationToken cancellationToken)
{
var project = context.Git().RootDirectory.FindFile(x => x.Name == "TUnit.Aspire.Tests.csproj").AssertExists();

return await context.DotNet().Run(new DotNetRunOptions
{
Project = project.Name,
NoBuild = true,
Configuration = "Release",
Framework = "net10.0",
Arguments = ["--hangdump", "--hangdump-filename", "hangdump.aspire-tests.dmp", "--hangdump-timeout", "5m"],
}, new CommandExecutionOptions
{
WorkingDirectory = project.Folder!.Path,
EnvironmentVariables = new Dictionary<string, string?>
{
["DISABLE_GITHUB_REPORTER"] = "true",
},
LogSettings = new CommandLoggingOptions
{
ShowCommandArguments = true,
ShowStandardError = true,
ShowExecutionTime = true,
ShowExitCode = true
}
}, cancellationToken);
}
}
2 changes: 2 additions & 0 deletions TUnit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,8 @@
<Project Path="TUnit.SourceGenerator.IncrementalTests/TUnit.SourceGenerator.IncrementalTests.csproj" />
<Project Path="TUnit.Templates.Tests/TUnit.Templates.Tests.csproj" />
<Project Path="TUnit.UnitTests/TUnit.UnitTests.csproj" />
<Project Path="TUnit.Aspire.Tests/TUnit.Aspire.Tests.csproj" />
<Project Path="TUnit.Aspire.Tests.AppHost/TUnit.Aspire.Tests.AppHost.csproj" />
</Folder>

<!-- Benchmarks -->
Expand Down
Loading