From a7f23ec23cb810adff8c44969be7fcb051b6b871 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Fri, 9 Aug 2024 14:28:06 -0400 Subject: [PATCH] [tests] Add `Aspire.Playground.Tests` (#5208) Add wrapper tests for apps in `playground/`. - The tests are based on https://github.com/dotnet/aspire-samples/blob/aee52a7a08ca3433bd1e54e8a90a7febdc852ebd/tests/SamplesIntegrationTests/AppHostTests.cs . - And test infrastructure from https://github.com/dotnet/aspire-samples/tree/aee52a7a08ca3433bd1e54e8a90a7febdc852ebd//tests/SamplesIntegrationTests/Infrastructure has been copied to the project here This effectively increases coverage for various components like Qdrant, and Seq, and their hosting bits. Details: - Skip dashboard project reference to playground apps when running building for tests - Use `MapDefaultEndpoints` to add `/alive` and `/health` endpoints needed for testing - Rename `playground/TestShop/ServiceDefaults` to `playground/TestShop/TestShop.ServiceDefaults` to disambiguate the project binaries in `artifacts` - Only a few of the playground apps are being tested here. More will be added in follow up PRs. Contributes to https://github.com/dotnet/aspire/issues/4297 . Co-authored-by: Eric Erhardt --- Aspire.sln | 9 +- .../AzureStorageEndToEnd.AppHost/Program.cs | 9 +- .../CosmosEndToEnd.AppHost/Program.cs | 9 +- .../CustomResources.AppHost/Program.cs | 9 +- .../DatabaseMigration.AppHost/Program.cs | 9 +- playground/Directory.Build.props | 5 +- playground/Directory.Build.targets | 11 +- playground/Directory.Packages.props | 5 + .../Elasticsearch.ApiService/Program.cs | 1 + .../Elasticsearch.AppHost/Program.cs | 9 +- .../ParameterEndToEnd.AppHost/Program.cs | 9 +- .../PostgresEndToEnd.ApiService/Program.cs | 1 + .../PostgresEndToEnd.AppHost/Program.cs | 9 +- .../ProxylessEndToEnd.ApiService/Program.cs | 1 + .../ProxylessEndToEnd.AppHost/Program.cs | 9 +- .../SqlServerEndToEnd.ApiService/Program.cs | 1 + .../SqlServerEndToEnd.AppHost/Program.cs | 9 +- playground/Stress/Stress.AppHost/Program.cs | 9 +- .../TestShop/ApiGateway/ApiGateway.csproj | 2 +- .../BasketService/BasketService.csproj | 2 +- .../TestShop/CatalogDb/CatalogDb.csproj | 2 +- .../CatalogService/CatalogService.csproj | 2 +- .../TestShop/MyFrontend/MyFrontend.csproj | 2 +- .../OrderProcessor/OrderProcessor.csproj | 2 +- .../TestShop/TestShop.AppHost/Program.cs | 9 +- .../Extensions.cs | 0 .../TestShop.ServiceDefaults.csproj} | 0 playground/TestShop/TestShop.sln | 2 +- playground/dapr/ServiceA/DaprServiceA.csproj | 2 +- playground/dapr/ServiceB/DaprServiceB.csproj | 2 +- .../kafka/KafkaBasic.AppHost/Program.cs | 9 +- playground/mongo/Mongo.ApiService/Program.cs | 1 + playground/mongo/Mongo.AppHost/Program.cs | 9 +- playground/mysql/MySql.ApiService/Program.cs | 1 + playground/nats/Nats.ApiService/Program.cs | 1 + playground/nats/Nats.Backend/Program.cs | 2 +- playground/orleans/Orleans.AppHost/Program.cs | 9 +- playground/seq/Seq.ApiService/Program.cs | 2 + playground/seq/Seq.AppHost/Program.cs | 9 +- .../WithDockerfile.AppHost/Program.cs | 9 +- tests/Aspire.Playground.Tests/.runsettings | 23 ++ tests/Aspire.Playground.Tests/AppHostTests.cs | 218 ++++++++++++++ .../Aspire.Playground.Tests.csproj | 54 ++++ .../DistributedApplicationExtensions.cs | 266 ++++++++++++++++++ .../DistributedApplicationTestFactory.cs | 50 ++++ .../Infrastructure/ResourceExtensions.cs | 18 ++ tests/Aspire.Playground.Tests/README.md | 5 + tests/Directory.Build.props | 1 + .../WorkloadTesting/EnvironmentVariables.cs | 1 - .../send-to-helix-buildonhelixtests.targets | 58 ++++ tests/helix/send-to-helix-ci.proj | 2 + tests/helix/send-to-helix-inner.proj | 35 ++- .../helix/send-to-helix-workloadtests.targets | 7 - 53 files changed, 854 insertions(+), 87 deletions(-) create mode 100644 playground/Directory.Packages.props rename playground/TestShop/{ServiceDefaults => TestShop.ServiceDefaults}/Extensions.cs (100%) rename playground/TestShop/{ServiceDefaults/ServiceDefaults.csproj => TestShop.ServiceDefaults/TestShop.ServiceDefaults.csproj} (100%) create mode 100644 tests/Aspire.Playground.Tests/.runsettings create mode 100644 tests/Aspire.Playground.Tests/AppHostTests.cs create mode 100644 tests/Aspire.Playground.Tests/Aspire.Playground.Tests.csproj create mode 100644 tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationExtensions.cs create mode 100644 tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs create mode 100644 tests/Aspire.Playground.Tests/Infrastructure/ResourceExtensions.cs create mode 100644 tests/Aspire.Playground.Tests/README.md create mode 100644 tests/helix/send-to-helix-buildonhelixtests.targets diff --git a/Aspire.sln b/Aspire.sln index 229736044f..8df82a9350 100644 --- a/Aspire.sln +++ b/Aspire.sln @@ -13,7 +13,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "playground", "playground", playground\README.md = playground\README.md EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceDefaults", "playground\TestShop\ServiceDefaults\ServiceDefaults.csproj", "{C7B2309C-073A-4552-A508-A69768B64C6F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestShop.ServiceDefaults", "playground\TestShop\TestShop.ServiceDefaults\TestShop.ServiceDefaults.csproj", "{C7B2309C-073A-4552-A508-A69768B64C6F}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogService", "playground\TestShop\CatalogService\CatalogService.csproj", "{6D04BB34-1CC6-4FF3-A02A-1FFAC2A7A4F3}" EndProject @@ -507,6 +507,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "python", "python", "{7123AB EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Python.AppHost", "playground\python\Python.AppHost\Python.AppHost.csproj", "{173BDA6E-F175-4457-BF64-58CD184E9A81}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.Playground.Tests", "tests\Aspire.Playground.Tests\Aspire.Playground.Tests.csproj", "{8C07B9BF-87F4-450D-92FA-E03CF763013B}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Aspire.Hosting.Elasticsearch.Tests", "tests\Aspire.Hosting.Elasticsearch.Tests\Aspire.Hosting.Elasticsearch.Tests.csproj", "{62D8C73C-DAB3-4B9E-A508-34C886C374F9}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Components", "Components", "{C424395C-1235-41A4-BF55-07880A04368C}" @@ -1393,6 +1395,10 @@ Global {173BDA6E-F175-4457-BF64-58CD184E9A81}.Debug|Any CPU.Build.0 = Debug|Any CPU {173BDA6E-F175-4457-BF64-58CD184E9A81}.Release|Any CPU.ActiveCfg = Release|Any CPU {173BDA6E-F175-4457-BF64-58CD184E9A81}.Release|Any CPU.Build.0 = Release|Any CPU + {8C07B9BF-87F4-450D-92FA-E03CF763013B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C07B9BF-87F4-450D-92FA-E03CF763013B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C07B9BF-87F4-450D-92FA-E03CF763013B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C07B9BF-87F4-450D-92FA-E03CF763013B}.Release|Any CPU.Build.0 = Release|Any CPU {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Debug|Any CPU.Build.0 = Debug|Any CPU {62D8C73C-DAB3-4B9E-A508-34C886C374F9}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -1756,6 +1762,7 @@ Global {D5B392A4-29CD-41F9-8847-0C211C832713} = {C424395C-1235-41A4-BF55-07880A04368C} {7123AB7A-A4FD-4F64-8B05-D2DD0C3E2ABC} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0} {173BDA6E-F175-4457-BF64-58CD184E9A81} = {7123AB7A-A4FD-4F64-8B05-D2DD0C3E2ABC} + {8C07B9BF-87F4-450D-92FA-E03CF763013B} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {62D8C73C-DAB3-4B9E-A508-34C886C374F9} = {830A89EC-4029-4753-B25A-068BAE37DEC7} {C424395C-1235-41A4-BF55-07880A04368C} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} {830A89EC-4029-4753-B25A-068BAE37DEC7} = {4981B3A5-4AFD-4191-BF7D-8692D9783D60} diff --git a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs index 0f4c2e9b62..68e78dd822 100644 --- a/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs +++ b/playground/AzureStorageEndToEnd/AzureStorageEndToEnd.AppHost/Program.cs @@ -13,12 +13,15 @@ .WithExternalHttpEndpoints() .WithReference(blobs); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs index 2900210872..5271a2fea5 100644 --- a/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs +++ b/playground/CosmosEndToEnd/CosmosEndToEnd.AppHost/Program.cs @@ -11,11 +11,14 @@ .WithExternalHttpEndpoints() .WithReference(db); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/CustomResources/CustomResources.AppHost/Program.cs b/playground/CustomResources/CustomResources.AppHost/Program.cs index cb737c695a..5271d7a12d 100644 --- a/playground/CustomResources/CustomResources.AppHost/Program.cs +++ b/playground/CustomResources/CustomResources.AppHost/Program.cs @@ -7,11 +7,14 @@ builder.AddParameter("p0"); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/DatabaseMigration/DatabaseMigration.AppHost/Program.cs b/playground/DatabaseMigration/DatabaseMigration.AppHost/Program.cs index ef455af8dc..c4399511de 100644 --- a/playground/DatabaseMigration/DatabaseMigration.AppHost/Program.cs +++ b/playground/DatabaseMigration/DatabaseMigration.AppHost/Program.cs @@ -12,11 +12,14 @@ builder.AddProject("migration") .WithReference(db1); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/Directory.Build.props b/playground/Directory.Build.props index e3d81407da..42f5a81c61 100644 --- a/playground/Directory.Build.props +++ b/playground/Directory.Build.props @@ -1,9 +1,8 @@ - - + + - + + + + + SKIP_DASHBOARD_REFERENCE;$(DefineConstants) + diff --git a/playground/Directory.Packages.props b/playground/Directory.Packages.props new file mode 100644 index 0000000000..ebc3670efb --- /dev/null +++ b/playground/Directory.Packages.props @@ -0,0 +1,5 @@ + + + + + diff --git a/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs b/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs index c36a82d883..945656b86a 100644 --- a/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs +++ b/playground/Elasticsearch/Elasticsearch.ApiService/Program.cs @@ -12,6 +12,7 @@ var app = builder.Build(); +app.MapDefaultEndpoints(); app.MapGet("/get", async (ElasticsearchClient elasticClient) => { var response = await elasticClient.GetAsync("people", "1"); diff --git a/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs index b6d273b2ef..82f1391e38 100644 --- a/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs +++ b/playground/Elasticsearch/Elasticsearch.AppHost/Program.cs @@ -9,11 +9,14 @@ builder.AddProject("elasticsearch-apiservice") .WithReference(elasticsearch); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/ParameterEndToEnd/ParameterEndToEnd.AppHost/Program.cs b/playground/ParameterEndToEnd/ParameterEndToEnd.AppHost/Program.cs index 0958554c4b..8fbe8b542e 100644 --- a/playground/ParameterEndToEnd/ParameterEndToEnd.AppHost/Program.cs +++ b/playground/ParameterEndToEnd/ParameterEndToEnd.AppHost/Program.cs @@ -26,11 +26,14 @@ .WithEnvironment("InsertionRows", insertionrows) .WithReference(db); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/PostgresEndToEnd/PostgresEndToEnd.ApiService/Program.cs b/playground/PostgresEndToEnd/PostgresEndToEnd.ApiService/Program.cs index 727e411e4d..7428b8b962 100644 --- a/playground/PostgresEndToEnd/PostgresEndToEnd.ApiService/Program.cs +++ b/playground/PostgresEndToEnd/PostgresEndToEnd.ApiService/Program.cs @@ -23,6 +23,7 @@ var app = builder.Build(); +app.MapDefaultEndpoints(); app.MapGet("/", async (MyDb1Context db1Context, MyDb2Context db2Context, MyDb3Context db3Context, MyDb4Context db4Context, MyDb5Context db5Context, MyDb6Context db6Context, MyDb7Context db7Context, MyDb8Context db8Context, MyDb9Context db9Context) => { // You wouldn't normally do this on every call, diff --git a/playground/PostgresEndToEnd/PostgresEndToEnd.AppHost/Program.cs b/playground/PostgresEndToEnd/PostgresEndToEnd.AppHost/Program.cs index b56db918b7..5299dc2c65 100644 --- a/playground/PostgresEndToEnd/PostgresEndToEnd.AppHost/Program.cs +++ b/playground/PostgresEndToEnd/PostgresEndToEnd.AppHost/Program.cs @@ -34,11 +34,14 @@ .WithReference(db9) .WithReference(db10); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/ProxylessEndToEnd/ProxylessEndToEnd.ApiService/Program.cs b/playground/ProxylessEndToEnd/ProxylessEndToEnd.ApiService/Program.cs index 2f5e0d2ff2..6b4864b308 100644 --- a/playground/ProxylessEndToEnd/ProxylessEndToEnd.ApiService/Program.cs +++ b/playground/ProxylessEndToEnd/ProxylessEndToEnd.ApiService/Program.cs @@ -11,6 +11,7 @@ var app = builder.Build(); +app.MapDefaultEndpoints(); app.MapGet("/", () => { return Random.Shared.Next(); diff --git a/playground/ProxylessEndToEnd/ProxylessEndToEnd.AppHost/Program.cs b/playground/ProxylessEndToEnd/ProxylessEndToEnd.AppHost/Program.cs index 1ce368a3bb..fee6607256 100644 --- a/playground/ProxylessEndToEnd/ProxylessEndToEnd.AppHost/Program.cs +++ b/playground/ProxylessEndToEnd/ProxylessEndToEnd.AppHost/Program.cs @@ -20,11 +20,14 @@ .WithHttpEndpoint(port: 13456) .WithReference(redis); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/SqlServerEndToEnd/SqlServerEndToEnd.ApiService/Program.cs b/playground/SqlServerEndToEnd/SqlServerEndToEnd.ApiService/Program.cs index 1c3a2e17f0..e362eecd54 100644 --- a/playground/SqlServerEndToEnd/SqlServerEndToEnd.ApiService/Program.cs +++ b/playground/SqlServerEndToEnd/SqlServerEndToEnd.ApiService/Program.cs @@ -12,6 +12,7 @@ var app = builder.Build(); +app.MapDefaultEndpoints(); app.MapGet("/", async (MyDb1Context db1Context, MyDb2Context db2Context) => { // You wouldn't normally do this on every call, diff --git a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs index 2b6b7fbcbb..d8b4244796 100644 --- a/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs +++ b/playground/SqlServerEndToEnd/SqlServerEndToEnd.AppHost/Program.cs @@ -11,11 +11,14 @@ .WithReference(db1) .WithReference(db2); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/Stress/Stress.AppHost/Program.cs b/playground/Stress/Stress.AppHost/Program.cs index 6be76f2599..d4f5bad896 100644 --- a/playground/Stress/Stress.AppHost/Program.cs +++ b/playground/Stress/Stress.AppHost/Program.cs @@ -18,11 +18,14 @@ builder.AddProject("stress-telemetryservice"); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/TestShop/ApiGateway/ApiGateway.csproj b/playground/TestShop/ApiGateway/ApiGateway.csproj index 507ef5d1e2..0d0e8de030 100644 --- a/playground/TestShop/ApiGateway/ApiGateway.csproj +++ b/playground/TestShop/ApiGateway/ApiGateway.csproj @@ -16,7 +16,7 @@ - + diff --git a/playground/TestShop/BasketService/BasketService.csproj b/playground/TestShop/BasketService/BasketService.csproj index 67dfa50ae5..b0be5062a7 100644 --- a/playground/TestShop/BasketService/BasketService.csproj +++ b/playground/TestShop/BasketService/BasketService.csproj @@ -19,7 +19,7 @@ - + diff --git a/playground/TestShop/CatalogDb/CatalogDb.csproj b/playground/TestShop/CatalogDb/CatalogDb.csproj index e337e433fa..05a1b2ae48 100644 --- a/playground/TestShop/CatalogDb/CatalogDb.csproj +++ b/playground/TestShop/CatalogDb/CatalogDb.csproj @@ -8,7 +8,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/playground/TestShop/CatalogService/CatalogService.csproj b/playground/TestShop/CatalogService/CatalogService.csproj index f8fe18fb87..8ce7506a33 100644 --- a/playground/TestShop/CatalogService/CatalogService.csproj +++ b/playground/TestShop/CatalogService/CatalogService.csproj @@ -21,7 +21,7 @@ - + diff --git a/playground/TestShop/MyFrontend/MyFrontend.csproj b/playground/TestShop/MyFrontend/MyFrontend.csproj index 5e12ae4055..5193da6d85 100644 --- a/playground/TestShop/MyFrontend/MyFrontend.csproj +++ b/playground/TestShop/MyFrontend/MyFrontend.csproj @@ -25,7 +25,7 @@ - + diff --git a/playground/TestShop/OrderProcessor/OrderProcessor.csproj b/playground/TestShop/OrderProcessor/OrderProcessor.csproj index 43b13e8aa1..41e9bb251c 100644 --- a/playground/TestShop/OrderProcessor/OrderProcessor.csproj +++ b/playground/TestShop/OrderProcessor/OrderProcessor.csproj @@ -16,6 +16,6 @@ - + diff --git a/playground/TestShop/TestShop.AppHost/Program.cs b/playground/TestShop/TestShop.AppHost/Program.cs index e6b9730235..d2fc6910f1 100644 --- a/playground/TestShop/TestShop.AppHost/Program.cs +++ b/playground/TestShop/TestShop.AppHost/Program.cs @@ -40,11 +40,14 @@ builder.AddProject("catalogdbapp") .WithReference(catalogDb); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/TestShop/ServiceDefaults/Extensions.cs b/playground/TestShop/TestShop.ServiceDefaults/Extensions.cs similarity index 100% rename from playground/TestShop/ServiceDefaults/Extensions.cs rename to playground/TestShop/TestShop.ServiceDefaults/Extensions.cs diff --git a/playground/TestShop/ServiceDefaults/ServiceDefaults.csproj b/playground/TestShop/TestShop.ServiceDefaults/TestShop.ServiceDefaults.csproj similarity index 100% rename from playground/TestShop/ServiceDefaults/ServiceDefaults.csproj rename to playground/TestShop/TestShop.ServiceDefaults/TestShop.ServiceDefaults.csproj diff --git a/playground/TestShop/TestShop.sln b/playground/TestShop/TestShop.sln index 3b5bf1304b..25801183cf 100644 --- a/playground/TestShop/TestShop.sln +++ b/playground/TestShop/TestShop.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BasketService", "BasketServ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CatalogService", "CatalogService\CatalogService.csproj", "{F8F2A226-59D7-4023-A7BE-602B6B0366D2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ServiceDefaults", "ServiceDefaults\ServiceDefaults.csproj", "{938E6618-3061-4908-82AB-A2463F6B7BE8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestShop.ServiceDefaults", "TestShop.ServiceDefaults\TestShop.ServiceDefaults.csproj", "{938E6618-3061-4908-82AB-A2463F6B7BE8}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyFrontend", "MyFrontend\MyFrontend.csproj", "{A92E2CCE-5B2B-461E-80AA-1669FE2EEF2E}" EndProject diff --git a/playground/dapr/ServiceA/DaprServiceA.csproj b/playground/dapr/ServiceA/DaprServiceA.csproj index 5015b0dac0..97d19ac943 100644 --- a/playground/dapr/ServiceA/DaprServiceA.csproj +++ b/playground/dapr/ServiceA/DaprServiceA.csproj @@ -14,7 +14,7 @@ - + diff --git a/playground/dapr/ServiceB/DaprServiceB.csproj b/playground/dapr/ServiceB/DaprServiceB.csproj index 5015b0dac0..97d19ac943 100644 --- a/playground/dapr/ServiceB/DaprServiceB.csproj +++ b/playground/dapr/ServiceB/DaprServiceB.csproj @@ -14,7 +14,7 @@ - + diff --git a/playground/kafka/KafkaBasic.AppHost/Program.cs b/playground/kafka/KafkaBasic.AppHost/Program.cs index 94ff983d61..481bbde01d 100644 --- a/playground/kafka/KafkaBasic.AppHost/Program.cs +++ b/playground/kafka/KafkaBasic.AppHost/Program.cs @@ -16,11 +16,14 @@ builder.AddKafka("kafka2").WithKafkaUI(); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/mongo/Mongo.ApiService/Program.cs b/playground/mongo/Mongo.ApiService/Program.cs index 75cdc359e5..714bfcce1b 100644 --- a/playground/mongo/Mongo.ApiService/Program.cs +++ b/playground/mongo/Mongo.ApiService/Program.cs @@ -12,6 +12,7 @@ var app = builder.Build(); +app.MapDefaultEndpoints(); app.MapGet("/", async (IMongoClient mongoClient) => { const string collectionName = "entries"; diff --git a/playground/mongo/Mongo.AppHost/Program.cs b/playground/mongo/Mongo.AppHost/Program.cs index 950462fb76..0b347e6384 100644 --- a/playground/mongo/Mongo.AppHost/Program.cs +++ b/playground/mongo/Mongo.AppHost/Program.cs @@ -11,11 +11,14 @@ .WithExternalHttpEndpoints() .WithReference(db); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/playground/mysql/MySql.ApiService/Program.cs b/playground/mysql/MySql.ApiService/Program.cs index 498f36a23a..76493b2eb9 100644 --- a/playground/mysql/MySql.ApiService/Program.cs +++ b/playground/mysql/MySql.ApiService/Program.cs @@ -25,6 +25,7 @@ app.UseSwaggerUI(); } +app.MapDefaultEndpoints(); app.MapGet("/catalog", async (MySqlConnection db) => { const string sql = """ diff --git a/playground/nats/Nats.ApiService/Program.cs b/playground/nats/Nats.ApiService/Program.cs index f08be8be0c..503be50065 100644 --- a/playground/nats/Nats.ApiService/Program.cs +++ b/playground/nats/Nats.ApiService/Program.cs @@ -29,6 +29,7 @@ app.UseSwaggerUI(); } +app.MapDefaultEndpoints(); app.MapGet("/ping", async (INatsConnection nats) => { var rtt = await nats.PingAsync(); diff --git a/playground/nats/Nats.Backend/Program.cs b/playground/nats/Nats.Backend/Program.cs index 189e8e1741..a9e559c360 100644 --- a/playground/nats/Nats.Backend/Program.cs +++ b/playground/nats/Nats.Backend/Program.cs @@ -14,7 +14,7 @@ builder.Services.AddHostedService(); var app = builder.Build(); - +app.MapDefaultEndpoints(); app.Run(); public class AppEventsBackendService(INatsConnection nats, ILogger logger) : IHostedService diff --git a/playground/orleans/Orleans.AppHost/Program.cs b/playground/orleans/Orleans.AppHost/Program.cs index 29adf04007..a71789225e 100644 --- a/playground/orleans/Orleans.AppHost/Program.cs +++ b/playground/orleans/Orleans.AppHost/Program.cs @@ -24,12 +24,15 @@ .WithExternalHttpEndpoints() .WithReplicas(3); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif using var app = builder.Build(); diff --git a/playground/seq/Seq.ApiService/Program.cs b/playground/seq/Seq.ApiService/Program.cs index 41192c854d..5d49d05aee 100644 --- a/playground/seq/Seq.ApiService/Program.cs +++ b/playground/seq/Seq.ApiService/Program.cs @@ -13,6 +13,8 @@ ActivitySource source = new("MyApp.Source"); +app.MapDefaultEndpoints(); + app.MapGet("/", () => { var min = 1; diff --git a/playground/seq/Seq.AppHost/Program.cs b/playground/seq/Seq.AppHost/Program.cs index 60071dc5eb..bbffc313e2 100644 --- a/playground/seq/Seq.AppHost/Program.cs +++ b/playground/seq/Seq.AppHost/Program.cs @@ -9,11 +9,14 @@ .WithExternalHttpEndpoints() .WithReference(seq); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject("aspire-dashboard"); +#endif builder.Build().Run(); diff --git a/playground/withdockerfile/WithDockerfile.AppHost/Program.cs b/playground/withdockerfile/WithDockerfile.AppHost/Program.cs index a3584495bb..05130224a9 100644 --- a/playground/withdockerfile/WithDockerfile.AppHost/Program.cs +++ b/playground/withdockerfile/WithDockerfile.AppHost/Program.cs @@ -11,11 +11,14 @@ .WithBuildArg("GO_VERSION", goVersion) .WithBuildSecret("SECRET_ASENV", secret); +#if !SKIP_DASHBOARD_REFERENCE // This project is only added in playground projects to support development/debugging // of the dashboard. It is not required in end developer code. Comment out this code -// to test end developer dashboard launch experience. Refer to Directory.Build.props -// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output -// in the artifacts dir). +// or build with `/p:SkipDashboardReference=true`, to test end developer +// dashboard launch experience, Refer to Directory.Build.props for the path to +// the dashboard binary (defaults to the Aspire.Dashboard bin output in the +// artifacts dir). builder.AddProject(KnownResourceNames.AspireDashboard); +#endif builder.Build().Run(); diff --git a/tests/Aspire.Playground.Tests/.runsettings b/tests/Aspire.Playground.Tests/.runsettings new file mode 100644 index 0000000000..04b5df1c73 --- /dev/null +++ b/tests/Aspire.Playground.Tests/.runsettings @@ -0,0 +1,23 @@ + + + + + 600000 + + category!=failing + + + + + + TestResults.trx + + + + + normal + + + + + diff --git a/tests/Aspire.Playground.Tests/AppHostTests.cs b/tests/Aspire.Playground.Tests/AppHostTests.cs new file mode 100644 index 0000000000..bce1124fd9 --- /dev/null +++ b/tests/Aspire.Playground.Tests/AppHostTests.cs @@ -0,0 +1,218 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net; +using System.Text.Json; +using Aspire.Hosting.ApplicationModel; +using Aspire.Workload.Tests; +using Microsoft.Extensions.DependencyInjection; +using SamplesIntegrationTests; +using SamplesIntegrationTests.Infrastructure; +using Xunit; +using Xunit.Abstractions; + +namespace Aspire.Playground.Tests; + +public class AppHostTests +{ + private readonly TestOutputWrapper _testOutput; + private static readonly string? s_appHostNameFilter = Environment.GetEnvironmentVariable("TEST_PLAYGROUND_APPHOST_FILTER"); + + public AppHostTests(ITestOutputHelper testOutput) + { + this._testOutput = new TestOutputWrapper(testOutput); + } + + [Theory] + [MemberData(nameof(AppHostAssemblies))] + 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) + { + var appHostName = testEndpoints.AppHost!; + var resourceEndpoints = testEndpoints.ResourceEndpoints!; + + var appHostPath = $"{appHostName}.dll"; + var appHost = await DistributedApplicationTestFactory.CreateAsync(appHostPath, _testOutput); + var projects = appHost.Resources.OfType(); + await using var app = await appHost.BuildAsync(); + + await app.StartAsync(); + + var applicationModel = app.Services.GetRequiredService(); + var resourceNotificationService = app.Services.GetRequiredService(); + + await app.WaitForResources().WaitAsync(TimeSpan.FromMinutes(2)); + + if (testEndpoints.WaitForResources?.Count > 0) + { + // Wait until each resource transitions to the required state + var timeout = TimeSpan.FromMinutes(5); + foreach (var (ResourceName, TargetState) in testEndpoints.WaitForResources) + { + _testOutput.WriteLine($"Waiting for resource '{ResourceName}' to reach state '{TargetState}' in app '{Path.GetFileNameWithoutExtension(appHostPath)}'"); + await app.WaitForResource(ResourceName, TargetState).WaitAsync(TimeSpan.FromMinutes(5)); + } + } + + foreach (var resource in resourceEndpoints.Keys) + { + var endpoints = resourceEndpoints[resource]; + + if (endpoints.Count == 0) + { + // No test endpoints so ignore this resource + continue; + } + + HttpResponseMessage? response = null; + + using var client = app.CreateHttpClient(resource, null, clientBuilder => + { + 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; + }); + }); + + foreach (var path in endpoints) + { + _testOutput.WriteLine($"Calling endpoint '{client.BaseAddress}{path.TrimStart('/')} for resource '{resource}' in app '{Path.GetFileNameWithoutExtension(appHostPath)}'"); + response = await client.GetAsync(path); + + Assert.True(HttpStatusCode.OK == response.StatusCode, $"Endpoint '{client.BaseAddress}{path.TrimStart('/')}' for resource '{resource}' in app '{Path.GetFileNameWithoutExtension(appHostPath)}' returned status code {response.StatusCode}"); + } + } + + app.EnsureNoErrorsLogged(); + await app.StopAsync(); + } + + public static TheoryData AppHostAssemblies() + { + var appHostAssemblies = GetPlaygroundAppHostAssemblyPaths(); + var theoryData = new TheoryData(); + foreach (var asm in appHostAssemblies) + { + 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 InvalidOperationException($"No app host assemblies found matching filter '{s_appHostNameFilter}'"); + } + + return theoryData; + } + + public static TheoryData TestEndpoints() + { + IList candidates = + [ + new TestEndpoints("Mongo.AppHost", new() { { "api", ["/alive", "/health", "/"] } }), + new TestEndpoints("MySqlDb.AppHost", new() { { "apiservice", ["/alive", "/health", "/catalog"] }, }), + new TestEndpoints("Nats.AppHost", new() { + { "api", ["/alive", "/health"] }, + { "backend", ["/alive", "/health"] } + }), + new TestEndpoints("ProxylessEndToEnd.AppHost", new() { { "api", ["/alive", "/health", "/redis"] } }), + new TestEndpoints("Qdrant.AppHost", new() { { "apiservice", ["/alive", "/health"] } }), + new TestEndpoints("Seq.AppHost", new() { { "api", ["/alive", "/health"] } }), + new TestEndpoints("TestShop.AppHost", new() { + { "catalogdbapp", ["/alive", "/health"] }, + { "frontend", ["/alive", "/health"] }, + }), + ]; + + TheoryData theoryData = new(); + foreach (var candidateTestEndpoint in candidates) + { + if (string.IsNullOrEmpty(s_appHostNameFilter) || candidateTestEndpoint.AppHost?.Contains(s_appHostNameFilter, StringComparison.OrdinalIgnoreCase) == true) + { + theoryData.Add(candidateTestEndpoint); + } + } + + if (!theoryData.Any() && !string.IsNullOrEmpty(s_appHostNameFilter)) + { + throw new InvalidOperationException($"No test endpoints found matching filter '{s_appHostNameFilter}'"); + } + + return theoryData; + } + + private static IEnumerable GetPlaygroundAppHostAssemblyPaths() + { + // All the AppHost projects are referenced by this project so we can find them by looking for all their assemblies in the base directory + return Directory.GetFiles(AppContext.BaseDirectory, "*.AppHost.dll") + .Where(fileName => !fileName.EndsWith("Aspire.Hosting.AppHost.dll", StringComparison.OrdinalIgnoreCase)); + } +} + +public class TestEndpoints : IXunitSerializable +{ + // Required for deserialization + public TestEndpoints() { } + + public TestEndpoints(string appHost, Dictionary> resourceEndpoints) + { + AppHost = appHost; + ResourceEndpoints = resourceEndpoints; + } + + public string? AppHost { get; set; } + + public List? WaitForResources { get; set; } + + public Dictionary>? ResourceEndpoints { get; set; } + + public void Deserialize(IXunitSerializationInfo info) + { + AppHost = info.GetValue(nameof(AppHost)); + WaitForResources = JsonSerializer.Deserialize>(info.GetValue(nameof(WaitForResources))); + ResourceEndpoints = JsonSerializer.Deserialize>>(info.GetValue(nameof(ResourceEndpoints))); + } + + public void Serialize(IXunitSerializationInfo info) + { + info.AddValue(nameof(AppHost), AppHost); + info.AddValue(nameof(WaitForResources), JsonSerializer.Serialize(WaitForResources)); + info.AddValue(nameof(ResourceEndpoints), JsonSerializer.Serialize(ResourceEndpoints)); + } + + public override string? ToString() => $"{AppHost} ({ResourceEndpoints?.Count ?? 0} resources)"; + + public class ResourceWait(string resourceName, string targetState) + { + public string ResourceName { get; } = resourceName; + + public string TargetState { get; } = targetState; + + public void Deconstruct(out string resourceName, out string targetState) + { + resourceName = ResourceName; + targetState = TargetState; + } + } +} diff --git a/tests/Aspire.Playground.Tests/Aspire.Playground.Tests.csproj b/tests/Aspire.Playground.Tests/Aspire.Playground.Tests.csproj new file mode 100644 index 0000000000..cf91be7bc3 --- /dev/null +++ b/tests/Aspire.Playground.Tests/Aspire.Playground.Tests.csproj @@ -0,0 +1,54 @@ + + + + $(NetCurrent) + + + true + + + true + true + + staging-archive\ + + $(TestArchiveTestsDirForBuildOnHelixTests) + + $(MSBuildThisFileDirectory)..\..\playground\ + $(MSBuildThisFileDirectory)..\Shared\ + $(MSBuildThisFileDirectory).runsettings + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationExtensions.cs b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationExtensions.cs new file mode 100644 index 0000000000..1f9a0016dd --- /dev/null +++ b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationExtensions.cs @@ -0,0 +1,266 @@ +// 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.Net; +using System.Reflection; +using System.Security.Cryptography; +using Aspire.Hosting; +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using SamplesIntegrationTests.Infrastructure; +using Xunit; + +namespace SamplesIntegrationTests.Infrastructure; + +public static partial class DistributedApplicationExtensions +{ + /// + /// Ensures all parameters in the application configuration have values set. + /// + public static TBuilder WithRandomParameterValues(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + var parameters = builder.Resources.OfType().Where(p => !p.IsConnectionString).ToList(); + foreach (var parameter in parameters) + { + builder.Configuration[$"Parameters:{parameter.Name}"] = parameter.Secret + ? PasswordGenerator.Generate(16, true, true, true, false, 1, 1, 1, 0) + : Convert.ToHexString(RandomNumberGenerator.GetBytes(4)); + } + + return builder; + } + + /// + /// Replaces all named volumes with anonymous volumes so they're isolated across test runs and from the volume the app uses during development. + /// + /// + /// Note that if multiple resources share a volume, the volume will instead be given a random name so that it's still shared across those resources in the test run. + /// + public static TBuilder WithRandomVolumeNames(this TBuilder builder) + where TBuilder : IDistributedApplicationTestingBuilder + { + // Named volumes that aren't shared across resources should be replaced with anonymous volumes. + // Named volumes shared by mulitple resources need to have their name randomized but kept shared across those resources. + + // Find all shared volumes and make a map of their original name to a new randomized name + var allResourceNamedVolumes = builder.Resources.SelectMany(r => r.Annotations + .OfType() + .Where(m => m.Type == ContainerMountType.Volume && !string.IsNullOrEmpty(m.Source)) + .Select(m => (Resource: r, Volume: m))) + .ToList(); + var seenVolumes = new HashSet(); + var renamedVolumes = new Dictionary(); + foreach (var resourceVolume in allResourceNamedVolumes) + { + var name = resourceVolume.Volume.Source!; + if (!seenVolumes.Add(name) && !renamedVolumes.ContainsKey(name)) + { + renamedVolumes[name] = $"{name}-{Convert.ToHexString(RandomNumberGenerator.GetBytes(4))}"; + } + } + + // Replace all named volumes with randomly named or anonymous volumes + foreach (var resourceVolume in allResourceNamedVolumes) + { + var resource = resourceVolume.Resource; + var volume = resourceVolume.Volume; + var newName = renamedVolumes.TryGetValue(volume.Source!, out var randomName) ? randomName : null; + var newMount = new ContainerMountAnnotation(newName, volume.Target, ContainerMountType.Volume, volume.IsReadOnly); + resource.Annotations.Remove(volume); + resource.Annotations.Add(newMount); + } + + return builder; + } + + /// + /// Waits for the specified resource to reach the specified state. + /// + public static Task WaitForResource(this DistributedApplication app, string resourceName, string? targetState = null, CancellationToken cancellationToken = default) + { + targetState ??= KnownResourceStates.Running; + var resourceNotificationService = app.Services.GetRequiredService(); + + return resourceNotificationService.WaitForResourceAsync(resourceName, targetState, cancellationToken); + } + + /// + /// Waits for all resources in the application to reach one of the specified states. + /// + /// + /// If is null, the default states are and . + /// + public static Task WaitForResources(this DistributedApplication app, IEnumerable? targetStates = null, CancellationToken cancellationToken = default) + { + targetStates ??= [KnownResourceStates.Running, KnownResourceStates.Hidden]; + var applicationModel = app.Services.GetRequiredService(); + var resourceNotificationService = app.Services.GetRequiredService(); + + return Task.WhenAll(applicationModel.Resources.Select(r => resourceNotificationService.WaitForResourceAsync(r.Name, targetStates, cancellationToken))); + } + + /// + /// Gets the app host and resource logs from the application. + /// + public static (IReadOnlyList AppHostLogs, IReadOnlyList ResourceLogs) GetLogs(this DistributedApplication app) + { + var environment = app.Services.GetRequiredService(); + var logCollector = app.Services.GetFakeLogCollector(); + var logs = logCollector.GetSnapshot(); + var appHostLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == false).ToList(); + var resourceLogs = logs.Where(l => l.Category?.StartsWith($"{environment.ApplicationName}.Resources") == true).ToList(); + + return (appHostLogs, resourceLogs); + } + + /// + /// Asserts that no errors were logged by the application or any of its resources. + /// + /// + /// Some resource types are excluded from this check because they tend to write to stderr for various non-error reasons. + /// + /// + public static void EnsureNoErrorsLogged(this DistributedApplication app) + { + var environment = app.Services.GetRequiredService(); + var applicationModel = app.Services.GetRequiredService(); + var assertableResourceLogNames = applicationModel.Resources.Where(ShouldAssertErrorsForResource).Select(r => $"{environment.ApplicationName}.Resources.{r.Name}").ToList(); + + var (appHostlogs, resourceLogs) = app.GetLogs(); + + Assert.DoesNotContain(appHostlogs, log => log.Level >= LogLevel.Error); + Assert.DoesNotContain(resourceLogs, log => log.Category is { Length: > 0 } category && assertableResourceLogNames.Contains(category) && log.Level >= LogLevel.Error); + + static bool ShouldAssertErrorsForResource(IResource resource) + { + return resource + is + // Container resources tend to write to stderr for various reasons so only assert projects and executables + (ProjectResource or ExecutableResource) + // Node resources tend to have npm modules that write to stderr so ignore them + and not NodeAppResource + // Dapr resources write to stderr about deprecated --components-path flag + && !resource.Name.EndsWith("-dapr-cli"); + } + } + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, bool useHttpClientFactory) + => app.CreateHttpClient(resourceName, null, useHttpClientFactory); + + /// + /// Creates an configured to communicate with the specified resource. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, bool useHttpClientFactory) + { + if (useHttpClientFactory) + { + return app.CreateHttpClient(resourceName, endpointName); + } + + // Don't use the HttpClientFactory to create the HttpClient so, e.g., no resilience policies are applied + var httpClient = new HttpClient + { + BaseAddress = app.GetEndpoint(resourceName, endpointName) + }; + + return httpClient; + } + + /// + /// Creates an configured to communicate with the specified resource with custom configuration. + /// + public static HttpClient CreateHttpClient(this DistributedApplication app, string resourceName, string? endpointName, Action configure) + { + var services = new ServiceCollection() + .AddHttpClient() + .ConfigureHttpClientDefaults(configure) + .BuildServiceProvider(); + var httpClientFactory = services.GetRequiredService(); + + var httpClient = httpClientFactory.CreateClient(); + httpClient.BaseAddress = app.GetEndpoint(resourceName, endpointName); + + return httpClient; + } + + /// + /// Attempts to apply EF migrations for the specified project by sending a request to the migrations endpoint /ApplyDatabaseMigrations. + /// + public static async Task TryApplyEfMigrationsAsync(this DistributedApplication app, ProjectResource project) + { + var logger = app.Services.GetRequiredService().CreateLogger(nameof(TryApplyEfMigrationsAsync)); + var projectName = project.GetName(); + + // First check if the project has a migration endpoint, if it doesn't it will respond with a 404 + logger.LogInformation("Checking if project '{ProjectName}' has a migration endpoint", projectName); + using (var checkHttpClient = app.CreateHttpClient(project.Name)) + { + using var emptyDbContextContent = new FormUrlEncodedContent([new("context", "")]); + using var checkResponse = await checkHttpClient.PostAsync("/ApplyDatabaseMigrations", emptyDbContextContent); + if (checkResponse.StatusCode == HttpStatusCode.NotFound) + { + logger.LogInformation("Project '{ProjectName}' does not have a migration endpoint", projectName); + return false; + } + } + + logger.LogInformation("Attempting to apply EF migrations for project '{ProjectName}'", projectName); + + // Load the project assembly and find all DbContext types + var projectDirectory = Path.GetDirectoryName(project.GetProjectMetadata().ProjectPath) ?? throw new UnreachableException(); +#if DEBUG + var configuration = "Debug"; +#else + var configuration = "Release"; +#endif + var projectAssemblyPath = Path.Combine(projectDirectory, "bin", configuration, "net8.0", $"{projectName}.dll"); + var projectAssembly = Assembly.LoadFrom(projectAssemblyPath); + var dbContextTypes = projectAssembly.GetTypes().Where(DerivesFromDbContext); + + logger.LogInformation("Found {DbContextCount} DbContext types in project '{ProjectName}'", dbContextTypes.Count(), projectName); + + // Call the migration endpoint for each DbContext type + var migrationsApplied = false; + using var applyMigrationsHttpClient = app.CreateHttpClient(project.Name, useHttpClientFactory: false); + applyMigrationsHttpClient.Timeout = TimeSpan.FromSeconds(240); + foreach (var dbContextType in dbContextTypes) + { + logger.LogInformation("Applying migrations for DbContext '{DbContextType}' in project '{ProjectName}'", dbContextType.FullName, projectName); + using var content = new FormUrlEncodedContent([new("context", dbContextType.AssemblyQualifiedName)]); + using var response = await applyMigrationsHttpClient.PostAsync("/ApplyDatabaseMigrations", content); + if (response.StatusCode == HttpStatusCode.NoContent) + { + migrationsApplied = true; + logger.LogInformation("Migrations applied for DbContext '{DbContextType}' in project '{ProjectName}'", dbContextType.FullName, projectName); + } + } + + return migrationsApplied; + } + + private static bool DerivesFromDbContext(Type type) + { + var baseType = type.BaseType; + + while (baseType is not null) + { + if (baseType.FullName == "Microsoft.EntityFrameworkCore.DbContext" && baseType.Assembly.GetName().Name == "Microsoft.EntityFrameworkCore") + { + return true; + } + + baseType = baseType.BaseType; + } + + return false; + } +} diff --git a/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs new file mode 100644 index 0000000000..7ca7db4508 --- /dev/null +++ b/tests/Aspire.Playground.Tests/Infrastructure/DistributedApplicationTestFactory.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Reflection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using SamplesIntegrationTests.Infrastructure; +using Xunit.Abstractions; + +namespace SamplesIntegrationTests; + +internal static class DistributedApplicationTestFactory +{ + /// + /// Creates an for the specified app host assembly. + /// + public static async Task CreateAsync(string appHostAssemblyPath, ITestOutputHelper? testOutput) + { + var appHostProjectName = Path.GetFileNameWithoutExtension(appHostAssemblyPath) ?? throw new InvalidOperationException("AppHost assembly was not found."); + + var appHostAssembly = Assembly.LoadFrom(Path.Combine(AppContext.BaseDirectory, appHostAssemblyPath)); + + var appHostType = appHostAssembly.GetTypes().FirstOrDefault(t => t.Name.EndsWith("_AppHost")) + ?? throw new InvalidOperationException("Generated AppHost type not found."); + + var builder = await DistributedApplicationTestingBuilder.CreateAsync(appHostType); + + builder.WithRandomParameterValues(); + builder.WithRandomVolumeNames(); + + builder.Services.AddLogging(logging => + { + logging.ClearProviders(); + logging.AddSimpleConsole(configure => + { + configure.SingleLine = true; + }); + logging.AddFakeLogging(); + if (testOutput is not null) + { + logging.AddXunit(testOutput); + } + logging.SetMinimumLevel(LogLevel.Trace); + logging.AddFilter("Aspire", LogLevel.Trace); + logging.AddFilter(builder.Environment.ApplicationName, LogLevel.Trace); + }); + + return builder; + } +} diff --git a/tests/Aspire.Playground.Tests/Infrastructure/ResourceExtensions.cs b/tests/Aspire.Playground.Tests/Infrastructure/ResourceExtensions.cs new file mode 100644 index 0000000000..5e4b86188c --- /dev/null +++ b/tests/Aspire.Playground.Tests/Infrastructure/ResourceExtensions.cs @@ -0,0 +1,18 @@ +// 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.ApplicationModel; + +namespace SamplesIntegrationTests.Infrastructure; + +internal static class ResourceExtensions +{ + /// + /// Gets the name of the based on the project file path. + /// + public static string GetName(this ProjectResource project) + { + var metadata = project.GetProjectMetadata(); + return Path.GetFileNameWithoutExtension(metadata.ProjectPath); + } +} diff --git a/tests/Aspire.Playground.Tests/README.md b/tests/Aspire.Playground.Tests/README.md new file mode 100644 index 0000000000..b4b2db24d9 --- /dev/null +++ b/tests/Aspire.Playground.Tests/README.md @@ -0,0 +1,5 @@ +# Aspire.Playground.Tests + +Runs apps in `playground/` and hits some endpoints on them. + +Use `TEST_PLAYGROUND_APPHOST_FILTER` to run tests for specific playground apps. For example, `TEST_PLAYGROUND_APPHOST_FILTER=testshop`. diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 18f7d9a78b..028ba662a7 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -9,6 +9,7 @@ $([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'tests')) $([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'workload-tests')) $([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'e2e-tests')) + $([MSBuild]::NormalizeDirectory($(ArtifactsDir), 'helix', 'build-on-helix-tests')) $(ArtifactsBinDir)playwright-deps $(IntermediateOutputPath)Directory.Packages.Versions.props diff --git a/tests/Shared/WorkloadTesting/EnvironmentVariables.cs b/tests/Shared/WorkloadTesting/EnvironmentVariables.cs index d9d5994577..83cf3bf2c8 100644 --- a/tests/Shared/WorkloadTesting/EnvironmentVariables.cs +++ b/tests/Shared/WorkloadTesting/EnvironmentVariables.cs @@ -15,5 +15,4 @@ public static class EnvironmentVariables public static readonly bool TestsRunningOutsideOfRepo = Environment.GetEnvironmentVariable("TestsRunningOutsideOfRepo") is "true"; public static readonly string BuildConfiguration = Environment.GetEnvironmentVariable("BUILD_CONFIGURATION") ?? "Debug"; public static readonly string? TestScenario = Environment.GetEnvironmentVariable("TEST_SCENARIO"); - public static readonly string? BrowserPath = Environment.GetEnvironmentVariable(PlaywrightProvider.BrowserPathEnvironmentVariableName); } diff --git a/tests/helix/send-to-helix-buildonhelixtests.targets b/tests/helix/send-to-helix-buildonhelixtests.targets new file mode 100644 index 0000000000..011c95136f --- /dev/null +++ b/tests/helix/send-to-helix-buildonhelixtests.targets @@ -0,0 +1,58 @@ + + + + $(BuildHelixWorkItemsDependsOn);BuildHelixWorkItemsForBuildOnHelixTests + true + true + + $(TestArchiveTestsDirForBuildOnHelixTests)**/*.zip + + <_TestAssemblyRootDirEnvVar Condition="'$(OS)' != 'Windows_NT'">${HELIX_WORKITEM_ROOT}/test/ + <_TestAssemblyRootDirEnvVar Condition="'$(OS)' == 'Windows_NT'">%HELIX_WORKITEM_ROOT%\test\ + + + + + + + + + + + + + + <_TestRunCommandArguments Remove="@(_TestRunCommandArguments)" /> + + + <_TestRunCommandArguments Include="dotnet test -s .runsettings --results-directory $(_HelixLogsPath)" /> + <_TestRunCommandArguments Include="@(_TestBlameArguments, ' ')" /> + + + + <_TestRunCommand Condition="'$(RunWithCodeCoverage)' == 'true'">@(_TestCoverageCommand, ' ') "@(_TestRunCommandArguments, ' ')" + <_TestRunCommand Condition="'$(RunWithCodeCoverage)' != 'true'">@(_TestRunCommandArguments, ' ') + + <_TestRunCommand>dotnet build -bl:$(_HelixLogsPath)/build.binlog $(_ShellCommandSeparator) $(_TestRunCommand) + + <_SetPathEnvVar Condition="'$(OS)' != 'Windows_NT'">PATH=${SDK_FOR_WORKLOAD_TESTING_PATH}:$PATH + <_SetPathEnvVar Condition="'$(OS)' == 'Windows_NT'">PATH=%SDK_FOR_WORKLOAD_TESTING_PATH%%3B%PATH% + + + + <_DefaultWorkItems Include="$(WorkItemArchiveWildCard)" /> + + + %(Identity) + + $(_EnvVarSetKeyword) "TEST_NAME=%(FileName)" $(_ShellCommandSeparator) $(_EnvVarSetKeyword) "$(_SetPathEnvVar)" + + cd tests/%(FileName) %3B $(_TestRunCommand) + 00:15:00 + + + logs/%(FileName).cobertura.xml + + + + diff --git a/tests/helix/send-to-helix-ci.proj b/tests/helix/send-to-helix-ci.proj index 8915785d2e..f83854ac1f 100644 --- a/tests/helix/send-to-helix-ci.proj +++ b/tests/helix/send-to-helix-ci.proj @@ -5,6 +5,8 @@ + + <_ProjectsToBuild Include="send-to-helix-inner.proj" diff --git a/tests/helix/send-to-helix-inner.proj b/tests/helix/send-to-helix-inner.proj index 754adfe57c..3180a03d61 100644 --- a/tests/helix/send-to-helix-inner.proj +++ b/tests/helix/send-to-helix-inner.proj @@ -37,6 +37,9 @@ <_TestNameEnvVar Condition="'$(OS)' != 'Windows_NT'">${TEST_NAME} <_TestNameEnvVar Condition="'$(OS)' == 'Windows_NT'">%TEST_NAME% + <_TestAssemblyRootDirEnvVar Condition="'$(OS)' != 'Windows_NT'">${HELIX_WORKITEM_ROOT} + <_TestAssemblyRootDirEnvVar Condition="'$(OS)' == 'Windows_NT'">%HELIX_WORKITEM_ROOT% + <_CodeCoverageReportFileNameSuffixEnvVar Condition="'$(OS)' != 'Windows_NT'">${CODE_COV_FILE_SUFFIX} <_CodeCoverageReportFileNameSuffixEnvVar Condition="'$(OS)' == 'Windows_NT'">%CODE_COV_FILE_SUFFIX% @@ -48,6 +51,9 @@ <_ShutdownDockerContainersCommand Condition="'$(OS)' != 'Windows_NT'">for f in `docker ps -aq`%3B do docker stop $f%3B docker rm $f%3B done <_DeleteDockerVolumesCommand Condition="'$(OS)' != 'Windows_NT'">for f in `docker volume ls -q`%3B do docker volume rm $f%3B done + + <_CleanupProcessesCommand Condition="'$(OS)' == 'Windows_NT'">powershell -ExecutionPolicy ByPass -NoProfile -command "& get-ciminstance win32_process | where-object ExecutablePath -Match 'dotnet-latest|dcp.exe|dcpctrl.exe' | foreach-object { echo $_.ProcessId $_.ExecutablePath %3B stop-process -id $_.ProcessId -force -ErrorAction SilentlyContinue }" + <_CleanupProcessesCommand Condition="'$(OS)' != 'Windows_NT'">pgrep -lf "dotnet-latest|dcp.exe|dcpctrl.exe" | awk '{print %3B system("kill -9 "$1)}' @@ -55,26 +61,27 @@ <_TestCoverageCommand Include="--settings $(_HelixCorrelationPayloadEnvVar)/support-data/CodeCoverage.config" /> <_TestCoverageCommand Include="--output $(_HelixLogsPath)/$(_TestNameEnvVar)$(_CodeCoverageReportFileNameSuffixEnvVar).cobertura.xml" /> - <_TestRunCommandArguments Include="dotnet test" /> - <_TestRunCommandArguments Include="-s .runsettings" /> - <_TestRunCommandArguments Include="$(_TestNameEnvVar).dll" /> - <_TestRunCommandArguments Include="--ResultsDirectory:$(_HelixLogsPath)" /> - <_TestRunCommandArguments Include="--blame-hang" /> - <_TestRunCommandArguments Include="--blame-hang-dump-type" /> - <_TestRunCommandArguments Include="full" /> - <_TestRunCommandArguments Include="--blame-hang-timeout" /> - <_TestRunCommandArguments Include="10m" /> - <_TestRunCommandArguments Include="--blame-crash" /> - <_TestRunCommandArguments Include="--blame-crash-dump-type" /> - <_TestRunCommandArguments Include="full" /> + <_TestBlameArguments Include="--blame-hang" /> + <_TestBlameArguments Include="--blame-hang-dump-type full" /> + <_TestBlameArguments Include="--blame-hang-timeout 10m" /> + <_TestBlameArguments Include="--blame-crash" /> + <_TestBlameArguments Include="--blame-crash-dump-type full" /> + + <_TestRunCommandArguments Include="dotnet test -s .runsettings $(_TestNameEnvVar).dll --ResultsDirectory:$(_HelixLogsPath)" /> + <_TestRunCommandArguments Include="@(_TestBlameArguments, ' ')" /> + + + + + - + @@ -119,6 +126,8 @@ + + diff --git a/tests/helix/send-to-helix-workloadtests.targets b/tests/helix/send-to-helix-workloadtests.targets index c8cd5e597a..3810d9a666 100644 --- a/tests/helix/send-to-helix-workloadtests.targets +++ b/tests/helix/send-to-helix-workloadtests.targets @@ -6,19 +6,12 @@ true Aspire.Workload.Tests - - <_CleanupProcessesCommand Condition="'$(OS)' == 'Windows_NT'">powershell -ExecutionPolicy ByPass -NoProfile -command "& get-ciminstance win32_process | where-object ExecutablePath -Match 'dotnet-latest|dcp.exe|dcpctrl.exe' | foreach-object { echo $_.ProcessId $_.ExecutablePath %3B stop-process -id $_.ProcessId -force -ErrorAction SilentlyContinue }" - - <_CleanupProcessesCommand Condition="'$(OS)' != 'Windows_NT'">pgrep -lf "dotnet-latest|dcp.exe|dcpctrl.exe" | awk '{print %3B system("kill -9 "$1)}' - - -