// 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; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; /// /// Tracks, for one source, the outcome of every test that takes part in the dependency graph, and answers /// the only question the executor asks of it: whether a test may run, or must be skipped because a /// prerequisite did not pass. /// /// /// Instances are shared by the parallel workers, so every access to the outcome state is synchronized. The /// state is tiny (two bits per test) and touched twice per test, so a single lock is cheaper and easier to /// reason about than finer-grained synchronization. /// internal sealed class TestDependencyCoordinator { private readonly TestDependencyGraph _graph; private readonly Dictionary _indexByTest; private readonly bool[] _completed; private readonly bool[] _passed; #if NET9_0_OR_GREATER private readonly Lock _lock = new(); #else private readonly object _lock = new(); #endif public TestDependencyCoordinator(TestDependencyGraph graph) { _graph = graph; _completed = new bool[graph.Tests.Length]; _passed = new bool[graph.Tests.Length]; // The scheduler hands back the very instances the graph was built from, so reference identity is the // correct - and cheapest - way to find a test's node. Value equality is not an option: two data rows // of the same test method are distinct nodes but compare equal on their identifying fields. _indexByTest = new Dictionary(graph.Tests.Length, ReferenceComparer.Instance); for (int i = 0; i < graph.Tests.Length; i++) { _indexByTest[graph.Tests[i]] = i; } } /// /// Determines whether must be skipped because one of its prerequisites did not /// pass, and if so produces the message to report. A test that declares no dependency, or that opted /// into ProceedOnFailure, is never skipped by this method. /// /// /// A prerequisite that has not completed counts as not passed. That happens for a test dropped from the /// run because it takes part in a cycle, and it is the conservative reading: the dependent's precondition /// demonstrably did not hold. /// public bool ShouldSkip(UnitTestElement test, [NotNullWhen(true)] out string? reason) { reason = null; if (!_indexByTest.TryGetValue(test, out int index)) { return false; } int[] prerequisites = _graph.TestPrerequisites[index]; if (prerequisites.Length == 0 || _graph.ProceedOnFailure[index]) { return false; } lock (_lock) { foreach (int prerequisite in prerequisites) { if (!_completed[prerequisite] || !_passed[prerequisite]) { reason = string.Format( CultureInfo.CurrentCulture, Resource.DependsOnPrerequisiteNotPassed, _graph.Tests[prerequisite].TestMethod.FullyQualifiedName); return true; } } } return false; } /// /// Records the outcome of a test that the scheduler ran, so that its dependents can be gated on it. /// public void RecordOutcome(UnitTestElement test, bool passed) { if (!_indexByTest.TryGetValue(test, out int index)) { return; } lock (_lock) { _completed[index] = true; _passed[index] = passed; } } /// /// Records that a test will never run - because it takes part in a cycle, or because it was skipped - /// so that everything downstream of it is skipped in turn. /// public void RecordNotRun(UnitTestElement test) { if (!_indexByTest.TryGetValue(test, out int index)) { return; } lock (_lock) { _completed[index] = true; _passed[index] = false; } } private sealed class ReferenceComparer : IEqualityComparer { public static readonly ReferenceComparer Instance = new(); public bool Equals(UnitTestElement? x, UnitTestElement? y) => ReferenceEquals(x, y); public int GetHashCode(UnitTestElement obj) => RuntimeHelpers.GetHashCode(obj); } }