Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
54.17% covered (warning)
54.17%
13 / 24
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TestSuiteBuilder
54.17% covered (warning)
54.17%
13 / 24
0.00% covered (danger)
0.00%
0 / 3
19.63
0.00% covered (danger)
0.00%
0 / 1
 sortByTimeDescending
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
3.33
 smallestGroup
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 buildSuites
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
4.06
1<?php
2
3declare( strict_types = 1 );
4
5namespace MediaWiki\Composer\PhpUnitSplitter;
6
7/**
8 * @license GPL-2.0-or-later
9 */
10class TestSuiteBuilder {
11
12    private static function sortByTimeDescending( TestDescriptor $a, TestDescriptor $b ): int {
13        if ( $a->getDuration() === $b->getDuration() ) {
14            return 0;
15        }
16        return ( $a->getDuration() > $b->getDuration() ? -1 : 1 );
17    }
18
19    private static function smallestGroup( array $suites ): int {
20        $min = 10000;
21        $minIndex = 0;
22        $groups = count( $suites );
23        for ( $i = 0; $i < $groups; $i++ ) {
24            if ( $suites[$i]["time"] < $min ) {
25                $min = $suites[$i]["time"];
26                $minIndex = $i;
27            }
28        }
29        return $minIndex;
30    }
31
32    public function buildSuites( array $testDescriptors, int $groups ): array {
33        $suites = array_fill( 0, $groups, [ "list" => [], "time" => 0 ] );
34        $roundRobin = 0;
35        usort( $testDescriptors, [ self::class, "sortByTimeDescending" ] );
36        foreach ( $testDescriptors as $testDescriptor ) {
37            if ( !$testDescriptor->getFilename() ) {
38                // We didn't resolve a matching file for this test, so we skip it
39                // from the suite here. This only happens for "known" missing test
40                // classes (see PhpUnitXmlManager::EXPECTED_MISSING_CLASSES) - in
41                // all other cases a missing test file will throw an exception during
42                // suite building.
43                continue;
44            }
45            if ( $testDescriptor->getDuration() === 0 ) {
46                // If no explicit timing information is available for a test, we just
47                // drop it round-robin into the next bucket.
48                $nextSuite = $roundRobin;
49                $roundRobin = ( $roundRobin + 1 ) % $groups;
50            } else {
51                // If we have information about the test duration, we try and balance
52                // out the tests suites by having an even amount of time spent on
53                // each suite.
54                $nextSuite = self::smallestGroup( $suites );
55            }
56            $suites[$nextSuite]["list"][] = $testDescriptor->getFilename();
57            $suites[$nextSuite]["time"] += $testDescriptor->getDuration();
58        }
59        return $suites;
60    }
61}