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]