38 public function buildSuites( array $testDescriptors,
int $groups, ?
int $chunkSize =
null ): array {
39 $suites = array_fill( 0, $groups, [
"list" => [],
"time" => 0 ] );
41 if ( $testDescriptors === [] ) {
46 usort( $testDescriptors, [ self::class,
"sortByNameAscending" ] );
48 $descriptorCount = count( $testDescriptors );
49 if ( $chunkSize ===
null ) {
51 $chunkSize = intval( ceil( $descriptorCount / 200 ) );
58 while ( $startIndex < $descriptorCount && $testDescriptors[$startIndex]->getDuration() == 0 ) {
62 if ( $startIndex === 0 ) {
63 $testDescriptorsWithoutLeadingZeros = $testDescriptors;
65 } elseif ( $startIndex < $descriptorCount ) {
66 $leadingZeros = array_map(
67 static fn ( $testDescriptor ) => $testDescriptor->getFilename(),
68 array_slice( $testDescriptors, 0, $startIndex )
70 $testDescriptorsWithoutLeadingZeros = array_slice( $testDescriptors, $startIndex );
74 return $this->buildSuitesNoDurationInformation( $testDescriptors, $groups );
78 $this->buildSuitesWithDurationInformationWithoutLeadingEmptyTests(
79 $testDescriptorsWithoutLeadingZeros, $suites, $groups, $chunkSize
81 }
catch ( SuiteSplittingException ) {
82 return $this->buildSuitesNoDurationInformation( $testDescriptors, $groups );
86 if ( $leadingZeros !== [] ) {
87 $suites[0][
"list"] = array_merge( $leadingZeros, $suites[0][
"list"] );
96 private function buildSuitesWithDurationInformationWithoutLeadingEmptyTests(
97 array $testDescriptorsWithoutLeadingZeros,
102 $n = count( $testDescriptorsWithoutLeadingZeros );
107 $chunks = $this->createChunksOfTests( $n, $testDescriptorsWithoutLeadingZeros, $chunkSize );
109 $numChunks = count( $chunks );
110 $durations = array_column( $chunks,
"time" );
113 $prefixSum = $this->calculatePrefixSum( $numChunks, $durations );
116 $backtrack = $this->generateBacktrackingTable( $numChunks, $numGroups, $prefixSum );
118 $this->constructSplitGroups( $numGroups, $numChunks, $chunks, $backtrack, $suites );
129 private function buildSuitesNoDurationInformation( array $testDescriptors,
int $groups ): array {
130 $suites = array_fill( 0, $groups, [
"list" => [],
"time" => 0 ] );
131 $testCount = count( $testDescriptors );
132 $splitGroupSize = ceil( $testCount / $groups );
135 for ( $currentGroup = 0; $currentGroup < $groups, $testIndex < $testCount; $testIndex++, $bucketIndex++ ) {
136 if ( $bucketIndex >= $splitGroupSize ) {
139 if ( $currentGroup === $groups ) {
143 $suites[$currentGroup][
"list"][] = $testDescriptors[$testIndex]->getFilename();
144 $suites[$currentGroup][
"time"] += $testDescriptors[$testIndex]->getDuration();
150 private function createChunksOfTests(
int $n, array $testDescriptors,
int $chunkSize ): array {
152 for ( $i = 0; $i < $n; $i += $chunkSize ) {
153 $chunk = array_slice( $testDescriptors, $i, min( $chunkSize, $n - $i ) );
156 "time" => array_reduce( $chunk,
static fn ( $sum, $test ) => $sum + $test->getDuration(), 0 )
162 private function calculatePrefixSum(
int $numChunks, array $durations ): array {
163 $prefixSum = array_fill( 0, $numChunks + 1, 0 );
164 for ( $i = 1; $i <= $numChunks; $i++ ) {
165 $prefixSum[$i] = $prefixSum[$i - 1] + $durations[$i - 1];
174 private function constructSplitGroups(
181 for ( $splitGroupId = $numGroups, $i = $numChunks; $splitGroupId > 0; --$splitGroupId ) {
182 $startIndex = $backtrack[$i][$splitGroupId];
183 if ( $startIndex === -1 ) {
184 throw new SuiteSplittingException(
"Invalid backtracking index building group " . $splitGroupId );
186 $suites[$splitGroupId - 1][
"list"] = array_merge(
187 ...array_map(
static fn ( $chunk ) => array_map(
188 static fn ( $test ) => $test->getFilename(),
191 array_slice( $chunks, $startIndex, $i - $startIndex )
194 $suites[$splitGroupId - 1][
"time"] = array_reduce(
195 array_slice( $chunks, $startIndex, $i - $startIndex ),
196 static fn ( $acc, $chunk ) => $acc + $chunk[
"time"],
207 private function generateBacktrackingTable(
int $numChunks,
int $numGroups, array $prefixSum ): array {
210 $minimumLargestBucket = array_fill( 0, $numChunks + 1, array_fill( 0, $numGroups + 1, PHP_INT_MAX ) );
212 $backtrack = array_fill( 0, $numChunks + 1, array_fill( 0, $numGroups + 1, -1 ) );
214 $minimumLargestBucket[0][0] = 0;
218 for ( $i = 1; $i <= $numChunks; $i++ ) {
220 for ( $j = 1; $j <= min( $numGroups, $i ); $j++ ) {
222 for ( $k = 0; $k < $i; $k++ ) {
224 $currentSplitGroupDuration = $prefixSum[$i] - $prefixSum[$k];
227 $newBestCaseMinimumLargestBucket = max(
228 $minimumLargestBucket[$k][$j - 1], $currentSplitGroupDuration
233 if ( $newBestCaseMinimumLargestBucket < $minimumLargestBucket[$i][$j] ) {
236 $minimumLargestBucket[$i][$j] = $newBestCaseMinimumLargestBucket;
238 $backtrack[$i][$j] = $k;