Skip to content

Commit efd6caa

Browse files
authored
[release/10.0.1xx] Fix solution parsing in dotnet test for Microsoft.Testing.Platform (#51794)
1 parent d6a4c47 commit efd6caa

File tree

20 files changed

+541
-27
lines changed

20 files changed

+541
-27
lines changed

src/Cli/dotnet/Commands/Test/CliConstants.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ internal static class ProjectProperties
7676
internal const string IsTestProject = "IsTestProject";
7777
internal const string TargetFramework = "TargetFramework";
7878
internal const string TargetFrameworks = "TargetFrameworks";
79+
internal const string Configuration = "Configuration";
80+
internal const string Platform = "Platform";
7981
internal const string TargetPath = "TargetPath";
8082
internal const string ProjectFullPath = "MSBuildProjectFullPath";
8183
internal const string RunCommand = "RunCommand";

src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs

Lines changed: 45 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Collections.Concurrent;
55
using System.CommandLine;
6+
using System.Runtime.CompilerServices;
7+
using Microsoft.Build.Construction;
68
using Microsoft.Build.Evaluation;
79
using Microsoft.Build.Evaluation.Context;
810
using Microsoft.Build.Execution;
@@ -18,28 +20,55 @@ internal static class MSBuildUtility
1820
{
1921
private const string dotnetTestVerb = "dotnet-test";
2022

23+
// Related: https://github.com/dotnet/msbuild/pull/7992
24+
// Related: https://github.com/dotnet/msbuild/issues/12711
25+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ProjectShouldBuild")]
26+
static extern bool ProjectShouldBuild(SolutionFile solutionFile, string projectFile);
27+
2128
public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions)
2229
{
23-
SolutionModel solutionModel = SlnFileFactory.CreateFromFileOrDirectory(solutionFilePath, includeSolutionFilterFiles: true, includeSolutionXmlFiles: true);
24-
2530
bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(solutionFilePath, buildOptions);
2631

2732
if (!isBuiltOrRestored)
2833
{
2934
return (Array.Empty<ParallelizableTestModuleGroupWithSequentialInnerModules>(), isBuiltOrRestored);
3035
}
3136

32-
string rootDirectory = solutionFilePath.HasExtension(".slnf") ?
33-
Path.GetDirectoryName(solutionModel.Description)! :
34-
SolutionAndProjectUtility.GetRootDirectory(solutionFilePath);
37+
var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(buildOptions.MSBuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), CommonOptions.VerbosityOption());
38+
var solutionFile = SolutionFile.Parse(Path.GetFullPath(solutionFilePath));
39+
var globalProperties = CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs);
3540

36-
FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb);
41+
globalProperties.TryGetValue("Configuration", out var activeSolutionConfiguration);
42+
globalProperties.TryGetValue("Platform", out var activeSolutionPlatform);
3743

38-
var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(buildOptions.MSBuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), CommonOptions.VerbosityOption());
44+
if (string.IsNullOrEmpty(activeSolutionConfiguration))
45+
{
46+
activeSolutionConfiguration = solutionFile.GetDefaultConfigurationName();
47+
}
48+
49+
if (string.IsNullOrEmpty(activeSolutionPlatform))
50+
{
51+
activeSolutionPlatform = solutionFile.GetDefaultPlatformName();
52+
}
53+
54+
var solutionConfiguration = solutionFile.SolutionConfigurations.FirstOrDefault(c => activeSolutionConfiguration.Equals(c.ConfigurationName, StringComparison.OrdinalIgnoreCase) && activeSolutionPlatform.Equals(c.PlatformName, StringComparison.OrdinalIgnoreCase))
55+
?? throw new InvalidOperationException($"The solution configuration '{activeSolutionConfiguration}|{activeSolutionPlatform}' is invalid.");
56+
57+
// Note: MSBuild seems to be special casing web projects specifically.
58+
// https://github.com/dotnet/msbuild/blob/243fb764b25affe8cc5f233001ead3b5742a297e/src/Build/Construction/Solution/SolutionProjectGenerator.cs#L659-L672
59+
// There is no interest to duplicate this workaround here in test command, unless MSBuild provides a public API that does it.
60+
// https://github.com/dotnet/msbuild/issues/12711 tracks having a better public API.
61+
var projectPaths = solutionFile.ProjectsInOrder
62+
.Where(p => ProjectShouldBuild(solutionFile, p.RelativePath) && p.ProjectConfigurations.ContainsKey(solutionConfiguration.FullName))
63+
.Select(p => (p.ProjectConfigurations[solutionConfiguration.FullName], p.AbsolutePath))
64+
.Where(p => p.Item1.IncludeInBuild)
65+
.Select(p => (p.AbsolutePath, (string?)p.Item1.ConfigurationName, (string?)p.Item1.PlatformName));
66+
67+
FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb);
3968

40-
using var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs), loggers: logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
69+
using var collection = new ProjectCollection(globalProperties, loggers: logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
4170
var evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared);
42-
ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = GetProjectsProperties(collection, evaluationContext, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions);
71+
ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = GetProjectsProperties(collection, evaluationContext, projectPaths, buildOptions);
4372
logger?.ReallyShutdown();
4473
collection.UnloadAllProjects();
4574

@@ -61,7 +90,7 @@ public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModul
6190

6291
using var collection = new ProjectCollection(globalProperties: CommonRunHelpers.GetGlobalPropertiesFromArgs(msbuildArgs), logger is null ? null : [logger], toolsetDefinitionLocations: ToolsetDefinitionLocations.Default);
6392
var evaluationContext = EvaluationContext.Create(EvaluationContext.SharingPolicy.Shared);
64-
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions);
93+
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions, configuration: null, platform: null);
6594
logger?.ReallyShutdown();
6695
collection.UnloadAllProjects();
6796
return (projects, isBuiltOrRestored);
@@ -130,7 +159,11 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption
130159
return result == (int)BuildResultCode.Success;
131160
}
132161

133-
private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectsProperties(ProjectCollection projectCollection, EvaluationContext evaluationContext, IEnumerable<string> projects, BuildOptions buildOptions)
162+
private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectsProperties(
163+
ProjectCollection projectCollection,
164+
EvaluationContext evaluationContext,
165+
IEnumerable<(string ProjectFilePath, string? Configuration, string? Platform)> projects,
166+
BuildOptions buildOptions)
134167
{
135168
var allProjects = new ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules>();
136169

@@ -141,7 +174,7 @@ private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerMod
141174
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
142175
(project) =>
143176
{
144-
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions);
177+
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, projectCollection, evaluationContext, buildOptions, project.Configuration, project.Platform);
145178
foreach (var projectMetadata in projectsMetadata)
146179
{
147180
allProjects.Add(projectMetadata);

src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs

Lines changed: 48 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,17 +177,51 @@ private static string[] GetSolutionFilterFilePaths(string directory)
177177

178178
private static string[] GetProjectFilePaths(string directory) => Directory.GetFiles(directory, CliConstants.ProjectExtensionPattern, SearchOption.TopDirectoryOnly);
179179

180-
private static ProjectInstance EvaluateProject(ProjectCollection collection, EvaluationContext evaluationContext, string projectFilePath, string? tfm)
180+
private static ProjectInstance EvaluateProject(
181+
ProjectCollection collection,
182+
EvaluationContext evaluationContext,
183+
string projectFilePath,
184+
string? tfm,
185+
string? configuration,
186+
string? platform)
181187
{
182188
Debug.Assert(projectFilePath is not null);
183189

184190
Dictionary<string, string>? globalProperties = null;
191+
var capacity = 0;
192+
185193
if (tfm is not null)
186194
{
187-
globalProperties = new Dictionary<string, string>(capacity: 1)
195+
capacity++;
196+
}
197+
198+
if (configuration is not null)
199+
{
200+
capacity++;
201+
}
202+
203+
if (platform is not null)
204+
{
205+
capacity++;
206+
}
207+
208+
if (capacity > 0)
209+
{
210+
globalProperties = new Dictionary<string, string>(capacity);
211+
if (tfm is not null)
188212
{
189-
{ ProjectProperties.TargetFramework, tfm }
190-
};
213+
globalProperties.Add(ProjectProperties.TargetFramework, tfm);
214+
}
215+
216+
if (configuration is not null)
217+
{
218+
globalProperties.Add(ProjectProperties.Configuration, configuration);
219+
}
220+
221+
if (platform is not null)
222+
{
223+
globalProperties.Add(ProjectProperties.Platform, platform);
224+
}
191225
}
192226

193227
// Merge the global properties from the project collection.
@@ -209,17 +243,16 @@ private static ProjectInstance EvaluateProject(ProjectCollection collection, Eva
209243
});
210244
}
211245

212-
public static string GetRootDirectory(string solutionOrProjectFilePath)
213-
{
214-
string? fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath);
215-
Debug.Assert(fileDirectory is not null);
216-
return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory;
217-
}
218-
219-
public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions)
246+
public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(
247+
string projectFilePath,
248+
ProjectCollection projectCollection,
249+
EvaluationContext evaluationContext,
250+
BuildOptions buildOptions,
251+
string? configuration,
252+
string? platform)
220253
{
221254
var projects = new List<ParallelizableTestModuleGroupWithSequentialInnerModules>();
222-
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, null);
255+
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, tfm: null, configuration, platform);
223256

224257
var targetFramework = projectInstance.GetPropertyValue(ProjectProperties.TargetFramework);
225258
var targetFrameworks = projectInstance.GetPropertyValue(ProjectProperties.TargetFrameworks);
@@ -253,7 +286,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
253286
{
254287
foreach (var framework in frameworks)
255288
{
256-
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework);
289+
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework, configuration, platform);
257290
Logger.LogTrace($"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}').");
258291

259292
if (GetModuleFromProject(projectInstance, buildOptions) is { } module)
@@ -267,7 +300,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
267300
List<TestModule>? innerModules = null;
268301
foreach (var framework in frameworks)
269302
{
270-
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework);
303+
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework, configuration, platform);
271304
Logger.LogTrace($"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}').");
272305

273306
if (GetModuleFromProject(projectInstance, buildOptions) is { } module)
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<Solution>
2+
<Configurations>
3+
<Platform Name="Any CPU" />
4+
<Platform Name="NonWindows" />
5+
<Platform Name="x64" />
6+
<Platform Name="x86" />
7+
</Configurations>
8+
<Project Path="TestProject/TestProject.csproj" />
9+
<Project Path="OtherTestProject/OtherTestProject.csproj">
10+
<Build Solution="*|NonWindows" Project="false" />
11+
</Project>
12+
</Solution>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), testAsset.props))\testAsset.props" />
3+
4+
<PropertyGroup>
5+
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
6+
<OutputType>Exe</OutputType>
7+
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Nullable>enable</Nullable>
10+
11+
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Testing.Platform" Version="$(MicrosoftTestingPlatformVersion)" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using Microsoft.Testing.Platform.Builder;
2+
using Microsoft.Testing.Platform.Capabilities.TestFramework;
3+
using Microsoft.Testing.Platform.Extensions.Messages;
4+
using Microsoft.Testing.Platform.Extensions.TestFramework;
5+
6+
for (int i = 0; i < 3; i++)
7+
{
8+
Console.WriteLine(new string('a', 10000));
9+
Console.Error.WriteLine(new string('e', 10000));
10+
}
11+
12+
var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
13+
14+
testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new DummyTestAdapter());
15+
16+
using var testApplication = await testApplicationBuilder.BuildAsync();
17+
return await testApplication.RunAsync();
18+
19+
public class DummyTestAdapter : ITestFramework, IDataProducer
20+
{
21+
public string Uid => nameof(DummyTestAdapter);
22+
23+
public string Version => "2.0.0";
24+
25+
public string DisplayName => nameof(DummyTestAdapter);
26+
27+
public string Description => nameof(DummyTestAdapter);
28+
29+
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
30+
31+
public Type[] DataTypesProduced => new[] {
32+
typeof(TestNodeUpdateMessage)
33+
};
34+
35+
public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
36+
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });
37+
38+
public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
39+
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });
40+
41+
public async Task ExecuteRequestAsync(ExecuteRequestContext context)
42+
{
43+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
44+
{
45+
Uid = "Test1",
46+
DisplayName = "Test1",
47+
Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")),
48+
}));
49+
50+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
51+
{
52+
Uid = "Test2",
53+
DisplayName = "Test2",
54+
Properties = new PropertyBag(new SkippedTestNodeStateProperty("skipped")),
55+
}));
56+
57+
context.Complete();
58+
}
59+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
using Microsoft.Testing.Platform.Builder;
2+
using Microsoft.Testing.Platform.Capabilities.TestFramework;
3+
using Microsoft.Testing.Platform.Extensions.Messages;
4+
using Microsoft.Testing.Platform.Extensions.TestFramework;
5+
6+
for (int i = 0; i < 3; i++)
7+
{
8+
Console.WriteLine(new string('a', 10000));
9+
Console.Error.WriteLine(new string('e', 10000));
10+
}
11+
12+
var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);
13+
14+
testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new DummyTestAdapter());
15+
16+
using var testApplication = await testApplicationBuilder.BuildAsync();
17+
return await testApplication.RunAsync();
18+
19+
public class DummyTestAdapter : ITestFramework, IDataProducer
20+
{
21+
public string Uid => nameof(DummyTestAdapter);
22+
23+
public string Version => "2.0.0";
24+
25+
public string DisplayName => nameof(DummyTestAdapter);
26+
27+
public string Description => nameof(DummyTestAdapter);
28+
29+
public Task<bool> IsEnabledAsync() => Task.FromResult(true);
30+
31+
public Type[] DataTypesProduced => new[] {
32+
typeof(TestNodeUpdateMessage)
33+
};
34+
35+
public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
36+
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });
37+
38+
public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
39+
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });
40+
41+
public async Task ExecuteRequestAsync(ExecuteRequestContext context)
42+
{
43+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
44+
{
45+
Uid = "Test0",
46+
DisplayName = "Test0",
47+
Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")),
48+
}));
49+
50+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
51+
{
52+
Uid = "Test1",
53+
DisplayName = "Test1",
54+
Properties = new PropertyBag(new SkippedTestNodeStateProperty("OK skipped!")),
55+
}));
56+
57+
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
58+
{
59+
Uid = "Test2",
60+
DisplayName = "Test2",
61+
Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("this is a failed test"), "not OK")),
62+
}));
63+
64+
context.Complete();
65+
}
66+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), testAsset.props))\testAsset.props" />
3+
4+
<PropertyGroup>
5+
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
6+
<OutputType>Exe</OutputType>
7+
8+
<ImplicitUsings>enable</ImplicitUsings>
9+
<Nullable>enable</Nullable>
10+
11+
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
12+
</PropertyGroup>
13+
14+
<ItemGroup>
15+
<PackageReference Include="Microsoft.Testing.Platform" Version="$(MicrosoftTestingPlatformVersion)" />
16+
</ItemGroup>
17+
</Project>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"test": {
3+
"runner": "Microsoft.Testing.Platform"
4+
}
5+
}

0 commit comments

Comments
 (0)