// 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);
}
}