// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.ObjectModel; using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// /// The resolved [DependsOn] dependency graph for one test source: which test must run before which, /// how the tests are grouped into scheduling chunks, and in what order a chunk's tests run. /// /// /// /// The graph is built once per source, before anything runs, from the dependencies carried on the /// s (see ), which come from /// [DependsOn] attributes and/or testconfig.json. It is a pure data structure: it neither /// runs tests nor observes outcomes - does that at run time. /// /// /// Edges are resolved between tests, but MSTest schedules chunks (a whole class under /// , a single test under ). /// The test-level graph is therefore projected onto chunks: an edge inside a chunk only orders the tests /// within it, while an edge between chunks gates when the dependent chunk may start. /// /// internal sealed partial class TestDependencyGraph { private TestDependencyGraph( UnitTestElement[] tests, int[][] testPrerequisites, bool[] proceedOnFailure, UnitTestElement[][] parallelChunks, int[][] parallelChunkPrerequisites, UnitTestElement[] sequentialTests, IReadOnlyList brokenTests, IReadOnlyList errors, IReadOnlyList warnings) { Tests = tests; TestPrerequisites = testPrerequisites; ProceedOnFailure = proceedOnFailure; ParallelChunks = parallelChunks; ParallelChunkPrerequisites = parallelChunkPrerequisites; SequentialTests = sequentialTests; BrokenTests = brokenTests; Errors = errors; Warnings = warnings; } /// Gets every test of the source, in the order the graph indexes them. public UnitTestElement[] Tests { get; } /// /// Gets, for each test, the indices into of the tests that must run first. /// public int[][] TestPrerequisites { get; } /// /// Gets, for each test, whether it runs even when a prerequisite did not pass. A test with several /// prerequisites has a single value because the flag is merged per dependent when the edges are /// resolved: it is set only when every declared edge asked to proceed. /// public bool[] ProceedOnFailure { get; } /// /// Gets the scheduling chunks of the parallel phase. The tests inside a chunk are already ordered so /// that intra-chunk prerequisites come first. /// public UnitTestElement[][] ParallelChunks { get; } /// /// Gets, for each parallel chunk, the indices of the chunks that must complete before it may start. /// public int[][] ParallelChunkPrerequisites { get; } /// /// Gets the tests of the sequential phase (those marked [DoNotParallelize], plus those pulled in /// by the demotion rule), ordered so that prerequisites come first. /// public UnitTestElement[] SequentialTests { get; } /// /// Gets the tests that take part in a dependency cycle, each paired with the description of the cycle /// it is in. They cannot be ordered, so they are reported as failed instead of being run; their /// dependents are then skipped by the ordinary "a prerequisite did not pass" rule. /// public IReadOnlyList BrokenTests { get; } /// /// A test that cannot be scheduled because it takes part in a dependency cycle, together with the /// description of that cycle. The message is per test rather than per run so that a suite containing two /// unrelated cycles reports each failure against its own cycle instead of every cycle in the assembly. /// internal sealed class BrokenTest { public BrokenTest(UnitTestElement element, string cycleMessage) { Element = element; CycleMessage = cycleMessage; } public UnitTestElement Element { get; } public string CycleMessage { get; } } /// /// Gets the fatal configuration errors: the dependency cycles that make tests unschedulable. These are /// reported as failures against the tests in the cycle, so nothing that merely degrades - such as a cycle /// in the class-level projection, which is recovered from by running those tests sequentially - belongs /// here; that goes to . /// public IReadOnlyList Errors { get; } /// /// Gets the non-fatal problems to report: dependencies that match no test in this run, and cycles in the /// class-level projection that were recovered from by demoting the affected tests to the sequential phase. /// public IReadOnlyList Warnings { get; } /// /// Builds the graph for , or returns when no test declares /// a dependency, in which case the caller keeps its ordinary scheduling path. The null fast path matters: /// it keeps every run that does not use the feature on exactly the code it uses today. /// /// The tests of one source, after filtering. /// The effective parallelization scope, which decides what a chunk is. /// /// Whether the parallel phase exists at all. When it does not, every test is placed in the sequential /// phase, so a declared order is still honored when parallelization is turned off. /// public static TestDependencyGraph? Build(UnitTestElement[] tests, ExecutionScope scope, bool parallelizationEnabled) { bool anyDependency = false; foreach (UnitTestElement test in tests) { if (test.Dependencies is { Length: > 0 }) { anyDependency = true; break; } } if (!anyDependency) { return null; } var warnings = new List(); var errors = new List(); (int[][] testPrerequisites, bool[] proceedOnFailure) = ResolveEdges(tests, warnings); string?[] cycleMessageByTest = FindCycles(testPrerequisites, tests, errors); bool[] isBroken = Array.ConvertAll(cycleMessageByTest, static cycleMessage => cycleMessage is not null); // A test in a cycle cannot be ordered, so it is dropped from scheduling and reported as failed, with // the description of the cycle *it* is in. Its dependents keep the edge: at run time the prerequisite // is recorded as "did not pass", so they are skipped with the ordinary message rather than silently // running out of order. var brokenTests = new List(); for (int i = 0; i < tests.Length; i++) { if (cycleMessageByTest[i] is { } cycleMessage) { brokenTests.Add(new BrokenTest(tests[i], cycleMessage)); } } bool[] isSequential = ComputeSequentialSet(tests, testPrerequisites, isBroken, parallelizationEnabled); (UnitTestElement[][] parallelChunks, int[][] parallelChunkPrerequisites) = BuildChunks( tests, testPrerequisites, SelectIndices(tests.Length, i => !isSequential[i] && !isBroken[i]), scope, warnings, out List? projectionCycleTests); // A cycle that exists only in the class-level projection means the declared order is satisfiable // between tests but not between whole classes. Rather than leave those tests unordered - which would // make the run-time gate skip them nondeterministically, because a prerequisite that has not run yet // is indistinguishable from one that failed - the affected tests are moved into the sequential phase, // where the topological order can be honoured exactly. The cost is parallelism for those tests only, // and the reported warning tells the user how to get it back. if (projectionCycleTests is not null) { foreach (int index in projectionCycleTests) { isSequential[index] = true; } // Anything that waited on a demoted test has to move too, for the same reason [DoNotParallelize] // dependents do: the sequential phase runs after the parallel one. PropagateSequential(tests.Length, testPrerequisites, isBroken, isSequential); (parallelChunks, parallelChunkPrerequisites) = BuildChunks( tests, testPrerequisites, SelectIndices(tests.Length, i => !isSequential[i] && !isBroken[i]), scope, warnings, out _); } UnitTestElement[] sequentialTests = OrderTopologically( SelectIndices(tests.Length, i => isSequential[i] && !isBroken[i]), testPrerequisites, tests); return new TestDependencyGraph( tests, testPrerequisites, proceedOnFailure, parallelChunks, parallelChunkPrerequisites, sequentialTests, brokenTests, errors, warnings); } }