From 55ff50734558ff1959d4ac5d0c7140fb9b5e7c2f Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Mon, 8 Apr 2024 17:11:06 -0400 Subject: [PATCH 1/5] [tests] Extract support for running an aspire app for tests .. to `tests/Shared/WorkloadTesting/AspireProject.cs`, so it can be used by other tests too. --- .../IntegrationServicesFixture.cs | 237 ++------------ .../IntegrationServicesTests.cs | 2 +- tests/Shared/WorkloadTesting/AspireProject.cs | 295 ++++++++++++++++++ .../WorkloadTesting/BuildEnvironment.cs | 10 +- .../WorkloadTesting}/ProjectInfo.cs | 6 +- 5 files changed, 328 insertions(+), 222 deletions(-) create mode 100644 tests/Shared/WorkloadTesting/AspireProject.cs rename tests/{Aspire.EndToEnd.Tests => Shared/WorkloadTesting}/ProjectInfo.cs (90%) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index a420f5240d..159096f778 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -1,11 +1,7 @@ // 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.Runtime.InteropServices; -using System.Text; -using System.Text.Json; -using Microsoft.Extensions.DependencyInjection; using Xunit; using Xunit.Abstractions; using Aspire.TestProject; @@ -27,28 +23,23 @@ public sealed class IntegrationServicesFixture : IAsyncLifetime public static bool TestsRunningOutsideOfRepo; #endif - public static string? TestScenario = EnvironmentVariables.TestScenario; - public Dictionary Projects => _projects!; - public BuildEnvironment BuildEnvironment { get; init; } - public ProjectInfo IntegrationServiceA => Projects["integrationservicea"]; - - private Process? _appHostProcess; - private readonly TaskCompletionSource _appExited = new(); + public static string? TestScenario { get; } = EnvironmentVariables.TestScenario; + public Dictionary Projects => Project?.InfoTable ?? throw new InvalidOperationException("Project is not initialized"); private TestResourceNames _resourcesToSkip; - private Dictionary? _projects; private readonly IMessageSink _diagnosticMessageSink; private readonly TestOutputWrapper _testOutput; + private AspireProject? _project; + + public BuildEnvironment BuildEnvironment { get; init; } + public ProjectInfo IntegrationServiceA => Projects["integrationservicea"]; + public AspireProject Project => _project ?? throw new InvalidOperationException("Project is not initialized"); public IntegrationServicesFixture(IMessageSink diagnosticMessageSink) { _diagnosticMessageSink = diagnosticMessageSink; _testOutput = new TestOutputWrapper(messageSink: _diagnosticMessageSink); BuildEnvironment = new(TestsRunningOutsideOfRepo, (probePath, solutionRoot) => - { - throw new InvalidProgramException( - $"Running outside-of-repo: Could not find {probePath} computed from solutionRoot={solutionRoot}. " + - $"Build all the packages with `./build -pack`. And install the sdk+workload 'dotnet build tests/Aspire.EndToEnd.Tests/Aspire.EndToEnd.csproj /t:InstallWorkloadUsingArtifacts /p:Configuration="); - }); + $"Running outside-of-repo: Could not find {probePath} computed from solutionRoot={solutionRoot}. "); if (BuildEnvironment.HasSdkWithWorkload) { BuildEnvironment.EnvVars["TestsRunningOutsideOfRepo"] = "true"; @@ -58,211 +49,33 @@ public IntegrationServicesFixture(IMessageSink diagnosticMessageSink) public async Task InitializeAsync() { - var appHostDirectory = Path.Combine(BuildEnvironment.TestProjectPath, "TestProject.AppHost"); + _project = new AspireProject("TestProject", BuildEnvironment.TestProjectPath, _testOutput, BuildEnvironment); if (TestsRunningOutsideOfRepo) { _testOutput.WriteLine(""); _testOutput.WriteLine($"****************************************"); - _testOutput.WriteLine($" Running tests outside-of-repo"); - _testOutput.WriteLine($" TestProject: {appHostDirectory}"); - _testOutput.WriteLine($" Using dotnet: {BuildEnvironment.DotNet}"); + _testOutput.WriteLine($" Running EndToEnd tests outside-of-repo"); + _testOutput.WriteLine($" TestProject: {Project.AppHostProjectDirectory}"); _testOutput.WriteLine($"****************************************"); _testOutput.WriteLine(""); } - await BuildProjectAsync(); - - // Run project - object outputLock = new(); - var output = new StringBuilder(); - var projectsParsed = new TaskCompletionSource(); - var appRunning = new TaskCompletionSource(); - var stdoutComplete = new TaskCompletionSource(); - var stderrComplete = new TaskCompletionSource(); - _appHostProcess = new Process(); + await Project.BuildAsync(); - string processArguments = $"run --no-build -- "; + string extraArgs = ""; _resourcesToSkip = GetResourcesToSkip(); - if (_resourcesToSkip != TestResourceNames.None) - { - if (_resourcesToSkip.ToCSVString() is string skipArg) - { - processArguments += $"--skip-resources {skipArg}"; - } - } - _appHostProcess.StartInfo = new ProcessStartInfo(BuildEnvironment.DotNet, processArguments) - { - RedirectStandardOutput = true, - RedirectStandardError = true, - RedirectStandardInput = true, - UseShellExecute = false, - CreateNoWindow = true, - WorkingDirectory = appHostDirectory - }; - - foreach (var item in BuildEnvironment.EnvVars) + if (_resourcesToSkip != TestResourceNames.None && _resourcesToSkip.ToCSVString() is string skipArg) { - _appHostProcess.StartInfo.Environment[item.Key] = item.Value; - _testOutput.WriteLine($"\t[{item.Key}] = {item.Value}"); + extraArgs += $"--skip-resources {skipArg}"; } + await Project.StartAsync([extraArgs]); - _testOutput.WriteLine($"Starting the process: {BuildEnvironment.DotNet} {processArguments} in {_appHostProcess.StartInfo.WorkingDirectory}"); - _appHostProcess.OutputDataReceived += (sender, e) => - { - if (e.Data is null) - { - stdoutComplete.SetResult(); - return; - } - - lock(outputLock) - { - output.AppendLine(e.Data); - } - _testOutput.WriteLine($"[apphost] {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(); - } - }; - _appHostProcess.ErrorDataReceived += (sender, e) => - { - if (e.Data is null) - { - stderrComplete.SetResult(); - return; - } - - lock(outputLock) - { - output.AppendLine(e.Data); - } - _testOutput.WriteLine($"[apphost] {e.Data}"); - }; - - EventHandler appExitedCallback = (sender, e) => - { - _testOutput.WriteLine(""); - _testOutput.WriteLine($"----------- app has exited -------------"); - _testOutput.WriteLine(""); - _appExited.SetResult(); - }; - _appHostProcess.EnableRaisingEvents = true; - _appHostProcess.Exited += appExitedCallback; - - _appHostProcess.EnableRaisingEvents = true; - - _appHostProcess.Start(); - _appHostProcess.BeginOutputReadLine(); - _appHostProcess.BeginErrorReadLine(); - - var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); - var failedAppTask = _appExited.Task; - var timeoutTask = Task.Delay(TimeSpan.FromMinutes(5)); - - string outputMessage; - var resultTask = await Task.WhenAny(successfulTask, failedAppTask, timeoutTask); - if (resultTask == failedAppTask) - { - // wait for all the output to be read - var allOutputComplete = Task.WhenAll(stdoutComplete.Task, stderrComplete.Task); - var appExitTimeout = Task.Delay(TimeSpan.FromSeconds(5)); - var t = await Task.WhenAny(allOutputComplete, appExitTimeout); - if (t == appExitTimeout) - { - _testOutput.WriteLine($"\tand timed out waiting for the full output"); - } - - lock(outputLock) - { - outputMessage = output.ToString(); - } - var exceptionMessage = $"App run failed: {Environment.NewLine}{outputMessage}"; - if (outputMessage.Contains("docker was found but appears to be unhealthy", StringComparison.OrdinalIgnoreCase)) - { - exceptionMessage = "Docker was found but appears to be unhealthy. " + exceptionMessage; - } - - // should really fail and quit after this - throw new ArgumentException(exceptionMessage); - } - - lock(outputLock) - { - outputMessage = output.ToString(); - } - Assert.True(resultTask == successfulTask, $"App run failed: {Environment.NewLine}{outputMessage}"); - - var client = CreateHttpClient(); foreach (var project in Projects.Values) { - project.Client = client; - } - - async Task BuildProjectAsync() - { - using var cmd = new DotNetCommand(BuildEnvironment, _testOutput, label: "build") - .WithWorkingDirectory(appHostDirectory); - - (await cmd.ExecuteAsync(CancellationToken.None, $"build -bl:{Path.Combine(BuildEnvironment.LogRootPath, "testproject-build.binlog")} -v m")) - .EnsureSuccessful(); + project.Client = AspireProject.Client.Value; } } - private HttpClient CreateHttpClient() - { - var services = new ServiceCollection(); - services.AddHttpClient() - .ConfigureHttpClientDefaults(b => - { - b.ConfigureHttpClient(client => - { - // Disable the HttpClient timeout to allow the timeout strategies to control the timeout. - client.Timeout = Timeout.InfiniteTimeSpan; - }); - - 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(2); - options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(5); // needs to be at least double the AttemptTimeout to pass options validation - options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); - options.Retry.OnRetry = async (args) => - { - var msg = $"Retry #{args.AttemptNumber+1} for '{args.Outcome.Result?.RequestMessage?.RequestUri}'" + - $" due to StatusCode: {(int?)args.Outcome.Result?.StatusCode} ReasonPhrase: '{args.Outcome.Result?.ReasonPhrase}'"; - - msg += (args.Outcome.Exception is not null) ? $" Exception: {args.Outcome.Exception} " : ""; - if (args.Outcome.Result?.Content is HttpContent content && (await content.ReadAsStringAsync()) is string contentStr) - { - msg += $" Content:{Environment.NewLine}{contentStr}"; - } - - _testOutput.WriteLine(msg); - }; - options.Retry.MaxRetryAttempts = 20; - }); - }); - - return services.BuildServiceProvider().GetRequiredService().CreateClient(); - } - - private static Dictionary ParseProjectInfo(string json) => - JsonSerializer.Deserialize>(json)!; - public async Task DumpDockerInfoAsync(ITestOutputHelper? testOutputArg = null) { var testOutput = testOutputArg ?? _testOutput!; @@ -316,23 +129,13 @@ public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutput public async Task DisposeAsync() { - if (_appHostProcess is not null) + if (Project?.Process is not null) { await DumpDockerInfoAsync(new TestOutputWrapper(null)); - - if (!_appHostProcess.HasExited) - { - _appHostProcess.StandardInput.WriteLine("Stop"); - } - await _appHostProcess.WaitForExitAsync(); } - } - - public void EnsureAppHostRunning() - { - if (_appHostProcess is null || _appHostProcess.HasExited || _appExited.Task.IsCompleted) + if (Project is not null) { - throw new InvalidOperationException("The app host process is not running."); + await Project.DisposeAsync(); } } diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index 7000e42428..ca4e6a478f 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -98,7 +98,7 @@ public Task VerifyHealthyOnIntegrationServiceA() private async Task RunTestAsync(Func test) { - _integrationServicesFixture.EnsureAppHostRunning(); + _integrationServicesFixture.Project!.EnsureAppHostRunning(); try { await test(); diff --git a/tests/Shared/WorkloadTesting/AspireProject.cs b/tests/Shared/WorkloadTesting/AspireProject.cs new file mode 100644 index 0000000000..965ff9e93d --- /dev/null +++ b/tests/Shared/WorkloadTesting/AspireProject.cs @@ -0,0 +1,295 @@ +// 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.RegularExpressions; +using Microsoft.Extensions.DependencyInjection; +using Xunit.Abstractions; + +namespace Aspire.Workload.Tests; + +public class AspireProject : IAsyncDisposable +{ + public static Lazy Client => new(CreateHttpClient); + public static string GetNuGetConfigPathFor(string targetFramework) => + Path.Combine(BuildEnvironment.TestDataPath, "nuget8.config"); + + public Process? Process { get; private set; } + public string Id { get; init; } + public string RootDir { get; init; } + public string LogPath { get; init; } + public string AppHostProjectDirectory => Path.Combine(RootDir, $"{Id}.AppHost"); + public string ServiceDefaultsProjectPath => Path.Combine(RootDir, $"{Id}.ServiceDefaults"); + public string TestsProjectDirectory => Path.Combine(RootDir, $"{Id}.Tests"); + public Dictionary InfoTable { get; private set; } = new(capacity: 0); + public TaskCompletionSource AppExited { get; } = new(); + + private readonly ITestOutputHelper _testOutput; + private readonly BuildEnvironment _buildEnv; + + public AspireProject(string id, string baseDir, ITestOutputHelper testOutput, BuildEnvironment buildEnv) + { + Id = id; + RootDir = baseDir; + _testOutput = testOutput; + _buildEnv = buildEnv; + LogPath = Path.Combine(_buildEnv.LogRootPath, Id); + } + + public async Task StartAsync(string[]? extraArgs = default, CancellationToken token = default, Action? configureProcess = null) + { + object outputLock = new(); + var output = new StringBuilder(); + var projectsParsed = new TaskCompletionSource(); + var appRunning = new TaskCompletionSource(); + var stdoutComplete = new TaskCompletionSource(); + var stderrComplete = new TaskCompletionSource(); + Process = new Process(); + + var processArguments = $"run --no-build"; + processArguments += extraArgs is not null ? " " + string.Join(" ", extraArgs) : ""; + Process.StartInfo = new ProcessStartInfo(_buildEnv.DotNet, processArguments) + { + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true, + UseShellExecute = false, + CreateNoWindow = true, + WorkingDirectory = AppHostProjectDirectory + }; + + foreach (var item in _buildEnv.EnvVars) + { + Process.StartInfo.Environment[item.Key] = item.Value; + _testOutput.WriteLine($"\t[{item.Key}] = {item.Value}"); + } + + _testOutput.WriteLine($"Starting the process: {_buildEnv.DotNet} {processArguments} in {Process.StartInfo.WorkingDirectory}"); + Process.OutputDataReceived += (sender, e) => + { + if (e.Data is null) + { + stdoutComplete.SetResult(); + return; + } + + string line = e.Data; + string logLine = $"[apphost] {line}"; + lock(outputLock) + { + output.AppendLine(logLine); + } + _testOutput.WriteLine(logLine); + + if (line?.StartsWith("$ENDPOINTS: ") == true) + { + InfoTable = ProjectInfo.Parse(line.Substring("$ENDPOINTS: ".Length)); + projectsParsed.SetResult(); + } + + if (line?.Contains("Distributed application started") == true) + { + appRunning.SetResult(); + } + }; + Process.ErrorDataReceived += (sender, e) => + { + if (e.Data is null) + { + stderrComplete.SetResult(); + return; + } + + string line = $"[apphost] {e.Data}"; + lock(outputLock) + { + output.AppendLine(line); + } + _testOutput.WriteLine(line); + }; + + EventHandler appExitedCallback = (sender, e) => + { + _testOutput.WriteLine(""); + _testOutput.WriteLine($"----------- [{Path.GetFileName(AppHostProjectDirectory)}] app has exited -------------"); + _testOutput.WriteLine(""); + AppExited.SetResult(); + }; + Process.EnableRaisingEvents = true; + Process.Exited += appExitedCallback; + + Process.EnableRaisingEvents = true; + + configureProcess?.Invoke(Process.StartInfo); + + Process.Start(); + Process.BeginOutputReadLine(); + Process.BeginErrorReadLine(); + + var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); + var failedAppTask = AppExited.Task; + var timeoutTask = Task.Delay(TimeSpan.FromMinutes(5), token); + + string outputMessage; + // FIXME: cancellation token for the successfulTask? + var resultTask = await Task.WhenAny(successfulTask, failedAppTask, timeoutTask); + if (resultTask == failedAppTask) + { + // wait for all the output to be read + var allOutputComplete = Task.WhenAll(stdoutComplete.Task, stderrComplete.Task); + var appExitTimeout = Task.Delay(TimeSpan.FromSeconds(5), token); + var t = await Task.WhenAny(allOutputComplete, appExitTimeout); + if (t == appExitTimeout) + { + _testOutput.WriteLine($"\tand timed out waiting for the full output"); + } + + lock(outputLock) + { + outputMessage = output.ToString(); + } + var exceptionMessage = $"App run failed: {Environment.NewLine}{outputMessage}"; + if (outputMessage.Contains("docker was found but appears to be unhealthy", StringComparison.OrdinalIgnoreCase)) + { + exceptionMessage = "Docker was found but appears to be unhealthy. " + exceptionMessage; + } + + // should really fail and quit after this + throw new ArgumentException(exceptionMessage); + } + + lock(outputLock) + { + outputMessage = output.ToString(); + } + if (resultTask != successfulTask) + { + throw new InvalidOperationException($"App run failed: {Environment.NewLine}{outputMessage}"); + } + + foreach (var project in InfoTable.Values) + { + project.Client = Client.Value; + } + + _testOutput.WriteLine($"-- Ready to run tests --"); + } + + public async Task BuildAsync(CancellationToken token = default) + { + using var restoreCmd = new DotNetCommand(_buildEnv, _testOutput, label: "restore") + .WithWorkingDirectory(Path.Combine(RootDir, $"{Id}.AppHost")); + var res = await restoreCmd.ExecuteAsync($"restore -bl:{Path.Combine(LogPath!, $"{Id}-restore.binlog")} /p:TreatWarningsAsErrors=true"); + res.EnsureSuccessful(); + + using var buildCmd = new DotNetCommand(_buildEnv, _testOutput, label: "build") + .WithWorkingDirectory(Path.Combine(RootDir, $"{Id}.AppHost")); + res = await buildCmd.ExecuteAsync($"build -bl:{Path.Combine(LogPath!, $"{Id}-build.binlog")} /p:TreatWarningsAsErrors=true"); + res.EnsureSuccessful(); + } + + public async ValueTask DisposeAsync() + { + // TODO: check that everything shutdown + if (Process is null) + { + return; + } + + await DumpDockerInfoAsync(new TestOutputWrapper(null)); + + if (!Process.HasExited) + { + Process.StandardInput.WriteLine("Stop"); + } + await Process.WaitForExitAsync(); + } + + public async Task DumpDockerInfoAsync(ITestOutputHelper? testOutputArg = null) + { + var testOutput = testOutputArg ?? _testOutput!; + testOutput.WriteLine("--------------------------- Docker info ---------------------------"); + + using var cmd = new ToolCommand("docker", testOutput!, "container-list"); + (await cmd.ExecuteAsync(CancellationToken.None, $"container list --all")) + .EnsureSuccessful(); + + testOutput.WriteLine("--------------------------- Docker info (end) ---------------------------"); + } + + public async Task DumpComponentLogsAsync(string component, ITestOutputHelper? testOutputArg = null) + { + var testOutput = testOutputArg ?? _testOutput!; + var cts = new CancellationTokenSource(); + + string containerName; + { + using var cmd = new ToolCommand("docker", testOutput, label: "container-list"); + var res = (await cmd.ExecuteAsync(cts.Token, $"container list --all --filter name={component} --format {{{{.Names}}}}")) + .EnsureSuccessful(); + containerName = res.Output; + } + + if (string.IsNullOrEmpty(containerName)) + { + testOutput.WriteLine($"No container found for {component}"); + } + else + { + using var cmd = new ToolCommand("docker", testOutput, label: component); + (await cmd.ExecuteAsync(cts.Token, $"container logs {containerName} -n 50")) + .EnsureSuccessful(); + } + } + + public void EnsureAppHostRunning() + { + if (Process is null || Process.HasExited || AppExited.Task.IsCompleted) + { + throw new InvalidOperationException("The app host process is not running."); + } + } + + private static HttpClient CreateHttpClient() + { + var services = new ServiceCollection(); + services.AddHttpClient() + .ConfigureHttpClientDefaults(b => + { + b.ConfigureHttpClient(client => + { + // Disable the HttpClient timeout to allow the timeout strategies to control the timeout. + client.Timeout = Timeout.InfiniteTimeSpan; + }); + + 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(2); + options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(5); // needs to be at least double the AttemptTimeout to pass options validation + options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10); + options.Retry.OnRetry = async (args) => + { + var msg = $"Retry #{args.AttemptNumber+1} for '{args.Outcome.Result?.RequestMessage?.RequestUri}'" + + $" due to StatusCode: {(int?)args.Outcome.Result?.StatusCode} ReasonPhrase: '{args.Outcome.Result?.ReasonPhrase}'"; + + msg += (args.Outcome.Exception is not null) ? $" Exception: {args.Outcome.Exception} " : ""; + if (args.Outcome.Result?.Content is HttpContent content && (await content.ReadAsStringAsync()) is string contentStr) + { + msg += $" Content:{Environment.NewLine}{contentStr}"; + } + }; + options.Retry.MaxRetryAttempts = 20; + }); + }); + + return services.BuildServiceProvider().GetRequiredService().CreateClient(); + } +} diff --git a/tests/Shared/WorkloadTesting/BuildEnvironment.cs b/tests/Shared/WorkloadTesting/BuildEnvironment.cs index c4f7b43f49..dba5537716 100644 --- a/tests/Shared/WorkloadTesting/BuildEnvironment.cs +++ b/tests/Shared/WorkloadTesting/BuildEnvironment.cs @@ -26,7 +26,7 @@ public class BuildEnvironment public static bool IsRunningOnCIBuildMachine => Environment.GetEnvironmentVariable("BUILD_BUILDID") is not null; public static bool IsRunningOnCI => IsRunningOnHelix || IsRunningOnCIBuildMachine; - public BuildEnvironment(bool expectSdkWithWorkload = true, Action? sdkWithWorkloadNotFound = null) + public BuildEnvironment(bool expectSdkWithWorkload = true, Func? sdkWithWorkloadNotFound = null) { DirectoryInfo? solutionRoot = new(AppContext.BaseDirectory); while (solutionRoot != null) @@ -54,8 +54,12 @@ public BuildEnvironment(bool expectSdkWithWorkload = true, Action Parse(string json) => + JsonSerializer.Deserialize>(json)!; } public record EndpointInfo(string Name, string Uri); From 562b43235ee96c55f4968c7514bf7e4ce86e6c8f Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Apr 2024 13:24:40 -0400 Subject: [PATCH 2/5] Address review feedback from @ eerhardt - Rename `AspireProject.Process` to `AspireProject.AppHostProcess` - remove nuget8.config which isn't needed yet --- .../IntegrationServicesFixture.cs | 2 +- tests/Shared/WorkloadTesting/AspireProject.cs | 42 +++++++++---------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 159096f778..6639061d75 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -129,7 +129,7 @@ public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutput public async Task DisposeAsync() { - if (Project?.Process is not null) + if (Project?.AppHostProcess is not null) { await DumpDockerInfoAsync(new TestOutputWrapper(null)); } diff --git a/tests/Shared/WorkloadTesting/AspireProject.cs b/tests/Shared/WorkloadTesting/AspireProject.cs index 965ff9e93d..432e33a4bf 100644 --- a/tests/Shared/WorkloadTesting/AspireProject.cs +++ b/tests/Shared/WorkloadTesting/AspireProject.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Text; -using System.Text.RegularExpressions; using Microsoft.Extensions.DependencyInjection; using Xunit.Abstractions; @@ -12,10 +11,7 @@ namespace Aspire.Workload.Tests; public class AspireProject : IAsyncDisposable { public static Lazy Client => new(CreateHttpClient); - public static string GetNuGetConfigPathFor(string targetFramework) => - Path.Combine(BuildEnvironment.TestDataPath, "nuget8.config"); - - public Process? Process { get; private set; } + public Process? AppHostProcess { get; private set; } public string Id { get; init; } public string RootDir { get; init; } public string LogPath { get; init; } @@ -45,11 +41,11 @@ public async Task StartAsync(string[]? extraArgs = default, CancellationToken to var appRunning = new TaskCompletionSource(); var stdoutComplete = new TaskCompletionSource(); var stderrComplete = new TaskCompletionSource(); - Process = new Process(); + AppHostProcess = new Process(); var processArguments = $"run --no-build"; processArguments += extraArgs is not null ? " " + string.Join(" ", extraArgs) : ""; - Process.StartInfo = new ProcessStartInfo(_buildEnv.DotNet, processArguments) + AppHostProcess.StartInfo = new ProcessStartInfo(_buildEnv.DotNet, processArguments) { RedirectStandardOutput = true, RedirectStandardError = true, @@ -61,12 +57,12 @@ public async Task StartAsync(string[]? extraArgs = default, CancellationToken to foreach (var item in _buildEnv.EnvVars) { - Process.StartInfo.Environment[item.Key] = item.Value; + AppHostProcess.StartInfo.Environment[item.Key] = item.Value; _testOutput.WriteLine($"\t[{item.Key}] = {item.Value}"); } - _testOutput.WriteLine($"Starting the process: {_buildEnv.DotNet} {processArguments} in {Process.StartInfo.WorkingDirectory}"); - Process.OutputDataReceived += (sender, e) => + _testOutput.WriteLine($"Starting the process: {_buildEnv.DotNet} {processArguments} in {AppHostProcess.StartInfo.WorkingDirectory}"); + AppHostProcess.OutputDataReceived += (sender, e) => { if (e.Data is null) { @@ -93,7 +89,7 @@ public async Task StartAsync(string[]? extraArgs = default, CancellationToken to appRunning.SetResult(); } }; - Process.ErrorDataReceived += (sender, e) => + AppHostProcess.ErrorDataReceived += (sender, e) => { if (e.Data is null) { @@ -116,16 +112,16 @@ public async Task StartAsync(string[]? extraArgs = default, CancellationToken to _testOutput.WriteLine(""); AppExited.SetResult(); }; - Process.EnableRaisingEvents = true; - Process.Exited += appExitedCallback; + AppHostProcess.EnableRaisingEvents = true; + AppHostProcess.Exited += appExitedCallback; - Process.EnableRaisingEvents = true; + AppHostProcess.EnableRaisingEvents = true; - configureProcess?.Invoke(Process.StartInfo); + configureProcess?.Invoke(AppHostProcess.StartInfo); - Process.Start(); - Process.BeginOutputReadLine(); - Process.BeginErrorReadLine(); + AppHostProcess.Start(); + AppHostProcess.BeginOutputReadLine(); + AppHostProcess.BeginErrorReadLine(); var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task); var failedAppTask = AppExited.Task; @@ -192,18 +188,18 @@ public async Task BuildAsync(CancellationToken token = default) public async ValueTask DisposeAsync() { // TODO: check that everything shutdown - if (Process is null) + if (AppHostProcess is null) { return; } await DumpDockerInfoAsync(new TestOutputWrapper(null)); - if (!Process.HasExited) + if (!AppHostProcess.HasExited) { - Process.StandardInput.WriteLine("Stop"); + AppHostProcess.StandardInput.WriteLine("Stop"); } - await Process.WaitForExitAsync(); + await AppHostProcess.WaitForExitAsync(); } public async Task DumpDockerInfoAsync(ITestOutputHelper? testOutputArg = null) @@ -245,7 +241,7 @@ public async Task DumpComponentLogsAsync(string component, ITestOutputHelper? te public void EnsureAppHostRunning() { - if (Process is null || Process.HasExited || AppExited.Task.IsCompleted) + if (AppHostProcess is null || AppHostProcess.HasExited || AppExited.Task.IsCompleted) { throw new InvalidOperationException("The app host process is not running."); } From 6589e6fed354b7acad80ba9ca1bb74f5ae7f9529 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Apr 2024 13:26:36 -0400 Subject: [PATCH 3/5] Update tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs Co-authored-by: Eric Erhardt --- tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index ca4e6a478f..ab9d421c92 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -98,7 +98,7 @@ public Task VerifyHealthyOnIntegrationServiceA() private async Task RunTestAsync(Func test) { - _integrationServicesFixture.Project!.EnsureAppHostRunning(); + _integrationServicesFixture.Project.EnsureAppHostRunning(); try { await test(); From 65b65022f2043c873ea4b52191d0728ff1ba2bd8 Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Apr 2024 13:30:03 -0400 Subject: [PATCH 4/5] address review feedback from @ eerhardt --- tests/Shared/WorkloadTesting/AspireProject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Shared/WorkloadTesting/AspireProject.cs b/tests/Shared/WorkloadTesting/AspireProject.cs index 432e33a4bf..0a4f62f123 100644 --- a/tests/Shared/WorkloadTesting/AspireProject.cs +++ b/tests/Shared/WorkloadTesting/AspireProject.cs @@ -281,6 +281,7 @@ private static HttpClient CreateHttpClient() { msg += $" Content:{Environment.NewLine}{contentStr}"; } + Console.WriteLine(msg); }; options.Retry.MaxRetryAttempts = 20; }); From 3478d924aa2beec7409fd82fded741138c4dc75b Mon Sep 17 00:00:00 2001 From: Ankit Jain Date: Thu, 11 Apr 2024 13:35:36 -0400 Subject: [PATCH 5/5] Address review feedback from @ eerhardt, and remove some duplication --- .../IntegrationServicesFixture.cs | 38 ++----------------- .../IntegrationServicesTests.cs | 2 +- 2 files changed, 4 insertions(+), 36 deletions(-) diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs index 6639061d75..f6e3aef478 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs @@ -76,19 +76,7 @@ public async Task InitializeAsync() } } - public async Task DumpDockerInfoAsync(ITestOutputHelper? testOutputArg = null) - { - var testOutput = testOutputArg ?? _testOutput!; - testOutput.WriteLine("--------------------------- Docker info ---------------------------"); - - using var cmd = new ToolCommand("docker", testOutput!, "container-list"); - (await cmd.ExecuteAsync(CancellationToken.None, $"container list --all")) - .EnsureSuccessful(); - - testOutput.WriteLine("--------------------------- Docker info (end) ---------------------------"); - } - - public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutputHelper? testOutputArg = null) + public Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutputHelper? testOutputArg = null) { string component = resource switch { @@ -104,34 +92,14 @@ public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutput _ => throw new ArgumentException($"Unknown resource: {resource}") }; - var testOutput = testOutputArg ?? _testOutput!; - var cts = new CancellationTokenSource(); - - string containerName; - { - using var cmd = new ToolCommand("docker", testOutput); - var res = (await cmd.ExecuteAsync(cts.Token, $"container list --all --filter name={component} --format {{{{.Names}}}}")) - .EnsureSuccessful(); - containerName = res.Output; - } - - if (string.IsNullOrEmpty(containerName)) - { - testOutput.WriteLine($"No container found for {component}"); - } - else - { - using var cmd = new ToolCommand("docker", testOutput, label: component); - (await cmd.ExecuteAsync(cts.Token, $"container logs {containerName} -n 50")) - .EnsureSuccessful(); - } + return Project.DumpComponentLogsAsync(component, testOutputArg); } public async Task DisposeAsync() { if (Project?.AppHostProcess is not null) { - await DumpDockerInfoAsync(new TestOutputWrapper(null)); + await Project.DumpDockerInfoAsync(new TestOutputWrapper(null)); } if (Project is not null) { diff --git a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs index ab9d421c92..3ff76f491c 100644 --- a/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs +++ b/tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs @@ -105,7 +105,7 @@ private async Task RunTestAsync(Func test) } catch { - await _integrationServicesFixture.DumpDockerInfoAsync(); + await _integrationServicesFixture.Project.DumpDockerInfoAsync(); throw; } }