1
1
// Licensed to the .NET Foundation under one or more agreements.
2
2
// The .NET Foundation licenses this file to you under the MIT license.
3
3
4
- using System . Diagnostics ;
5
4
using System . Runtime . InteropServices ;
6
- using System . Text ;
7
- using System . Text . Json ;
8
- using Microsoft . Extensions . DependencyInjection ;
9
5
using Xunit ;
10
6
using Xunit . Abstractions ;
11
7
using Aspire . TestProject ;
@@ -27,28 +23,23 @@ public sealed class IntegrationServicesFixture : IAsyncLifetime
27
23
public static bool TestsRunningOutsideOfRepo ;
28
24
#endif
29
25
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" ) ;
37
28
private TestResourceNames _resourcesToSkip ;
38
- private Dictionary < string , ProjectInfo > ? _projects ;
39
29
private readonly IMessageSink _diagnosticMessageSink ;
40
30
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" ) ;
41
36
42
37
public IntegrationServicesFixture ( IMessageSink diagnosticMessageSink )
43
38
{
44
39
_diagnosticMessageSink = diagnosticMessageSink ;
45
40
_testOutput = new TestOutputWrapper ( messageSink : _diagnosticMessageSink ) ;
46
41
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 } . ") ;
52
43
if ( BuildEnvironment . HasSdkWithWorkload )
53
44
{
54
45
BuildEnvironment . EnvVars [ "TestsRunningOutsideOfRepo" ] = "true" ;
@@ -58,224 +49,34 @@ public IntegrationServicesFixture(IMessageSink diagnosticMessageSink)
58
49
59
50
public async Task InitializeAsync ( )
60
51
{
61
- var appHostDirectory = Path . Combine ( BuildEnvironment . TestProjectPath , "TestProject.AppHost" ) ;
52
+ _project = new AspireProject ( "TestProject" , BuildEnvironment . TestProjectPath , _testOutput , BuildEnvironment ) ;
62
53
if ( TestsRunningOutsideOfRepo )
63
54
{
64
55
_testOutput . WriteLine ( "" ) ;
65
56
_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 } ") ;
69
59
_testOutput . WriteLine ( $ "****************************************") ;
70
60
_testOutput . WriteLine ( "" ) ;
71
61
}
72
62
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 ( ) ;
83
64
84
- string processArguments = $ "run --no-build -- ";
65
+ string extraArgs = " ";
85
66
_resourcesToSkip = GetResourcesToSkip ( ) ;
86
- if ( _resourcesToSkip != TestResourceNames . None )
67
+ if ( _resourcesToSkip != TestResourceNames . None && _resourcesToSkip . ToCSVString ( ) is string skipArg )
87
68
{
88
- if ( _resourcesToSkip . ToCSVString ( ) is string skipArg )
89
- {
90
- processArguments += $ "--skip-resources { skipArg } ";
91
- }
69
+ extraArgs += $ "--skip-resources { skipArg } ";
92
70
}
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 ] ) ;
142
72
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 ( $ "\t and 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 ( ) ;
204
73
foreach ( var project in Projects . Values )
205
74
{
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 ;
216
76
}
217
77
}
218
78
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 )
279
80
{
280
81
string component = resource switch
281
82
{
@@ -291,48 +92,18 @@ public async Task DumpComponentLogsAsync(TestResourceNames resource, ITestOutput
291
92
_ => throw new ArgumentException ( $ "Unknown resource: { resource } ")
292
93
} ;
293
94
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 ) ;
315
96
}
316
97
317
98
public async Task DisposeAsync ( )
318
99
{
319
- if ( _appHostProcess is not null )
100
+ if ( Project ? . AppHostProcess is not null )
320
101
{
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 ) ) ;
328
103
}
329
- }
330
-
331
- public void EnsureAppHostRunning ( )
332
- {
333
- if ( _appHostProcess is null || _appHostProcess . HasExited || _appExited . Task . IsCompleted )
104
+ if ( Project is not null )
334
105
{
335
- throw new InvalidOperationException ( "The app host process is not running." ) ;
106
+ await Project . DisposeAsync ( ) ;
336
107
}
337
108
}
338
109
0 commit comments