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