class WorkAllocator

Allocates available tests to test workers.

@phpstan-import-type TestClassInfo from \Drupal\Core\Test\PhpUnitTestDiscovery @phpstan-import-type TestClassInfoList from \Drupal\Core\Test\PhpUnitTestDiscovery @phpstan-import-type GroupedTestClassInfoList from \Drupal\Core\Test\PhpUnitTestDiscovery

@internal

Hierarchy

Expanded class hierarchy of WorkAllocator

1 file declares its use of WorkAllocator
WorkAllocatorTest.php in core/tests/Drupal/Tests/Core/Test/WorkAllocatorTest.php

File

core/tests/Drupal/TestTools/TestRunner/WorkAllocator.php, line 21

Namespace

Drupal\TestTools\TestRunner
View source
class WorkAllocator {
  
  /**
   * The sorted tests list, prior to allocation.
   *
   * @var TestClassInfoList
   */
  protected array $sortedList = [];
  
  /**
   * The tests list, allocated to current bin.
   *
   * @var TestClassInfoList
   */
  protected array $allocatedList = [];
  
  /**
   * @param GroupedTestClassInfoList $groupedTestClassInfoList
   *   The tests to allocate to workers.
   * @param int $totalBins
   *   The number of bins available for allocation.
   * @param int $binIndex
   *   The current bin.
   */
  public function __construct(public readonly array $groupedTestClassInfoList, public readonly int $totalBins = 1, public readonly int $binIndex = 1) {
    $this->process();
  }
  
  /**
   * Returns the sorted tests list, prior to allocation.
   *
   * @return TestClassInfoList
   *   The allocated tests.
   */
  public function getSortedList() : array {
    return $this->sortedList;
  }
  
  /**
   * Returns the list of tests allocated to current bin.
   *
   * @return TestClassInfoList
   *   The allocated tests.
   */
  public function getAllocatedList() : array {
    return $this->allocatedList;
  }
  
  /**
   * Allocates tests to workers.
   */
  private function process() : void {
    // Separate tests in the '#slow' group from the rest.
    $slowTests = $this->groupedTestClassInfoList['#slow'] ?? [];
    $notSlowTests = [];
    foreach ($this->groupedTestClassInfoList as $group => $tests) {
      if ($group === '#slow') {
        continue;
      }
      $notSlowTests = array_merge($notSlowTests, $tests);
    }
    // Filter slow tests out of the not slow tests and ensure a unique list
    // since tests may appear in more than one group.
    $notSlowTests = array_diff_key($notSlowTests, $slowTests);
    // Sort all tests.
    $this->sortTestsByTypeAndCount($slowTests);
    $this->sortTestsByTypeAndCount($notSlowTests);
    $this->sortedList = array_merge($slowTests, $notSlowTests);
    $this->assignTestsSequence($this->sortedList, 'sorted_sequence');
    // If the tests are not being run in parallel, the sorted list is enough.
    if ($this->totalBins <= 1) {
      $this->allocatedList = $this->sortedList;
      $this->assignTestsSequence($this->allocatedList, 'worker_sequence');
      return;
    }
    // Set up a bin per test runner. Loop over the slow tests and add them to
    // a bin one by one, this distributes the tests evenly across the bins.
    $binnedSlowTests = $this->placeTestsIntoBins($slowTests);
    $slowTestsForJob = $binnedSlowTests[$this->binIndex - 1];
    // And the same for the rest of the tests.
    $binnedOtherTests = $this->placeTestsIntoBins($notSlowTests);
    $otherTestsForJob = $binnedOtherTests[$this->binIndex - 1];
    $this->allocatedList = array_merge($slowTestsForJob, $otherTestsForJob);
    $this->assignTestsSequence($this->allocatedList, 'worker_sequence');
  }
  
  /**
   * Sort tests by test type and count of tests to be executed.
   *
   * Tests with several methods take longer to run than tests with a single
   * method all else being equal, so this allows tests runs to be sorted by
   * approximately the slowest to fastest tests. Tests that are exceptionally
   * slow can be added to the '#slow' group so they are placed first in each
   * test run regardless of the number of methods.
   *
   * @param TestClassInfoList $tests
   *   The tests to sort.
   */
  private function sortTestsByTypeAndCount(array &$tests) : void {
    uasort($tests, fn(array $a, array $b): int => $this->getTestTypeWeight($b['name']) <=> $this->getTestTypeWeight($a['name']) ?: $b['tests_count'] <=> $a['tests_count']);
  }
  
  /**
   * Weights a test class based on which test base class it extends.
   *
   * @param string $class
   *   The test class name.
   */
  private function getTestTypeWeight(string $class) : int {
    assert(class_exists($class), "{$class} does not exist");
    return match (TRUE) {  is_subclass_of($class, WebDriverTestBase::class) => 3,
      is_subclass_of($class, BrowserTestBase::class) => 2,
      is_subclass_of($class, BuildTestBase::class) => 2,
      is_subclass_of($class, KernelTestBase::class) => 1,
      default => 0,
    
    };
  }
  
  /**
   * Assigns the test sequence.
   *
   * @param TestClassInfoList $tests
   *   The array of test class info.
   * @param string $sequenceKey
   *   The key of the TestClassInfo to add.
   */
  private function assignTestsSequence(array &$tests, string $sequenceKey) : void {
    $i = 0;
    foreach ($tests as &$testInfo) {
      $testInfo[$sequenceKey] = ++$i;
    }
  }
  
  /**
   * Distribute tests into roughly equal sized bins.
   *
   * @param TestClassInfoList $tests
   *   An array of test class names.
   *
   * @return array<int,TestClassInfoList>
   *   An associative array of bins and the test class names in each bin.
   */
  private function placeTestsIntoBins(array $tests) : array {
    $bins = array_fill(0, $this->totalBins, []);
    $i = 0;
    foreach ($tests as $key => $test) {
      $bins[$i++ % $this->totalBins][$key] = $test;
    }
    return $bins;
  }

}

Members

Title Sort descending Modifiers Object type Summary
WorkAllocator::$allocatedList protected property The tests list, allocated to current bin.
WorkAllocator::$sortedList protected property The sorted tests list, prior to allocation.
WorkAllocator::assignTestsSequence private function Assigns the test sequence.
WorkAllocator::getAllocatedList public function Returns the list of tests allocated to current bin.
WorkAllocator::getSortedList public function Returns the sorted tests list, prior to allocation.
WorkAllocator::getTestTypeWeight private function Weights a test class based on which test base class it extends.
WorkAllocator::placeTestsIntoBins private function Distribute tests into roughly equal sized bins.
WorkAllocator::process private function Allocates tests to workers.
WorkAllocator::sortTestsByTypeAndCount private function Sort tests by test type and count of tests to be executed.
WorkAllocator::__construct public function

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.