diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs index 6eac525c93..d96897ca41 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostControllersTestHost.cs @@ -69,7 +69,6 @@ protected override async Task 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; @@ -80,6 +79,10 @@ protected override async Task 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 partialCommandLine = [ .. executableInfo.Arguments, @@ -223,7 +226,7 @@ protected override async Task 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; @@ -242,59 +245,68 @@ protected override async Task 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 diff --git a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs index b8346d8472..66365f11ae 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs @@ -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 @@ -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")] @@ -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 arguments = isAppHost - ? commandLineArguments.Skip(1) - : isDotnetMuxer - ? MuxerExec.Concat(commandLineArguments) - : commandLineArguments; + IEnumerable 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)!); } diff --git a/src/Platform/Microsoft.Testing.Platform/Services/ExecutableInfo.cs b/src/Platform/Microsoft.Testing.Platform/Services/ExecutableInfo.cs index 04181ec713..df39f53cce 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/ExecutableInfo.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/ExecutableInfo.cs @@ -10,4 +10,7 @@ internal sealed class ExecutableInfo(string fileName, IEnumerable argume public IEnumerable Arguments { get; } = arguments; public string Workspace { get; } = workspace; + + public override string ToString() + => $"Process: {FileName}, Arguments: {string.Join(" ", Arguments)}, Workspace: {Workspace}"; }