Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/Cli/dotnet/Commands/Test/CliConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ internal static class ProjectProperties
internal const string IsTestProject = "IsTestProject";
internal const string TargetFramework = "TargetFramework";
internal const string TargetFrameworks = "TargetFrameworks";
internal const string Configuration = "Configuration";
internal const string Platform = "Platform";
internal const string TargetPath = "TargetPath";
internal const string ProjectFullPath = "MSBuildProjectFullPath";
internal const string RunCommand = "RunCommand";
Expand Down
57 changes: 45 additions & 12 deletions src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

using System.Collections.Concurrent;
using System.CommandLine;
using System.Runtime.CompilerServices;
using Microsoft.Build.Construction;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Evaluation.Context;
using Microsoft.Build.Execution;
Expand All @@ -18,28 +20,55 @@ internal static class MSBuildUtility
{
private const string dotnetTestVerb = "dotnet-test";

// Related: https://github.com/dotnet/msbuild/pull/7992
// Related: https://github.com/dotnet/msbuild/issues/12711
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ProjectShouldBuild")]
static extern bool ProjectShouldBuild(SolutionFile solutionFile, string projectFile);

public static (IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions)
{
SolutionModel solutionModel = SlnFileFactory.CreateFromFileOrDirectory(solutionFilePath, includeSolutionFilterFiles: true, includeSolutionXmlFiles: true);

bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(solutionFilePath, buildOptions);

if (!isBuiltOrRestored)
{
return (Array.Empty<ParallelizableTestModuleGroupWithSequentialInnerModules>(), isBuiltOrRestored);
}

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

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

var msbuildArgs = MSBuildArgs.AnalyzeMSBuildArguments(buildOptions.MSBuildArgs, CommonOptions.PropertiesOption, CommonOptions.RestorePropertiesOption, CommonOptions.MSBuildTargetOption(), CommonOptions.VerbosityOption());
if (string.IsNullOrEmpty(activeSolutionConfiguration))
{
activeSolutionConfiguration = solutionFile.GetDefaultConfigurationName();
}

if (string.IsNullOrEmpty(activeSolutionPlatform))
{
activeSolutionPlatform = solutionFile.GetDefaultPlatformName();
}

var solutionConfiguration = solutionFile.SolutionConfigurations.FirstOrDefault(c => activeSolutionConfiguration.Equals(c.ConfigurationName, StringComparison.OrdinalIgnoreCase) && activeSolutionPlatform.Equals(c.PlatformName, StringComparison.OrdinalIgnoreCase))
?? throw new InvalidOperationException($"The solution configuration '{activeSolutionConfiguration}|{activeSolutionPlatform}' is invalid.");

// Note: MSBuild seems to be special casing web projects specifically.
// https://github.com/dotnet/msbuild/blob/243fb764b25affe8cc5f233001ead3b5742a297e/src/Build/Construction/Solution/SolutionProjectGenerator.cs#L659-L672
// There is no interest to duplicate this workaround here in test command, unless MSBuild provides a public API that does it.
// https://github.com/dotnet/msbuild/issues/12711 tracks having a better public API.
var projectPaths = solutionFile.ProjectsInOrder
.Where(p => ProjectShouldBuild(solutionFile, p.RelativePath) && p.ProjectConfigurations.ContainsKey(solutionConfiguration.FullName))
.Select(p => (p.ProjectConfigurations[solutionConfiguration.FullName], p.AbsolutePath))
.Where(p => p.Item1.IncludeInBuild)
.Select(p => (p.AbsolutePath, (string?)p.Item1.ConfigurationName, (string?)p.Item1.PlatformName));

FacadeLogger? logger = LoggerUtility.DetermineBinlogger([.. buildOptions.MSBuildArgs], dotnetTestVerb);

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

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

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

private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectsProperties(ProjectCollection projectCollection, EvaluationContext evaluationContext, IEnumerable<string> projects, BuildOptions buildOptions)
private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectsProperties(
ProjectCollection projectCollection,
EvaluationContext evaluationContext,
IEnumerable<(string ProjectFilePath, string? Configuration, string? Platform)> projects,
BuildOptions buildOptions)
{
var allProjects = new ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerModules>();

Expand All @@ -141,7 +174,7 @@ private static ConcurrentBag<ParallelizableTestModuleGroupWithSequentialInnerMod
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount },
(project) =>
{
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions);
IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, projectCollection, evaluationContext, buildOptions, project.Configuration, project.Platform);
foreach (var projectMetadata in projectsMetadata)
{
allProjects.Add(projectMetadata);
Expand Down
63 changes: 48 additions & 15 deletions src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,17 +177,51 @@ private static string[] GetSolutionFilterFilePaths(string directory)

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

private static ProjectInstance EvaluateProject(ProjectCollection collection, EvaluationContext evaluationContext, string projectFilePath, string? tfm)
private static ProjectInstance EvaluateProject(
ProjectCollection collection,
EvaluationContext evaluationContext,
string projectFilePath,
string? tfm,
string? configuration,
string? platform)
{
Debug.Assert(projectFilePath is not null);

Dictionary<string, string>? globalProperties = null;
var capacity = 0;

if (tfm is not null)
{
globalProperties = new Dictionary<string, string>(capacity: 1)
capacity++;
}

if (configuration is not null)
{
capacity++;
}

if (platform is not null)
{
capacity++;
}

if (capacity > 0)
{
globalProperties = new Dictionary<string, string>(capacity);
if (tfm is not null)
{
{ ProjectProperties.TargetFramework, tfm }
};
globalProperties.Add(ProjectProperties.TargetFramework, tfm);
}

if (configuration is not null)
{
globalProperties.Add(ProjectProperties.Configuration, configuration);
}

if (platform is not null)
{
globalProperties.Add(ProjectProperties.Platform, platform);
}
}

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

public static string GetRootDirectory(string solutionOrProjectFilePath)
{
string? fileDirectory = Path.GetDirectoryName(solutionOrProjectFilePath);
Debug.Assert(fileDirectory is not null);
return string.IsNullOrEmpty(fileDirectory) ? Directory.GetCurrentDirectory() : fileDirectory;
}

public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions)
public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModules> GetProjectProperties(
string projectFilePath,
ProjectCollection projectCollection,
EvaluationContext evaluationContext,
BuildOptions buildOptions,
string? configuration,
string? platform)
{
var projects = new List<ParallelizableTestModuleGroupWithSequentialInnerModules>();
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, null);
ProjectInstance projectInstance = EvaluateProject(projectCollection, evaluationContext, projectFilePath, tfm: null, configuration, platform);

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

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

if (GetModuleFromProject(projectInstance, buildOptions) is { } module)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Solution>
<Configurations>
<Platform Name="Any CPU" />
<Platform Name="NonWindows" />
<Platform Name="x64" />
<Platform Name="x86" />
</Configurations>
<Project Path="TestProject/TestProject.csproj" />
<Project Path="OtherTestProject/OtherTestProject.csproj">
<Build Solution="*|NonWindows" Project="false" />
</Project>
</Solution>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), testAsset.props))\testAsset.props" />

<PropertyGroup>
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>

<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Testing.Platform" Version="$(MicrosoftTestingPlatformVersion)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;

for (int i = 0; i < 3; i++)
{
Console.WriteLine(new string('a', 10000));
Console.Error.WriteLine(new string('e', 10000));
}

var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);

testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new DummyTestAdapter());

using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();

public class DummyTestAdapter : ITestFramework, IDataProducer
{
public string Uid => nameof(DummyTestAdapter);

public string Version => "2.0.0";

public string DisplayName => nameof(DummyTestAdapter);

public string Description => nameof(DummyTestAdapter);

public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public Type[] DataTypesProduced => new[] {
typeof(TestNodeUpdateMessage)
};

public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });

public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });

public async Task ExecuteRequestAsync(ExecuteRequestContext context)
{
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
{
Uid = "Test1",
DisplayName = "Test1",
Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")),
}));

await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
{
Uid = "Test2",
DisplayName = "Test2",
Properties = new PropertyBag(new SkippedTestNodeStateProperty("skipped")),
}));

context.Complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
using Microsoft.Testing.Platform.Builder;
using Microsoft.Testing.Platform.Capabilities.TestFramework;
using Microsoft.Testing.Platform.Extensions.Messages;
using Microsoft.Testing.Platform.Extensions.TestFramework;

for (int i = 0; i < 3; i++)
{
Console.WriteLine(new string('a', 10000));
Console.Error.WriteLine(new string('e', 10000));
}

var testApplicationBuilder = await TestApplication.CreateBuilderAsync(args);

testApplicationBuilder.RegisterTestFramework(_ => new TestFrameworkCapabilities(), (_, __) => new DummyTestAdapter());

using var testApplication = await testApplicationBuilder.BuildAsync();
return await testApplication.RunAsync();

public class DummyTestAdapter : ITestFramework, IDataProducer
{
public string Uid => nameof(DummyTestAdapter);

public string Version => "2.0.0";

public string DisplayName => nameof(DummyTestAdapter);

public string Description => nameof(DummyTestAdapter);

public Task<bool> IsEnabledAsync() => Task.FromResult(true);

public Type[] DataTypesProduced => new[] {
typeof(TestNodeUpdateMessage)
};

public Task<CreateTestSessionResult> CreateTestSessionAsync(CreateTestSessionContext context)
=> Task.FromResult(new CreateTestSessionResult() { IsSuccess = true });

public Task<CloseTestSessionResult> CloseTestSessionAsync(CloseTestSessionContext context)
=> Task.FromResult(new CloseTestSessionResult() { IsSuccess = true });

public async Task ExecuteRequestAsync(ExecuteRequestContext context)
{
await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
{
Uid = "Test0",
DisplayName = "Test0",
Properties = new PropertyBag(new PassedTestNodeStateProperty("OK")),
}));

await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
{
Uid = "Test1",
DisplayName = "Test1",
Properties = new PropertyBag(new SkippedTestNodeStateProperty("OK skipped!")),
}));

await context.MessageBus.PublishAsync(this, new TestNodeUpdateMessage(context.Request.Session.SessionUid, new TestNode()
{
Uid = "Test2",
DisplayName = "Test2",
Properties = new PropertyBag(new FailedTestNodeStateProperty(new Exception("this is a failed test"), "not OK")),
}));

context.Complete();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), testAsset.props))\testAsset.props" />

<PropertyGroup>
<TargetFramework>$(CurrentTargetFramework)</TargetFramework>
<OutputType>Exe</OutputType>

<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Testing.Platform" Version="$(MicrosoftTestingPlatformVersion)" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
Loading
Loading