-
Notifications
You must be signed in to change notification settings - Fork 748
Expand file tree
/
Copy pathDotnetIntegrationTestFixture.cs
More file actions
480 lines (393 loc) · 24.1 KB
/
DotnetIntegrationTestFixture.cs
File metadata and controls
480 lines (393 loc) · 24.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
#nullable disable
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using FluentAssertions;
using Microsoft.Internal.NuGet.Testing.SignedPackages.ChildProcess;
using NuGet.Commands;
using NuGet.Common;
using NuGet.Test.Utility;
using Xunit.Abstractions;
namespace Dotnet.Integration.Test
{
public class DotnetIntegrationTestFixture : IDisposable
{
/// <summary>
/// A value indicating if the test is running on a hosted agent with diagnostics enabled.
/// </summary>
internal static readonly bool CIDebug = string.Equals(Environment.GetEnvironmentVariable("SYSTEM_DEBUG"), bool.TrueString, StringComparison.OrdinalIgnoreCase);
/// <summary>
/// A value indicating the path to a directory where logs should be written to so they can be published as an artifact from a hosted build agent.
/// </summary>
internal static readonly string BinLogDirectory = Environment.GetEnvironmentVariable("BINLOG_DIRECTORY");
private static readonly object LockObject = new();
private readonly TestDirectory _cliDirectory;
private readonly SimpleTestPathContext _templateDirectory;
internal readonly string TestDotnetCli;
internal readonly string MsBuildSdksPath;
internal string SdkVersion { get; private set; }
internal DirectoryInfo SdkDirectory { get; }
public DotnetIntegrationTestFixture()
{
string testAssemblyPath = Path.GetFullPath(Assembly.GetExecutingAssembly().Location);
_cliDirectory = TestDotnetCLiUtility.CopyAndPatchLatestDotnetCli(testAssemblyPath);
var dotnetExecutableName = RuntimeEnvironmentHelper.IsWindows ? "dotnet.exe" : "dotnet";
TestDotnetCli = Path.Combine(_cliDirectory, dotnetExecutableName);
var sdkPath = Directory.EnumerateDirectories(Path.Combine(_cliDirectory, "sdk"))
.Single(d => !string.Equals(Path.GetFileName(d), "NuGetFallbackFolder", StringComparison.OrdinalIgnoreCase));
SdkDirectory = new DirectoryInfo(sdkPath);
MsBuildSdksPath = Path.Combine(sdkPath, "Sdks");
_templateDirectory = new SimpleTestPathContext();
TestDotnetCLiUtility.WriteGlobalJson(_templateDirectory.WorkingDirectory);
// some project templates use implicit packages. For example, class libraries targeting netstandard2.0
// will have an implicit package reference for NETStandard.Library, and its dependencies.
// .NET Core SDK 3.0 and later no longer ship these packages in a NuGetFallbackFolder. Therefore, we need
// to be able to download these packages. We'll download it once into the template cache's global packages
// folder, and then use that as a local source for individual tests, to minimise network access.
AddPackageSource("nuget.org", "https://api.nuget.org/v3/index.json");
// This is for pre-release packages.
AddPackageSource("dotnet", Constants.DotNetPackageSource.AbsoluteUri);
}
private void AddPackageSource(string name, string source)
{
AddSourceArgs addSourceArgs = new()
{
Configfile = _templateDirectory.NuGetConfig,
Name = name,
Source = source
};
AddSourceRunner.Run(addSourceArgs, () => NullLogger.Instance);
}
/// <summary>
/// Creates a new dotnet project of the specified type. Note that restore/build are not run when this command is invoked.
/// That is because the project generation is cached.
/// </summary>
internal void CreateDotnetNewProject(string solutionRoot, string projectName, string args, ITestOutputHelper testOutputHelper = null)
{
args = args.Trim();
string template = args.Replace(" ", "_");
var workingDirectory = Path.Combine(solutionRoot, projectName);
if (!Directory.Exists(workingDirectory))
{
Directory.CreateDirectory(workingDirectory);
}
var templateDirectory = new DirectoryInfo(Path.Combine(_templateDirectory.SolutionRoot, template));
lock (LockObject)
{
if (!templateDirectory.Exists)
{
string templateArgs = args + " --name template";
if (!templateArgs.Contains("langVersion") && (templateArgs.StartsWith("console") || templateArgs.StartsWith("classlib")))
{
templateArgs = templateArgs + " --langVersion 7.3";
}
templateDirectory.Create();
RunDotnetExpectSuccess(templateDirectory.FullName, $"new {templateArgs}", testOutputHelper: testOutputHelper);
// Delete the obj directory because it contains assets generated by running restore at dotnet new <template> time.
// These are not relevant when the project is renamed
Directory.Delete(Path.Combine(templateDirectory.FullName, "template", "obj"), recursive: true);
}
}
foreach (var file in Directory.EnumerateFiles(new DirectoryInfo(Path.Combine(templateDirectory.FullName, "template")).FullName))
{
File.Copy(file, Path.Combine(workingDirectory, Path.GetFileName(file)));
}
File.Move(
Path.Combine(workingDirectory, "template.csproj"),
Path.Combine(workingDirectory, projectName + ".csproj"));
}
internal CommandRunnerResult RestoreToolProjectExpectFailure(string workingDirectory, string projectName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RunDotnetExpectFailure(workingDirectory, $"restore {projectName}.csproj {args}", testOutputHelper: testOutputHelper);
internal CommandRunnerResult RestoreToolProjectExpectSuccess(string workingDirectory, string projectName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RunDotnetExpectSuccess(workingDirectory, $"restore {projectName}.csproj {args}", testOutputHelper: testOutputHelper);
internal CommandRunnerResult RestoreProjectExpectFailure(string workingDirectory, string projectName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RestoreProjectOrSolution(workingDirectory, $"{projectName}.csproj", args, expectSuccess: false, testOutputHelper: testOutputHelper);
internal CommandRunnerResult RestoreProjectExpectSuccess(string workingDirectory, string projectName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RestoreProjectOrSolution(workingDirectory, $"{projectName}.csproj", args, expectSuccess: true, testOutputHelper: testOutputHelper);
internal CommandRunnerResult RestoreSolutionExpectFailure(string workingDirectory, string solutionName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RestoreProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: false, testOutputHelper: testOutputHelper);
internal CommandRunnerResult RestoreSolutionExpectSuccess(string workingDirectory, string solutionName, string args = "", ITestOutputHelper testOutputHelper = null)
=> RestoreProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: true, testOutputHelper: testOutputHelper);
private CommandRunnerResult RestoreProjectOrSolution(string workingDirectory, string fileName, string args, bool expectSuccess, ITestOutputHelper testOutputHelper = null)
=> RunDotnet(workingDirectory, $"restore {fileName} {args ?? string.Empty} -nodereuse:false", expectSuccess, testOutputHelper: testOutputHelper);
/// <summary>
/// Runs dotnet with the specified arguments and expects the command to succeed. If dotnet returns a non-zero exit code, an assertion is thrown with diagnostic information.
/// </summary>
/// <param name="workingDirectory">The working directory to use when executing the command.</param>
/// <param name="args">The command-line arguments to pass to dotnet.</param>
/// <param name="environmentVariables">An optional <see cref="IReadOnlyDictionary{TKey, TValue}" /> containing environment variables to use when executing the command.</param>
internal CommandRunnerResult RunDotnetExpectSuccess(string workingDirectory, string args = "", IReadOnlyDictionary<string, string> environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action<StreamWriter> inputAction = null)
=> RunDotnet(workingDirectory, args, expectSuccess: true, environmentVariables, testOutputHelper, inputAction);
/// <summary>
/// Runs dotnet with the specified arguments and expects the command to fail. If dotnet returns an exit code of zero, an assertion is thrown with diagnostic information.
/// </summary>
/// <param name="workingDirectory">The working directory to use when executing the command.</param>
/// <param name="args">The command-line arguments to pass to dotnet.</param>
/// <param name="environmentVariables">An optional <see cref="IReadOnlyDictionary{TKey, TValue}" /> containing environment variables to use when executing the command.</param>
internal CommandRunnerResult RunDotnetExpectFailure(string workingDirectory, string args = "", IReadOnlyDictionary<string, string> environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action<StreamWriter> inputAction = null)
=> RunDotnet(workingDirectory, args, expectSuccess: false, environmentVariables, testOutputHelper, inputAction);
internal CommandRunnerResult RunDotnet(string workingDirectory, string args = "", bool expectSuccess = true, IReadOnlyDictionary<string, string> environmentVariables = null, ITestOutputHelper testOutputHelper = null, Action<StreamWriter> inputAction = null)
{
bool enableDiagnostics = CIDebug && !string.IsNullOrWhiteSpace(BinLogDirectory);
FileInfo coreHostLogFileInfo = null;
Dictionary<string, string> finalEnvironmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["MSBuildSDKsPath"] = MsBuildSdksPath,
["DOTNET_MULTILEVEL_LOOKUP"] = "0",
["DOTNET_ROOT"] = _cliDirectory,
// We need to force-override this because otherwise the MSBuildExtensionsPath
// set from the _outer_ dotnet cli call (which could be any version) will override
// the one set by the _inner_ (test environment-specific) dotnet cli call we're about to make.
// We need to ensure that this points to the correct SDK directory because this value
// is used to locate many Tasks - especially those located by relative path or name.
["MSBuildExtensionsPath"] = SdkDirectory.FullName,
["PATH"] = $"{_cliDirectory}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}"
};
if (enableDiagnostics)
{
coreHostLogFileInfo = new FileInfo(Path.GetTempFileName());
coreHostLogFileInfo.Delete();
finalEnvironmentVariables["COREHOST_TRACE"] = "1";
finalEnvironmentVariables["COREHOST_TRACEFILE"] = coreHostLogFileInfo.FullName;
finalEnvironmentVariables["COMPlus_DbgEnableElfDumpOnMacOS"] = "1";
finalEnvironmentVariables["COMPlus_DbgEnableMiniDump"] = "1";
finalEnvironmentVariables["COMPLUS_DBGENABLEMINIDUMP"] = "1";
finalEnvironmentVariables["COMPlus_DbgMiniDumpName"] = Path.Combine(BinLogDirectory, $"minidump-%p-%t.dmp");
finalEnvironmentVariables["COMPLUS_DBGMINIDUMPNAME"] = Path.Combine(BinLogDirectory, $"minidump-%p-%t.dmp");
finalEnvironmentVariables["COMPlus_DbgMiniDumpType"] = "4";
finalEnvironmentVariables["COMPLUS_DBGMINIDUMPTYPE"] = "4";
finalEnvironmentVariables["DOTNET_DbgEnableMiniDump"] = "1";
finalEnvironmentVariables["DOTNET_DbgMiniDumpName"] = Path.Combine(BinLogDirectory, $"minidump-%p-%t.dmp");
}
if (environmentVariables != null)
{
foreach (var item in environmentVariables)
{
finalEnvironmentVariables[item.Key] = item.Value;
}
}
CommandRunnerResult result = null;
try
{
Stopwatch stopwatch = Stopwatch.StartNew();
result = CommandRunner.Run(TestDotnetCli, workingDirectory, args, inputAction: inputAction, environmentVariables: finalEnvironmentVariables, testOutputHelper: testOutputHelper);
stopwatch.Stop();
if (expectSuccess)
{
result.ExitCode.Should().Be(
0,
"{0} {1} should have succeeded but returned exit code {2} after {3:N1}s with the following output:{4}{5}",
TestDotnetCli,
args,
result.ExitCode,
stopwatch.Elapsed.TotalSeconds,
Environment.NewLine,
result.AllOutput);
}
else
{
result.ExitCode.Should().NotBe(
0,
"{0} {1} should have failed with a non-zero exit code but returned exit code {2} after {3:N1}s with the following output:{4}{5}",
TestDotnetCli,
args,
result.ExitCode,
stopwatch.Elapsed.TotalSeconds,
Environment.NewLine,
result.AllOutput);
}
return result;
}
catch (Exception e) when (enableDiagnostics)
{
CreateDiagnostics(args, workingDirectory, result, finalEnvironmentVariables, coreHostLogFileInfo, e);
throw;
}
void CreateDiagnostics(string args, string workingDirectory, CommandRunnerResult result, Dictionary<string, string> environmentVariables, FileInfo coreHostLogFileInfo, Exception exception = null)
{
string key = Guid.NewGuid().ToString("N");
FileInfo diagLogFileInfo = new FileInfo(Path.Combine(BinLogDirectory, $"diagnostics-{key}.log"));
Directory.CreateDirectory(diagLogFileInfo.DirectoryName);
if (coreHostLogFileInfo != null && coreHostLogFileInfo.Exists)
{
coreHostLogFileInfo.CopyTo(Path.Combine(BinLogDirectory, $"corehost-{key}.log"));
}
using StreamWriter writer = diagLogFileInfo.CreateText();
if (result != null)
{
writer.WriteLine("Exit Code = {0}", result.ExitCode);
writer.WriteLine("Output:");
writer.WriteLine(result.Output);
writer.WriteLine("Errors:");
writer.WriteLine(result.Errors);
}
writer.WriteLine("Args = {0}", args);
writer.WriteLine("Runtime description: {0}", RuntimeInformation.FrameworkDescription);
writer.WriteLine("Runtime identifier: {0}", RuntimeInformation.RuntimeIdentifier);
if (exception != null)
{
writer.WriteLine("Exception = {0}", exception);
}
writer.WriteLine("Environment Variables:");
IDictionary actualEnvironmentVariables = Environment.GetEnvironmentVariables();
foreach (KeyValuePair<string, string> item in environmentVariables)
{
actualEnvironmentVariables[item.Key] = item.Value;
}
foreach (DictionaryEntry item in actualEnvironmentVariables.Cast<DictionaryEntry>().OrderBy(i => i.Key))
{
writer.WriteLine(" {0}={1}", item.Key, item.Value);
}
}
}
internal CommandRunnerResult PackProjectExpectFailure(string workingDirectory, string projectName, string args = "", string nuspecOutputPath = "obj", string configuration = "Debug", ITestOutputHelper testOutputHelper = null)
=> PackProjectOrSolution(workingDirectory, $"{projectName}.csproj", args, expectSuccess: false, nuspecOutputPath, configuration, testOutputHelper);
internal CommandRunnerResult PackProjectExpectSuccess(string workingDirectory, string projectName, string args = "", string nuspecOutputPath = "obj", string configuration = "Debug", ITestOutputHelper testOutputHelper = null)
=> PackProjectOrSolution(workingDirectory, $"{projectName}.csproj", args, expectSuccess: true, nuspecOutputPath, configuration, testOutputHelper);
internal CommandRunnerResult PackSolutionExpectFailure(string workingDirectory, string solutionName, string args = "", string nuspecOutputPath = "obj", string configuration = "Debug", ITestOutputHelper testOutputHelper = null)
=> PackProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: false, nuspecOutputPath, configuration, testOutputHelper);
internal CommandRunnerResult PackSolutionExpectSuccess(string workingDirectory, string solutionName, string args = "", string nuspecOutputPath = "obj", string configuration = "Debug", ITestOutputHelper testOutputHelper = null)
=> PackProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: true, nuspecOutputPath, configuration, testOutputHelper);
private CommandRunnerResult PackProjectOrSolution(string workingDirectory, string file, string args, bool expectSuccess, string nuspecOutputPath = "obj", string configuration = "Debug", ITestOutputHelper testOutputHelper = null)
{
if (nuspecOutputPath != null)
{
args = $"{args} /p:NuspecOutputPath={nuspecOutputPath}";
}
args = $"{args} /Property:Configuration={configuration}";
return RunDotnet(workingDirectory, $"pack {file} {args}", expectSuccess, testOutputHelper: testOutputHelper);
}
internal void BuildProjectExpectSuccess(string workingDirectory, string projectName, string args = "", bool? appendRidToOutputPath = false, ITestOutputHelper testOutputHelper = null)
{
if (appendRidToOutputPath != null)
{
args = $"{args} /p:AppendRuntimeIdentifierToOutputPath={appendRidToOutputPath}";
}
BuildProjectOrSolution(workingDirectory, $"{projectName}.csproj", args, expectSuccess: true, testOutputHelper: testOutputHelper);
}
internal void BuildSolutionExpectFailure(string workingDirectory, string solutionName, string args = "", bool? appendRidToOutputPath = false, ITestOutputHelper testOutputHelper = null)
=> BuildProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: false, appendRidToOutputPath, testOutputHelper);
internal void BuildSolutionExpectSuccess(string workingDirectory, string solutionName, string args = "", bool? appendRidToOutputPath = false, ITestOutputHelper testOutputHelper = null)
=> BuildProjectOrSolution(workingDirectory, $"{solutionName}.slnx", args, expectSuccess: true, appendRidToOutputPath, testOutputHelper);
private CommandRunnerResult BuildProjectOrSolution(string workingDirectory, string file, string args, bool expectSuccess = true, bool? appendRidToOutputPath = false, ITestOutputHelper testOutputHelper = null)
{
if (appendRidToOutputPath != null)
{
args = $"{args} /p:AppendRuntimeIdentifierToOutputPath={appendRidToOutputPath}";
}
return RunDotnet(workingDirectory, $"msbuild {file} {args}", expectSuccess, testOutputHelper: testOutputHelper);
}
internal TestDirectory CreateTestDirectory()
{
var testDirectory = TestDirectory.Create();
TestDotnetCLiUtility.WriteGlobalJson(testDirectory);
return testDirectory;
}
internal SimpleTestPathContext CreateSimpleTestPathContext(bool addTemplateFeed = true)
{
var simpleTestPathContext = new SimpleTestPathContext();
TestDotnetCLiUtility.WriteGlobalJson(simpleTestPathContext.WorkingDirectory);
if (addTemplateFeed)
{
// Some template and TFM combinations need packages, for example NETStandard.Library.
// The template cache should have downloaded it already, so use the template cache's
// global packages folder as a local source.
var addSourceArgs = new AddSourceArgs()
{
Configfile = simpleTestPathContext.NuGetConfig,
Name = "template",
Source = _templateDirectory.UserPackagesFolder
};
AddSourceRunner.Run(addSourceArgs, () => NullLogger.Instance);
}
return simpleTestPathContext;
}
internal TestDirectory Build(TestDirectoryBuilder testDirectoryBuilder)
{
var testDirectory = testDirectoryBuilder.Build();
TestDotnetCLiUtility.WriteGlobalJson(testDirectory);
return testDirectory;
}
public void Dispose()
{
KillDotnetExe(TestDotnetCli, _cliDirectory.Path, _templateDirectory.WorkingDirectory);
_cliDirectory.Dispose();
_templateDirectory.Dispose();
}
private static void KillDotnetExe(string pathToDotnetExe, params string[] workingDirectories)
{
foreach (Process process in Process.GetProcessesByName("dotnet"))
{
try
{
if (string.Equals(process.MainModule.FileName, pathToDotnetExe, StringComparison.OrdinalIgnoreCase))
{
process.Kill();
}
}
catch (Exception)
{
}
}
foreach (Process process in Process.GetProcesses())
{
try
{
if (workingDirectories.Any(i => process.MainModule.FileName.StartsWith(i, StringComparison.OrdinalIgnoreCase)))
{
process.Kill();
}
}
catch
{
}
}
}
/// <summary>
/// Depth-first recursive delete, with handling for descendant
/// directories open in Windows Explorer or used by another process
/// </summary>
private static void DeleteDirectory(string path)
{
foreach (string directory in Directory.EnumerateDirectories(path))
{
DeleteDirectory(directory);
}
try
{
Directory.Delete(path, true);
}
catch (IOException)
{
Directory.Delete(path, true);
}
catch (UnauthorizedAccessException)
{
var MaxTries = 100;
for (var i = 0; i < MaxTries; i++)
{
try
{
Directory.Delete(path, recursive: true);
break;
}
catch (UnauthorizedAccessException) when (i < (MaxTries - 1))
{
Thread.Sleep(100);
}
}
}
catch
{
}
}
}
}