Skip to content

Commit 0a0a993

Browse files
Youssef1313Copilot
andauthored
Fix solution parsing in dotnet test for Microsoft.Testing.Platform (#51411)
Co-authored-by: Copilot <[email protected]>
1 parent 6532cd7 commit 0a0a993

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;
@@ -19,28 +21,55 @@ internal static class MSBuildUtility
1921
{
2022
private const string dotnetTestVerb = "dotnet-test";
2123

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

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

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

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

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

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

@@ -62,7 +91,7 @@ public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModul
6291

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

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

@@ -142,7 +175,7 @@ private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerMod
142175
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
143176
(project) =>
144177
{
145-
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions);
178+
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, projectCollection, evaluationContext, buildOptions, project.Configuration, project.Platform);
146179
foreach (var projectMetadata in projectsMetadata)
147180
{
148181
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
@@ -152,17 +152,51 @@ private static string[] GetSolutionFilterFilePaths(string directory)
152152

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

155-
private static ProjectInstance EvaluateProject(ProjectCollection collection, EvaluationContext evaluationContext, string projectFilePath, string? tfm)
155+
private static ProjectInstance EvaluateProject(
156+
ProjectCollection collection,
157+
EvaluationContext evaluationContext,
158+
string projectFilePath,
159+
string? tfm,
160+
string? configuration,
161+
string? platform)
156162
{
157163
Debug.Assert(projectFilePath is not null);
158164

159165
Dictionary<string, string>? globalProperties = null;
166+
var capacity = 0;
167+
160168
if (tfm is not null)
161169
{
162-
globalProperties = new Dictionary<string, string>(capacity: 1)
170+
capacity++;
171+
}
172+
173+
if (configuration is not null)
174+
{
175+
capacity++;
176+
}
177+
178+
if (platform is not null)
179+
{
180+
capacity++;
181+
}
182+
183+
if (capacity > 0)
184+
{
185+
globalProperties = new Dictionary<string, string>(capacity);
186+
if (tfm is not null)
163187
{
164-
{ ProjectProperties.TargetFramework, tfm }
165-
};
188+
globalProperties.Add(ProjectProperties.TargetFramework, tfm);
189+
}
190+
191+
if (configuration is not null)
192+
{
193+
globalProperties.Add(ProjectProperties.Configuration, configuration);
194+
}
195+
196+
if (platform is not null)
197+
{
198+
globalProperties.Add(ProjectProperties.Platform, platform);
199+
}
166200
}
167201

168202
// Merge the global properties from the project collection.
@@ -184,17 +218,16 @@ private static ProjectInstance EvaluateProject(ProjectCollection collection, Eva
184218
});
185219
}
186220

187-
public static string GetRootDirectory(string solutionOrProjectFilePath)
188-
{
189-
string? fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath);
190-
Debug.Assert(fileDirectory is not null);
191-
return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory;
192-
}
193-
194-
public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions)
221+
public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(
222+
string projectFilePath,
223+
ProjectCollection projectCollection,
224+
EvaluationContext evaluationContext,
225+
BuildOptions buildOptions,
226+
string? configuration,
227+
string? platform)
195228
{
196229
var projects = new List<ParallelizableTestModuleGroupWithSequentialInnerModules>();
197-
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, null);
230+
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, tfm: null, configuration, platform);
198231

199232
var targetFramework = projectInstance.GetPropertyValue(ProjectProperties.TargetFramework);
200233
var targetFrameworks = projectInstance.GetPropertyValue(ProjectProperties.TargetFrameworks);
@@ -228,7 +261,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
228261
{
229262
foreach (var framework in frameworks)
230263
{
231-
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework);
264+
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework, configuration, platform);
232265
Logger.LogTrace($"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}').");
233266

234267
if (GetModuleFromProject(projectInstance, buildOptions) is { } module)
@@ -242,7 +275,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
242275
List<TestModule>? innerModules = null;
243276
foreach (var framework in frameworks)
244277
{
245-
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework);
278+
projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, framework, configuration, platform);
246279
Logger.LogTrace($"Loaded inner project '{Path.GetFileName(projectFilePath)}' has '{ProjectProperties.IsTestingPlatformApplication}' = '{projectInstance.GetPropertyValue(ProjectProperties.IsTestingPlatformApplication)}' (TFM: '{framework}').");
247280

248281
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)