Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
0.00% |
0 / 169 |
|
0.00% |
0 / 21 |
CRAP | |
0.00% |
0 / 1 |
| PhpUnitConsoleOutputProcessor | |
0.00% |
0 / 169 |
|
0.00% |
0 / 21 |
6480 | |
0.00% |
0 / 1 |
| processInput | |
0.00% |
0 / 35 |
|
0.00% |
0 / 1 |
342 | |||
| handleSummaryTotals | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
132 | |||
| handleDotChartLine | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| handleTestSummarySection | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| handleTestSummary | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
| processPossibleErrorTotalsLine | |
0.00% |
0 / 18 |
|
0.00% |
0 / 1 |
56 | |||
| collectAndDumpFailureSummary | |
0.00% |
0 / 21 |
|
0.00% |
0 / 1 |
42 | |||
| getPhpVersion | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| getPhpUnitVersion | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
| wereTestsExecuted | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| close | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| hasFailures | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
| getFailureDetails | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
| getSlowTests | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| prettyPrintErrors | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| prettyPrintFailures | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
20 | |||
| getAssertionCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getErrorCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getFailureCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getTestCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| getSkippedCount | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | declare( strict_types = 1 ); |
| 4 | |
| 5 | namespace MediaWiki\Composer\PhpUnitSplitter; |
| 6 | |
| 7 | use Composer\IO\IOInterface; |
| 8 | |
| 9 | /** |
| 10 | * @license GPL-2.0-or-later |
| 11 | */ |
| 12 | class PhpUnitConsoleOutputProcessor { |
| 13 | |
| 14 | private const STATE_EXPECT_PHP_VERSION = 0; |
| 15 | private const STATE_EXPECT_PHPUNIT_VERSION = 1; |
| 16 | private const STATE_EXPECT_DOT_CHART = 2; |
| 17 | private const STATE_EXPECT_TEST_SUMMARY = 3; |
| 18 | private const STATE_EXPECT_ERROR_SUMMARY = 4; |
| 19 | private const STATE_EXPECT_FAILURE_SUMMARY = 5; |
| 20 | private const STATE_EXPECT_ERROR_TOTALS = 6; |
| 21 | private const STATE_EXPECT_SLOW_TESTS = 7; |
| 22 | private const STATE_EOF = 8; |
| 23 | private const STATE_CLOSED = 9; |
| 24 | |
| 25 | private int $state = self::STATE_EXPECT_PHP_VERSION; |
| 26 | private array $failures = []; |
| 27 | private array $errors = []; |
| 28 | private array $slowTests = []; |
| 29 | private ?string $phpVersion = null; |
| 30 | private ?string $phpUnitVersion = null; |
| 31 | private ?string $dotChart = null; |
| 32 | private bool $noTestsExecuted = false; |
| 33 | private ?PhpUnitFailure $currentFailure = null; |
| 34 | private int $testCount = 0; |
| 35 | private int $assertionCount = 0; |
| 36 | private int $errorCount = 0; |
| 37 | private int $failureCount = 0; |
| 38 | private int $skippedCount = 0; |
| 39 | |
| 40 | /** |
| 41 | * @throws PhpUnitConsoleOutputProcessingException |
| 42 | */ |
| 43 | public function processInput( string $data ): void { |
| 44 | $array = preg_split( "/\r\n|\n|\r/", $data ); |
| 45 | foreach ( $array as $inputLine ) { |
| 46 | $matches = []; |
| 47 | switch ( $this->state ) { |
| 48 | case self::STATE_EXPECT_PHP_VERSION: |
| 49 | if ( preg_match( "/^Using PHP (.*)$/", $inputLine, $matches ) ) { |
| 50 | $this->phpVersion = $matches[1]; |
| 51 | $this->state = self::STATE_EXPECT_PHPUNIT_VERSION; |
| 52 | } |
| 53 | break; |
| 54 | |
| 55 | case self::STATE_EXPECT_PHPUNIT_VERSION: |
| 56 | if ( preg_match( "/^PHPUnit (.*) by .*$/", $inputLine, $matches ) ) { |
| 57 | $this->phpUnitVersion = $matches[1]; |
| 58 | $this->state = self::STATE_EXPECT_DOT_CHART; |
| 59 | } |
| 60 | break; |
| 61 | |
| 62 | case self::STATE_EXPECT_DOT_CHART: |
| 63 | $this->handleDotChartLine( $inputLine ); |
| 64 | break; |
| 65 | |
| 66 | case self::STATE_EXPECT_TEST_SUMMARY: |
| 67 | $this->handleTestSummary( $inputLine ); |
| 68 | break; |
| 69 | |
| 70 | case self::STATE_EXPECT_ERROR_SUMMARY: |
| 71 | $this->handleSummaryTotals( $inputLine, false ); |
| 72 | break; |
| 73 | |
| 74 | case self::STATE_EXPECT_FAILURE_SUMMARY: |
| 75 | $this->handleSummaryTotals( $inputLine, true ); |
| 76 | break; |
| 77 | |
| 78 | case self::STATE_EXPECT_ERROR_TOTALS: |
| 79 | $this->processPossibleErrorTotalsLine( $inputLine ); |
| 80 | break; |
| 81 | |
| 82 | case self::STATE_EXPECT_SLOW_TESTS: |
| 83 | if ( preg_match( "/^ \d+\. (\d+)ms to run (.*)$/", $inputLine, $matches ) ) { |
| 84 | $this->slowTests[] = new PhpUnitSlowTest( intval( $matches[1] ), $matches[2] ); |
| 85 | } |
| 86 | break; |
| 87 | |
| 88 | case self::STATE_EOF: |
| 89 | if ( $inputLine ) { |
| 90 | throw new PhpUnitConsoleOutputProcessingException( |
| 91 | "Unexpected input in `EOF` state: '" . $inputLine . "'" |
| 92 | ); |
| 93 | } |
| 94 | break; |
| 95 | |
| 96 | default: |
| 97 | throw new PhpUnitConsoleOutputProcessingException( |
| 98 | "Unexpected processing state " . $this->state |
| 99 | ); |
| 100 | } |
| 101 | } |
| 102 | if ( $this->currentFailure && !$this->currentFailure->empty() ) { |
| 103 | $this->failures[] = $this->currentFailure; |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | /** |
| 108 | * @throws PhpUnitConsoleOutputProcessingException |
| 109 | */ |
| 110 | private function handleSummaryTotals( string $inputLine, bool $errorSectionComplete ) { |
| 111 | if ( preg_match( "/^.*ERRORS!.*$/", $inputLine ) || |
| 112 | preg_match( "/^.*FAILURES!.*$/", $inputLine ) ) { |
| 113 | $this->state = self::STATE_EXPECT_ERROR_TOTALS; |
| 114 | return; |
| 115 | } |
| 116 | if ( !$errorSectionComplete && ( |
| 117 | preg_match( "/^There were (\d+) failures:$/", $inputLine, $matches ) || |
| 118 | $inputLine === "There was 1 failure:" ) |
| 119 | ) { |
| 120 | if ( $this->currentFailure && !$this->currentFailure->empty() ) { |
| 121 | $this->errors[] = $this->currentFailure; |
| 122 | } |
| 123 | $this->state = self::STATE_EXPECT_FAILURE_SUMMARY; |
| 124 | $this->currentFailure = new PhpUnitFailure(); |
| 125 | return; |
| 126 | } |
| 127 | if ( $this->processPossibleErrorTotalsLine( $inputLine ) ) { |
| 128 | return; |
| 129 | } |
| 130 | if ( !$this->currentFailure->processLine( $inputLine ) ) { |
| 131 | if ( !$errorSectionComplete ) { |
| 132 | $this->errors[] = $this->currentFailure; |
| 133 | } else { |
| 134 | $this->failures[] = $this->currentFailure; |
| 135 | } |
| 136 | $this->currentFailure = new PhpUnitFailure(); |
| 137 | $this->currentFailure->processLine( $inputLine ); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | private function handleDotChartLine( string $inputLine ) { |
| 142 | if ( $this->dotChart === null ) { |
| 143 | $this->dotChart = ""; |
| 144 | } |
| 145 | $this->dotChart .= $inputLine . PHP_EOL; |
| 146 | if ( preg_match( "/^.* \(100%\)$/", $inputLine ) ) { |
| 147 | $this->state = self::STATE_EXPECT_TEST_SUMMARY; |
| 148 | return; |
| 149 | } |
| 150 | if ( preg_match( "/^.*No tests executed!.*$/", $inputLine ) ) { |
| 151 | $this->noTestsExecuted = true; |
| 152 | $this->state = self::STATE_EOF; |
| 153 | } |
| 154 | } |
| 155 | |
| 156 | private function handleTestSummarySection( string $inputLine, string $keyword, int $nextState ): bool { |
| 157 | if ( preg_match( "/^There were (\d+) " . $keyword . "s:$/", $inputLine ) || |
| 158 | $inputLine === "There was 1 " . $keyword . ":" ) { |
| 159 | $this->state = $nextState; |
| 160 | $this->currentFailure = new PhpUnitFailure(); |
| 161 | return true; |
| 162 | } |
| 163 | return false; |
| 164 | } |
| 165 | |
| 166 | private function handleTestSummary( string $inputLine ) { |
| 167 | if ( $this->handleTestSummarySection( |
| 168 | $inputLine, |
| 169 | "error", |
| 170 | self::STATE_EXPECT_ERROR_SUMMARY |
| 171 | ) ) { |
| 172 | return; |
| 173 | } |
| 174 | if ( $this->handleTestSummarySection( |
| 175 | $inputLine, |
| 176 | "failure", |
| 177 | self::STATE_EXPECT_FAILURE_SUMMARY |
| 178 | ) ) { |
| 179 | return; |
| 180 | } |
| 181 | $this->processPossibleErrorTotalsLine( $inputLine ); |
| 182 | } |
| 183 | |
| 184 | private function processPossibleErrorTotalsLine( string $inputLine ): bool { |
| 185 | $matches = []; |
| 186 | if ( preg_match( "/^.*Tests: (\d+).*$/", $inputLine, $matches ) ) { |
| 187 | $this->testCount = intval( $matches[1] ); |
| 188 | if ( preg_match( "/^.*Assertions: (\d+).*$/", $inputLine, $matches ) ) { |
| 189 | $this->assertionCount = intval( $matches[1] ); |
| 190 | } |
| 191 | if ( preg_match( "/^.*Failures: (\d+).*$/", $inputLine, $matches ) ) { |
| 192 | $this->failureCount = intval( $matches[1] ); |
| 193 | } |
| 194 | if ( preg_match( "/^.*Errors: (\d+).*$/", $inputLine, $matches ) ) { |
| 195 | $this->errorCount = intval( $matches[1] ); |
| 196 | } |
| 197 | if ( preg_match( "/^.*Skipped: (\d+).*$/", $inputLine, $matches ) ) { |
| 198 | $this->skippedCount = intval( $matches[1] ); |
| 199 | } |
| 200 | $this->state = self::STATE_EXPECT_SLOW_TESTS; |
| 201 | return true; |
| 202 | } |
| 203 | if ( preg_match( "/^.*OK \((\d+) tests?, (\d+) assertions?\).*$/", $inputLine, $matches ) ) { |
| 204 | $this->testCount = intval( $matches[1] ); |
| 205 | $this->assertionCount = intval( $matches[2] ); |
| 206 | $this->state = self::STATE_EXPECT_SLOW_TESTS; |
| 207 | } |
| 208 | return false; |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * @throws PhpUnitConsoleOutputProcessingException |
| 213 | */ |
| 214 | public static function collectAndDumpFailureSummary( string $filePattern, int $groupCount, IOInterface $io ): bool { |
| 215 | $failuresFound = false; |
| 216 | $slowTests = []; |
| 217 | for ( $i = 0; $i < $groupCount; $i++ ) { |
| 218 | $filename = sprintf( $filePattern, $i ); |
| 219 | if ( file_exists( $filename ) ) { |
| 220 | $summary = new PhpUnitConsoleOutputProcessor(); |
| 221 | $summary->processInput( file_get_contents( $filename ) ); |
| 222 | $summary->close(); |
| 223 | $slowTests = array_values( array_merge( $slowTests, $summary->getSlowTests() ) ); |
| 224 | $failureDetails = $summary->getFailureDetails(); |
| 225 | if ( $failureDetails ) { |
| 226 | $io->write( "Report from `split_group" . $i . "`:" . PHP_EOL ); |
| 227 | $io->write( $failureDetails ); |
| 228 | $failuresFound = true; |
| 229 | } |
| 230 | } |
| 231 | } |
| 232 | if ( count( $slowTests ) > 0 ) { |
| 233 | $io->write( PHP_EOL . "You should really speed up these slow tests (>100ms)..." . PHP_EOL ); |
| 234 | usort( $slowTests, static fn ( $t1, $t2 ) => $t2->getDuration() - $t1->getDuration() ); |
| 235 | for ( $i = 0; $i < min( 10, count( $slowTests ) ); $i++ ) { |
| 236 | $test = $slowTests[$i]; |
| 237 | $io->write( " " . ( $i + 1 ) . ". " . $test->getDuration() . "ms to run " . $test->getTest() ); |
| 238 | } |
| 239 | } |
| 240 | return $failuresFound; |
| 241 | } |
| 242 | |
| 243 | /** |
| 244 | * @throws PhpUnitConsoleOutputProcessingException |
| 245 | */ |
| 246 | public function getPhpVersion(): string { |
| 247 | if ( $this->state !== self::STATE_CLOSED ) { |
| 248 | throw new PhpUnitConsoleOutputProcessingException( "Still processing. Call `close()` first" ); |
| 249 | } |
| 250 | if ( !$this->phpVersion ) { |
| 251 | throw new PhpUnitConsoleOutputProcessingException( "No php version string detceted" ); |
| 252 | } |
| 253 | return $this->phpVersion; |
| 254 | } |
| 255 | |
| 256 | /** |
| 257 | * @throws PhpUnitConsoleOutputProcessingException |
| 258 | */ |
| 259 | public function getPhpUnitVersion(): string { |
| 260 | if ( $this->state !== self::STATE_CLOSED ) { |
| 261 | throw new PhpUnitConsoleOutputProcessingException( "Still processing. Call `close()` first" ); |
| 262 | } |
| 263 | if ( !$this->phpUnitVersion ) { |
| 264 | throw new PhpUnitConsoleOutputProcessingException( "No phpunit version string detceted" ); |
| 265 | } |
| 266 | return $this->phpUnitVersion; |
| 267 | } |
| 268 | |
| 269 | /** |
| 270 | * @throws PhpUnitConsoleOutputProcessingException |
| 271 | */ |
| 272 | public function wereTestsExecuted(): bool { |
| 273 | if ( $this->state !== self::STATE_CLOSED ) { |
| 274 | throw new PhpUnitConsoleOutputProcessingException( "Still processing. Call `close()` first" ); |
| 275 | } |
| 276 | return !$this->noTestsExecuted; |
| 277 | } |
| 278 | |
| 279 | public function close(): void { |
| 280 | $this->state = self::STATE_CLOSED; |
| 281 | } |
| 282 | |
| 283 | /** |
| 284 | * @throws PhpUnitConsoleOutputProcessingException |
| 285 | */ |
| 286 | public function hasFailures(): bool { |
| 287 | if ( $this->state !== self::STATE_CLOSED ) { |
| 288 | throw new PhpUnitConsoleOutputProcessingException( "Still processing. Call `close()` first" ); |
| 289 | } |
| 290 | return count( $this->failures ) + count( $this->errors ) > 0; |
| 291 | } |
| 292 | |
| 293 | public function getFailureDetails(): string { |
| 294 | $errorDetails = $this->prettyPrintErrors(); |
| 295 | $failureDetails = $this->prettyPrintFailures(); |
| 296 | $joiner = ""; |
| 297 | if ( $errorDetails && $failureDetails ) { |
| 298 | $joiner = PHP_EOL . "--" . PHP_EOL . PHP_EOL; |
| 299 | } |
| 300 | return $errorDetails . $joiner . $failureDetails; |
| 301 | } |
| 302 | |
| 303 | public function getSlowTests(): array { |
| 304 | return $this->slowTests; |
| 305 | } |
| 306 | |
| 307 | private function prettyPrintErrors(): string { |
| 308 | $errorCount = count( $this->errors ); |
| 309 | if ( $errorCount === 0 ) { |
| 310 | return ""; |
| 311 | } |
| 312 | $result = "There " . ( $errorCount > 1 ? "were " : "was " ) . $errorCount |
| 313 | . " error" . ( $errorCount > 1 ? "s" : "" ) . ":" . PHP_EOL . PHP_EOL; |
| 314 | return $result . implode( |
| 315 | PHP_EOL, |
| 316 | array_map( static fn ( $err ) => $err->getFailureDetails(), array_merge( $this->errors ) ) |
| 317 | ); |
| 318 | } |
| 319 | |
| 320 | private function prettyPrintFailures(): string { |
| 321 | $failureCount = count( $this->failures ); |
| 322 | if ( $failureCount === 0 ) { |
| 323 | return ""; |
| 324 | } |
| 325 | $result = "There " . ( $failureCount > 1 ? "were " : "was " ) . $failureCount |
| 326 | . " failure" . ( $failureCount > 1 ? "s" : "" ) . ":" . PHP_EOL . PHP_EOL; |
| 327 | return $result . implode( |
| 328 | PHP_EOL, |
| 329 | array_map( static fn ( $err ) => $err->getFailureDetails(), array_merge( $this->failures ) ) |
| 330 | ); |
| 331 | } |
| 332 | |
| 333 | public function getAssertionCount(): int { |
| 334 | return $this->assertionCount; |
| 335 | } |
| 336 | |
| 337 | public function getErrorCount(): int { |
| 338 | return $this->errorCount; |
| 339 | } |
| 340 | |
| 341 | public function getFailureCount(): int { |
| 342 | return $this->failureCount; |
| 343 | } |
| 344 | |
| 345 | public function getTestCount(): int { |
| 346 | return $this->testCount; |
| 347 | } |
| 348 | |
| 349 | public function getSkippedCount(): int { |
| 350 | return $this->skippedCount; |
| 351 | } |
| 352 | } |