From 6f765b0aa31b1bb0ebf3518089583b425fe462be Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Thu, 1 Feb 2024 18:16:47 -0600 Subject: [PATCH 01/10] Refactor functional tests - Split the functional Components Integration tests out of Aspire.Hosting.Tests into a new EndToEnd project. - Start the test AppHost project using `dotnet run` instead of running the DistributedAppBuilder inline in the tests. This allows for the tests to run on a separate machine than where the tests are built. - Along with this, we need to pass information from the test AppHost to the test about its endpoints, and give it a signal to "stop". Instead of trying to find a way to CTRL+C cross-platform, read from stdin for a "Stop" command. --- Aspire.sln | 7 ++ .../Aspire.EndToEnd.Tests.csproj | 7 ++ .../Cosmos/CosmosFunctionalTests.cs | 10 +- .../IntegrationServicesFixture.cs | 118 ++++++++++++++++++ .../IntegrationServicesTests.cs | 16 +-- .../Kafka/KafkaFunctionalTests.cs | 13 +- .../MongoDB/MongoDBFunctionalTests.cs | 10 +- .../MySql/MySqlFunctionalTests.cs | 10 +- .../MySql/PomeloEFCoreMySqlFunctionalTests.cs | 10 +- .../Oracle/OracleDatabaseFunctionalTests.cs | 10 +- .../Postgres/PostgresFunctionalTests.cs | 10 +- tests/Aspire.EndToEnd.Tests/ProjectInfo.cs | 54 ++++++++ .../RabbitMQ/RabbitMQFunctionalTests.cs | 10 +- .../Redis/RedisFunctionalTests.cs | 10 +- .../SqlServer/SqlServerFunctionalTests.cs | 10 +- ...locatedEndpointAnnotationTestExtensions.cs | 46 ------- .../TestProgramFixture.cs | 48 ------- .../TestProject.AppHost/Program.cs | 15 +++ .../TestProject.AppHost/TestProgram.cs | 36 ++++++ .../TestProject.AppHost/appsettings.json | 9 ++ 20 files changed, 280 insertions(+), 179 deletions(-) create mode 100644 tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/Cosmos/CosmosFunctionalTests.cs (67%) create mode 100644 tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/IntegrationServicesTests.cs (52%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/Kafka/KafkaFunctionalTests.cs (60%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/MongoDB/MongoDBFunctionalTests.cs (67%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/MySql/MySqlFunctionalTests.cs (74%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/MySql/PomeloEFCoreMySqlFunctionalTests.cs (68%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/Oracle/OracleDatabaseFunctionalTests.cs (68%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/Postgres/PostgresFunctionalTests.cs (68%) create mode 100644 tests/Aspire.EndToEnd.Tests/ProjectInfo.cs rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/RabbitMQ/RabbitMQFunctionalTests.cs (68%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/Redis/RedisFunctionalTests.cs (67%) rename tests/{Aspire.Hosting.Tests => Aspire.EndToEnd.Tests}/SqlServer/SqlServerFunctionalTests.cs (68%) create mode 100644 tests/testproject/TestProject.AppHost/appsettings.json diff --git a/Aspire.sln b/Aspire.sln index e6b01fb2da..0f92949b2b 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -219,6 +219,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mongo.ApiService", "playgro EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogModel", "playground\eShopLite\CatalogModel\CatalogModel.csproj", "{83267206-9438-42CD-860C-C92E7DBAA4C3}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.EndToEnd.Tests", "tests\Aspire.EndToEnd.Tests\Aspire.EndToEnd.Tests.csproj", "{C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ParameterEndToEnd", "ParameterEndToEnd", "{F1387494-34DE-4B31-B587-699B2E9A20CA}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ParameterEndToEnd.AppHost", "playground\ParameterEndToEnd\ParameterEndToEnd.AppHost\ParameterEndToEnd.AppHost.csproj", "{54B66163-016D-4122-9364-409AB61BC36B}" @@ -591,6 +593,10 @@ Global {83267206-9438-42CD-860C-C92E7DBAA4C3}.Debug|Any CPU.Build.0 = Debug|Any CPU {83267206-9438-42CD-860C-C92E7DBAA4C3}.Release|Any CPU.ActiveCfg = Release|Any CPU {83267206-9438-42CD-860C-C92E7DBAA4C3}.Release|Any CPU.Build.0 = Release|Any CPU + {C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E}.Release|Any CPU.Build.0 = Release|Any CPU {54B66163-016D-4122-9364-409AB61BC36B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {54B66163-016D-4122-9364-409AB61BC36B}.Debug|Any CPU.Build.0 = Debug|Any CPU {54B66163-016D-4122-9364-409AB61BC36B}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -702,6 +708,7 @@ Global {8F132275-233C-4121-AC6F-352C902FA064} = {C544D8A6-977E-40EA-8B1A-1FB2146A2108} {40EC38A2-69DB-4759-81C8-13F31090FEA6} = {C544D8A6-977E-40EA-8B1A-1FB2146A2108} {83267206-9438-42CD-860C-C92E7DBAA4C3} = {A68BA1A5-1604-433D-9778-DC0199831C2A} + {C1483B79-4FE9-47FF-B544-EB5DBB7A0A3E} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {F1387494-34DE-4B31-B587-699B2E9A20CA} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {54B66163-016D-4122-9364-409AB61BC36B} = {F1387494-34DE-4B31-B587-699B2E9A20CA} {FD63D574-8512-421D-B7FC-310AFA974361} = {F1387494-34DE-4B31-B587-699B2E9A20CA} diff --git a/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj new file mode 100644 index 0000000000..873cff51ce --- /dev/null +++ b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj @@ -0,0 +1,7 @@ + + + + $(NetCurrent) + + + diff --git a/tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs similarity index 67% rename from tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs index a54f9891c8..518d841bbd 100644 --- a/tests/Aspire.Hosting.Tests/Cosmos/CosmosFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.Cosmos; +namespace Aspire.EndToEnd.Tests.Cosmos; [Collection("IntegrationServices")] public class CosmosFunctionalTests @@ -16,15 +15,12 @@ public CosmosFunctionalTests(IntegrationServicesFixture integrationServicesFixtu _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyCosmosWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(6)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/cosmos/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/cosmos/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs new file mode 100644 index 0000000000..246a34c77a --- /dev/null +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -0,0 +1,118 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using Xunit; + +namespace Aspire.EndToEnd.Tests; + +/// +/// This fixture ensures the TestProject.AppHost application is started before a test is executed. +/// +/// Represents the the IntegrationServiceA project in the test application used to send HTTP requests +/// to the project's endpoints. +/// +/// +/// Use [Collection("IntegrationServices")] to inject this fixture in test constructors. +/// +public sealed class IntegrationServicesFixture : IAsyncLifetime +{ + private Process? _appHostProcess; + private Dictionary? _projects; + + public Dictionary Projects => _projects!; + + public ProjectInfo IntegrationServiceA => Projects["integrationservicea"]; + + public HttpClient HttpClient { get; } = new HttpClient(); + + public async Task InitializeAsync() + { + var appHostDirectory = Path.Combine(GetRepoRoot(), "tests", "testproject", "TestProject.AppHost"); + + var output = new StringBuilder(); + var appExited = new TaskCompletionSource(); + var appRunning = new TaskCompletionSource(); + var projectsParsed = new TaskCompletionSource(); + _appHostProcess = new Process(); + _appHostProcess.StartInfo = new ProcessStartInfo("dotnet", "run") + { + RedirectStandardOutput = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = appHostDirectory + }; + _appHostProcess.OutputDataReceived += (sender, e) => + { + output.AppendLine(e.Data); + + if (e.Data?.StartsWith("$ENDPOINTS: ") == true) + { + _projects = ParseProjectInfo(e.Data.Substring("$ENDPOINTS: ".Length)); + projectsParsed.SetResult(); + } + + if (e.Data?.Contains("Distributed application started") == true) + { + appRunning.SetResult(); + } + }; + EventHandler appExitedCallback = (sender, e) => appExited.SetResult(); + _appHostProcess.Exited += appExitedCallback; + + _appHostProcess.Start(); + _appHostProcess.BeginOutputReadLine(); + + var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); + var failedTask = appExited.Task; + + await Task.WhenAny(successfulTask, failedTask) + .ContinueWith(t => + { + Assert.True(successfulTask == t.Result, $"App run failed: {Environment.NewLine}{output}"); + }, TaskScheduler.Default) + .WaitAsync(TimeSpan.FromMinutes(5)); + + _appHostProcess.Exited -= appExitedCallback; + + foreach (var project in Projects.Values) + { + project.Client = HttpClient; + } + } + + private static Dictionary ParseProjectInfo(string json) => + JsonSerializer.Deserialize>(json)!; + + public async Task DisposeAsync() + { + if (_appHostProcess is not null) + { + _appHostProcess.StandardInput.WriteLine("Stop"); + await _appHostProcess.WaitForExitAsync(); + } + } + + private static string GetRepoRoot() + { + var directory = AppContext.BaseDirectory; + + while (directory != null && !Directory.Exists(Path.Combine(directory, ".git"))) + { + directory = Directory.GetParent(directory)!.FullName; + } + + return directory!; + } +} + +[CollectionDefinition("IntegrationServices")] +public class IntegrationServicesCollection : ICollectionFixture +{ + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. +} diff --git a/tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs similarity index 52% rename from tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs rename to tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 9ce5cb4445..8286557264 100644 --- a/tests/Aspire.Hosting.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests; +namespace Aspire.EndToEnd.Tests; [Collection("IntegrationServices")] public class IntegrationServicesTests @@ -16,22 +15,13 @@ public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFi _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact] + [Fact] public async Task VerifyHealthyOnIntegrationServiceA() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - // Make sure all services are running - await testProgram.ServiceABuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceBBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.ServiceCBuilder.HttpGetPidAsync(client, "http", cts.Token); - await testProgram.IntegrationServiceABuilder!.HttpGetPidAsync(client, "http", cts.Token); - // We wait until timeout for the /health endpoint to return successfully. We assume // that components wired up into this project have health checks enabled. - await testProgram.IntegrationServiceABuilder!.WaitForHealthyStatusAsync(client, "http", cts.Token); + await _integrationServicesFixture.IntegrationServiceA.WaitForHealthyStatusAsync("http", cts.Token); } } diff --git a/tests/Aspire.Hosting.Tests/Kafka/KafkaFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs similarity index 60% rename from tests/Aspire.Hosting.Tests/Kafka/KafkaFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs index 95247ea1df..660196942f 100644 --- a/tests/Aspire.Hosting.Tests/Kafka/KafkaFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs @@ -1,30 +1,25 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.Kafka; +namespace Aspire.EndToEnd.Tests.Kafka; [Collection("IntegrationServices")] public class KafkaFunctionalTests(IntegrationServicesFixture integrationServicesFixture) { - [LocalOnlyFact] + [Fact] public async Task KafkaComponentCanProduceAndConsume() { - var testProgram = integrationServicesFixture.TestProgram; - - var client = integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); string topic = $"topic-{Guid.NewGuid()}"; - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", $"/kafka/produce/{topic}", cts.Token); + var response = await integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/produce/{topic}", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); - response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", $"/kafka/consume/{topic}", cts.Token); + response = await integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/consume/{topic}", cts.Token); responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); } diff --git a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs similarity index 67% rename from tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs index 125a1570a0..962ba77396 100644 --- a/tests/Aspire.Hosting.Tests/MongoDB/MongoDBFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.MongoDB; +namespace Aspire.EndToEnd.Tests.MongoDB; [Collection("IntegrationServices")] public class MongoDBFunctionalTests @@ -16,15 +15,12 @@ public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixt _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyMongoWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mongodb/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mongodb/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs similarity index 74% rename from tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs index a61d177781..7d77957f30 100644 --- a/tests/Aspire.Hosting.Tests/MySql/MySqlFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.MySql; +namespace Aspire.EndToEnd.Tests.MySql; [Collection("IntegrationServices")] public class MySqlFunctionalTests @@ -16,18 +15,15 @@ public MySqlFunctionalTests(IntegrationServicesFixture integrationServicesFixtur _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyMySqlWorks() { // MySql health check reports healthy during temporary server phase, c.f. https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2031 // This is mitigated by standard resilience handlers in the IntegrationServicesFixture HttpClient configuration - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/mysql/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mysql/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs similarity index 68% rename from tests/Aspire.Hosting.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs index 62669e3a3c..338c389d00 100644 --- a/tests/Aspire.Hosting.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.MySql; +namespace Aspire.EndToEnd.Tests.MySql; [Collection("IntegrationServices")] public class PomeloEFCoreMySqlFunctionalTests @@ -16,15 +15,12 @@ public PomeloEFCoreMySqlFunctionalTests(IntegrationServicesFixture integrationSe _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyPomeloEFCoreMySqlWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/pomelo/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/pomelo/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs similarity index 68% rename from tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs index 570cbf805a..2170e50c75 100644 --- a/tests/Aspire.Hosting.Tests/Oracle/OracleDatabaseFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.Oracle; +namespace Aspire.EndToEnd.Tests.Oracle; [Collection("IntegrationServices")] public class OracleDatabaseFunctionalTests @@ -16,15 +15,12 @@ public OracleDatabaseFunctionalTests(IntegrationServicesFixture integrationServi _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyOracleDatabaseWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/oracledatabase/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/oracledatabase/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs similarity index 68% rename from tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs index 27e83a0461..9c79b5dc2e 100644 --- a/tests/Aspire.Hosting.Tests/Postgres/PostgresFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.Postgres; +namespace Aspire.EndToEnd.Tests.Postgres; [Collection("IntegrationServices")] public class PostgresFunctionalTests @@ -16,15 +15,12 @@ public PostgresFunctionalTests(IntegrationServicesFixture integrationServicesFix _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyPostgresWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/postgres/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/postgres/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs new file mode 100644 index 0000000000..8a92f7cd74 --- /dev/null +++ b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Aspire.EndToEnd.Tests; + +public sealed class ProjectInfo +{ + [JsonIgnore] + public HttpClient Client { get; set; } = default!; + + public EndpointInfo[] Endpoints { get; set; } = default!; + + /// + /// Sends a GET request to the specified resource and returns the response message. + /// + public Task HttpGetAsync(string bindingName, string path, CancellationToken cancellationToken) + { + var allocatedEndpoint = Endpoints.Single(e => e.Name == bindingName); + var url = $"{allocatedEndpoint.Uri}{path}"; + + return Client.GetAsync(url, cancellationToken); + } + + /// + /// Sends a GET request to the specified resource and returns the response body as a string. + /// + public Task HttpGetStringAsync(string bindingName, string path, CancellationToken cancellationToken) + { + var allocatedEndpoint = Endpoints.Single(e => e.Name == bindingName); + var url = $"{allocatedEndpoint.Uri}{path}"; + + return Client.GetStringAsync(url, cancellationToken); + } + + public async Task WaitForHealthyStatusAsync(string bindingName, CancellationToken cancellationToken) + { + while (true) + { + try + { + await HttpGetStringAsync(bindingName, "/health", cancellationToken); + return; + } + catch + { + await Task.Delay(100, cancellationToken); + } + } + } +} + +public record EndpointInfo(string Name, string Uri); diff --git a/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs similarity index 68% rename from tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs index 80a99549fe..6c08cfc731 100644 --- a/tests/Aspire.Hosting.Tests/RabbitMQ/RabbitMQFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.RabbitMQ; +namespace Aspire.EndToEnd.Tests.RabbitMQ; [Collection("IntegrationServices")] public class RabbitMQFunctionalTests @@ -16,15 +15,12 @@ public RabbitMQFunctionalTests(IntegrationServicesFixture integrationServicesFix _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyRabbitMQWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/rabbitmq/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/rabbitmq/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs similarity index 67% rename from tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs index 0c1a292d7b..56db34db0a 100644 --- a/tests/Aspire.Hosting.Tests/Redis/RedisFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.Redis; +namespace Aspire.EndToEnd.Tests.Redis; [Collection("IntegrationServices")] public class RedisFunctionalTests @@ -16,15 +15,12 @@ public RedisFunctionalTests(IntegrationServicesFixture integrationServicesFixtur _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifyRedisWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/redis/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/redis/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs similarity index 68% rename from tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs rename to tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs index 8120e87941..5540496c0e 100644 --- a/tests/Aspire.Hosting.Tests/SqlServer/SqlServerFunctionalTests.cs +++ b/tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Aspire.Hosting.Tests.Helpers; using Xunit; -namespace Aspire.Hosting.Tests.SqlServer; +namespace Aspire.EndToEnd.Tests.SqlServer; [Collection("IntegrationServices")] public class SqlServerFunctionalTests @@ -16,15 +15,12 @@ public SqlServerFunctionalTests(IntegrationServicesFixture integrationServicesFi _integrationServicesFixture = integrationServicesFixture; } - [LocalOnlyFact()] + [Fact] public async Task VerifySqlServerWorks() { - var testProgram = _integrationServicesFixture.TestProgram; - var client = _integrationServicesFixture.HttpClient; - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await testProgram.IntegrationServiceABuilder!.HttpGetAsync(client, "http", "/sqlserver/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/sqlserver/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); diff --git a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs index e536af87a9..a3cb4bf0a4 100644 --- a/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs +++ b/tests/Aspire.Hosting.Tests/Helpers/AllocatedEndpointAnnotationTestExtensions.cs @@ -25,52 +25,6 @@ public static async Task HttpGetStringAsync(this IResourceBuilder return response; } - /// - /// Sends a GET request to the specified resource and returns the response message. - /// - /// The type of the resource. - /// The resource. - /// The instance to use. - /// The name of the binding. - /// The path the request is sent to. - /// The cancellation token to cancel the operation. - /// The response message. - public static async Task HttpGetAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, CancellationToken cancellationToken) - where T : IResourceWithEndpoints - { - var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); - var url = $"{allocatedEndpoint.UriString}{path}"; - - var response = await client.GetAsync(url, cancellationToken); - return response; - } - - /// - /// Sends a POST request to the specified resource and returns the response message. - /// - /// The type of the resource. - /// The resource. - /// The instance to use. - /// The name of the binding. - /// The path the request is sent to. - /// The HTTP request content sent to the server. - /// The cancellation token to cancel the operation. - /// The response message. - public static async Task HttpPostAsync(this IResourceBuilder builder, HttpClient client, string bindingName, string path, HttpContent? content, CancellationToken cancellationToken) - where T : IResourceWithEndpoints - { - var allocatedEndpoint = builder.Resource.Annotations.OfType().Single(a => a.Name == bindingName); - var url = $"{allocatedEndpoint.UriString}{path}"; - - var response = await client.PostAsync(url, content, cancellationToken); - return response; - } - - public static Task WaitForHealthyStatusAsync(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) - { - return HttpGetStringWithRetryAsync(builder, client, bindingName, "/health", cancellationToken); - } - public static Task HttpGetPidAsync(this IResourceBuilder builder, HttpClient client, string bindingName, CancellationToken cancellationToken) where T : IResourceWithEndpoints { diff --git a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs index fcad90b853..d9593bdc01 100644 --- a/tests/Aspire.Hosting.Tests/TestProgramFixture.cs +++ b/tests/Aspire.Hosting.Tests/TestProgramFixture.cs @@ -89,46 +89,6 @@ public override async Task WaitReadyStateAsync(CancellationToken cancellationTok } } -/// -/// TestProgram with integration services but no dashboard or node app. -/// -/// -/// Use [Collection("IntegrationServices")] to inject this fixture in test constructors. -/// -public class IntegrationServicesFixture : TestProgramFixture -{ - public override TestProgram CreateTestProgram() - { - var testProgram = TestProgram.Create(includeIntegrationServices: true); - - testProgram.AppBuilder.Services - .AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.UseSocketsHttpHandler((handler, sp) => - { - handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5); - handler.ConnectTimeout = TimeSpan.FromSeconds(5); - }); - - // Ensure transient errors are retried for up to 5 minutes - b.AddStandardResilienceHandler(options => - { - options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(1); - options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(2); // needs to be at least double the AttemptTimeout to pass options validation - options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(5); - }); - }); - - return testProgram; - } - - public override Task WaitReadyStateAsync(CancellationToken cancellationToken = default) - { - return TestProgram.IntegrationServiceABuilder!.HttpGetPidAsync(HttpClient, "http", cancellationToken); - } -} - /// /// TestProgram with node app but no dashboard or integration services. /// @@ -165,14 +125,6 @@ public class SlimTestProgramCollection : ICollectionFixture interfaces. } -[CollectionDefinition("IntegrationServices")] -public class IntegrationServicesCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} - [CollectionDefinition("NodeApp")] public class NodeJsCollection : ICollectionFixture { diff --git a/tests/testproject/TestProject.AppHost/Program.cs b/tests/testproject/TestProject.AppHost/Program.cs index 8b5296cae0..413c25fe15 100644 --- a/tests/testproject/TestProject.AppHost/Program.cs +++ b/tests/testproject/TestProject.AppHost/Program.cs @@ -2,4 +2,19 @@ // The .NET Foundation licenses this file to you under the MIT license. var testProgram = TestProgram.Create(args, includeIntegrationServices: true, disableDashboard: false, includeNodeApp: true); + +// Run a task to read from the console and stop the app if an external process sends "Stop". +// This allows for easier control than sending CTRL+C to the console in a cross-platform way. +_ = Task.Run(async () => +{ + var s = Console.ReadLine(); + if (s == "Stop") + { + if (testProgram.App is not null) + { + await testProgram.App.StopAsync(); + } + } +}); + await testProgram.RunAsync(); diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 011809c56f..27af76ca53 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Reflection; +using System.Text.Json; +using System.Text.Json.Nodes; +using Aspire.Hosting.Lifecycle; public class TestProgram { @@ -66,6 +69,8 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer .WithReference(kafka) .WithReference(cosmos); } + + AppBuilder.Services.TryAddLifecycleHook(); } public static TestProgram Create(string[]? args = null, bool includeIntegrationServices = false, bool includeNodeApp = false, bool disableDashboard = true) => @@ -103,5 +108,36 @@ public void Run() Build(); App!.Run(); } + + /// + /// Writes the allocated endpoints to the console in JSON format. + /// This allows for easier consumption by the external test process. + /// + private sealed class EndPointWriterHook : IDistributedApplicationLifecycleHook + { + public async Task AfterEndpointsAllocatedAsync(DistributedApplicationModel appModel, CancellationToken cancellationToken) + { + var root = new JsonObject(); + foreach (var project in appModel.Resources.OfType()) + { + var projectJson = new JsonObject(); + root[project.Name] = projectJson; + + var endpointsJsonArray = new JsonArray(); + projectJson["Endpoints"] = endpointsJsonArray; + + foreach (var endpoint in project.Annotations.OfType()) + { + var endpointJsonObject = new JsonObject(); + endpointJsonObject["Name"] = endpoint.Name; + endpointJsonObject["Uri"] = endpoint.UriString; + endpointsJsonArray.Add(endpointJsonObject); + } + } + + // write the whole json in a single line so it's easier to parse by the external process + await Console.Out.WriteLineAsync("$ENDPOINTS: " + JsonSerializer.Serialize(root, JsonSerializerOptions.Default)); + } + } } diff --git a/tests/testproject/TestProject.AppHost/appsettings.json b/tests/testproject/TestProject.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/tests/testproject/TestProject.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} From 01ee0d61a889352b06888015d5010ea5b67f56aa Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 14:29:50 -0600 Subject: [PATCH 02/10] Collapse integration tests into one class. --- .../Cosmos/CosmosFunctionalTests.cs | 28 ---- .../IntegrationServicesFixture.cs | 11 -- .../IntegrationServicesTests.cs | 121 +++++++++++++++++- .../Kafka/KafkaFunctionalTests.cs | 26 ---- .../MongoDB/MongoDBFunctionalTests.cs | 28 ---- .../MySql/MySqlFunctionalTests.cs | 31 ----- .../MySql/PomeloEFCoreMySqlFunctionalTests.cs | 28 ---- .../Oracle/OracleDatabaseFunctionalTests.cs | 28 ---- .../Postgres/PostgresFunctionalTests.cs | 28 ---- .../RabbitMQ/RabbitMQFunctionalTests.cs | 28 ---- .../Redis/RedisFunctionalTests.cs | 28 ---- .../SqlServer/SqlServerFunctionalTests.cs | 28 ---- 12 files changed, 119 insertions(+), 294 deletions(-) delete mode 100644 tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs delete mode 100644 tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs diff --git a/tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs deleted file mode 100644 index 518d841bbd..0000000000 --- a/tests/Aspire.EndToEnd.Tests/Cosmos/CosmosFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.Cosmos; - -[Collection("IntegrationServices")] -public class CosmosFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public CosmosFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyCosmosWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(6)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/cosmos/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 246a34c77a..02d3edece9 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -14,9 +14,6 @@ namespace Aspire.EndToEnd.Tests; /// Represents the the IntegrationServiceA project in the test application used to send HTTP requests /// to the project's endpoints. /// -/// -/// Use [Collection("IntegrationServices")] to inject this fixture in test constructors. -/// public sealed class IntegrationServicesFixture : IAsyncLifetime { private Process? _appHostProcess; @@ -108,11 +105,3 @@ private static string GetRepoRoot() return directory!; } } - -[CollectionDefinition("IntegrationServices")] -public class IntegrationServicesCollection : ICollectionFixture -{ - // This class has no code, and is never created. Its purpose is simply - // to be the place to apply [CollectionDefinition] and all the - // ICollectionFixture<> interfaces. -} diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 8286557264..c778607580 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -5,8 +5,7 @@ namespace Aspire.EndToEnd.Tests; -[Collection("IntegrationServices")] -public class IntegrationServicesTests +public class IntegrationServicesTests : IClassFixture { private readonly IntegrationServicesFixture _integrationServicesFixture; @@ -15,6 +14,124 @@ public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFi _integrationServicesFixture = integrationServicesFixture; } + [Fact] + public async Task VerifyCosmosWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(6)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/cosmos/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task KafkaComponentCanProduceAndConsume() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + string topic = $"topic-{Guid.NewGuid()}"; + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/produce/{topic}", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode, responseContent); + + response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/consume/{topic}", cts.Token); + responseContent = await response.Content.ReadAsStringAsync(); + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyMongoWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mongodb/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyMySqlWorks() + { + // MySql health check reports healthy during temporary server phase, c.f. https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2031 + // This is mitigated by standard resilience handlers in the IntegrationServicesFixture HttpClient configuration + + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mysql/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyPomeloEFCoreMySqlWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/pomelo/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyOracleDatabaseWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/oracledatabase/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyPostgresWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/postgres/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyRabbitMQWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/rabbitmq/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifyRedisWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/redis/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + + [Fact] + public async Task VerifySqlServerWorks() + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); + + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/sqlserver/verify", cts.Token); + var responseContent = await response.Content.ReadAsStringAsync(); + + Assert.True(response.IsSuccessStatusCode, responseContent); + } + [Fact] public async Task VerifyHealthyOnIntegrationServiceA() { diff --git a/tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs deleted file mode 100644 index 660196942f..0000000000 --- a/tests/Aspire.EndToEnd.Tests/Kafka/KafkaFunctionalTests.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.Kafka; - -[Collection("IntegrationServices")] -public class KafkaFunctionalTests(IntegrationServicesFixture integrationServicesFixture) -{ - [Fact] - public async Task KafkaComponentCanProduceAndConsume() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - string topic = $"topic-{Guid.NewGuid()}"; - - var response = await integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/produce/{topic}", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode, responseContent); - - response = await integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/consume/{topic}", cts.Token); - responseContent = await response.Content.ReadAsStringAsync(); - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs deleted file mode 100644 index 962ba77396..0000000000 --- a/tests/Aspire.EndToEnd.Tests/MongoDB/MongoDBFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.MongoDB; - -[Collection("IntegrationServices")] -public class MongoDBFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public MongoDBFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyMongoWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mongodb/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs deleted file mode 100644 index 7d77957f30..0000000000 --- a/tests/Aspire.EndToEnd.Tests/MySql/MySqlFunctionalTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.MySql; - -[Collection("IntegrationServices")] -public class MySqlFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public MySqlFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyMySqlWorks() - { - // MySql health check reports healthy during temporary server phase, c.f. https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2031 - // This is mitigated by standard resilience handlers in the IntegrationServicesFixture HttpClient configuration - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mysql/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs deleted file mode 100644 index 338c389d00..0000000000 --- a/tests/Aspire.EndToEnd.Tests/MySql/PomeloEFCoreMySqlFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.MySql; - -[Collection("IntegrationServices")] -public class PomeloEFCoreMySqlFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public PomeloEFCoreMySqlFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyPomeloEFCoreMySqlWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/pomelo/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs deleted file mode 100644 index 2170e50c75..0000000000 --- a/tests/Aspire.EndToEnd.Tests/Oracle/OracleDatabaseFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.Oracle; - -[Collection("IntegrationServices")] -public class OracleDatabaseFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public OracleDatabaseFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyOracleDatabaseWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/oracledatabase/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs deleted file mode 100644 index 9c79b5dc2e..0000000000 --- a/tests/Aspire.EndToEnd.Tests/Postgres/PostgresFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.Postgres; - -[Collection("IntegrationServices")] -public class PostgresFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public PostgresFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyPostgresWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/postgres/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs deleted file mode 100644 index 6c08cfc731..0000000000 --- a/tests/Aspire.EndToEnd.Tests/RabbitMQ/RabbitMQFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.RabbitMQ; - -[Collection("IntegrationServices")] -public class RabbitMQFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public RabbitMQFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyRabbitMQWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/rabbitmq/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs deleted file mode 100644 index 56db34db0a..0000000000 --- a/tests/Aspire.EndToEnd.Tests/Redis/RedisFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.Redis; - -[Collection("IntegrationServices")] -public class RedisFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public RedisFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifyRedisWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/redis/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} diff --git a/tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs b/tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs deleted file mode 100644 index 5540496c0e..0000000000 --- a/tests/Aspire.EndToEnd.Tests/SqlServer/SqlServerFunctionalTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Xunit; - -namespace Aspire.EndToEnd.Tests.SqlServer; - -[Collection("IntegrationServices")] -public class SqlServerFunctionalTests -{ - private readonly IntegrationServicesFixture _integrationServicesFixture; - - public SqlServerFunctionalTests(IntegrationServicesFixture integrationServicesFixture) - { - _integrationServicesFixture = integrationServicesFixture; - } - - [Fact] - public async Task VerifySqlServerWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/sqlserver/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } -} From c121a682ff69be718be4e10a2f89919f8e55b107 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 14:46:32 -0600 Subject: [PATCH 03/10] Make common tests into a theory --- .../IntegrationServicesTests.cs | 108 +++--------------- 1 file changed, 13 insertions(+), 95 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index c778607580..5601c87b99 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -14,12 +14,21 @@ public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFi _integrationServicesFixture = integrationServicesFixture; } - [Fact] - public async Task VerifyCosmosWorks() + [Theory] + [InlineData("cosmos")] + [InlineData("mongodb")] + [InlineData("mysql")] + [InlineData("pomelo")] + [InlineData("oracledatabase")] + [InlineData("postgres")] + [InlineData("rabbitmq")] + [InlineData("redis")] + [InlineData("sqlserver")] + public async Task VerifyComponentWorks(string component) { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(6)); + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/cosmos/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/{component}/verify", cts.Token); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); @@ -41,97 +50,6 @@ public async Task KafkaComponentCanProduceAndConsume() Assert.True(response.IsSuccessStatusCode, responseContent); } - [Fact] - public async Task VerifyMongoWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mongodb/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyMySqlWorks() - { - // MySql health check reports healthy during temporary server phase, c.f. https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks/issues/2031 - // This is mitigated by standard resilience handlers in the IntegrationServicesFixture HttpClient configuration - - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/mysql/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyPomeloEFCoreMySqlWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/pomelo/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyOracleDatabaseWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/oracledatabase/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyPostgresWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/postgres/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyRabbitMQWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/rabbitmq/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifyRedisWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/redis/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - - [Fact] - public async Task VerifySqlServerWorks() - { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", "/sqlserver/verify", cts.Token); - var responseContent = await response.Content.ReadAsStringAsync(); - - Assert.True(response.IsSuccessStatusCode, responseContent); - } - [Fact] public async Task VerifyHealthyOnIntegrationServiceA() { From cb3b408e4dd61782363b1e20826bad7a4e9ce632 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 15:06:40 -0600 Subject: [PATCH 04/10] Remove timeouts from tests. The tests will run for as long as they need to. The larger timeout will handle any real hangs. Also handle retries in Cosmos. --- .../IntegrationServicesTests.cs | 14 ++++---------- tests/Aspire.EndToEnd.Tests/ProjectInfo.cs | 6 +++--- .../Cosmos/CosmosExtensions.cs | 10 +++++++++- .../MySql/MySqlExtensions.cs | 2 +- .../Oracle/OracleDatabaseExtensions.cs | 2 +- .../SqlServer/SqlServerExtensions.cs | 2 +- 6 files changed, 19 insertions(+), 17 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 5601c87b99..afc3ce93f7 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -26,9 +26,7 @@ public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFi [InlineData("sqlserver")] public async Task VerifyComponentWorks(string component) { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/{component}/verify", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/{component}/verify"); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); @@ -37,15 +35,13 @@ public async Task VerifyComponentWorks(string component) [Fact] public async Task KafkaComponentCanProduceAndConsume() { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - string topic = $"topic-{Guid.NewGuid()}"; - var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/produce/{topic}", cts.Token); + var response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/produce/{topic}"); var responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); - response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/consume/{topic}", cts.Token); + response = await _integrationServicesFixture.IntegrationServiceA.HttpGetAsync("http", $"/kafka/consume/{topic}"); responseContent = await response.Content.ReadAsStringAsync(); Assert.True(response.IsSuccessStatusCode, responseContent); } @@ -53,10 +49,8 @@ public async Task KafkaComponentCanProduceAndConsume() [Fact] public async Task VerifyHealthyOnIntegrationServiceA() { - using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(1)); - // We wait until timeout for the /health endpoint to return successfully. We assume // that components wired up into this project have health checks enabled. - await _integrationServicesFixture.IntegrationServiceA.WaitForHealthyStatusAsync("http", cts.Token); + await _integrationServicesFixture.IntegrationServiceA.WaitForHealthyStatusAsync("http"); } } diff --git a/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs index 8a92f7cd74..30ecacd215 100644 --- a/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs +++ b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs @@ -15,7 +15,7 @@ public sealed class ProjectInfo /// /// Sends a GET request to the specified resource and returns the response message. /// - public Task HttpGetAsync(string bindingName, string path, CancellationToken cancellationToken) + public Task HttpGetAsync(string bindingName, string path, CancellationToken cancellationToken = default) { var allocatedEndpoint = Endpoints.Single(e => e.Name == bindingName); var url = $"{allocatedEndpoint.Uri}{path}"; @@ -26,7 +26,7 @@ public Task HttpGetAsync(string bindingName, string path, C /// /// Sends a GET request to the specified resource and returns the response body as a string. /// - public Task HttpGetStringAsync(string bindingName, string path, CancellationToken cancellationToken) + public Task HttpGetStringAsync(string bindingName, string path, CancellationToken cancellationToken = default) { var allocatedEndpoint = Endpoints.Single(e => e.Name == bindingName); var url = $"{allocatedEndpoint.Uri}{path}"; @@ -34,7 +34,7 @@ public Task HttpGetStringAsync(string bindingName, string path, Cancella return Client.GetStringAsync(url, cancellationToken); } - public async Task WaitForHealthyStatusAsync(string bindingName, CancellationToken cancellationToken) + public async Task WaitForHealthyStatusAsync(string bindingName, CancellationToken cancellationToken = default) { while (true) { diff --git a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs index c834afb0f3..96dfb1d888 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Cosmos/CosmosExtensions.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.Azure.Cosmos; +using Polly; public static class CosmosExtensions { @@ -14,7 +15,14 @@ private static async Task VerifyCosmosAsync(CosmosClient cosmosClient) { try { - var db = (await cosmosClient.CreateDatabaseIfNotExistsAsync("db")).Database; + var policy = Policy + .Handle() + // retry 60 times with a 1 second delay between retries + .WaitAndRetryAsync(60, retryAttempt => TimeSpan.FromSeconds(1)); + + var db = await policy.ExecuteAsync( + async () => (await cosmosClient.CreateDatabaseIfNotExistsAsync("db")).Database); + var container = (await db.CreateContainerIfNotExistsAsync("todos", "/id")).Container; var id = Guid.NewGuid().ToString(); diff --git a/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs index ee8a921f57..a0758878dd 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/MySql/MySqlExtensions.cs @@ -17,7 +17,7 @@ private static async Task VerifyMySqlAsync(MySqlConnection connection) { var policy = Policy .Handle() - // retry every second for 60 seconds + // retry 60 times with a 1 second delay between retries .WaitAndRetryAsync(60, retryAttempt => TimeSpan.FromSeconds(1)); await policy.ExecuteAsync(connection.OpenAsync); diff --git a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs index c27394f5ae..918fa317e7 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/Oracle/OracleDatabaseExtensions.cs @@ -18,7 +18,7 @@ private static IResult VerifyOracleDatabase(MyDbContext context) { var policy = Policy .Handle() - // retry every second for 60 seconds + // retry 60 times with a 1 second delay between retries .WaitAndRetry(60, retryAttempt => TimeSpan.FromSeconds(1)); return policy.Execute(() => diff --git a/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs index 755760adcf..eacc44213f 100644 --- a/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs +++ b/tests/testproject/TestProject.IntegrationServiceA/SqlServer/SqlServerExtensions.cs @@ -17,7 +17,7 @@ private static async Task VerifySqlServerAsync(SqlConnection connection { var policy = Policy .Handle() - // retry every second for 60 seconds + // retry 60 times with a 1 second delay between retries .WaitAndRetryAsync(60, retryAttempt => TimeSpan.FromSeconds(1)); await policy.ExecuteAsync(connection.OpenAsync); From 718b06e775c2bd9b2891a69b489d717895377ff8 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 15:12:47 -0600 Subject: [PATCH 05/10] Write output when running the AppHost times out. --- .../IntegrationServicesFixture.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 02d3edece9..72bda85d4f 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -66,12 +66,19 @@ public async Task InitializeAsync() var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); var failedTask = appExited.Task; - await Task.WhenAny(successfulTask, failedTask) - .ContinueWith(t => - { - Assert.True(successfulTask == t.Result, $"App run failed: {Environment.NewLine}{output}"); - }, TaskScheduler.Default) - .WaitAsync(TimeSpan.FromMinutes(5)); + try + { + await Task.WhenAny(successfulTask, failedTask) + .ContinueWith(t => + { + Assert.True(successfulTask == t.Result, $"App run failed: {Environment.NewLine}{output}"); + }, TaskScheduler.Default) + .WaitAsync(TimeSpan.FromMinutes(5)); + } + catch (TimeoutException) + { + Assert.Fail($"Running the TestProject.AppHost timed out: {Environment.NewLine}{output}"); + } _appHostProcess.Exited -= appExitedCallback; From 504f4eaef85ead0e8281ba6eec626d1da119cc25 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 16:58:38 -0600 Subject: [PATCH 06/10] Disable the TestProject's dashboard during test run. Also set up resilience handler for the HttpClient. --- .../Aspire.EndToEnd.Tests.csproj | 4 +++ .../IntegrationServicesFixture.cs | 32 ++++++++++++++++--- .../TestProject.AppHost/TestProgram.cs | 9 ++++-- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj index 873cff51ce..2010289494 100644 --- a/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj +++ b/tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.Tests.csproj @@ -4,4 +4,8 @@ $(NetCurrent) + + + + diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 72bda85d4f..495aff9469 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Text; using System.Text.Json; +using Microsoft.Extensions.DependencyInjection; using Xunit; namespace Aspire.EndToEnd.Tests; @@ -23,8 +24,6 @@ public sealed class IntegrationServicesFixture : IAsyncLifetime public ProjectInfo IntegrationServiceA => Projects["integrationservicea"]; - public HttpClient HttpClient { get; } = new HttpClient(); - public async Task InitializeAsync() { var appHostDirectory = Path.Combine(GetRepoRoot(), "tests", "testproject", "TestProject.AppHost"); @@ -34,7 +33,7 @@ public async Task InitializeAsync() var appRunning = new TaskCompletionSource(); var projectsParsed = new TaskCompletionSource(); _appHostProcess = new Process(); - _appHostProcess.StartInfo = new ProcessStartInfo("dotnet", "run") + _appHostProcess.StartInfo = new ProcessStartInfo("dotnet", "run -- --disable-dashboard") { RedirectStandardOutput = true, RedirectStandardInput = true, @@ -82,12 +81,37 @@ await Task.WhenAny(successfulTask, failedTask) _appHostProcess.Exited -= appExitedCallback; + var client = CreateHttpClient(); foreach (var project in Projects.Values) { - project.Client = HttpClient; + project.Client = client; } } + private static HttpClient CreateHttpClient() + { + var services = new ServiceCollection(); + services.AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.UseSocketsHttpHandler((handler, sp) => + { + handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5); + handler.ConnectTimeout = TimeSpan.FromSeconds(5); + }); + + // Ensure transient errors are retried for up to 5 minutes + b.AddStandardResilienceHandler(options => + { + options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(1); + options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(2); // needs to be at least double the AttemptTimeout to pass options validation + options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(5); + }); + }); + + return services.BuildServiceProvider().GetRequiredService().CreateClient(); + } + private static Dictionary ParseProjectInfo(string json) => JsonSerializer.Deserialize>(json)!; diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 27af76ca53..3819a6a242 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -8,8 +8,13 @@ public class TestProgram { - private TestProgram(string[] args, Assembly assembly, bool includeIntegrationServices = false, bool disableDashboard = true, bool includeNodeApp = false) + private TestProgram(string[] args, Assembly assembly, bool includeIntegrationServices, bool includeNodeApp, bool disableDashboard) { + if (args.Contains("--disable-dashboard")) + { + disableDashboard = true; + } + AppBuilder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { Args = args, DisableDashboard = disableDashboard, AssemblyName = assembly.FullName }); var serviceAPath = Path.Combine(Projects.TestProject_AppHost.ProjectPath, @"..\TestProject.ServiceA\TestProject.ServiceA.csproj"); @@ -74,7 +79,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer } public static TestProgram Create(string[]? args = null, bool includeIntegrationServices = false, bool includeNodeApp = false, bool disableDashboard = true) => - new TestProgram(args ?? [], typeof(T).Assembly, includeIntegrationServices, disableDashboard, includeNodeApp: includeNodeApp); + new TestProgram(args ?? [], typeof(T).Assembly, includeIntegrationServices, includeNodeApp, disableDashboard); public IDistributedApplicationBuilder AppBuilder { get; private set; } public IResourceBuilder ServiceABuilder { get; private set; } From 20c3091c385adec56b5ed021baa1f0146c8bbd3c Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 17:45:45 -0600 Subject: [PATCH 07/10] Log stderr and handle waiting better. Also set EnableRaisingEvents so Exited fires. --- .../IntegrationServicesFixture.cs | 47 ++++++++++++++----- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 495aff9469..74997ed5a1 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -30,12 +30,15 @@ public async Task InitializeAsync() var output = new StringBuilder(); var appExited = new TaskCompletionSource(); - var appRunning = new TaskCompletionSource(); var projectsParsed = new TaskCompletionSource(); + var appRunning = new TaskCompletionSource(); + var stdoutComplete = new TaskCompletionSource(); + var stderrComplete = new TaskCompletionSource(); _appHostProcess = new Process(); _appHostProcess.StartInfo = new ProcessStartInfo("dotnet", "run -- --disable-dashboard") { RedirectStandardOutput = true, + RedirectStandardError = true, RedirectStandardInput = true, UseShellExecute = false, CreateNoWindow = true, @@ -43,6 +46,12 @@ public async Task InitializeAsync() }; _appHostProcess.OutputDataReceived += (sender, e) => { + if (e.Data is null) + { + stdoutComplete.SetResult(); + return; + } + output.AppendLine(e.Data); if (e.Data?.StartsWith("$ENDPOINTS: ") == true) @@ -56,28 +65,37 @@ public async Task InitializeAsync() appRunning.SetResult(); } }; + _appHostProcess.ErrorDataReceived += (sender, e) => + { + if (e.Data is null) + { + stderrComplete.SetResult(); + return; + } + + output.AppendLine(e.Data); + }; + EventHandler appExitedCallback = (sender, e) => appExited.SetResult(); + _appHostProcess.EnableRaisingEvents = true; _appHostProcess.Exited += appExitedCallback; _appHostProcess.Start(); _appHostProcess.BeginOutputReadLine(); + _appHostProcess.BeginErrorReadLine(); var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); var failedTask = appExited.Task; + var timeoutTask = Task.Delay(TimeSpan.FromMinutes(5)); - try - { - await Task.WhenAny(successfulTask, failedTask) - .ContinueWith(t => - { - Assert.True(successfulTask == t.Result, $"App run failed: {Environment.NewLine}{output}"); - }, TaskScheduler.Default) - .WaitAsync(TimeSpan.FromMinutes(5)); - } - catch (TimeoutException) + var resultTask = await Task.WhenAny(successfulTask, failedTask, timeoutTask); + if (resultTask == failedTask) { - Assert.Fail($"Running the TestProject.AppHost timed out: {Environment.NewLine}{output}"); + // wait for all the output to be read + var allOutputComplete = Task.WhenAll(stdoutComplete.Task, stderrComplete.Task); + await Task.WhenAny(allOutputComplete, timeoutTask); } + Assert.True(resultTask == successfulTask, $"App run failed: {Environment.NewLine}{output}"); _appHostProcess.Exited -= appExitedCallback; @@ -119,7 +137,10 @@ public async Task DisposeAsync() { if (_appHostProcess is not null) { - _appHostProcess.StandardInput.WriteLine("Stop"); + if (!_appHostProcess.HasExited) + { + _appHostProcess.StandardInput.WriteLine("Stop"); + } await _appHostProcess.WaitForExitAsync(); } } From e4bf95c44363ef2cc5da6176e91fe59e33170538 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Fri, 2 Feb 2024 21:57:45 -0600 Subject: [PATCH 08/10] Make EndToEnd tests local-only Until they can be run in Helix --- .../IntegrationServicesTests.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index afc3ce93f7..36852f2ba9 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -14,7 +14,7 @@ public IntegrationServicesTests(IntegrationServicesFixture integrationServicesFi _integrationServicesFixture = integrationServicesFixture; } - [Theory] + [LocalOnlyTheory] [InlineData("cosmos")] [InlineData("mongodb")] [InlineData("mysql")] @@ -32,7 +32,7 @@ public async Task VerifyComponentWorks(string component) Assert.True(response.IsSuccessStatusCode, responseContent); } - [Fact] + [LocalOnlyFact] public async Task KafkaComponentCanProduceAndConsume() { string topic = $"topic-{Guid.NewGuid()}"; @@ -46,7 +46,7 @@ public async Task KafkaComponentCanProduceAndConsume() Assert.True(response.IsSuccessStatusCode, responseContent); } - [Fact] + [LocalOnlyFact] public async Task VerifyHealthyOnIntegrationServiceA() { // We wait until timeout for the /health endpoint to return successfully. We assume @@ -54,3 +54,41 @@ public async Task VerifyHealthyOnIntegrationServiceA() await _integrationServicesFixture.IntegrationServiceA.WaitForHealthyStatusAsync("http"); } } + +// TODO: remove these attributes when the above tests are running in CI + +public class LocalOnlyFactAttribute : FactAttribute +{ + public override string Skip + { + get + { + // BUILD_BUILDID is defined by Azure Dev Ops + + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) + { + return "LocalOnlyFactAttribute tests are not run as part of CI."; + } + + return null!; + } + } +} + +public class LocalOnlyTheoryAttribute : TheoryAttribute +{ + public override string Skip + { + get + { + // BUILD_BUILDID is defined by Azure Dev Ops + + if (Environment.GetEnvironmentVariable("BUILD_BUILDID") != null) + { + return "LocalOnlyTheoryAttribute tests are not run as part of CI."; + } + + return null!; + } + } +} From 5d6b911132016fd608b6e08f9b65dc0f1975b7aa Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 5 Feb 2024 11:39:48 -0600 Subject: [PATCH 09/10] Check for Healthy. --- tests/Aspire.EndToEnd.Tests/ProjectInfo.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs index 30ecacd215..109e342bc7 100644 --- a/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs +++ b/tests/Aspire.EndToEnd.Tests/ProjectInfo.cs @@ -40,8 +40,11 @@ public async Task WaitForHealthyStatusAsync(string bindingName, CancellationToke { try { - await HttpGetStringAsync(bindingName, "/health", cancellationToken); - return; + var status = await HttpGetStringAsync(bindingName, "/health", cancellationToken); + if (status == "Healthy") + { + return; + } } catch { From 68023ec3a4221681108b5da075ed94e705b4112d Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Mon, 5 Feb 2024 15:23:11 -0600 Subject: [PATCH 10/10] Drop "try" when adding lifecycle hook. --- tests/testproject/TestProject.AppHost/TestProgram.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/testproject/TestProject.AppHost/TestProgram.cs b/tests/testproject/TestProject.AppHost/TestProgram.cs index 3819a6a242..86cf040604 100644 --- a/tests/testproject/TestProject.AppHost/TestProgram.cs +++ b/tests/testproject/TestProject.AppHost/TestProgram.cs @@ -75,7 +75,7 @@ private TestProgram(string[] args, Assembly assembly, bool includeIntegrationSer .WithReference(cosmos); } - AppBuilder.Services.TryAddLifecycleHook(); + AppBuilder.Services.AddLifecycleHook(); } public static TestProgram Create(string[]? args = null, bool includeIntegrationServices = false, bool includeNodeApp = false, bool disableDashboard = true) =>