Skip to content

Commit 49e108d

Browse files
committed
TestHost controller support mono
1 parent e5765a0 commit 49e108d

File tree

3 files changed

+79
-44
lines changed

3 files changed

+79
-44
lines changed

src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs

+50-38
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ protected override async Task<int> InternalRunAsync()
6969
IEnvironment environment = ServiceProvider.GetEnvironment();
7070
IProcessHandler process = ServiceProvider.GetProcessHandler();
7171
ITestApplicationModuleInfo testApplicationModuleInfo = ServiceProvider.GetTestApplicationModuleInfo();
72-
ExecutableInfo executableInfo = testApplicationModuleInfo.GetCurrentExecutableInfo();
7372
ITelemetryCollector telemetry = ServiceProvider.GetTelemetryCollector();
7473
ITelemetryInformation telemetryInformation = ServiceProvider.GetTelemetryInformation();
7574
string? extensionInformation = null;
@@ -80,6 +79,10 @@ protected override async Task<int> InternalRunAsync()
8079
using IProcess currentProcess = process.GetCurrentProcess();
8180
int currentPID = currentProcess.Id;
8281
string processIdString = currentPID.ToString(CultureInfo.InvariantCulture);
82+
83+
ExecutableInfo executableInfo = testApplicationModuleInfo.GetCurrentExecutableInfo();
84+
await _logger.LogDebugAsync($"Test host controller process info: {executableInfo}");
85+
8386
List<string> partialCommandLine =
8487
[
8588
.. executableInfo.Arguments,
@@ -223,7 +226,7 @@ protected override async Task<int> InternalRunAsync()
223226
string testHostProcessStartupTime = _clock.UtcNow.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
224227
processStartInfo.EnvironmentVariables.Add($"{EnvironmentVariableConstants.TESTINGPLATFORM_TESTHOSTCONTROLLER_TESTHOSTPROCESSSTARTTIME}_{currentPID}", testHostProcessStartupTime);
225228
await _logger.LogDebugAsync($"{EnvironmentVariableConstants.TESTINGPLATFORM_TESTHOSTCONTROLLER_TESTHOSTPROCESSSTARTTIME}_{currentPID} '{testHostProcessStartupTime}'");
226-
await _logger.LogDebugAsync("Starting test host process");
229+
await _logger.LogDebugAsync($"Starting test host process '{processStartInfo.FileName}' with args '{processStartInfo.Arguments}'");
227230
using IProcess testHostProcess = process.Start(processStartInfo);
228231

229232
int? testHostProcessId = null;
@@ -242,59 +245,68 @@ protected override async Task<int> InternalRunAsync()
242245

243246
await _logger.LogDebugAsync($"Started test host process '{testHostProcessId}' HasExited: {testHostProcess.HasExited}");
244247

245-
string? seconds = configuration[PlatformConfigurationConstants.PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds];
246-
int timeoutSeconds = seconds is null ? TimeoutHelper.DefaultHangTimeoutSeconds : int.Parse(seconds, CultureInfo.InvariantCulture);
247-
await _logger.LogDebugAsync($"Setting PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds '{timeoutSeconds}'");
248-
249-
// Wait for the test host controller to connect
250-
using (CancellationTokenSource timeout = new(TimeSpan.FromSeconds(timeoutSeconds)))
251-
using (var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, abortRun))
248+
if (testHostProcess.HasExited || testHostProcessId is null)
252249
{
253-
await _logger.LogDebugAsync("Wait connection from the test host process");
254-
await testHostControllerIpc.WaitConnectionAsync(linkedToken.Token);
250+
await _logger.LogDebugAsync("Test host process exited prematurely");
255251
}
256-
257-
// Wait for the test host controller to send the PID of the test host process
258-
using (CancellationTokenSource timeout = new(TimeoutHelper.DefaultHangTimeSpanTimeout))
252+
else
259253
{
260-
_waitForPid.Wait(timeout.Token);
261-
}
254+
string? seconds = configuration[PlatformConfigurationConstants.PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds];
255+
int timeoutSeconds = seconds is null ? TimeoutHelper.DefaultHangTimeoutSeconds : int.Parse(seconds, CultureInfo.InvariantCulture);
256+
await _logger.LogDebugAsync($"Setting PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds '{timeoutSeconds}'");
262257

263-
await _logger.LogDebugAsync("Fire OnTestHostProcessStartedAsync");
258+
// Wait for the test host controller to connect
259+
using (CancellationTokenSource timeout = new(TimeSpan.FromSeconds(timeoutSeconds)))
260+
using (var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, abortRun))
261+
{
262+
await _logger.LogDebugAsync("Wait connection from the test host process");
263+
await testHostControllerIpc.WaitConnectionAsync(linkedToken.Token);
264+
}
264265

265-
if (_testHostPID is null)
266-
{
267-
throw ApplicationStateGuard.Unreachable();
268-
}
266+
// Wait for the test host controller to send the PID of the test host process
267+
using (CancellationTokenSource timeout = new(TimeoutHelper.DefaultHangTimeSpanTimeout))
268+
{
269+
_waitForPid.Wait(timeout.Token);
270+
}
269271

270-
if (_testHostsInformation.LifetimeHandlers.Length > 0)
271-
{
272-
// We don't block the host during the 'OnTestHostProcessStartedAsync' by-design, if 'ITestHostProcessLifetimeHandler' extensions needs
273-
// to block the execution of the test host should add an in-process extension like an 'ITestApplicationLifecycleCallbacks' and
274-
// wait for a connection/signal to return.
275-
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value);
276-
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
272+
await _logger.LogDebugAsync("Fire OnTestHostProcessStartedAsync");
273+
274+
if (_testHostPID is null)
277275
{
278-
await lifetimeHandler.OnTestHostProcessStartedAsync(testHostProcessInformation, abortRun);
276+
throw ApplicationStateGuard.Unreachable();
277+
}
278+
279+
if (_testHostsInformation.LifetimeHandlers.Length > 0)
280+
{
281+
// We don't block the host during the 'OnTestHostProcessStartedAsync' by-design, if 'ITestHostProcessLifetimeHandler' extensions needs
282+
// to block the execution of the test host should add an in-process extension like an 'ITestApplicationLifecycleCallbacks' and
283+
// wait for a connection/signal to return.
284+
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value);
285+
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
286+
{
287+
await lifetimeHandler.OnTestHostProcessStartedAsync(testHostProcessInformation, abortRun);
288+
}
279289
}
280-
}
281290

282-
await _logger.LogDebugAsync("Wait for test host process exit");
283-
await testHostProcess.WaitForExitAsync();
291+
await _logger.LogDebugAsync("Wait for test host process exit");
292+
await testHostProcess.WaitForExitAsync();
293+
}
284294

285295
if (_testHostsInformation.LifetimeHandlers.Length > 0)
286296
{
287297
await _logger.LogDebugAsync($"Fire OnTestHostProcessExitedAsync testHostGracefullyClosed: {_testHostGracefullyClosed}");
288298
var messageBusProxy = (MessageBusProxy)ServiceProvider.GetMessageBus();
289299

290-
ApplicationStateGuard.Ensure(_testHostPID is not null);
291-
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value, testHostProcess.ExitCode, _testHostGracefullyClosed);
292-
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
300+
if (_testHostPID is not null)
293301
{
294-
await lifetimeHandler.OnTestHostProcessExitedAsync(testHostProcessInformation, abortRun);
302+
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value, testHostProcess.ExitCode, _testHostGracefullyClosed);
303+
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
304+
{
305+
await lifetimeHandler.OnTestHostProcessExitedAsync(testHostProcessInformation, abortRun);
295306

296-
// OnTestHostProcess could produce information that needs to be handled by others.
297-
await messageBusProxy.DrainDataAsync();
307+
// OnTestHostProcess could produce information that needs to be handled by others.
308+
await messageBusProxy.DrainDataAsync();
309+
}
298310
}
299311

300312
// We disable after the drain because it's possible that the drain will produce more messages

src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs

+26-6
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,17 @@ public bool IsCurrentTestApplicationHostDotnetMuxer
2121
}
2222
}
2323

24+
public bool IsCurrentTestApplicationHostMonoMuxer
25+
{
26+
get
27+
{
28+
string? processPath = GetProcessPath(_environment, _process);
29+
return processPath is not null
30+
&& Path.GetFileNameWithoutExtension(processPath) is { } processName
31+
&& processName is "mono" or "mono-sgen";
32+
}
33+
}
34+
2435
public bool IsCurrentTestApplicationModuleExecutable
2536
{
2637
get
@@ -31,7 +42,9 @@ public bool IsCurrentTestApplicationModuleExecutable
3142
}
3243

3344
public bool IsAppHostOrSingleFileOrNativeAot
34-
=> IsCurrentTestApplicationModuleExecutable && !IsCurrentTestApplicationHostDotnetMuxer;
45+
=> IsCurrentTestApplicationModuleExecutable
46+
&& !IsCurrentTestApplicationHostDotnetMuxer
47+
&& !IsCurrentTestApplicationHostMonoMuxer;
3548

3649
#if NETCOREAPP
3750
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "We handle the singlefile/native aot use case")]
@@ -91,14 +104,21 @@ public ExecutableInfo GetCurrentExecutableInfo()
91104
string currentTestApplicationFullPath = GetCurrentTestApplicationFullPath();
92105
bool isDotnetMuxer = IsCurrentTestApplicationHostDotnetMuxer;
93106
bool isAppHost = IsAppHostOrSingleFileOrNativeAot;
107+
bool isMonoMuxer = IsCurrentTestApplicationHostMonoMuxer;
94108
string processPath = GetProcessPath();
95109
string[] commandLineArguments = GetCommandLineArgs();
96110
string fileName = processPath;
97-
IEnumerable<string> arguments = isAppHost
98-
? commandLineArguments.Skip(1)
99-
: isDotnetMuxer
100-
? MuxerExec.Concat(commandLineArguments)
101-
: commandLineArguments;
111+
IEnumerable<string> arguments = (isAppHost, isDotnetMuxer, isMonoMuxer) switch
112+
{
113+
// When executable
114+
(true, _, _) => commandLineArguments.Skip(1),
115+
// When dotnet
116+
(_, true, _) => MuxerExec.Concat(commandLineArguments),
117+
// When mono
118+
(_, _, true) => commandLineArguments,
119+
// Otherwise
120+
_ => commandLineArguments,
121+
};
102122

103123
return new(fileName, arguments, Path.GetDirectoryName(currentTestApplicationFullPath)!);
104124
}

src/Platform/Microsoft.Testing.Platform/Services/ExecutableInfo.cs

+3
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ internal sealed class ExecutableInfo(string fileName, IEnumerable<string> argume
1010
public IEnumerable<string> Arguments { get; } = arguments;
1111

1212
public string Workspace { get; } = workspace;
13+
14+
public override string ToString()
15+
=> $"Process: {FileName}, Arguments: {string.Join(" ", Arguments)}, Workspace: {Workspace}";
1316
}

0 commit comments

Comments
 (0)