Skip to content

Commit

Permalink
TestHost controller support mono (#4831)
Browse files Browse the repository at this point in the history
  • Loading branch information
Evangelink authored Jan 29, 2025
1 parent e5765a0 commit ee39352
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 44 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ protected override async Task<int> InternalRunAsync()
IEnvironment environment = ServiceProvider.GetEnvironment();
IProcessHandler process = ServiceProvider.GetProcessHandler();
ITestApplicationModuleInfo testApplicationModuleInfo = ServiceProvider.GetTestApplicationModuleInfo();
ExecutableInfo executableInfo = testApplicationModuleInfo.GetCurrentExecutableInfo();
ITelemetryCollector telemetry = ServiceProvider.GetTelemetryCollector();
ITelemetryInformation telemetryInformation = ServiceProvider.GetTelemetryInformation();
string? extensionInformation = null;
Expand All @@ -80,6 +79,10 @@ protected override async Task<int> InternalRunAsync()
using IProcess currentProcess = process.GetCurrentProcess();
int currentPID = currentProcess.Id;
string processIdString = currentPID.ToString(CultureInfo.InvariantCulture);

ExecutableInfo executableInfo = testApplicationModuleInfo.GetCurrentExecutableInfo();
await _logger.LogDebugAsync($"Test host controller process info: {executableInfo}");

List<string> partialCommandLine =
[
.. executableInfo.Arguments,
Expand Down Expand Up @@ -223,7 +226,7 @@ protected override async Task<int> InternalRunAsync()
string testHostProcessStartupTime = _clock.UtcNow.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
processStartInfo.EnvironmentVariables.Add($"{EnvironmentVariableConstants.TESTINGPLATFORM_TESTHOSTCONTROLLER_TESTHOSTPROCESSSTARTTIME}_{currentPID}", testHostProcessStartupTime);
await _logger.LogDebugAsync($"{EnvironmentVariableConstants.TESTINGPLATFORM_TESTHOSTCONTROLLER_TESTHOSTPROCESSSTARTTIME}_{currentPID} '{testHostProcessStartupTime}'");
await _logger.LogDebugAsync("Starting test host process");
await _logger.LogDebugAsync($"Starting test host process '{processStartInfo.FileName}' with args '{processStartInfo.Arguments}'");
using IProcess testHostProcess = process.Start(processStartInfo);

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

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

string? seconds = configuration[PlatformConfigurationConstants.PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds];
int timeoutSeconds = seconds is null ? TimeoutHelper.DefaultHangTimeoutSeconds : int.Parse(seconds, CultureInfo.InvariantCulture);
await _logger.LogDebugAsync($"Setting PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds '{timeoutSeconds}'");

// Wait for the test host controller to connect
using (CancellationTokenSource timeout = new(TimeSpan.FromSeconds(timeoutSeconds)))
using (var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, abortRun))
if (testHostProcess.HasExited || testHostProcessId is null)
{
await _logger.LogDebugAsync("Wait connection from the test host process");
await testHostControllerIpc.WaitConnectionAsync(linkedToken.Token);
await _logger.LogDebugAsync("Test host process exited prematurely");
}

// Wait for the test host controller to send the PID of the test host process
using (CancellationTokenSource timeout = new(TimeoutHelper.DefaultHangTimeSpanTimeout))
else
{
_waitForPid.Wait(timeout.Token);
}
string? seconds = configuration[PlatformConfigurationConstants.PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds];
int timeoutSeconds = seconds is null ? TimeoutHelper.DefaultHangTimeoutSeconds : int.Parse(seconds, CultureInfo.InvariantCulture);
await _logger.LogDebugAsync($"Setting PlatformTestHostControllersManagerSingleConnectionNamedPipeServerWaitConnectionTimeoutSeconds '{timeoutSeconds}'");

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

if (_testHostPID is null)
{
throw ApplicationStateGuard.Unreachable();
}
// Wait for the test host controller to send the PID of the test host process
using (CancellationTokenSource timeout = new(TimeoutHelper.DefaultHangTimeSpanTimeout))
{
_waitForPid.Wait(timeout.Token);
}

if (_testHostsInformation.LifetimeHandlers.Length > 0)
{
// We don't block the host during the 'OnTestHostProcessStartedAsync' by-design, if 'ITestHostProcessLifetimeHandler' extensions needs
// to block the execution of the test host should add an in-process extension like an 'ITestApplicationLifecycleCallbacks' and
// wait for a connection/signal to return.
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value);
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
await _logger.LogDebugAsync("Fire OnTestHostProcessStartedAsync");

if (_testHostPID is null)
{
await lifetimeHandler.OnTestHostProcessStartedAsync(testHostProcessInformation, abortRun);
throw ApplicationStateGuard.Unreachable();
}

if (_testHostsInformation.LifetimeHandlers.Length > 0)
{
// We don't block the host during the 'OnTestHostProcessStartedAsync' by-design, if 'ITestHostProcessLifetimeHandler' extensions needs
// to block the execution of the test host should add an in-process extension like an 'ITestApplicationLifecycleCallbacks' and
// wait for a connection/signal to return.
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value);
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
{
await lifetimeHandler.OnTestHostProcessStartedAsync(testHostProcessInformation, abortRun);
}
}
}

await _logger.LogDebugAsync("Wait for test host process exit");
await testHostProcess.WaitForExitAsync();
await _logger.LogDebugAsync("Wait for test host process exit");
await testHostProcess.WaitForExitAsync();
}

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

ApplicationStateGuard.Ensure(_testHostPID is not null);
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value, testHostProcess.ExitCode, _testHostGracefullyClosed);
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
if (_testHostPID is not null)
{
await lifetimeHandler.OnTestHostProcessExitedAsync(testHostProcessInformation, abortRun);
TestHostProcessInformation testHostProcessInformation = new(_testHostPID.Value, testHostProcess.ExitCode, _testHostGracefullyClosed);
foreach (ITestHostProcessLifetimeHandler lifetimeHandler in _testHostsInformation.LifetimeHandlers)
{
await lifetimeHandler.OnTestHostProcessExitedAsync(testHostProcessInformation, abortRun);

// OnTestHostProcess could produce information that needs to be handled by others.
await messageBusProxy.DrainDataAsync();
// OnTestHostProcess could produce information that needs to be handled by others.
await messageBusProxy.DrainDataAsync();
}
}

// We disable after the drain because it's possible that the drain will produce more messages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ public bool IsCurrentTestApplicationHostDotnetMuxer
}
}

public bool IsCurrentTestApplicationHostMonoMuxer
{
get
{
string? processPath = GetProcessPath(_environment, _process);
return processPath is not null
&& Path.GetFileNameWithoutExtension(processPath) is { } processName
&& processName is "mono" or "mono-sgen";
}
}

public bool IsCurrentTestApplicationModuleExecutable
{
get
Expand All @@ -31,7 +42,9 @@ public bool IsCurrentTestApplicationModuleExecutable
}

public bool IsAppHostOrSingleFileOrNativeAot
=> IsCurrentTestApplicationModuleExecutable && !IsCurrentTestApplicationHostDotnetMuxer;
=> IsCurrentTestApplicationModuleExecutable
&& !IsCurrentTestApplicationHostDotnetMuxer
&& !IsCurrentTestApplicationHostMonoMuxer;

#if NETCOREAPP
[UnconditionalSuppressMessage("SingleFile", "IL3000:Avoid accessing Assembly file path when publishing as a single file", Justification = "We handle the singlefile/native aot use case")]
Expand Down Expand Up @@ -91,14 +104,21 @@ public ExecutableInfo GetCurrentExecutableInfo()
string currentTestApplicationFullPath = GetCurrentTestApplicationFullPath();
bool isDotnetMuxer = IsCurrentTestApplicationHostDotnetMuxer;
bool isAppHost = IsAppHostOrSingleFileOrNativeAot;
bool isMonoMuxer = IsCurrentTestApplicationHostMonoMuxer;
string processPath = GetProcessPath();
string[] commandLineArguments = GetCommandLineArgs();
string fileName = processPath;
IEnumerable<string> arguments = isAppHost
? commandLineArguments.Skip(1)
: isDotnetMuxer
? MuxerExec.Concat(commandLineArguments)
: commandLineArguments;
IEnumerable<string> arguments = (isAppHost, isDotnetMuxer, isMonoMuxer) switch
{
// When executable
(true, _, _) => commandLineArguments.Skip(1),
// When dotnet
(_, true, _) => MuxerExec.Concat(commandLineArguments),
// When mono
(_, _, true) => commandLineArguments,
// Otherwise
_ => commandLineArguments,
};

return new(fileName, arguments, Path.GetDirectoryName(currentTestApplicationFullPath)!);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@ internal sealed class ExecutableInfo(string fileName, IEnumerable<string> argume
public IEnumerable<string> Arguments { get; } = arguments;

public string Workspace { get; } = workspace;

public override string ToString()
=> $"Process: {FileName}, Arguments: {string.Join(" ", Arguments)}, Workspace: {Workspace}";
}

0 comments on commit ee39352

Please sign in to comment.