Skip to content

Commit ccbaf50

Browse files
authored
[rel/3.8] Consider --results-directory before configuration (#5204)
2 parents fa97437 + ec34a47 commit ccbaf50

File tree

7 files changed

+90
-91
lines changed

7 files changed

+90
-91
lines changed
Lines changed: 61 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Microsoft Corporation. All rights reserved.
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
33

44
using Microsoft.Testing.Platform.CommandLine;
@@ -8,27 +8,34 @@
88

99
namespace Microsoft.Testing.Platform.Configurations;
1010

11-
internal sealed class AggregatedConfiguration(IConfigurationProvider[] configurationProviders, ITestApplicationModuleInfo testApplicationModuleInfo, IFileSystem fileSystem) : IConfiguration
11+
internal sealed class AggregatedConfiguration(
12+
IConfigurationProvider[] configurationProviders,
13+
ITestApplicationModuleInfo testApplicationModuleInfo,
14+
IFileSystem fileSystem,
15+
CommandLineParseResult commandLineParseResult) : IConfiguration
1216
{
1317
public const string DefaultTestResultFolderName = "TestResults";
1418
private readonly IConfigurationProvider[] _configurationProviders = configurationProviders;
1519
private readonly ITestApplicationModuleInfo _testApplicationModuleInfo = testApplicationModuleInfo;
1620
private readonly IFileSystem _fileSystem = fileSystem;
17-
private string? _resultDirectory;
21+
private readonly CommandLineParseResult _commandLineParseResult = commandLineParseResult;
22+
private string? _resultsDirectory;
1823
private string? _currentWorkingDirectory;
1924
private string? _testHostWorkingDirectory;
2025

2126
public string? this[string key]
2227
{
2328
get
2429
{
25-
if (key == PlatformConfigurationConstants.PlatformResultDirectory && _resultDirectory is not null)
30+
if (key == PlatformConfigurationConstants.PlatformResultDirectory)
2631
{
27-
return _resultDirectory;
32+
_resultsDirectory = GetResultsDirectoryCore(_commandLineParseResult);
33+
return _resultsDirectory;
2834
}
2935

30-
if (key == PlatformConfigurationConstants.PlatformCurrentWorkingDirectory && _currentWorkingDirectory is not null)
36+
if (key == PlatformConfigurationConstants.PlatformCurrentWorkingDirectory)
3137
{
38+
_currentWorkingDirectory = GetCurrentWorkingDirectoryCore();
3239
return _currentWorkingDirectory;
3340
}
3441

@@ -37,55 +44,73 @@ public string? this[string key]
3744
return _testHostWorkingDirectory;
3845
}
3946

40-
foreach (IConfigurationProvider source in _configurationProviders)
47+
// Fallback to calculating from configuration providers.
48+
return CalculateFromConfigurationProviders(key);
49+
}
50+
}
51+
52+
private string? CalculateFromConfigurationProviders(string key)
53+
{
54+
foreach (IConfigurationProvider source in _configurationProviders)
55+
{
56+
if (source.TryGet(key, out string? value))
4157
{
42-
if (source.TryGet(key, out string? value))
43-
{
44-
return value;
45-
}
58+
return value;
4659
}
47-
48-
return null;
4960
}
50-
}
5161

52-
public /* for testing */ void SetResultDirectory(string resultDirectory) =>
53-
_resultDirectory = Guard.NotNull(resultDirectory);
62+
return null;
63+
}
5464

5565
public /* for testing */ void SetCurrentWorkingDirectory(string workingDirectory) =>
5666
_currentWorkingDirectory = Guard.NotNull(workingDirectory);
5767

5868
public void SetTestHostWorkingDirectory(string workingDirectory) =>
5969
_testHostWorkingDirectory = Guard.NotNull(workingDirectory);
6070

61-
public async Task CheckTestResultsDirectoryOverrideAndCreateItAsync(ICommandLineOptions commandLineOptions, IFileLoggerProvider? fileLoggerProvider)
71+
public async Task CheckTestResultsDirectoryOverrideAndCreateItAsync(IFileLoggerProvider? fileLoggerProvider)
6272
{
63-
// Load Configuration
64-
_currentWorkingDirectory = Path.GetDirectoryName(_testApplicationModuleInfo.GetCurrentTestApplicationFullPath())!;
73+
_resultsDirectory = _fileSystem.CreateDirectory(this[PlatformConfigurationConstants.PlatformResultDirectory]!);
6574

66-
string? resultDirectory = this[PlatformConfigurationConstants.PlatformResultDirectory];
67-
if (resultDirectory is null)
75+
// In case of the result directory is overridden by the config file we move logs to it.
76+
// This can happen in case of VSTest mode where the result directory is set to a different location.
77+
// This behavior is non documented and we reserve the right to change it in the future.
78+
if (fileLoggerProvider is not null)
6879
{
69-
if (commandLineOptions.TryGetOptionArgumentList(PlatformCommandLineProvider.ResultDirectoryOptionKey, out string[]? resultDirectoryArg))
70-
{
71-
_resultDirectory = resultDirectoryArg[0];
72-
}
80+
await fileLoggerProvider.CheckLogFolderAndMoveToTheNewIfNeededAsync(_resultsDirectory);
7381
}
74-
else
82+
}
83+
84+
private string GetResultsDirectoryCore(CommandLineParseResult commandLineParseResult)
85+
{
86+
// We already calculated it before, or was set by unit tests via SetCurrentWorkingDirectory.
87+
if (_resultsDirectory is not null)
7588
{
76-
_resultDirectory = resultDirectory;
89+
return _resultsDirectory;
7790
}
7891

79-
_resultDirectory ??= Path.Combine(_currentWorkingDirectory, DefaultTestResultFolderName);
80-
81-
_resultDirectory = _fileSystem.CreateDirectory(_resultDirectory);
82-
83-
// In case of the result directory is overridden by the config file we move logs to it.
84-
// This can happen in case of VSTest mode where the result directory is set to a different location.
85-
// This behavior is non documented and we reserve the right to change it in the future.
86-
if (fileLoggerProvider is not null)
92+
// NOTE: It's important to consider the `--results-directory` option before the configurations.
93+
// It makes more sense to respect it first, and is also relevant for Retry extension.
94+
// Consider user is setting ResultsDirectory in configuration file.
95+
// When tests are retried, we re-run the executable again, but we pass --results-directory containing the folder where the current retry attempt
96+
// results should be stored. But we will also still pass configuration file (e.g, runsettings via --settings) that the user originally specified.
97+
// In that case, we will want to respect --results-directory.
98+
if (commandLineParseResult.TryGetOptionArgumentList(PlatformCommandLineProvider.ResultDirectoryOptionKey, out string[]? resultDirectoryArg))
8799
{
88-
await fileLoggerProvider.CheckLogFolderAndMoveToTheNewIfNeededAsync(_resultDirectory);
100+
return resultDirectoryArg[0];
89101
}
102+
103+
// If not specified by command line, then use the configuration providers.
104+
// And finally fallback to DefaultTestResultFolderName relative to the current working directory.
105+
return CalculateFromConfigurationProviders(PlatformConfigurationConstants.PlatformResultDirectory)
106+
?? Path.Combine(this[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]!, DefaultTestResultFolderName);
90107
}
108+
109+
private string GetCurrentWorkingDirectoryCore()
110+
// If the value is already set, use that.
111+
=> _currentWorkingDirectory
112+
// If first time calculating it, prefer the value from configuration,
113+
?? CalculateFromConfigurationProviders(PlatformConfigurationConstants.PlatformCurrentWorkingDirectory)
114+
// then fallback to the actual working directory.
115+
?? Path.GetDirectoryName(_testApplicationModuleInfo.GetCurrentTestApplicationFullPath())!;
91116
}

src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ public static string GetCurrentWorkingDirectory(this IConfiguration configuratio
3535
/// </summary>
3636
/// <param name="configuration">The configuration.</param>
3737
/// <returns>The test host working directory.</returns>
38+
// TODO: This is only used in unit tests. Should that be removed (maybe in v2)?
3839
public static string GetTestHostWorkingDirectory(this IConfiguration configuration)
3940
{
4041
string? workingDirectory = configuration[PlatformConfigurationConstants.PlatformTestHostWorkingDirectory];

src/Platform/Microsoft.Testing.Platform/Configurations/ConfigurationManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,6 @@ internal async Task<IConfiguration> BuildAsync(IFileLoggerProvider? syncFileLogg
5858

5959
return defaultJsonConfiguration is null
6060
? throw new InvalidOperationException(PlatformResources.ConfigurationManagerCannotFindDefaultJsonConfigurationErrorMessage)
61-
: new AggregatedConfiguration(configurationProviders.OrderBy(x => x.Order).Select(x => x.ConfigurationProvider).ToArray(), _testApplicationModuleInfo, _fileSystem);
61+
: new AggregatedConfiguration(configurationProviders.OrderBy(x => x.Order).Select(x => x.ConfigurationProvider).ToArray(), _testApplicationModuleInfo, _fileSystem, commandLineParseResult);
6262
}
6363
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ public async Task<ITestHost> BuildAsync(
288288
// here we ensure to override the result directory if user passed the argument --results-directory in command line.
289289
// After this check users can get the result directory using IConfiguration["platformOptions:resultDirectory"] or the
290290
// extension method helper serviceProvider.GetConfiguration()
291-
await configuration.CheckTestResultsDirectoryOverrideAndCreateItAsync(commandLineOptions, loggingState.FileLoggerProvider);
291+
await configuration.CheckTestResultsDirectoryOverrideAndCreateItAsync(loggingState.FileLoggerProvider);
292292

293293
// Display banner now because we need capture the output in case of MSBuild integration and we want to forward
294294
// to file disc also the banner, so at this point we need to have all services and configuration(result directory) built.

test/UnitTests/MSTest.Engine.UnitTests/Adapter_ExecuteRequestAsyncTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public Services()
116116
ServiceProvider.AddService(new FakeClock());
117117
ServiceProvider.AddService(new SystemTask());
118118
#pragma warning disable TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
119-
ServiceProvider.AddService(new AggregatedConfiguration(Array.Empty<IConfigurationProvider>(), new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem()));
119+
ServiceProvider.AddService(new AggregatedConfiguration(Array.Empty<IConfigurationProvider>(), new CurrentTestApplicationModuleInfo(new SystemEnvironment(), new SystemProcessHandler()), new SystemFileSystem(), new(null, [], [])));
120120
#pragma warning restore TPEXP // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
121121
}
122122

test/UnitTests/Microsoft.Testing.Platform.UnitTests/Configuration/AggregatedConfigurationTests.cs

Lines changed: 24 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,28 @@ public sealed class AggregatedConfigurationTests
2222
[DataRow(PlatformConfigurationConstants.PlatformResultDirectory)]
2323
[DataRow(PlatformConfigurationConstants.PlatformCurrentWorkingDirectory)]
2424
[DataRow(PlatformConfigurationConstants.PlatformTestHostWorkingDirectory)]
25-
public void IndexerTest_DirectoryNotSetAndNoConfigurationProviders_DirectoryIsNull(string key)
26-
=> Assert.IsNull(new AggregatedConfiguration([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object)[key]);
25+
public void IndexerTest_DirectoryNotSetAndNoConfigurationProviders(string key)
26+
{
27+
_testApplicationModuleInfoMock.Setup(x => x.GetCurrentTestApplicationFullPath()).Returns("TestAppDir/test.exe");
28+
string? expected = key switch
29+
{
30+
PlatformConfigurationConstants.PlatformResultDirectory => Path.Combine("TestAppDir", "TestResults"),
31+
PlatformConfigurationConstants.PlatformCurrentWorkingDirectory => "TestAppDir",
32+
PlatformConfigurationConstants.PlatformTestHostWorkingDirectory => null,
33+
_ => throw ApplicationStateGuard.Unreachable(),
34+
};
35+
36+
Assert.AreEqual(expected, new AggregatedConfiguration([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []))[key]);
37+
}
2738

2839
[TestMethod]
29-
[DataRow(PlatformConfigurationConstants.PlatformResultDirectory)]
3040
[DataRow(PlatformConfigurationConstants.PlatformCurrentWorkingDirectory)]
3141
[DataRow(PlatformConfigurationConstants.PlatformTestHostWorkingDirectory)]
3242
public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_DirectoryIsNull(string key)
3343
{
3444
Mock<IConfigurationProvider> mockProvider = new();
3545

36-
AggregatedConfiguration aggregatedConfiguration = new([mockProvider.Object], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object);
46+
AggregatedConfiguration aggregatedConfiguration = new([mockProvider.Object], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []));
3747
Assert.IsNull(aggregatedConfiguration[key]);
3848
}
3949

@@ -43,23 +53,21 @@ public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_Director
4353
[DataRow(PlatformConfigurationConstants.PlatformTestHostWorkingDirectory)]
4454
public void IndexerTest_DirectoryNotSetButConfigurationProvidersPresent_DirectoryIsNotNull(string key)
4555
{
46-
AggregatedConfiguration aggregatedConfiguration = new([new FakeConfigurationProvider(ExpectedPath)], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object);
56+
AggregatedConfiguration aggregatedConfiguration = new([new FakeConfigurationProvider(ExpectedPath)], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []));
4757
Assert.AreEqual(ExpectedPath, aggregatedConfiguration[key]);
4858
}
4959

5060
[TestMethod]
5161
public void IndexerTest_ResultDirectorySet_DirectoryIsNotNull()
5262
{
53-
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object);
54-
55-
aggregatedConfiguration.SetResultDirectory(ExpectedPath);
63+
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], []));
5664
Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformResultDirectory]);
5765
}
5866

5967
[TestMethod]
6068
public void IndexerTest_CurrentWorkingDirectorySet_DirectoryIsNotNull()
6169
{
62-
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object);
70+
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []));
6371

6472
aggregatedConfiguration.SetCurrentWorkingDirectory(ExpectedPath);
6573
Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformCurrentWorkingDirectory]);
@@ -68,7 +76,7 @@ public void IndexerTest_CurrentWorkingDirectorySet_DirectoryIsNotNull()
6876
[TestMethod]
6977
public void IndexerTest_TestHostWorkingDirectorySet_DirectoryIsNotNull()
7078
{
71-
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object);
79+
AggregatedConfiguration aggregatedConfiguration = new([], _testApplicationModuleInfoMock.Object, _fileSystemMock.Object, new(null, [], []));
7280

7381
aggregatedConfiguration.SetTestHostWorkingDirectory(ExpectedPath);
7482
Assert.AreEqual(ExpectedPath, aggregatedConfiguration[PlatformConfigurationConstants.PlatformTestHostWorkingDirectory]);
@@ -86,9 +94,8 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results
8694
Mock<IFileLoggerProvider> mockFileLogger = new();
8795
mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny<string>())).Callback(() => { });
8896

89-
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object);
90-
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(
91-
new FakeCommandLineOptions(ExpectedPath), mockFileLogger.Object);
97+
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], []));
98+
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object);
9299

93100
mockFileSystem.Verify(x => x.CreateDirectory(ExpectedPath), Times.Once);
94101
mockFileLogger.Verify(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(ExpectedPath), Times.Once);
@@ -108,9 +115,8 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results
108115
Mock<IFileLoggerProvider> mockFileLogger = new();
109116
mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny<string>())).Callback(() => { });
110117

111-
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object);
112-
aggregatedConfiguration.SetResultDirectory(ExpectedPath);
113-
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(new FakeCommandLineOptions(ExpectedPath), mockFileLogger.Object);
118+
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [new CommandLineParseOption("results-directory", [ExpectedPath])], []));
119+
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object);
114120

115121
mockFileSystem.Verify(x => x.CreateDirectory(ExpectedPath), Times.Once);
116122
mockFileLogger.Verify(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(ExpectedPath), Times.Once);
@@ -130,9 +136,8 @@ public async ValueTask CheckTestResultsDirectoryOverrideAndCreateItAsync_Results
130136
Mock<IFileLoggerProvider> mockFileLogger = new();
131137
mockFileLogger.Setup(x => x.CheckLogFolderAndMoveToTheNewIfNeededAsync(It.IsAny<string>())).Callback(() => { });
132138

133-
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object);
134-
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(
135-
new FakeCommandLineOptions(ExpectedPath, bypass: true), mockFileLogger.Object);
139+
AggregatedConfiguration aggregatedConfiguration = new([], mockTestApplicationModuleInfo.Object, mockFileSystem.Object, new(null, [], []));
140+
await aggregatedConfiguration.CheckTestResultsDirectoryOverrideAndCreateItAsync(mockFileLogger.Object);
136141

137142
string expectedPath = "a" + Path.DirectorySeparatorChar + "b" + Path.DirectorySeparatorChar + "TestResults";
138143
mockFileSystem.Verify(x => x.CreateDirectory(expectedPath), Times.Once);
@@ -166,35 +171,3 @@ public bool TryGet(string key, out string? value)
166171
}
167172
}
168173
}
169-
170-
internal sealed class FakeCommandLineOptions : ICommandLineOptions
171-
{
172-
private readonly string _path;
173-
private readonly bool _bypass;
174-
175-
public FakeCommandLineOptions(string path, bool bypass = false)
176-
{
177-
_path = path;
178-
_bypass = bypass;
179-
}
180-
181-
public bool IsOptionSet(string optionName) => throw new NotImplementedException();
182-
183-
public bool TryGetOptionArgumentList(string optionName, [NotNullWhen(true)] out string[]? arguments)
184-
{
185-
arguments = null;
186-
if (_bypass)
187-
{
188-
return false;
189-
}
190-
191-
switch (optionName)
192-
{
193-
case PlatformCommandLineProvider.ResultDirectoryOptionKey:
194-
arguments = [_path];
195-
return true;
196-
default:
197-
return false;
198-
}
199-
}
200-
}

0 commit comments

Comments
 (0)