diff --git a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs index fc8c2870ef..546f13b598 100644 --- a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs +++ b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs @@ -96,7 +96,7 @@ public static async Task CreateBuilderAsync(string[] ar } // All checks are fine, create the TestApplication. - return new TestApplicationBuilder(loggingState, createBuilderStart, testApplicationOptions, s_unhandledExceptionHandler); + return new TestApplicationBuilder(loggingState, createBuilderStart, testApplicationOptions, s_unhandledExceptionHandler, args); } private static async Task LogInformationAsync( diff --git a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplicationBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplicationBuilder.cs index 1a5214b4ea..b664a16384 100644 --- a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplicationBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplicationBuilder.cs @@ -28,6 +28,7 @@ internal sealed class TestApplicationBuilder : ITestApplicationBuilder private readonly ApplicationLoggingState _loggingState; private readonly TestApplicationOptions _testApplicationOptions; private readonly IUnhandledExceptionsHandler _unhandledExceptionsHandler; + private readonly string[] _originalCommandLineArguments; private readonly TestHostBuilder _testHostBuilder; private IHost? _host; private Func? _testFrameworkFactory; @@ -37,13 +38,15 @@ internal TestApplicationBuilder( ApplicationLoggingState loggingState, DateTimeOffset createBuilderStart, TestApplicationOptions testApplicationOptions, - IUnhandledExceptionsHandler unhandledExceptionsHandler) + IUnhandledExceptionsHandler unhandledExceptionsHandler, + string[] originalCommandLineArguments) { _testHostBuilder = new TestHostBuilder(new SystemFileSystem(), new SystemRuntimeFeature(), new SystemEnvironment(), new SystemProcessHandler(), new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler())); _createBuilderStart = createBuilderStart; _loggingState = loggingState; _testApplicationOptions = testApplicationOptions; _unhandledExceptionsHandler = unhandledExceptionsHandler; + _originalCommandLineArguments = originalCommandLineArguments; } public ITestHostManager TestHost => _testHostBuilder.TestHost; @@ -109,7 +112,7 @@ public async Task BuildAsync() throw new InvalidOperationException(PlatformResources.TestApplicationBuilderApplicationAlreadyRegistered); } - _host = await _testHostBuilder.BuildAsync(_loggingState, _testApplicationOptions, _unhandledExceptionsHandler, _createBuilderStart).ConfigureAwait(false); + _host = await _testHostBuilder.BuildAsync(_loggingState, _testApplicationOptions, _unhandledExceptionsHandler, _createBuilderStart, _originalCommandLineArguments).ConfigureAwait(false); return new TestApplication(_host); } diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index b51e0dc081..7f55d5823f 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -56,7 +56,8 @@ public async Task BuildAsync( ApplicationLoggingState loggingState, TestApplicationOptions testApplicationOptions, IUnhandledExceptionsHandler unhandledExceptionsHandler, - DateTimeOffset createBuilderStart) + DateTimeOffset createBuilderStart, + string[] originalCommandLineArguments) { // ============= SETUP COMMON SERVICE USED IN ALL MODES ===============// ApplicationStateGuard.Ensure(TestFramework is not null); @@ -93,6 +94,15 @@ public async Task BuildAsync( serviceProvider.TryAddService(processHandler); serviceProvider.TryAddService(_fileSystem); serviceProvider.TryAddService(_testApplicationModuleInfo); + + // Register the original command line arguments service and configure the module info to use it + CommandLineArgumentsProvider commandLineArgumentsProvider = new(originalCommandLineArguments); + serviceProvider.TryAddService(commandLineArgumentsProvider); + if (_testApplicationModuleInfo is CurrentTestApplicationModuleInfo currentModuleInfo) + { + currentModuleInfo.SetCommandLineArgumentsProvider(commandLineArgumentsProvider); + } + TestHostControllerInfo testHostControllerInfo = new(loggingState.CommandLineParseResult); serviceProvider.TryAddService(testHostControllerInfo); diff --git a/src/Platform/Microsoft.Testing.Platform/Services/CommandLineArgumentsProvider.cs b/src/Platform/Microsoft.Testing.Platform/Services/CommandLineArgumentsProvider.cs new file mode 100644 index 0000000000..2bb8c0bc89 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Services/CommandLineArgumentsProvider.cs @@ -0,0 +1,11 @@ +// 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.Testing.Platform.Services; + +internal sealed class CommandLineArgumentsProvider(string[] originalArgs) : ICommandLineArgumentsProvider +{ + private readonly string[] _originalArgs = originalArgs; + + public string[] GetOriginalCommandLineArguments() => _originalArgs; +} \ No newline at end of file diff --git a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs index 8f8fffd0ed..4311a94868 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs @@ -9,8 +9,14 @@ internal sealed class CurrentTestApplicationModuleInfo(IEnvironment environment, { private readonly IEnvironment _environment = environment; private readonly IProcessHandler _process = process; + private ICommandLineArgumentsProvider? _commandLineArgumentsProvider; private static readonly string[] MuxerExec = ["exec"]; + internal void SetCommandLineArgumentsProvider(ICommandLineArgumentsProvider? commandLineArgumentsProvider) + { + _commandLineArgumentsProvider = commandLineArgumentsProvider; + } + public bool IsCurrentTestApplicationHostDotnetMuxer { get @@ -102,7 +108,45 @@ public ExecutableInfo GetCurrentExecutableInfo() bool isDotnetMuxer = IsCurrentTestApplicationHostDotnetMuxer; bool isAppHost = IsAppHostOrSingleFileOrNativeAot; bool isMonoMuxer = IsCurrentTestApplicationHostMonoMuxer; - string[] commandLineArguments = _environment.GetCommandLineArgs(); + + string[] environmentArgs = _environment.GetCommandLineArgs(); + string[] commandLineArguments; + + if (_commandLineArgumentsProvider is not null) + { + string[] customArgs = _commandLineArgumentsProvider.GetOriginalCommandLineArguments(); + + if (isDotnetMuxer && environmentArgs.Length >= 2) + { + // For dotnet scenarios, we need to preserve the assembly path from environment + // and combine it with the custom arguments + // Environment: ["dotnet", "MyTest.dll"] + // Custom: ["--retry-failed-tests", "1"] + // Result: ["dotnet", "MyTest.dll", "--retry-failed-tests", "1"] + commandLineArguments = environmentArgs.Take(2).Concat(customArgs).ToArray(); + } + else + { + // For executable scenarios, use custom args but preserve the executable name from environment + // Environment: ["MyTest.exe"] + // Custom: ["--retry-failed-tests", "1"] + // Result: ["MyTest.exe", "--retry-failed-tests", "1"] + if (environmentArgs.Length >= 1) + { + commandLineArguments = environmentArgs.Take(1).Concat(customArgs).ToArray(); + } + else + { + commandLineArguments = customArgs; + } + } + } + else + { + // Fallback to original behavior when no custom args are provided + commandLineArguments = environmentArgs; + } + IEnumerable arguments = (isAppHost, isDotnetMuxer, isMonoMuxer) switch { // When executable diff --git a/src/Platform/Microsoft.Testing.Platform/Services/ICommandLineArgumentsProvider.cs b/src/Platform/Microsoft.Testing.Platform/Services/ICommandLineArgumentsProvider.cs new file mode 100644 index 0000000000..6b6ca4b5f9 --- /dev/null +++ b/src/Platform/Microsoft.Testing.Platform/Services/ICommandLineArgumentsProvider.cs @@ -0,0 +1,9 @@ +// 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.Testing.Platform.Services; + +internal interface ICommandLineArgumentsProvider +{ + string[] GetOriginalCommandLineArguments(); +} \ No newline at end of file diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/CurrentTestApplicationModuleInfoTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/CurrentTestApplicationModuleInfoTests.cs new file mode 100644 index 0000000000..708f09e9bf --- /dev/null +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Services/CurrentTestApplicationModuleInfoTests.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using Microsoft.Testing.Platform.Helpers; +using Microsoft.Testing.Platform.Services; + +namespace Microsoft.Testing.Platform.UnitTests.Services; + +[TestClass] +public class CurrentTestApplicationModuleInfoTests +{ + [TestMethod] + public void GetCurrentExecutableInfo_WithCustomArgs_ForExecutable_ShouldUseCustomArguments() + { + // Arrange + var environment = new TestEnvironment(); + environment.SetCommandLineArgs(["MyTest.exe"]); + environment.SetProcessPath("MyTest.exe"); + + var processHandler = new TestProcessHandler(); + var moduleInfo = new CurrentTestApplicationModuleInfo(environment, processHandler); + + var customArgs = new[] { "--retry-failed-tests", "1", "--results-directory", "test" }; + var argsProvider = new CommandLineArgumentsProvider(customArgs); + moduleInfo.SetCommandLineArgumentsProvider(argsProvider); + + // Act + var executableInfo = moduleInfo.GetCurrentExecutableInfo(); + + // Assert + Assert.IsNotNull(executableInfo); + var arguments = executableInfo.Arguments.ToArray(); + + // For executable scenarios, arguments should be: custom args (without executable name) + Assert.AreEqual(4, arguments.Length); + Assert.AreEqual("--retry-failed-tests", arguments[0]); + Assert.AreEqual("1", arguments[1]); + Assert.AreEqual("--results-directory", arguments[2]); + Assert.AreEqual("test", arguments[3]); + } + + [TestMethod] + public void GetCurrentExecutableInfo_WithCustomArgs_ForDotnet_ShouldCombineWithAssemblyPath() + { + // Arrange + var environment = new TestEnvironment(); + environment.SetCommandLineArgs(["dotnet", "MyTest.dll"]); + environment.SetProcessPath("dotnet"); + + var processHandler = new TestProcessHandler(); + var moduleInfo = new CurrentTestApplicationModuleInfo(environment, processHandler); + + var customArgs = new[] { "--retry-failed-tests", "1" }; + var argsProvider = new CommandLineArgumentsProvider(customArgs); + moduleInfo.SetCommandLineArgumentsProvider(argsProvider); + + // Act + var executableInfo = moduleInfo.GetCurrentExecutableInfo(); + + // Assert + Assert.IsNotNull(executableInfo); + var arguments = executableInfo.Arguments.ToArray(); + + // For dotnet scenarios, arguments should be: ["exec", "dotnet", "MyTest.dll", custom args...] + Assert.AreEqual(5, arguments.Length); + Assert.AreEqual("exec", arguments[0]); + Assert.AreEqual("dotnet", arguments[1]); + Assert.AreEqual("MyTest.dll", arguments[2]); + Assert.AreEqual("--retry-failed-tests", arguments[3]); + Assert.AreEqual("1", arguments[4]); + } + + [TestMethod] + public void GetCurrentExecutableInfo_WithoutCustomArgs_ShouldUseEnvironmentArguments() + { + // Arrange + var environment = new TestEnvironment(); + environment.SetCommandLineArgs(["MyTest.exe", "--existing-arg"]); + environment.SetProcessPath("MyTest.exe"); + + var processHandler = new TestProcessHandler(); + var moduleInfo = new CurrentTestApplicationModuleInfo(environment, processHandler); + + // Act (no custom args provider set) + var executableInfo = moduleInfo.GetCurrentExecutableInfo(); + + // Assert + Assert.IsNotNull(executableInfo); + var arguments = executableInfo.Arguments.ToArray(); + + // Should fall back to environment args (minus executable name) + Assert.AreEqual(1, arguments.Length); + Assert.AreEqual("--existing-arg", arguments[0]); + } + + private class TestEnvironment : IEnvironment + { + private string[] _commandLineArgs = Array.Empty(); + private string? _processPath; + + public void SetCommandLineArgs(string[] args) => _commandLineArgs = args; + public void SetProcessPath(string path) => _processPath = path; + + public string[] GetCommandLineArgs() => _commandLineArgs; + public int ProcessId => 1234; + public string? ProcessPath => _processPath; + + public string? GetEnvironmentVariable(string name) => null; + public void SetEnvironmentVariable(string name, string? value) { } + } + + private class TestProcessHandler : IProcessHandler + { + public IProcess GetCurrentProcess() => new TestProcess(); + public IProcess Start(ProcessStartInfo startInfo) => new TestProcess(); + } + + private class TestProcess : IProcess + { + public int Id => 1234; + public string Name => "test"; + public ProcessModule? MainModule => new TestProcessModule(); + public int ExitCode => 0; + public event EventHandler? Exited; + public void WaitForExit() { } + public Task WaitForExitAsync() => Task.CompletedTask; + public void Dispose() { } + } + + private class TestProcessModule : ProcessModule + { + public override string? FileName => "MyTest.exe"; + } +} \ No newline at end of file