Skip to content
Merged
7 changes: 7 additions & 0 deletions playground/AspireEventHub/EventHubsApi/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@

var app = builder.Build();

app.MapGet("/test", async (EventHubProducerClient producerClient) =>
{
var binaryData = BinaryData.FromString("Hello, from /test sent via producerClient");
await producerClient.SendAsync([new EventData(binaryData)]);

return Results.Ok();
});
app.MapPost("/message", async (Stream body, EventHubProducerClient producerClient) =>
{
var binaryData = await BinaryData.FromStreamAsync(body);
Expand Down
121 changes: 60 additions & 61 deletions tests/Aspire.Playground.Tests/AppHostTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Tests.Utils;
using Microsoft.DotNet.XUnitExtensions;
Expand All @@ -27,21 +28,6 @@ public AppHostTests(ITestOutputHelper testOutput)
_testOutput = testOutput;
}

[Theory]
[MemberData(nameof(AppHostAssembliesWithNoTestEndpoints))]
public async Task AppHostRunsCleanly(string appHostPath)
{
var appHost = await DistributedApplicationTestFactory.CreateAsync(appHostPath, _testOutput);
await using var app = await appHost.BuildAsync();

await app.StartAsync();
await app.WaitForResources().WaitAsync(TimeSpan.FromMinutes(2));

app.EnsureNoErrorsLogged();

await app.StopAsync();
}

[Theory]
[MemberData(nameof(TestEndpoints))]
public async Task TestEndpointsReturnOk(TestEndpoints testEndpoints)
Expand Down Expand Up @@ -108,19 +94,7 @@ public async Task TestEndpointsReturnOk(TestEndpoints testEndpoints)
}

HttpResponseMessage? response = null;

using var client = app.CreateHttpClient(resource, null, clientBuilder =>
{
clientBuilder
.ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan)
.AddStandardResilienceHandler(resilience =>
{
resilience.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(4);
resilience.AttemptTimeout.Timeout = TimeSpan.FromSeconds(90);
resilience.Retry.MaxRetryAttempts = 30;
resilience.CircuitBreaker.SamplingDuration = resilience.AttemptTimeout.Timeout * 2;
});
});
using var client = CreateHttpClientWithResilience(app, resource);

foreach (var path in endpoints)
{
Expand All @@ -137,49 +111,42 @@ public async Task TestEndpointsReturnOk(TestEndpoints testEndpoints)
Assert.True(HttpStatusCode.OK == response.StatusCode, $"Endpoint '{client.BaseAddress}{path.TrimStart('/')}' for resource '{resource}' in app '{Path.GetFileNameWithoutExtension(appHostPath)}' returned status code {response.StatusCode}");
}
}
if (testEndpoints.WhenReady != null)
{
await testEndpoints.WhenReady(app, appHostPath, _testOutput);
}

app.EnsureNoErrorsLogged();
await app.StopAsync();
}

public static TheoryData<string> AppHostAssembliesWithNoTestEndpoints()
{
var appHostAssemblies = GetPlaygroundAppHostAssemblyPaths();

HashSet<string> appHostsWithTestEndpoints = new();
foreach (var testEndpoint in GetAllTestEndpoints())
public static HttpClient CreateHttpClientWithResilience(DistributedApplication app, string resource)
=> app.CreateHttpClient(resource, null, clientBuilder =>
{
appHostsWithTestEndpoints.Add(testEndpoint.AppHost);
}

var theoryData = new TheoryData<string>();
foreach (var asm in appHostAssemblies)
{
var appHostName = Path.GetFileNameWithoutExtension(asm);
if (appHostsWithTestEndpoints.Contains(appHostName))
{
// Skipping this as it will be tested by TestEndpointsReturnOk
continue;
}

if (string.IsNullOrEmpty(s_appHostNameFilter) || asm.Contains(s_appHostNameFilter, StringComparison.OrdinalIgnoreCase))
{
theoryData.Add(Path.GetRelativePath(AppContext.BaseDirectory, asm));
}
}

if (!theoryData.Any() && !string.IsNullOrEmpty(s_appHostNameFilter))
{
throw new SkipTestException($"No app host assemblies found matching filter '{s_appHostNameFilter}'");
}

return theoryData;
}
clientBuilder
.ConfigureHttpClient(client => client.Timeout = Timeout.InfiniteTimeSpan)
.AddStandardResilienceHandler(resilience =>
{
resilience.TotalRequestTimeout.Timeout = TimeSpan.FromSeconds(120);
resilience.AttemptTimeout.Timeout = TimeSpan.FromSeconds(60);
resilience.Retry.MaxRetryAttempts = 30;
resilience.CircuitBreaker.SamplingDuration = resilience.AttemptTimeout.Timeout * 2;
});
});

public static IList<TestEndpoints> GetAllTestEndpoints()
{
IList<TestEndpoints> candidates =
[
new TestEndpoints("EventHubs.AppHost",
resourceEndpoints: new() { { "api", ["/alive", "/health"] } },
waitForTexts: [
new ("eventhubns", "Emulator Service is Successfully Up"),
new ("eventhubns-storage", "Azurite Table service is successfully listening"),
new ("ehstorage", "Azurite Table service is successfully listening"),
new ("consumer", "Completed retrieving properties for Event Hub")
],
whenReady: TestEventHubsAppHost),
new TestEndpoints("Redis.AppHost",
resourceEndpoints: new() { { "apiservice", ["/alive", "/health", "/ping", "get", "set"] } },
waitForTexts: [
Expand Down Expand Up @@ -290,6 +257,32 @@ public static IList<TestEndpoints> GetAllTestEndpoints()
return candidates;
}

private static async Task TestEventHubsAppHost(DistributedApplication app, string appHostPath, ITestOutputHelper testOutput)
{
using var client = CreateHttpClientWithResilience(app, "api");

var consumerMessage = "Hello, from /test sent via producerClient";
var consumerMessageLoggedTask =
app.WaitForTextAsync(log => log.Contains(consumerMessage), resourceName: "consumer")
.WaitAsync(TimeSpan.FromMinutes(2))
.ConfigureAwait(false);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this declared up here? Why not just inline it in the await inside the try catch below?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to avoid missing this message.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't it read through the whole log from the start? If it already happened, I think it would still get seen.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are correct, it does play the whole log from the start! Maybe it should be renamed to like FindOrWaitForTextAsync.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's "Waiting for this text to happen", if it has already happened, you are done waiting.


var path = "/test";
testOutput.WriteLine($"*** TestEventHubsAppHost calling {path} endpoint");

var response = await client.GetAsync(path);
Assert.True(HttpStatusCode.OK == response.StatusCode, $"Endpoint '{client.BaseAddress}{path.TrimStart('/')}' for resource 'consumer' in app '{Path.GetFileNameWithoutExtension(appHostPath)}' returned status code {response.StatusCode}");

try
{
await consumerMessageLoggedTask;
}
catch (TimeoutException te)
{
throw new XunitException($"Timed out waiting for the consumer message to be logged: '{consumerMessage}'", te);
}
}

public static TheoryData<TestEndpoints> TestEndpoints()
{
TheoryData<TestEndpoints> theoryData = new();
Expand Down Expand Up @@ -319,11 +312,15 @@ private static IEnumerable<string> GetPlaygroundAppHostAssemblyPaths()

public class TestEndpoints
{
public TestEndpoints(string appHost, Dictionary<string, List<string>> resourceEndpoints, List<ReadyStateText>? waitForTexts = null)
public TestEndpoints(string appHost,
Dictionary<string, List<string>> resourceEndpoints,
List<ReadyStateText>? waitForTexts = null,
Func<DistributedApplication, string, ITestOutputHelper, Task>? whenReady = null)
{
AppHost = appHost;
ResourceEndpoints = resourceEndpoints;
WaitForTexts = waitForTexts;
WhenReady = whenReady;
}

public string AppHost { get; set; }
Expand All @@ -334,6 +331,8 @@ public TestEndpoints(string appHost, Dictionary<string, List<string>> resourceEn

public Dictionary<string, List<string>>? ResourceEndpoints { get; set; }

public Func<DistributedApplication, string, ITestOutputHelper, Task>? WhenReady { get; set; }

public override string? ToString() => $"{AppHost} ({ResourceEndpoints?.Count ?? 0} resources)";

public class ResourceWait(string resourceName, string targetState)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
<PackageReference Include="Microsoft.Extensions.Diagnostics.Testing" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" />

<ProjectReference Include="$(PlaygroundSourceDir)AspireEventHub/EventHubs.AppHost/EventHubs.AppHost.csproj" />
<ProjectReference Include="$(PlaygroundSourceDir)AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/AzureStorageEndToEnd.AppHost.csproj" />
<ProjectReference Include="$(PlaygroundSourceDir)AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/AzureFunctionsEndToEnd.AppHost.csproj" Condition="'$(ContinuousIntegrationBuild)' != 'true'" />
<ProjectReference Include="$(PlaygroundSourceDir)CosmosEndToEnd/CosmosEndToEnd.AppHost/CosmosEndToEnd.AppHost.csproj" />
Expand Down