| File: DescribeCommandTests.cs | Web Access |
| Project: src\tests\Aspire.Cli.EndToEnd.Tests\Aspire.Cli.EndToEnd.Tests.csproj (Aspire.Cli.EndToEnd.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Cli.EndToEnd.Tests.Helpers; using Aspire.Cli.Resources; using Aspire.Cli.Tests.Utils; using Hex1b.Automation; using Xunit; namespace Aspire.Cli.EndToEnd.Tests; /// <summary> /// End-to-end tests for the aspire describe command. /// Each test class runs as a separate CI job for parallelization. /// </summary> public sealed class DescribeCommandTests(ITestOutputHelper output) { [Fact] public async Task DescribeCommandShowsRunningResources() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliInDockerAsync(installMode, counter); // Create a new project using aspire new await auto.AspireNewAsync("AspireResourcesTestApp", counter); // Navigate to the AppHost directory await auto.TypeAsync("cd AspireResourcesTestApp/AspireResourcesTestApp.AppHost"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Start the AppHost in the background using aspire start await auto.TypeAsync("aspire start"); await auto.EnterAsync(); await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); // Wait a bit for resources to stabilize await auto.TypeAsync("sleep 5"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Now verify aspire describe shows the running resources (human-readable table) await auto.TypeAsync("aspire describe"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("Name", timeout: TimeSpan.FromSeconds(30)); await auto.WaitUntilTextAsync("webfrontend", timeout: TimeSpan.FromSeconds(5)); await auto.WaitUntilTextAsync("apiservice", timeout: TimeSpan.FromSeconds(5)); await auto.WaitForSuccessPromptAsync(counter); // Test aspire describe --format json output - pipe to file to avoid terminal buffer issues await auto.TypeAsync("aspire describe --format json > resources.json"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Verify the JSON file contains expected resources await auto.TypeAsync("cat resources.json | grep webfrontend"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("webfrontend", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); // Stop the AppHost using aspire stop await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); // Exit the shell await auto.TypeAsync("exit"); await auto.EnterAsync(); await pendingRun; } [Fact] public async Task DescribeCommandResolvesReplicaNames() { var repoRoot = CliE2ETestHelpers.GetRepoRoot(); var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); var workspace = TemporaryWorkspace.Create(output); using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal(repoRoot, installMode, output, mountDockerSocket: true, workspace: workspace); var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); // Pattern for describe output showing a specific replica var waitForApiserviceReplicaName = new CellPatternSearcher() .FindPattern("apiservice-[a-z0-9]+"); var counter = new SequenceCounter(); var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); await auto.PrepareDockerEnvironmentAsync(counter, workspace); await auto.InstallAspireCliInDockerAsync(installMode, counter); // Create a new project using aspire new await auto.AspireNewAsync("AspireReplicaTestApp", counter); // Navigate to the AppHost directory await auto.TypeAsync("cd AspireReplicaTestApp/AspireReplicaTestApp.AppHost"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Add .WithReplicas(2) to the apiservice resource in the AppHost var projectDir = Path.Combine(workspace.WorkspaceRoot.FullName, "AspireReplicaTestApp"); var appHostDir = Path.Combine(projectDir, "AspireReplicaTestApp.AppHost"); var appHostFilePath = Path.Combine(appHostDir, "AppHost.cs"); output.WriteLine($"Looking for AppHost.cs at: {appHostFilePath}"); var content = File.ReadAllText(appHostFilePath); // Add .WithReplicas(2) to the first .WithHttpHealthCheck("/health"); occurrence (apiservice) var originalPattern = ".WithHttpHealthCheck(\"/health\");"; var replacement = ".WithHttpHealthCheck(\"/health\").WithReplicas(2);"; // Only replace the first occurrence (apiservice), not the second (webfrontend) var index = content.IndexOf(originalPattern); if (index >= 0) { content = content[..index] + replacement + content[(index + originalPattern.Length)..]; } File.WriteAllText(appHostFilePath, content); output.WriteLine($"Modified AppHost.cs to add .WithReplicas(2) to apiservice"); // Start the AppHost in the background using aspire start await auto.TypeAsync("aspire start"); await auto.EnterAsync(); await auto.WaitUntilTextAsync(RunCommandStrings.AppHostStartedSuccessfully, timeout: TimeSpan.FromMinutes(3)); await auto.WaitForSuccessPromptAsync(counter); // Wait for resources to stabilize await auto.TypeAsync("sleep 10"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Test 1: aspire describe with friendly name for a non-replicated resource (cache) // This should resolve via DisplayName since cache has only one instance await auto.TypeAsync("aspire describe cache --format json > cache-describe.json"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Verify cache resource was found in the output await auto.TypeAsync("cat cache-describe.json | grep cache"); await auto.EnterAsync(); await auto.WaitUntilTextAsync("cache", timeout: TimeSpan.FromSeconds(10)); await auto.WaitForSuccessPromptAsync(counter); // Test 2: Get all resources to find an apiservice replica name await auto.TypeAsync("aspire describe --format json > all-resources.json"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Extract a replica name from the JSON - apiservice replicas have names like apiservice-<suffix> await auto.TypeAsync("REPLICA_NAME=$(cat all-resources.json | grep -o '\"name\": *\"apiservice-[a-z0-9]*\"' | head -1 | sed 's/.*\"\\(apiservice-[a-z0-9]*\\)\"/\\1/')"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Verify we captured a replica name await auto.TypeAsync("echo \"Found replica: $REPLICA_NAME\""); await auto.EnterAsync(); await auto.WaitUntilAsync(s => waitForApiserviceReplicaName.Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "waiting for apiservice replica name"); await auto.WaitForSuccessPromptAsync(counter); // Test 3: aspire describe with the replica name // This should resolve via exact Name match await auto.TypeAsync("aspire describe $REPLICA_NAME --format json > replica-describe.json"); await auto.EnterAsync(); await auto.WaitForSuccessPromptAsync(counter); // Verify the replica was found and output contains the replica name await auto.TypeAsync("cat replica-describe.json | grep apiservice"); await auto.EnterAsync(); await auto.WaitUntilAsync(s => waitForApiserviceReplicaName.Search(s).Count > 0, timeout: TimeSpan.FromSeconds(10), description: "waiting for apiservice replica in describe output"); await auto.WaitForSuccessPromptAsync(counter); // Stop the AppHost using aspire stop await auto.TypeAsync("aspire stop"); await auto.EnterAsync(); await auto.WaitUntilTextAsync(StopCommandStrings.AppHostStoppedSuccessfully, timeout: TimeSpan.FromMinutes(1)); await auto.WaitForSuccessPromptAsync(counter); // Exit the shell await auto.TypeAsync("exit"); await auto.EnterAsync(); await pendingRun; } }