diff --git a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs index 1421a35431..c976bc61e5 100644 --- a/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs +++ b/src/Platform/Microsoft.Testing.Platform/Builder/TestApplication.cs @@ -305,7 +305,7 @@ private static ApplicationLoggingState CreateFileLoggerIfDiagnosticIsEnabled( } // Set the directory to the default test result directory - string directory = Path.Combine(testApplicationModuleInfo.GetCurrentTestApplicationDirectory(), AggregatedConfiguration.DefaultTestResultFolderName); + string directory = GetDiagnosticDefaultDirectory(environment, testApplicationModuleInfo); bool customDirectory = false; if (result.TryGetOptionArgumentList(PlatformCommandLineProvider.ResultDirectoryOptionKey, out string[]? resultDirectoryArg)) @@ -381,6 +381,17 @@ private static ApplicationLoggingState CreateFileLoggerIfDiagnosticIsEnabled( || ThrowInvalidDiagnosticVerbosity(environmentLogLevel)); } + internal /* for testing purposes */ static string GetDiagnosticDefaultDirectory(IEnvironment environment, ITestApplicationModuleInfo testApplicationModuleInfo) + { + string? effectiveWorkingDirectory = environment.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY); + if (RoslynString.IsNullOrWhiteSpace(effectiveWorkingDirectory)) + { + effectiveWorkingDirectory = testApplicationModuleInfo.GetCurrentTestApplicationDirectory(); + } + + return Path.Combine(effectiveWorkingDirectory, AggregatedConfiguration.DefaultTestResultFolderName); + } + private static bool ThrowInvalidDiagnosticVerbosity(string environmentLogLevel) => throw new NotSupportedException($"Invalid environment value '{nameof(EnvironmentVariableConstants.TESTINGPLATFORM_DIAGNOSTIC_VERBOSITY)}', was expecting 'Trace', 'Debug', 'Information', 'Warning', 'Error', or 'Critical' but got '{environmentLogLevel}'."); } diff --git a/src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.cs b/src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.cs index 5fd22d057b..a8279e777d 100644 --- a/src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.cs +++ b/src/Platform/Microsoft.Testing.Platform/Configurations/AggregatedConfiguration.cs @@ -12,12 +12,14 @@ internal sealed class AggregatedConfiguration( IConfigurationProvider[] configurationProviders, ITestApplicationModuleInfo testApplicationModuleInfo, IFileSystem fileSystem, + IEnvironment environment, CommandLineParseResult commandLineParseResult) : IConfiguration { public const string DefaultTestResultFolderName = "TestResults"; private readonly IConfigurationProvider[] _configurationProviders = configurationProviders; private readonly ITestApplicationModuleInfo _testApplicationModuleInfo = testApplicationModuleInfo; private readonly IFileSystem _fileSystem = fileSystem; + private readonly IEnvironment _environment = environment; private readonly CommandLineParseResult _commandLineParseResult = commandLineParseResult; private string? _resultsDirectory; private string? _currentWorkingDirectory; @@ -93,15 +95,36 @@ private string GetResultsDirectoryCore(CommandLineParseResult commandLineParseRe // If not specified by command line, then use the configuration providers. // And finally fallback to DefaultTestResultFolderName relative to the current working directory. + // Note: PlatformCurrentWorkingDirectory already incorporates DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY + // (via GetCurrentWorkingDirectoryCore), so we don't need to check that env var separately here. return CalculateFromConfigurationProviders(PlatformConfigurationConstants.PlatformResultDirectory) ?? Path.Combine(this[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]!, DefaultTestResultFolderName); } private string GetCurrentWorkingDirectoryCore() - // If the value is already set, use that. - => _currentWorkingDirectory - // If first time calculating it, prefer the value from configuration, - ?? CalculateFromConfigurationProviders(PlatformConfigurationConstants.PlatformCurrentWorkingDirectory) - // then fallback to the actual working directory. - ?? _testApplicationModuleInfo.GetCurrentTestApplicationDirectory(); + { + // If the value is already set, use that. + if (_currentWorkingDirectory is not null) + { + return _currentWorkingDirectory; + } + + // If first time calculating it, prefer the value from configuration, + string? fromConfig = CalculateFromConfigurationProviders(PlatformConfigurationConstants.PlatformCurrentWorkingDirectory); + if (fromConfig is not null) + { + return fromConfig; + } + + // then check if dotnet test working directory is set (to keep PlatformCurrentWorkingDirectory and + // PlatformResultDirectory consistent when running under 'dotnet test'), + string? dotnetTestCwd = _environment.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY); + if (!RoslynString.IsNullOrWhiteSpace(dotnetTestCwd)) + { + return dotnetTestCwd; + } + + // then fallback to the actual working directory. + return _testApplicationModuleInfo.GetCurrentTestApplicationDirectory(); + } } diff --git a/src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationManager.cs b/src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationManager.cs index f550e3c32d..3da0c0b798 100644 --- a/src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationManager.cs +++ b/src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationManager.cs @@ -12,11 +12,12 @@ namespace Microsoft.Testing.Platform.Configurations; -internal sealed class ConfigurationManager(IFileSystem fileSystem, ITestApplicationModuleInfo testApplicationModuleInfo) : IConfigurationManager +internal sealed class ConfigurationManager(IFileSystem fileSystem, ITestApplicationModuleInfo testApplicationModuleInfo, IEnvironment environment) : IConfigurationManager { private readonly List> _configurationSources = []; private readonly IFileSystem _fileSystem = fileSystem; private readonly ITestApplicationModuleInfo _testApplicationModuleInfo = testApplicationModuleInfo; + private readonly IEnvironment _environment = environment; public void AddConfigurationSource(Func source) => _configurationSources.Add(source); @@ -68,6 +69,6 @@ internal async Task BuildAsync(IFileLoggerProvider? syncFileLogg } } - return new AggregatedConfiguration(configurationProvidersArray, _testApplicationModuleInfo, _fileSystem, commandLineParseResult); + return new AggregatedConfiguration(configurationProvidersArray, _testApplicationModuleInfo, _fileSystem, _environment, commandLineParseResult); } } diff --git a/src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs b/src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs index 1a62c3ca1a..21fcb57a77 100644 --- a/src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs +++ b/src/Platform/Microsoft.Testing.Platform/Helpers/EnvironmentVariableConstants.cs @@ -39,6 +39,7 @@ internal static class EnvironmentVariableConstants // dotnet test public const string TESTINGPLATFORM_DOTNETTEST_EXECUTIONID = nameof(TESTINGPLATFORM_DOTNETTEST_EXECUTIONID); + public const string DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY = nameof(DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY); // Unhandled Exception public const string TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION = nameof(TESTINGPLATFORM_EXIT_PROCESS_ON_UNHANDLED_EXCEPTION); diff --git a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs index 6bdabb565b..f63eb424cb 100644 --- a/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs +++ b/src/Platform/Microsoft.Testing.Platform/Hosts/TestHostBuilder.cs @@ -35,7 +35,7 @@ internal sealed partial class TestHostBuilder(IFileSystem fileSystem, IRuntimeFe public ITestHostManager TestHost { get; } = new TestHostManager(); - public IConfigurationManager Configuration { get; } = new ConfigurationManager(fileSystem, testApplicationModuleInfo); + public IConfigurationManager Configuration { get; } = new ConfigurationManager(fileSystem, testApplicationModuleInfo, environment); public ILoggingManager Logging { get; } = new LoggingManager(); diff --git a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs index 506004d009..9fe955a27f 100644 --- a/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs +++ b/src/Platform/Microsoft.Testing.Platform/Services/CurrentTestApplicationModuleInfo.cs @@ -113,6 +113,6 @@ public ExecutableInfo GetCurrentExecutableInfo() _ => commandLineArguments, }; - return new(GetProcessPath(), arguments, GetCurrentTestApplicationDirectory()); + return new ExecutableInfo(GetProcessPath(), arguments, GetCurrentTestApplicationDirectory()); } } diff --git a/src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.cs b/src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.cs index acaa00bfce..295fb3c94f 100644 --- a/src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.cs +++ b/src/Platform/Microsoft.Testing.Platform/TestHostControllers/TestHostControllersManager.cs @@ -1,7 +1,6 @@ // 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.Configurations; using Microsoft.Testing.Platform.Extensions; using Microsoft.Testing.Platform.Extensions.TestHostControllers; using Microsoft.Testing.Platform.Helpers; @@ -107,13 +106,6 @@ public void AddDataConsumer(CompositeExtensionFactory compositeServiceFact internal async Task BuildAsync(ServiceProvider serviceProvider) { - // For now the test host working directory and the current working directory are the same. - // In future we could move the test host in a different directory for instance in case of - // the need to rewrite binary files. If we don't move files are locked by ourself. - var aggregatedConfiguration = (AggregatedConfiguration)serviceProvider.GetConfiguration(); - string? currentWorkingDirectory = aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]; - ApplicationStateGuard.Ensure(currentWorkingDirectory is not null); - List<(ITestHostEnvironmentVariableProvider TestHostEnvironmentVariableProvider, int RegistrationOrder)> environmentVariableProviders = []; foreach (Func environmentVariableProviderFactory in _environmentVariableProviderFactories) { diff --git a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs b/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs index de446c0c19..d7fb5bb4a5 100644 --- a/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs +++ b/test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs @@ -109,7 +109,7 @@ public Services() ServiceProvider.AddService(new LoggerFactory()); ServiceProvider.AddService(new FakeClock()); ServiceProvider.AddService(new SystemTask()); - ServiceProvider.AddService(new AggregatedConfiguration([], new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem(), new(null, [], []))); + ServiceProvider.AddService(new AggregatedConfiguration([], new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem(), new SystemEnvironment(), new(null, [], []))); } public MessageBus MessageBus { get; } diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/AggregatedConfigurationTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/AggregatedConfigurationTests.cs index 284dd62341..1e84185a34 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/AggregatedConfigurationTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/AggregatedConfigurationTests.cs @@ -17,6 +17,7 @@ public sealed class AggregatedConfigurationTests private const string ExpectedPath = "a/b/c"; private readonly Mock _testApplicationModuleInfoMock = new(); private readonly Mock _fileSystemMock = new(); + private readonly Mock _environmentMock = new(); [TestMethod] [DataRow(PlatformConfigurationConstants.PlatformResultDirectory)] @@ -33,7 +34,51 @@ public void IndexerTest_DirectoryNotSetAndNoConfigurationProviders(string key) _ => throw ApplicationStateGuard.Unreachable(), }; - Assert.AreEqual(expected, new AggregatedConfiguration([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []))[key]); + Assert.AreEqual(expected, new AggregatedConfiguration([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], []))[key]); + } + + [TestMethod] + public void IndexerTest_DotnetCliTestCommandWorkingDirectorySet_UsedAsWorkingDirectoryBase() + { + const string dotnetTestWorkingDir = "DotnetTestWorkingDir"; + _environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(dotnetTestWorkingDir); + + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], [])); + Assert.AreEqual(Path.Combine(dotnetTestWorkingDir, "TestResults"), aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); + Assert.AreEqual(dotnetTestWorkingDir, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); + Assert.AreEqual(dotnetTestWorkingDir, aggregatedConfiguration[PlatformConfigurationConstants.PlatformTestHostWorkingDirectory]); + } + + [TestMethod] + [DataRow("")] + [DataRow(" ")] + public void IndexerTest_DotnetCliTestCommandWorkingDirectoryIsWhitespace_FallsBackToTestApplicationDirectory(string envVarValue) + { + const string appDirectory = "AppDirectory"; + _environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(envVarValue); + _testApplicationModuleInfoMock.Setup(x => x.GetCurrentTestApplicationDirectory()).Returns(appDirectory); + + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], [])); + Assert.AreEqual(Path.Combine(appDirectory, "TestResults"), aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); + Assert.AreEqual(appDirectory, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); + Assert.AreEqual(appDirectory, aggregatedConfiguration[PlatformConfigurationConstants.PlatformTestHostWorkingDirectory]); + } + + [TestMethod] + public void IndexerTest_ResultsDirectoryCliArgTakesPrecedenceOverDotnetCliTestCommandWorkingDirectory() + { + const string dotnetTestWorkingDir = "DotnetTestWorkingDir"; + _environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(dotnetTestWorkingDir); + + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); + + // --results-directory CLI arg should win over DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY + Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); + // Current working directory still comes from env var + Assert.AreEqual(dotnetTestWorkingDir, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); } [TestMethod] @@ -43,7 +88,7 @@ public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_Director { Mock mockProvider = new(); - AggregatedConfiguration aggregatedConfiguration = new([mockProvider.Object], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], [])); + AggregatedConfiguration aggregatedConfiguration = new([mockProvider.Object], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], [])); Assert.IsNull(aggregatedConfiguration[key]); } @@ -53,21 +98,21 @@ public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_Director [DataRow(PlatformConfigurationConstants.PlatformTestHostWorkingDirectory)] public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_DirectoryIsNotNull(string key) { - AggregatedConfiguration aggregatedConfiguration = new([new FakeConfigurationProvider(ExpectedPath)], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], [])); + AggregatedConfiguration aggregatedConfiguration = new([new FakeConfigurationProvider(ExpectedPath)], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], [])); Assert.AreEqual(ExpectedPath, aggregatedConfiguration[key]); } [TestMethod] public void IndexerTest_ResultDirectorySet_DirectoryIsNotNull() { - AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); } [TestMethod] public void IndexerTest_CurrentWorkingDirectorySet_DirectoryIsNotNull() { - AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], [])); + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, _environmentMock.Object, new(null, [], [])); aggregatedConfiguration.SetCurrentWorkingDirectory(ExpectedPath); Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); @@ -86,7 +131,7 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results Mock mockFileLogger = new(); mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny())).Callback(() => { }); - AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); + AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, _environmentMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object); mockFileSystem.Verify(x => x.CreateDirectory(ExpectedPath), Times.Once); @@ -108,13 +153,15 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results Mock mockFileLogger = new(); mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny())).Callback(() => { }); - AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); + // Results directory comes from configuration provider (store), not CLI args + AggregatedConfiguration aggregatedConfiguration = new([new FakeConfigurationProvider(ExpectedPath)], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, _environmentMock.Object, new(null, [], [])); await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object); mockFileSystem.Verify(x => x.CreateDirectory(ExpectedPath), Times.Once); mockFileLogger.Verify(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(ExpectedPath), Times.Once); Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); - Assert.AreEqual("a" + Path.DirectorySeparatorChar + "b", aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); + // PlatformCurrentWorkingDirectory also comes from the configuration provider + Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); } [TestMethod] @@ -130,7 +177,7 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results Mock mockFileLogger = new(); mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny())).Callback(() => { }); - AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [], [])); + AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, _environmentMock.Object, new(null, [], [])); await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object); string expectedPath = "a" + Path.DirectorySeparatorChar + "b" + Path.DirectorySeparatorChar + "TestResults"; @@ -139,6 +186,57 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results Assert.AreEqual(expectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); Assert.AreEqual("a" + Path.DirectorySeparatorChar + "b", aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); } + + [TestMethod] + public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_DotnetCliTestCommandWorkingDirectorySet_UsedAsResultDirectoryBase() + { + const string dotnetTestWorkingDir = "DotnetTestWorkingDir"; + _environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(dotnetTestWorkingDir); + + Mock mockFileSystem = new(); + mockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())).Returns((string path) => path); + + Mock mockFileLogger = new(); + mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny())).Callback(() => { }); + + AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, mockFileSystem.Object, _environmentMock.Object, new(null, [], [])); + await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object); + + string expectedPath = Path.Combine(dotnetTestWorkingDir, "TestResults"); + mockFileSystem.Verify(x => x.CreateDirectory(expectedPath), Times.Once); + mockFileLogger.Verify(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(expectedPath), Times.Once); + Assert.AreEqual(expectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); + Assert.AreEqual(dotnetTestWorkingDir, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); + } + + [TestMethod] + public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_ResultsDirectoryCliArgTakesPrecedenceOverDotnetCliTestCommandWorkingDirectory() + { + const string dotnetTestWorkingDir = "DotnetTestWorkingDir"; + _environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(dotnetTestWorkingDir); + + Mock mockTestApplicationModuleInfo = new(); + mockTestApplicationModuleInfo.Setup(x => x.GetCurrentTestApplicationFullPath()).Returns(ExpectedPath); + mockTestApplicationModuleInfo.Setup(x => x.GetCurrentTestApplicationDirectory()).Returns(Path.GetDirectoryName(ExpectedPath) ?? AppContext.BaseDirectory); + + Mock mockFileSystem = new(); + mockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())).Returns((string path) => path); + + Mock mockFileLogger = new(); + mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny())).Callback(() => { }); + + // --results-directory CLI arg takes precedence over DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY + AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, _environmentMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], [])); + await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object); + + mockFileSystem.Verify(x => x.CreateDirectory(ExpectedPath), Times.Once); + mockFileLogger.Verify(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(ExpectedPath), Times.Once); + Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]); + // Current working directory still comes from env var + Assert.AreEqual(dotnetTestWorkingDir, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]); + } } internal sealed class FakeConfigurationProvider : IConfigurationProvider diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/ConfigurationManagerTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/ConfigurationManagerTests.cs index 063e9b5116..6304368516 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/ConfigurationManagerTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/ConfigurationManagerTests.cs @@ -27,7 +27,7 @@ public async ValueTask GetConfigurationValueFromJson(string jsonFileConfig, stri fileSystem.Setup(x => x.NewFileStream(It.IsAny(), FileMode.Open, FileAccess.Read)) .Returns(new MemoryFileStream(Encoding.UTF8.GetBytes(jsonFileConfig))); CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo); + ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo, new SystemEnvironment()); configurationManager.AddConfigurationSource(() => new JsonConfigurationSource(testApplicationModuleInfo, fileSystem.Object, null)); IConfiguration configuration = await configurationManager.BuildAsync(null, new CommandLineParseResult(null, new List(), [])); Assert.AreEqual(result, configuration[key], $"Expected '{result}' found '{configuration[key]}'"); @@ -57,7 +57,7 @@ public async ValueTask InvalidJson_Fail() fileSystem.Setup(x => x.ExistFile(It.IsAny())).Returns(true); fileSystem.Setup(x => x.NewFileStream(It.IsAny(), FileMode.Open, FileAccess.Read)).Returns(() => new MemoryFileStream(Encoding.UTF8.GetBytes(string.Empty))); CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo); + ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo, new SystemEnvironment()); configurationManager.AddConfigurationSource(() => new JsonConfigurationSource(testApplicationModuleInfo, fileSystem.Object, null)); @@ -87,7 +87,7 @@ public async ValueTask GetConfigurationValueFromJsonWithFileLoggerProvider(strin loggerProviderMock.Setup(x => x.CreateLogger(It.IsAny())).Returns(loggerMock.Object); CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo); + ConfigurationManager configurationManager = new(fileSystem.Object, testApplicationModuleInfo, new SystemEnvironment()); configurationManager.AddConfigurationSource(() => new JsonConfigurationSource(testApplicationModuleInfo, fileSystem.Object, null)); @@ -101,7 +101,7 @@ public async ValueTask GetConfigurationValueFromJsonWithFileLoggerProvider(strin public async ValueTask BuildAsync_EmptyConfigurationSources_ThrowsException() { CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo); + ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo, new SystemEnvironment()); await Assert.ThrowsAsync(() => configurationManager.BuildAsync(null, new CommandLineParseResult(null, new List(), []))); } @@ -112,7 +112,7 @@ public async ValueTask BuildAsync_ConfigurationSourcesNotEnabledAsync_ThrowsExce mockConfigurationSource.Setup(x => x.IsEnabledAsync()).ReturnsAsync(false); CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo); + ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo, new SystemEnvironment()); configurationManager.AddConfigurationSource(() => mockConfigurationSource.Object); await Assert.ThrowsAsync(() => configurationManager.BuildAsync(null, new CommandLineParseResult(null, new List(), []))); @@ -132,7 +132,7 @@ public async ValueTask BuildAsync_ConfigurationSourceIsAsyncInitializableExtensi }; CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo); + ConfigurationManager configurationManager = new(new SystemFileSystem(), testApplicationModuleInfo, new SystemEnvironment()); configurationManager.AddConfigurationSource(() => fakeConfigurationSource); await Assert.ThrowsAsync(() => configurationManager.BuildAsync(null, new CommandLineParseResult(null, new List(), []))); diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.cs index 294f21ff64..8a67fbfb40 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationBuilderTests.cs @@ -21,7 +21,7 @@ public sealed class TestApplicationBuilderTests public TestApplicationBuilderTests() { CurrentTestApplicationModuleInfo testApplicationModuleInfo = new(new SystemEnvironment(), new SystemProcessHandler()); - AggregatedConfiguration configuration = new([], testApplicationModuleInfo, new SystemFileSystem(), new(null, [], [])); + AggregatedConfiguration configuration = new([], testApplicationModuleInfo, new SystemFileSystem(), new SystemEnvironment(), new(null, [], [])); configuration.SetCurrentWorkingDirectory(string.Empty); configuration.SetCurrentWorkingDirectory(string.Empty); _serviceProvider.AddService(configuration); diff --git a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationDiagnosticVerbosityTests.cs b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationDiagnosticVerbosityTests.cs index eb7481ec46..192d219a69 100644 --- a/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationDiagnosticVerbosityTests.cs +++ b/test/UnitTests/Microsoft.Testing.Platform.UnitTests/TestApplicationDiagnosticVerbosityTests.cs @@ -1,7 +1,12 @@ // 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.Configurations; +using Microsoft.Testing.Platform.Helpers; using Microsoft.Testing.Platform.Logging; +using Microsoft.Testing.Platform.Services; + +using Moq; namespace Microsoft.Testing.Platform.UnitTests; @@ -29,4 +34,37 @@ public void TryParseDiagnosticVerbosity_WhenNullValue_ReturnsFalse() [TestMethod] public void TryParseDiagnosticVerbosity_WhenInvalidValue_ThrowsNotSupportedException() => Assert.ThrowsExactly(() => _ = TestApplication.TryParseDiagnosticVerbosity("invalid", out _)); + + [TestMethod] + public void GetDiagnosticDefaultDirectory_WhenDotnetCliTestCommandWorkingDirectorySet_UsesEnvVarAsBase() + { + const string dotnetTestWorkingDir = "DotnetTestWorkingDir"; + Mock environmentMock = new(); + environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(dotnetTestWorkingDir); + Mock moduleInfoMock = new(); + + string result = TestApplication.GetDiagnosticDefaultDirectory(environmentMock.Object, moduleInfoMock.Object); + + Assert.AreEqual(Path.Combine(dotnetTestWorkingDir, AggregatedConfiguration.DefaultTestResultFolderName), result); + moduleInfoMock.Verify(x => x.GetCurrentTestApplicationDirectory(), Times.Never); + } + + [TestMethod] + [DataRow(null)] + [DataRow("")] + [DataRow(" ")] + public void GetDiagnosticDefaultDirectory_WhenDotnetCliTestCommandWorkingDirectoryNullOrWhitespace_FallsBackToAppDirectory(string? envVarValue) + { + const string appDirectory = "AppDirectory"; + Mock environmentMock = new(); + environmentMock.Setup(x => x.GetEnvironmentVariable(EnvironmentVariableConstants.DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY)) + .Returns(envVarValue); + Mock moduleInfoMock = new(); + moduleInfoMock.Setup(x => x.GetCurrentTestApplicationDirectory()).Returns(appDirectory); + + string result = TestApplication.GetDiagnosticDefaultDirectory(environmentMock.Object, moduleInfoMock.Object); + + Assert.AreEqual(Path.Combine(appDirectory, AggregatedConfiguration.DefaultTestResultFolderName), result); + } } diff --git a/test/Utilities/Microsoft.Testing.TestInfrastructure/WellKnownEnvironmentVariables.cs b/test/Utilities/Microsoft.Testing.TestInfrastructure/WellKnownEnvironmentVariables.cs index 4d636c0cec..4921721b60 100644 --- a/test/Utilities/Microsoft.Testing.TestInfrastructure/WellKnownEnvironmentVariables.cs +++ b/test/Utilities/Microsoft.Testing.TestInfrastructure/WellKnownEnvironmentVariables.cs @@ -41,6 +41,7 @@ public static class WellKnownEnvironmentVariables // dotnet test "TESTINGPLATFORM_DOTNETTEST_EXECUTIONID", + "DOTNET_CLI_TEST_COMMAND_WORKING_DIRECTORY", // Isolate from the skip banner in case of parent, children tests "TESTINGPLATFORM_CONSOLEOUTPUTDEVICE_SKIP_BANNER"