-
Notifications
You must be signed in to change notification settings - Fork 335
/
Copy pathDotnetTestHostManager.cs
426 lines (364 loc) · 19.3 KB
/
DotnetTestHostManager.cs
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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
namespace Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Hosting
{
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyModel;
using Microsoft.TestPlatform.TestHostProvider.Hosting;
using Microsoft.TestPlatform.TestHostProvider.Resources;
using Microsoft.VisualStudio.TestPlatform.CoreUtilities.Extensions;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Helpers;
using Microsoft.VisualStudio.TestPlatform.CrossPlatEngine.Helpers.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Client.Interfaces;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Host;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Logging;
using Microsoft.VisualStudio.TestPlatform.ObjectModel.Utilities;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions;
using Microsoft.VisualStudio.TestPlatform.PlatformAbstractions.Interfaces;
using Microsoft.VisualStudio.TestPlatform.Utilities;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers;
using Microsoft.VisualStudio.TestPlatform.Utilities.Helpers.Interfaces;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
/// <summary>
/// A host manager for <c>dotnet</c> core runtime.
/// </summary>
/// <remarks>
/// Note that some functionality of this entity overlaps with that of <see cref="DefaultTestHostManager"/>. That is
/// intentional since we want to move this to a separate assembly (with some runtime extensibility discovery).
/// </remarks>
[ExtensionUri(DotnetTestHostUri)]
[FriendlyName(DotnetTestHostFriendlyName)]
public class DotnetTestHostManager : ITestRuntimeProvider
{
private const string DotnetTestHostUri = "HostProvider://DotnetTestHost";
private const string DotnetTestHostFriendlyName = "DotnetTestHost";
private const string TestAdapterRegexPattern = @"TestAdapter.dll";
private IDotnetHostHelper dotnetHostHelper;
private IProcessHelper processHelper;
private IFileHelper fileHelper;
private ITestHostLauncher customTestHostLauncher;
private Process testHostProcess;
private StringBuilder testHostProcessStdError;
private IMessageLogger messageLogger;
private bool hostExitedEventRaised;
private string hostPackageVersion = "15.0.0";
/// <summary>
/// Initializes a new instance of the <see cref="DotnetTestHostManager"/> class.
/// </summary>
public DotnetTestHostManager()
: this(new ProcessHelper(), new FileHelper(), new DotnetHostHelper())
{
}
/// <summary>
/// Initializes a new instance of the <see cref="DotnetTestHostManager"/> class.
/// </summary>
/// <param name="processHelper">Process helper instance.</param>
/// <param name="fileHelper">File helper instance.</param>
/// <param name="dotnetHostHelper">DotnetHostHelper helper instance.</param>
internal DotnetTestHostManager(
IProcessHelper processHelper,
IFileHelper fileHelper,
IDotnetHostHelper dotnetHostHelper)
{
this.processHelper = processHelper;
this.fileHelper = fileHelper;
this.dotnetHostHelper = dotnetHostHelper;
}
/// <inheritdoc />
public event EventHandler<HostProviderEventArgs> HostLaunched;
/// <inheritdoc />
public event EventHandler<HostProviderEventArgs> HostExited;
/// <summary>
/// Gets a value indicating whether gets a value indicating if the test host can be shared for multiple sources.
/// </summary>
/// <remarks>
/// Dependency resolution for .net core projects are pivoted by the test project. Hence each test
/// project must be launched in a separate test host process.
/// </remarks>
public bool Shared => false;
/// <summary>
/// Gets a value indicating whether the test host supports protocol version check
/// </summary>
internal virtual bool IsVersionCheckRequired => !this.hostPackageVersion.StartsWith("15.0.0");
/// <summary>
/// Gets a value indicating whether the test host supports protocol version check
/// </summary>
internal bool MakeRunsettingsCompatible => this.hostPackageVersion.StartsWith("15.0.0-preview");
/// <summary>
/// Gets callback on process exit
/// </summary>
private Action<object> ExitCallBack => (process) =>
{
TestHostManagerCallbacks.ExitCallBack(this.processHelper, process, this.testHostProcessStdError, this.OnHostExited);
};
/// <summary>
/// Gets callback to read from process error stream
/// </summary>
private Action<object, string> ErrorReceivedCallback => (process, data) =>
{
TestHostManagerCallbacks.ErrorReceivedCallback(this.testHostProcessStdError, data);
};
/// <inheritdoc/>
public void Initialize(IMessageLogger logger, string runsettingsXml)
{
this.messageLogger = logger;
this.hostExitedEventRaised = false;
}
/// <inheritdoc/>
public void SetCustomLauncher(ITestHostLauncher customLauncher)
{
this.customTestHostLauncher = customLauncher;
}
/// <inheritdoc/>
public TestHostConnectionInfo GetTestHostConnectionInfo()
{
return new TestHostConnectionInfo { Endpoint = "127.0.0.1:0", Role = ConnectionRole.Client, Transport = Transport.Sockets };
}
/// <inheritdoc/>
public async Task<bool> LaunchTestHostAsync(TestProcessStartInfo testHostStartInfo, CancellationToken cancellationToken)
{
return await Task.Run(() => this.LaunchHost(testHostStartInfo, cancellationToken), cancellationToken);
}
/// <inheritdoc/>
public virtual TestProcessStartInfo GetTestHostProcessStartInfo(
IEnumerable<string> sources,
IDictionary<string, string> environmentVariables,
TestRunnerConnectionInfo connectionInfo)
{
var startInfo = new TestProcessStartInfo();
var currentProcessPath = this.processHelper.GetCurrentProcessFileName();
// This host manager can create process start info for dotnet core targets only.
// If already running with the dotnet executable, use it; otherwise pick up the dotnet available on path.
// Wrap the paths with quotes in case dotnet executable is installed on a path with whitespace.
if (currentProcessPath.EndsWith("dotnet", StringComparison.OrdinalIgnoreCase)
|| currentProcessPath.EndsWith("dotnet.exe", StringComparison.OrdinalIgnoreCase))
{
startInfo.FileName = currentProcessPath;
}
else
{
startInfo.FileName = this.dotnetHostHelper.GetDotnetPath();
}
EqtTrace.Verbose("DotnetTestHostmanager: Full path of dotnet.exe is {0}", startInfo.FileName);
// .NET core host manager is not a shared host. It will expect a single test source to be provided.
var args = "exec";
var sourcePath = sources.Single();
var sourceFile = Path.GetFileNameWithoutExtension(sourcePath);
var sourceDirectory = Path.GetDirectoryName(sourcePath);
// Probe for runtimeconfig and deps file for the test source
var runtimeConfigPath = Path.Combine(sourceDirectory, string.Concat(sourceFile, ".runtimeconfig.json"));
if (this.fileHelper.Exists(runtimeConfigPath))
{
string argsToAdd = " --runtimeconfig " + runtimeConfigPath.AddDoubleQuote();
args += argsToAdd;
EqtTrace.Verbose("DotnetTestHostmanager: Adding {0} in args", argsToAdd);
}
else
{
EqtTrace.Verbose("DotnetTestHostmanager: File {0}, doesnot exist", runtimeConfigPath);
}
// Use the deps.json for test source
var depsFilePath = Path.Combine(sourceDirectory, string.Concat(sourceFile, ".deps.json"));
if (this.fileHelper.Exists(depsFilePath))
{
string argsToAdd = " --depsfile " + depsFilePath.AddDoubleQuote();
args += argsToAdd;
EqtTrace.Verbose("DotnetTestHostmanager: Adding {0} in args", argsToAdd);
}
else
{
EqtTrace.Verbose("DotnetTestHostmanager: File {0}, doesnot exist", depsFilePath);
}
var runtimeConfigDevPath = Path.Combine(sourceDirectory, string.Concat(sourceFile, ".runtimeconfig.dev.json"));
var testHostPath = this.GetTestHostPath(runtimeConfigDevPath, depsFilePath, sourceDirectory);
EqtTrace.Verbose("DotnetTestHostmanager: Full path of testhost.dll is {0}", testHostPath);
args += " " + testHostPath.AddDoubleQuote() + " " + connectionInfo.ToCommandLineOptions();
// Create a additional probing path args with Nuget.Client
// args += "--additionalprobingpath xxx"
// TODO this may be required in ASP.net, requires validation
// Sample command line for the spawned test host
// "D:\dd\gh\Microsoft\vstest\tools\dotnet\dotnet.exe" exec
// --runtimeconfig G:\tmp\netcore-test\bin\Debug\netcoreapp1.0\netcore-test.runtimeconfig.json
// --depsfile G:\tmp\netcore-test\bin\Debug\netcoreapp1.0\netcore-test.deps.json
// --additionalprobingpath C:\Users\username\.nuget\packages\
// G:\nuget-package-path\microsoft.testplatform.testhost\version\**\testhost.dll
// G:\tmp\netcore-test\bin\Debug\netcoreapp1.0\netcore-test.dll
startInfo.Arguments = args;
startInfo.EnvironmentVariables = environmentVariables ?? new Dictionary<string, string>();
startInfo.WorkingDirectory = sourceDirectory;
return startInfo;
}
/// <inheritdoc/>
public IEnumerable<string> GetTestPlatformExtensions(IEnumerable<string> sources, IEnumerable<string> extensions)
{
var sourceDirectory = Path.GetDirectoryName(sources.Single());
if (!string.IsNullOrEmpty(sourceDirectory) && this.fileHelper.DirectoryExists(sourceDirectory))
{
return this.fileHelper.EnumerateFiles(sourceDirectory, SearchOption.TopDirectoryOnly, TestAdapterRegexPattern);
}
return Enumerable.Empty<string>();
}
/// <inheritdoc/>
public IEnumerable<string> GetTestSources(IEnumerable<string> sources)
{
// We do not have scenario where netcore tests are deployed to remote machine, so no need to udpate sources
return sources;
}
/// <inheritdoc/>
public bool CanExecuteCurrentRunConfiguration(string runsettingsXml)
{
var config = XmlRunSettingsUtilities.GetRunConfigurationNode(runsettingsXml);
var framework = config.TargetFramework;
// This is expected to be called once every run so returning a new instance every time.
if (framework.Name.IndexOf("netstandard", StringComparison.OrdinalIgnoreCase) >= 0
|| framework.Name.IndexOf("netcoreapp", StringComparison.OrdinalIgnoreCase) >= 0)
{
return true;
}
return false;
}
/// <inheritdoc/>
public Task CleanTestHostAsync(CancellationToken cancellationToken)
{
try
{
this.processHelper.TerminateProcess(this.testHostProcess);
}
catch (Exception ex)
{
EqtTrace.Warning("DotnetTestHostManager: Unable to terminate test host process: " + ex);
}
this.testHostProcess?.Dispose();
return Task.FromResult(true);
}
/// <summary>
/// Raises HostLaunched event
/// </summary>
/// <param name="e">hostprovider event args</param>
private void OnHostLaunched(HostProviderEventArgs e)
{
this.HostLaunched.SafeInvoke(this, e, "HostProviderEvents.OnHostLaunched");
}
/// <summary>
/// Raises HostExited event
/// </summary>
/// <param name="e">hostprovider event args</param>
private void OnHostExited(HostProviderEventArgs e)
{
if (!this.hostExitedEventRaised)
{
this.hostExitedEventRaised = true;
this.HostExited.SafeInvoke(this, e, "HostProviderEvents.OnHostExited");
}
}
private bool LaunchHost(TestProcessStartInfo testHostStartInfo, CancellationToken cancellationToken)
{
this.testHostProcessStdError = new StringBuilder(0, CoreUtilities.Constants.StandardErrorMaxLength);
if (this.customTestHostLauncher == null)
{
EqtTrace.Verbose("DotnetTestHostManager: Starting process '{0}' with command line '{1}'", testHostStartInfo.FileName, testHostStartInfo.Arguments);
cancellationToken.ThrowIfCancellationRequested();
this.testHostProcess = this.processHelper.LaunchProcess(testHostStartInfo.FileName, testHostStartInfo.Arguments, testHostStartInfo.WorkingDirectory, testHostStartInfo.EnvironmentVariables, this.ErrorReceivedCallback, this.ExitCallBack) as Process;
}
else
{
var processId = this.customTestHostLauncher.LaunchTestHost(testHostStartInfo, cancellationToken);
this.testHostProcess = Process.GetProcessById(processId);
this.processHelper.SetExitCallback(processId, this.ExitCallBack);
}
this.OnHostLaunched(new HostProviderEventArgs("Test Runtime launched", 0, this.testHostProcess.Id));
return this.testHostProcess != null;
}
private string GetTestHostPath(string runtimeConfigDevPath, string depsFilePath, string sourceDirectory)
{
string testHostPackageName = "microsoft.testplatform.testhost";
string testHostPath = string.Empty;
string errorMessage = null;
if (this.fileHelper.Exists(depsFilePath))
{
if (this.fileHelper.Exists(runtimeConfigDevPath))
{
EqtTrace.Verbose("DotnetTestHostmanager: Reading file {0} to get path of testhost.dll", depsFilePath);
// Get testhost relative path
using (var stream = this.fileHelper.GetStream(depsFilePath, FileMode.Open, FileAccess.Read))
{
var context = new DependencyContextJsonReader().Read(stream);
var testhostPackage = context.RuntimeLibraries.Where(lib => lib.Name.Equals(testHostPackageName, StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
if (testhostPackage != null)
{
foreach (var runtimeAssemblyGroup in testhostPackage.RuntimeAssemblyGroups)
{
foreach (var path in runtimeAssemblyGroup.AssetPaths)
{
if (path.EndsWith("testhost.dll", StringComparison.OrdinalIgnoreCase))
{
testHostPath = path;
break;
}
}
}
testHostPath = Path.Combine(testhostPackage.Path, testHostPath);
this.hostPackageVersion = testhostPackage.Version;
EqtTrace.Verbose("DotnetTestHostmanager: Relative path of testhost.dll with respect to package folder is {0}", testHostPath);
}
}
// Get probing path
using (StreamReader file = new StreamReader(this.fileHelper.GetStream(runtimeConfigDevPath, FileMode.Open, FileAccess.Read)))
using (JsonTextReader reader = new JsonTextReader(file))
{
JObject context = (JObject)JToken.ReadFrom(reader);
JObject runtimeOptions = (JObject)context.GetValue("runtimeOptions");
JToken additionalProbingPaths = runtimeOptions.GetValue("additionalProbingPaths");
foreach (var x in additionalProbingPaths)
{
EqtTrace.Verbose("DotnetTestHostmanager: Looking for path {0} in folder {1}", testHostPath, x.ToString());
string testHostFullPath;
try
{
testHostFullPath = Path.Combine(x.ToString(), testHostPath);
}
catch (ArgumentException)
{
// https://github.com/Microsoft/vstest/issues/847
// skip any invalid paths and continue checking the others
continue;
}
if (this.fileHelper.Exists(testHostFullPath))
{
return testHostFullPath;
}
}
}
}
}
else
{
errorMessage = string.Format(CultureInfo.CurrentCulture, Resources.UnableToFindDepsFile, depsFilePath);
}
// If we are here it means it couldnt resolve testhost.dll from nuget cache.
// Try resolving testhost from output directory of test project. This is required if user has published the test project
// and is running tests in an isolated machine. A second scenario is self test: test platform unit tests take a project
// dependency on testhost (instead of nuget dependency), this drops testhost to output path.
testHostPath = Path.Combine(sourceDirectory, "testhost.dll");
EqtTrace.Verbose("DotnetTestHostManager: Assume published test project, with test host path = {0}.", testHostPath);
if (!this.fileHelper.Exists(testHostPath))
{
// If deps file is not found, suggest adding Microsoft.Net.Test.Sdk reference to the project
// Otherwise, suggest publishing the test project so that test host gets dropped next to the test source.
errorMessage = errorMessage ?? string.Format(CultureInfo.CurrentCulture, Resources.SuggestPublishTestProject, testHostPath);
throw new TestPlatformException(errorMessage);
}
return testHostPath;
}
}
}