diff --git a/src/Cli/dotnet/Commands/Test/CliConstants.cs b/src/Cli/dotnet/Commands/Test/CliConstants.cs index 1ba9b1332e3a..043f42b5a1de 100644 --- a/src/Cli/dotnet/Commands/Test/CliConstants.cs +++ b/src/Cli/dotnet/Commands/Test/CliConstants.cs @@ -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"; diff --git a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs index c83efdecce8d..9f9df7963242 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/MSBuildUtility.cs @@ -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; @@ -18,10 +20,13 @@ 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 Projects, bool IsBuiltOrRestored) GetProjectsFromSolution(string solutionFilePath, BuildOptions buildOptions) { - SolutionModel solutionModel = SlnFileFactory.CreateFromFileOrDirectory(solutionFilePath, includeSolutionFilterFiles: true, includeSolutionXmlFiles: true); - bool isBuiltOrRestored = BuildOrRestoreProjectOrSolution(solutionFilePath, buildOptions); if (!isBuiltOrRestored) @@ -29,17 +34,41 @@ public static (IEnumerable(), 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 projects = GetProjectsProperties(collection, evaluationContext, solutionModel.SolutionProjects.Select(p => Path.Combine(rootDirectory, p.FilePath)), buildOptions); + ConcurrentBag projects = GetProjectsProperties(collection, evaluationContext, projectPaths, buildOptions); logger?.ReallyShutdown(); collection.UnloadAllProjects(); @@ -61,7 +90,7 @@ public static (IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions); + IEnumerable projects = SolutionAndProjectUtility.GetProjectProperties(projectFilePath, collection, evaluationContext, buildOptions, configuration: null, platform: null); logger?.ReallyShutdown(); collection.UnloadAllProjects(); return (projects, isBuiltOrRestored); @@ -130,7 +159,11 @@ private static bool BuildOrRestoreProjectOrSolution(string filePath, BuildOption return result == (int)BuildResultCode.Success; } - private static ConcurrentBag GetProjectsProperties(ProjectCollection projectCollection, EvaluationContext evaluationContext, IEnumerable projects, BuildOptions buildOptions) + private static ConcurrentBag GetProjectsProperties( + ProjectCollection projectCollection, + EvaluationContext evaluationContext, + IEnumerable<(string ProjectFilePath, string? Configuration, string? Platform)> projects, + BuildOptions buildOptions) { var allProjects = new ConcurrentBag(); @@ -141,7 +174,7 @@ private static ConcurrentBag { - IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project, projectCollection, evaluationContext, buildOptions); + IEnumerable projectsMetadata = SolutionAndProjectUtility.GetProjectProperties(project.ProjectFilePath, projectCollection, evaluationContext, buildOptions, project.Configuration, project.Platform); foreach (var projectMetadata in projectsMetadata) { allProjects.Add(projectMetadata); diff --git a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs index c5ec75139a91..b9ae9763a56b 100644 --- a/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs +++ b/src/Cli/dotnet/Commands/Test/MTP/SolutionAndProjectUtility.cs @@ -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? globalProperties = null; + var capacity = 0; + if (tfm is not null) { - globalProperties = new Dictionary(capacity: 1) + capacity++; + } + + if (configuration is not null) + { + capacity++; + } + + if (platform is not null) + { + capacity++; + } + + if (capacity > 0) + { + globalProperties = new Dictionary(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. @@ -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 GetProjectProperties(string projectFilePath, ProjectCollection projectCollection, EvaluationContext evaluationContext, BuildOptions buildOptions) + public static IEnumerable GetProjectProperties( + string projectFilePath, + ProjectCollection projectCollection, + EvaluationContext evaluationContext, + BuildOptions buildOptions, + string? configuration, + string? platform) { var projects = new List(); - 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); @@ -253,7 +286,7 @@ public static IEnumerable? 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) diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/MultiTestProjectSolutionWithPlatforms.slnx b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/MultiTestProjectSolutionWithPlatforms.slnx new file mode 100644 index 000000000000..7f8ab63ff566 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/MultiTestProjectSolutionWithPlatforms.slnx @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/OtherTestProject.csproj b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/OtherTestProject.csproj new file mode 100644 index 000000000000..8699e5d53e86 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/OtherTestProject.csproj @@ -0,0 +1,17 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/Program.cs b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/Program.cs new file mode 100644 index 000000000000..e7beaf759a5c --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/OtherTestProject/Program.cs @@ -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 IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] { + typeof(TestNodeUpdateMessage) + }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task 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(); + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/Program.cs b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/Program.cs new file mode 100644 index 000000000000..7ce9f69b6e9f --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/Program.cs @@ -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 IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] { + typeof(TestNodeUpdateMessage) + }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task 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(); + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/TestProject.csproj b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/TestProject.csproj new file mode 100644 index 000000000000..8699e5d53e86 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/TestProject/TestProject.csproj @@ -0,0 +1,17 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/global.json b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/global.json new file mode 100644 index 000000000000..8f73781ca7c8 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithPlatforms/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/MultiTestProjectSolutionWithSharedProject.slnx b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/MultiTestProjectSolutionWithSharedProject.slnx new file mode 100644 index 000000000000..3aa9eb0b0e3c --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/MultiTestProjectSolutionWithSharedProject.slnx @@ -0,0 +1,5 @@ + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/OtherTestProject.csproj b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/OtherTestProject.csproj new file mode 100644 index 000000000000..8699e5d53e86 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/OtherTestProject.csproj @@ -0,0 +1,17 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/Program.cs b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/Program.cs new file mode 100644 index 000000000000..e7beaf759a5c --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/OtherTestProject/Program.cs @@ -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 IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] { + typeof(TestNodeUpdateMessage) + }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task 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(); + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedClass.cs b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedClass.cs new file mode 100644 index 000000000000..760b66570ec0 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedClass.cs @@ -0,0 +1,7 @@ +namespace SharedProject +{ + public class SharedClass + { + public static string GetMessage() => "Hello from shared project"; + } +} diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.projitems b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.projitems new file mode 100644 index 000000000000..34d010f9a5f0 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.projitems @@ -0,0 +1,14 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D + + + SharedProject + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.shproj b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.shproj new file mode 100644 index 000000000000..3b86d62c4aa8 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/SharedProject/SharedProject.shproj @@ -0,0 +1,13 @@ + + + + A1B2C3D4-E5F6-4A5B-9C8D-7E6F5A4B3C2D + 14.0 + + + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/Program.cs b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/Program.cs new file mode 100644 index 000000000000..7ce9f69b6e9f --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/Program.cs @@ -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 IsEnabledAsync() => Task.FromResult(true); + + public Type[] DataTypesProduced => new[] { + typeof(TestNodeUpdateMessage) + }; + + public Task CreateTestSessionAsync(CreateTestSessionContext context) + => Task.FromResult(new CreateTestSessionResult() { IsSuccess = true }); + + public Task 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(); + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/TestProject.csproj b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/TestProject.csproj new file mode 100644 index 000000000000..8699e5d53e86 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProject/TestProject.csproj @@ -0,0 +1,17 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + + + + + + diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProjectsWithShared.slnf b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProjectsWithShared.slnf new file mode 100644 index 000000000000..994113050c51 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/TestProjectsWithShared.slnf @@ -0,0 +1,9 @@ +{ + "solution": { + "path": "MultiTestProjectSolutionWithSharedProject.slnx", + "projects": [ + "TestProject\\TestProject.csproj", + "SharedProject\\SharedProject.shproj" + ] + } +} \ No newline at end of file diff --git a/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/global.json b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/global.json new file mode 100644 index 000000000000..8f73781ca7c8 --- /dev/null +++ b/test/TestAssets/TestProjects/MultiTestProjectSolutionWithSharedProject/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} \ No newline at end of file diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs index 4559f8933439..e915e7491828 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTestsWithDifferentOptions.cs @@ -566,5 +566,63 @@ public void RunWithTraceFileLoggingAndNonExistingDirectory_ShouldReturnExitCodeG result.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); } + + [InlineData(TestingConstants.Debug)] + [InlineData(TestingConstants.Release)] + [Theory] + public void RunWithSolutionFilterContainingSharedProject_ShouldSkipSharedProjectAndSucceed(string configuration) + { + TestAsset testInstance = _testAssetsManager.CopyTestAsset("MultiTestProjectSolutionWithSharedProject", Guid.NewGuid().ToString()).WithSource(); + + string testSolutionFilterPath = "TestProjectsWithShared.slnf"; + + CommandResult result = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute(MicrosoftTestingPlatformOptions.SolutionOption.Name, testSolutionFilterPath, + MicrosoftTestingPlatformOptions.ConfigurationOption.Name, configuration); + + // Validate that TestProject ran (shared project should be skipped automatically) + result.StdOut.Should().Contain("TestProject.dll"); + // OtherTestProject should not be included since it's not in the solution filter + result.StdOut.Should().NotContain("OtherTestProject.dll"); + + result.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); + } + + [InlineData(TestingConstants.Debug)] + [InlineData(TestingConstants.Release)] + [Theory] + public void RunWithSolutionAndPlatformConfiguration_ShouldRespectPlatform(string configuration) + { + TestAsset testInstance = _testAssetsManager.CopyTestAsset("MultiTestProjectSolutionWithPlatforms", Guid.NewGuid().ToString()).WithSource(); + + string testSolutionPath = "MultiTestProjectSolutionWithPlatforms.slnx"; + + // Test with "NonWindows" platform - OtherTestProject should NOT be included + CommandResult resultX86 = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute(MicrosoftTestingPlatformOptions.SolutionOption.Name, testSolutionPath, + MicrosoftTestingPlatformOptions.ConfigurationOption.Name, configuration, + "--property:Platform=NonWindows"); + + // Validate that TestProject ran but OtherTestProject did not (not marked for build on x86) + resultX86.StdOut.Should().Contain("TestProject.dll"); + resultX86.StdOut.Should().NotContain("OtherTestProject.dll"); + + resultX86.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); + + // Test with x64 platform - both projects should be included + CommandResult resultX64 = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .Execute(MicrosoftTestingPlatformOptions.SolutionOption.Name, testSolutionPath, + MicrosoftTestingPlatformOptions.ConfigurationOption.Name, configuration, + "--property:Platform=x64"); + + // Validate that both TestProject and OtherTestProject ran + resultX64.StdOut.Should().Contain("TestProject.dll"); + resultX64.StdOut.Should().Contain("OtherTestProject.dll"); + + resultX64.ExitCode.Should().Be(ExitCodes.AtLeastOneTestFailed); + } } }