// 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 Microsoft.VisualStudio.TestPlatform.MSTestAdapter.PlatformServices.Interface; using Microsoft.VisualStudio.TestTools.UnitTesting; using ExecutionScope = Microsoft.VisualStudio.TestTools.UnitTesting.ExecutionScope; namespace Microsoft.VisualStudio.TestPlatform.MSTest.TestAdapter.Execution; internal partial class TestExecutionManager { /// /// The ordinary parallel path: split the tests into a parallelizable and a non-parallelizable set, chunk the /// former according to , drain the chunks with a fixed pool of workers, then /// run the non-parallelizable set. /// private async Task ExecuteParallelizedTestsAsync( UnitTestElement[] testsToRun, IAdapterMessageLogger adapterMessageLogger, string source, IDictionary sourceLevelParameters, UnitTestRunner testRunner, bool usesAppDomains, int parallelWorkers, ExecutionScope parallelScope) { // Create test sets for execution, we can execute them in parallel based on parallel settings // Parallel and not parallel sets. // Single-pass partition: enumerate testsToRun once via GroupBy (lazy GroupBy evaluated twice // — once per FirstOrDefault — caused 2 full passes over testsToRun). IEnumerable? parallelizableTestSet = null; IEnumerable? nonParallelizableTestSet = null; foreach (IGrouping group in testsToRun.GroupBy(t => t.DoNotParallelize)) { if (group.Key) { nonParallelizableTestSet = group; } else { parallelizableTestSet = group; } } if (parallelizableTestSet != null) { ConcurrentQueue>? queue = null; // Chunk the sets into further groups based on parallel level switch (parallelScope) { case ExecutionScope.MethodLevel: if (_testOrderRandom is { } methodRandom) { IEnumerable[] methodChunks = [.. parallelizableTestSet.Select(t => (IEnumerable)[t])]; Shuffle(methodRandom, methodChunks); queue = new ConcurrentQueue>(methodChunks); } else { queue = new ConcurrentQueue>(parallelizableTestSet.Select(t => new[] { t })); } break; case ExecutionScope.ClassLevel: if (_testOrderRandom is { } classRandom) { IEnumerable[] classChunks = [ .. parallelizableTestSet .GroupBy(t => t.TestMethod.FullClassName) .Select(g => (IEnumerable)g.ToArray()), ]; Shuffle(classRandom, classChunks); queue = new ConcurrentQueue>(classChunks); } else { queue = new ConcurrentQueue>(parallelizableTestSet.GroupBy(t => t.TestMethod.FullClassName)); } break; } var tasks = new List(); var resourceLockManager = new ResourceLockManager(); for (int i = 0; i < parallelWorkers; i++) { _testRunCancellationToken?.ThrowIfCancellationRequested(); tasks.Add(_taskFactory(async () => { try { while (!queue!.IsEmpty) { _testRunCancellationToken?.ThrowIfCancellationRequested(); if (queue.TryDequeue(out IEnumerable? testSet)) { IReadOnlyList chunkLocks = ResourceLockManager.GetChunkLocks(testSet); if (chunkLocks.Count == 0) { await ExecuteTestsWithTestRunnerAsync(testSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } else { await resourceLockManager.ExecuteWithLocksAsync( chunkLocks, () => ExecuteTestsWithTestRunnerAsync(testSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains), _testRunCancellationToken?.CancellationToken ?? CancellationToken.None).ConfigureAwait(false); } } } } catch (OperationCanceledException) when (_testRunCancellationToken?.Canceled == true) { // Expected when the test run is canceled. Swallow it so the worker exits gracefully. if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsInfoEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Info("Parallel test worker canceled for source {0}", source); } } })); } try { await Task.WhenAll(tasks).ConfigureAwait(false); } catch (Exception ex) { string exceptionToString = ex.ToString(); if (PlatformServiceProvider.Instance.AdapterTraceLogger.IsErrorEnabled) { PlatformServiceProvider.Instance.AdapterTraceLogger.Error("Error occurred while executing tests in parallel{0}{1}", Environment.NewLine, exceptionToString); } adapterMessageLogger.SendMessage(MessageLevel.Error, exceptionToString); throw; } } // Queue the non parallel set if (nonParallelizableTestSet != null) { await ExecuteTestsWithTestRunnerAsync(nonParallelizableTestSet, adapterMessageLogger, source, sourceLevelParameters, testRunner, usesAppDomains).ConfigureAwait(false); } } }