diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/DeepLeafTestProject.csproj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/DeepLeafTestProject.csproj new file mode 100644 index 000000000000..7d8bdc9b174b --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/DeepLeafTestProject.csproj @@ -0,0 +1,19 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + false + true + + + + + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/Program.cs b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/Program.cs new file mode 100644 index 000000000000..3e4544e6169a --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/DeepLeafTestProject/Program.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestFramework; + +// Drop a uniquely-named marker file per process launch so tests can deterministically count how many +// times this test application actually ran (used to validate traversal de-duplication). +var markerDir = Environment.GetEnvironmentVariable("TRAVERSAL_MARKER_DIR"); +if (!string.IsNullOrEmpty(markerDir)) +{ + Directory.CreateDirectory(markerDir); + var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "unknown"; + File.WriteAllText(Path.Combine(markerDir, $"{assemblyName}-{Guid.NewGuid():N}.marker"), assemblyName); +} + +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 => []; + + 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) + { + context.Complete(); + await Task.CompletedTask; + } +} diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Level1TestProject.csproj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Level1TestProject.csproj new file mode 100644 index 000000000000..7d8bdc9b174b --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Level1TestProject.csproj @@ -0,0 +1,19 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + false + true + + + + + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Program.cs b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Program.cs new file mode 100644 index 000000000000..3e4544e6169a --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level1TestProject/Program.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestFramework; + +// Drop a uniquely-named marker file per process launch so tests can deterministically count how many +// times this test application actually ran (used to validate traversal de-duplication). +var markerDir = Environment.GetEnvironmentVariable("TRAVERSAL_MARKER_DIR"); +if (!string.IsNullOrEmpty(markerDir)) +{ + Directory.CreateDirectory(markerDir); + var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "unknown"; + File.WriteAllText(Path.Combine(markerDir, $"{assemblyName}-{Guid.NewGuid():N}.marker"), assemblyName); +} + +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 => []; + + 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) + { + context.Complete(); + await Task.CompletedTask; + } +} diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Level2TestProject.csproj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Level2TestProject.csproj new file mode 100644 index 000000000000..7d8bdc9b174b --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Level2TestProject.csproj @@ -0,0 +1,19 @@ + + + + + $(CurrentTargetFramework) + Exe + + enable + enable + + false + false + true + + + + + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Program.cs b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Program.cs new file mode 100644 index 000000000000..3e4544e6169a --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/Level2TestProject/Program.cs @@ -0,0 +1,49 @@ +using System.Reflection; +using Microsoft.Testing.Platform.Builder; +using Microsoft.Testing.Platform.Capabilities.TestFramework; +using Microsoft.Testing.Platform.Extensions.Messages; +using Microsoft.Testing.Platform.Extensions.TestFramework; + +// Drop a uniquely-named marker file per process launch so tests can deterministically count how many +// times this test application actually ran (used to validate traversal de-duplication). +var markerDir = Environment.GetEnvironmentVariable("TRAVERSAL_MARKER_DIR"); +if (!string.IsNullOrEmpty(markerDir)) +{ + Directory.CreateDirectory(markerDir); + var assemblyName = Assembly.GetEntryAssembly()?.GetName().Name ?? "unknown"; + File.WriteAllText(Path.Combine(markerDir, $"{assemblyName}-{Guid.NewGuid():N}.marker"), assemblyName); +} + +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 => []; + + 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) + { + context.Complete(); + await Task.CompletedTask; + } +} diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/Class1.cs b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/Class1.cs new file mode 100644 index 000000000000..6501778ab82b --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/Class1.cs @@ -0,0 +1,9 @@ +namespace NonTestLibrary; + +// A plain (non-test) class library referenced by a nested traversal project. +// dotnet test must silently skip it: it is neither IsTestProject nor +// IsTestingPlatformApplication, so it produces no test module and no error. +public static class Class1 +{ + public static int Add(int a, int b) => a + b; +} diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/NonTestLibrary.csproj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/NonTestLibrary.csproj new file mode 100644 index 000000000000..4f3a6fcc6eee --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/NonTestLibrary/NonTestLibrary.csproj @@ -0,0 +1,12 @@ + + + + + $(CurrentTargetFramework) + + enable + enable + + false + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/dirs.proj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/dirs.proj new file mode 100644 index 000000000000..f34006469ce2 --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/dirs.proj @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/global.json b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/global.json new file mode 100644 index 000000000000..9009caf0ba8f --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/dirs.proj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/dirs.proj new file mode 100644 index 000000000000..c1c21b23af70 --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/dirs.proj @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/level3/dirs.proj b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/level3/dirs.proj new file mode 100644 index 000000000000..f558da09c4eb --- /dev/null +++ b/test/TestAssets/TestProjects/TraversalTestProjectsDeepNested/level2/level3/dirs.proj @@ -0,0 +1,6 @@ + + + + + + diff --git a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs index 6f5855a0166a..315c897be613 100644 --- a/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs +++ b/test/dotnet.Tests/CommandTests/Test/GivenDotnetTestBuildsAndRunsTests.cs @@ -501,6 +501,39 @@ public void RunNestedTraversalProjectWithDiamond_ShouldRunSharedProjectOnce(stri result.ExitCode.Should().Be(ExitCodes.ZeroTests); } + [DataRow(TestingConstants.Debug)] + [DataRow(TestingConstants.Release)] + [TestMethod] + public void RunDeeplyNestedTraversalProject_ShouldRunEveryReferencedTestProjectAndSkipNonTestProjects(string configuration) + { + TestAsset testInstance = TestAssetsManager.CopyTestAsset("TraversalTestProjectsDeepNested", Guid.NewGuid().ToString()) + .WithSource(); + + // Each test-host launch drops a uniquely-named marker file, giving a deterministic count of + // how many times each referenced project actually ran. + string markerDir = Path.Combine(testInstance.Path, "run-markers"); + + CommandResult result = new DotnetTestCommand(Log, disableNewOutput: false) + .WithWorkingDirectory(testInstance.Path) + .WithEnvironmentVariable("TRAVERSAL_MARKER_DIR", markerDir) + .Execute("dirs.proj", "-c", configuration); + + // The graph is three levels deep: dirs.proj -> level2\dirs.proj -> level2\level3\dirs.proj. + // A test project is referenced at each level; all must run exactly once (proving deep recursion). + int MarkerCount(string project) => Directory.Exists(markerDir) ? Directory.GetFiles(markerDir, $"{project}-*.marker").Length : 0; + + MarkerCount("Level1TestProject").Should().Be(1); + MarkerCount("Level2TestProject").Should().Be(1); + MarkerCount("DeepLeafTestProject").Should().Be(1); + + // NonTestLibrary is a plain class library referenced by the level-2 traversal. It is neither a + // test project nor an MTP application, so it must be silently skipped: it never runs and does not + // cause an error. + MarkerCount("NonTestLibrary").Should().Be(0); + + result.ExitCode.Should().Be(ExitCodes.ZeroTests); + } + [DataRow(TestingConstants.Debug)] [DataRow(TestingConstants.Release)] [TestMethod]