Skip to content

Commit d0ddcbb

Browse files
radicaleerhardt
andauthored
[tests] Extract support for running an aspire app for tests (dotnet#3493)
* [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. * Address review feedback from @ eerhardt - Rename `AspireProject.Process` to `AspireProject.AppHostProcess` - remove nuget8.config which isn't needed yet * Update tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs Co-authored-by: Eric Erhardt <[email protected]> * address review feedback from @ eerhardt * Address review feedback from @ eerhardt, and remove some duplication --------- Co-authored-by: Eric Erhardt <[email protected]>
1 parent 2a2e6c7 commit d0ddcbb

File tree

5 files changed

+329
-258
lines changed

5 files changed

+329
-258
lines changed

tests/Aspire.EndToEnd.Tests/IntegrationServicesFixture.cs

+23-252
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Diagnostics;
54
using System.Runtime.InteropServices;
6-
using System.Text;
7-
using System.Text.Json;
8-
using Microsoft.Extensions.DependencyInjection;
95
using Xunit;
106
using Xunit.Abstractions;
117
using Aspire.TestProject;
@@ -27,28 +23,23 @@ public sealed class IntegrationServicesFixture : IAsyncLifetime
2723
public static bool TestsRunningOutsideOfRepo;
2824
#endif
2925

30-
public static string? TestScenario = EnvironmentVariables.TestScenario;
31-
public Dictionary<string, ProjectInfo> Projects => _projects!;
32-
public BuildEnvironment BuildEnvironment { get; init; }
33-
public ProjectInfo IntegrationServiceA => Projects["integrationservicea"];
34-
35-
private Process? _appHostProcess;
36-
private readonly TaskCompletionSource _appExited = new();
26+
public static string? TestScenario { get; } = EnvironmentVariables.TestScenario;
27+
public Dictionary<string, ProjectInfo> Projects => Project?.InfoTable ?? throw new InvalidOperationException("Project is not initialized");
3728
private TestResourceNames _resourcesToSkip;
38-
private Dictionary<string, ProjectInfo>? _projects;
3929
private readonly IMessageSink _diagnosticMessageSink;
4030
private readonly TestOutputWrapper _testOutput;
31+
private AspireProject? _project;
32+
33+
public BuildEnvironment BuildEnvironment { get; init; }
34+
public ProjectInfo IntegrationServiceA => Projects["integrationservicea"];
35+
public AspireProject Project => _project ?? throw new InvalidOperationException("Project is not initialized");
4136

4237
public IntegrationServicesFixture(IMessageSink diagnosticMessageSink)
4338
{
4439
_diagnosticMessageSink = diagnosticMessageSink;
4540
_testOutput = new TestOutputWrapper(messageSink: _diagnosticMessageSink);
4641
BuildEnvironment = new(TestsRunningOutsideOfRepo, (probePath, solutionRoot) =>
47-
{
48-
throw new InvalidProgramException(
49-
$"Running outside-of-repo: Could not find {probePath} computed from solutionRoot={solutionRoot}. " +
50-
$"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=<config>");
51-
});
42+
$"Running outside-of-repo: Could not find {probePath} computed from solutionRoot={solutionRoot}. ");
5243
if (BuildEnvironment.HasSdkWithWorkload)
5344
{
5445
BuildEnvironment.EnvVars["TestsRunningOutsideOfRepo"] = "true";
@@ -58,224 +49,34 @@ public IntegrationServicesFixture(IMessageSink diagnosticMessageSink)
5849

5950
public async Task InitializeAsync()
6051
{
61-
var appHostDirectory = Path.Combine(BuildEnvironment.TestProjectPath, "TestProject.AppHost");
52+
_project = new AspireProject("TestProject", BuildEnvironment.TestProjectPath, _testOutput, BuildEnvironment);
6253
if (TestsRunningOutsideOfRepo)
6354
{
6455
_testOutput.WriteLine("");
6556
_testOutput.WriteLine($"****************************************");
66-
_testOutput.WriteLine($" Running tests outside-of-repo");
67-
_testOutput.WriteLine($" TestProject: {appHostDirectory}");
68-
_testOutput.WriteLine($" Using dotnet: {BuildEnvironment.DotNet}");
57+
_testOutput.WriteLine($" Running EndToEnd tests outside-of-repo");
58+
_testOutput.WriteLine($" TestProject: {Project.AppHostProjectDirectory}");
6959
_testOutput.WriteLine($"****************************************");
7060
_testOutput.WriteLine("");
7161
}
7262

73-
await BuildProjectAsync();
74-
75-
// Run project
76-
object outputLock = new();
77-
var output = new StringBuilder();
78-
var projectsParsed = new TaskCompletionSource();
79-
var appRunning = new TaskCompletionSource();
80-
var stdoutComplete = new TaskCompletionSource();
81-
var stderrComplete = new TaskCompletionSource();
82-
_appHostProcess = new Process();
63+
await Project.BuildAsync();
8364

84-
string processArguments = $"run --no-build -- ";
65+
string extraArgs = "";
8566
_resourcesToSkip = GetResourcesToSkip();
86-
if (_resourcesToSkip != TestResourceNames.None)
67+
if (_resourcesToSkip != TestResourceNames.None && _resourcesToSkip.ToCSVString() is string skipArg)
8768
{
88-
if (_resourcesToSkip.ToCSVString() is string skipArg)
89-
{
90-
processArguments += $"--skip-resources {skipArg}";
91-
}
69+
extraArgs += $"--skip-resources {skipArg}";
9270
}
93-
_appHostProcess.StartInfo = new ProcessStartInfo(BuildEnvironment.DotNet, processArguments)
94-
{
95-
RedirectStandardOutput = true,
96-
RedirectStandardError = true,
97-
RedirectStandardInput = true,
98-
UseShellExecute = false,
99-
CreateNoWindow = true,
100-
WorkingDirectory = appHostDirectory
101-
};
102-
103-
foreach (var item in BuildEnvironment.EnvVars)
104-
{
105-
_appHostProcess.StartInfo.Environment[item.Key] = item.Value;
106-
_testOutput.WriteLine($"\t[{item.Key}] = {item.Value}");
107-
}
108-
109-
_testOutput.WriteLine($"Starting the process: {BuildEnvironment.DotNet} {processArguments} in {_appHostProcess.StartInfo.WorkingDirectory}");
110-
_appHostProcess.OutputDataReceived += (sender, e) =>
111-
{
112-
if (e.Data is null)
113-
{
114-
stdoutComplete.SetResult();
115-
return;
116-
}
117-
118-
lock(outputLock)
119-
{
120-
output.AppendLine(e.Data);
121-
}
122-
_testOutput.WriteLine($"[apphost] {e.Data}");
123-
124-
if (e.Data?.StartsWith("$ENDPOINTS: ") == true)
125-
{
126-
_projects = ParseProjectInfo(e.Data.Substring("$ENDPOINTS: ".Length));
127-
projectsParsed.SetResult();
128-
}
129-
130-
if (e.Data?.Contains("Distributed application started") == true)
131-
{
132-
appRunning.SetResult();
133-
}
134-
};
135-
_appHostProcess.ErrorDataReceived += (sender, e) =>
136-
{
137-
if (e.Data is null)
138-
{
139-
stderrComplete.SetResult();
140-
return;
141-
}
71+
await Project.StartAsync([extraArgs]);
14272

143-
lock(outputLock)
144-
{
145-
output.AppendLine(e.Data);
146-
}
147-
_testOutput.WriteLine($"[apphost] {e.Data}");
148-
};
149-
150-
EventHandler appExitedCallback = (sender, e) =>
151-
{
152-
_testOutput.WriteLine("");
153-
_testOutput.WriteLine($"----------- app has exited -------------");
154-
_testOutput.WriteLine("");
155-
_appExited.SetResult();
156-
};
157-
_appHostProcess.EnableRaisingEvents = true;
158-
_appHostProcess.Exited += appExitedCallback;
159-
160-
_appHostProcess.EnableRaisingEvents = true;
161-
162-
_appHostProcess.Start();
163-
_appHostProcess.BeginOutputReadLine();
164-
_appHostProcess.BeginErrorReadLine();
165-
166-
var successfulTask = Task.WhenAll(appRunning.Task, projectsParsed.Task);
167-
var failedAppTask = _appExited.Task;
168-
var timeoutTask = Task.Delay(TimeSpan.FromMinutes(5));
169-
170-
string outputMessage;
171-
var resultTask = await Task.WhenAny(successfulTask, failedAppTask, timeoutTask);
172-
if (resultTask == failedAppTask)
173-
{
174-
// wait for all the output to be read
175-
var allOutputComplete = Task.WhenAll(stdoutComplete.Task, stderrComplete.Task);
176-
var appExitTimeout = Task.Delay(TimeSpan.FromSeconds(5));
177-
var t = await Task.WhenAny(allOutputComplete, appExitTimeout);
178-
if (t == appExitTimeout)
179-
{
180-
_testOutput.WriteLine($"\tand timed out waiting for the full output");
181-
}
182-
183-
lock(outputLock)
184-
{
185-
outputMessage = output.ToString();
186-
}
187-
var exceptionMessage = $"App run failed: {Environment.NewLine}{outputMessage}";
188-
if (outputMessage.Contains("docker was found but appears to be unhealthy", StringComparison.OrdinalIgnoreCase))
189-
{
190-
exceptionMessage = "Docker was found but appears to be unhealthy. " + exceptionMessage;
191-
}
192-
193-
// should really fail and quit after this
194-
throw new ArgumentException(exceptionMessage);
195-
}
196-
197-
lock(outputLock)
198-
{
199-
outputMessage = output.ToString();
200-
}
201-
Assert.True(resultTask == successfulTask, $"App run failed: {Environment.NewLine}{outputMessage}");
202-
203-
var client = CreateHttpClient();
20473
foreach (var project in Projects.Values)
20574
{
206-
project.Client = client;
207-
}
208-
209-
async Task BuildProjectAsync()
210-
{
211-
using var cmd = new DotNetCommand(BuildEnvironment, _testOutput, label: "build")
212-
.WithWorkingDirectory(appHostDirectory);
213-
214-
(await cmd.ExecuteAsync(CancellationToken.None, $"build -bl:{Path.Combine(BuildEnvironment.LogRootPath, "testproject-build.binlog")} -v m"))
215-
.EnsureSuccessful();
75+
project.Client = AspireProject.Client.Value;
21676
}
21777
}
21878

219-
private HttpClient CreateHttpClient()
220-
{
221-
var services = new ServiceCollection();
222-
services.AddHttpClient()
223-
.ConfigureHttpClientDefaults(b =>
224-
{
225-
b.ConfigureHttpClient(client =>
226-
{
227-
// Disable the HttpClient timeout to allow the timeout strategies to control the timeout.
228-
client.Timeout = Timeout.InfiniteTimeSpan;
229-
});
230-
231-
b.UseSocketsHttpHandler((handler, sp) =>
232-
{
233-
handler.PooledConnectionLifetime = TimeSpan.FromSeconds(5);
234-
handler.ConnectTimeout = TimeSpan.FromSeconds(5);
235-
});
236-
237-
// Ensure transient errors are retried for up to 5 minutes
238-
b.AddStandardResilienceHandler(options =>
239-
{
240-
options.AttemptTimeout.Timeout = TimeSpan.FromMinutes(2);
241-
options.CircuitBreaker.SamplingDuration = TimeSpan.FromMinutes(5); // needs to be at least double the AttemptTimeout to pass options validation
242-
options.TotalRequestTimeout.Timeout = TimeSpan.FromMinutes(10);
243-
options.Retry.OnRetry = async (args) =>
244-
{
245-
var msg = $"Retry #{args.AttemptNumber+1} for '{args.Outcome.Result?.RequestMessage?.RequestUri}'" +
246-
$" due to StatusCode: {(int?)args.Outcome.Result?.StatusCode} ReasonPhrase: '{args.Outcome.Result?.ReasonPhrase}'";
247-
248-
msg += (args.Outcome.Exception is not null) ? $" Exception: {args.Outcome.Exception} " : "";
249-
if (args.Outcome.Result?.Content is HttpContent content && (await content.ReadAsStringAsync()) is string contentStr)
250-
{
251-
msg += $" Content:{Environment.NewLine}{contentStr}";
252-
}
253-
254-
_testOutput.WriteLine(msg);
255-
};
256-
options.Retry.MaxRetryAttempts = 20;
257-
});
258-
});
259-
260-
return services.BuildServiceProvider().GetRequiredService<IHttpClientFactory>().CreateClient();
261-
}
262-
263-
private static Dictionary<string, ProjectInfo> ParseProjectInfo(string json) =>
264-
JsonSerializer.Deserialize<Dictionary<string, ProjectInfo>>(json)!;
265-
266-
public async Task DumpDockerInfoAsync(ITestOutputHelper? testOutputArg = null)
267-
{
268-
var testOutput = testOutputArg ?? _testOutput!;
269-
testOutput.WriteLine("--------------------------- Docker info ---------------------------");
270-
271-
using var cmd = new ToolCommand("docker", testOutput!, "container-list");
272-
(await cmd.ExecuteAsync(CancellationToken.None, $"container list --all"))
273-
.EnsureSuccessful();
274-
275-
testOutput.WriteLine("--------------------------- Docker info (end) ---------------------------");
276-
}
277-
278-
public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutputHelper? testOutputArg = null)
79+
public Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutputHelper? testOutputArg = null)
27980
{
28081
string component = resource switch
28182
{
@@ -291,48 +92,18 @@ public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutput
29192
_ => throw new ArgumentException($"Unknown resource: {resource}")
29293
};
29394

294-
var testOutput = testOutputArg ?? _testOutput!;
295-
var cts = new CancellationTokenSource();
296-
297-
string containerName;
298-
{
299-
using var cmd = new ToolCommand("docker", testOutput);
300-
var res = (await cmd.ExecuteAsync(cts.Token, $"container list --all --filter name={component} --format {{{{.Names}}}}"))
301-
.EnsureSuccessful();
302-
containerName = res.Output;
303-
}
304-
305-
if (string.IsNullOrEmpty(containerName))
306-
{
307-
testOutput.WriteLine($"No container found for {component}");
308-
}
309-
else
310-
{
311-
using var cmd = new ToolCommand("docker", testOutput, label: component);
312-
(await cmd.ExecuteAsync(cts.Token, $"container logs {containerName} -n 50"))
313-
.EnsureSuccessful();
314-
}
95+
return Project.DumpComponentLogsAsync(component, testOutputArg);
31596
}
31697

31798
public async Task DisposeAsync()
31899
{
319-
if (_appHostProcess is not null)
100+
if (Project?.AppHostProcess is not null)
320101
{
321-
await DumpDockerInfoAsync(new TestOutputWrapper(null));
322-
323-
if (!_appHostProcess.HasExited)
324-
{
325-
_appHostProcess.StandardInput.WriteLine("Stop");
326-
}
327-
await _appHostProcess.WaitForExitAsync();
102+
await Project.DumpDockerInfoAsync(new TestOutputWrapper(null));
328103
}
329-
}
330-
331-
public void EnsureAppHostRunning()
332-
{
333-
if (_appHostProcess is null || _appHostProcess.HasExited || _appExited.Task.IsCompleted)
104+
if (Project is not null)
334105
{
335-
throw new InvalidOperationException("The app host process is not running.");
106+
await Project.DisposeAsync();
336107
}
337108
}
338109

tests/Aspire.EndToEnd.Tests/IntegrationServicesTests.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -99,14 +99,14 @@ public Task VerifyHealthyOnIntegrationServiceA()
9999

100100
private async Task RunTestAsync(Func<Task> test)
101101
{
102-
_integrationServicesFixture.EnsureAppHostRunning();
102+
_integrationServicesFixture.Project.EnsureAppHostRunning();
103103
try
104104
{
105105
await test();
106106
}
107107
catch
108108
{
109-
await _integrationServicesFixture.DumpDockerInfoAsync();
109+
await _integrationServicesFixture.Project.DumpDockerInfoAsync();
110110
throw;
111111
}
112112
}

0 commit comments

Comments
 (0)