3 require __DIR__ .
'/../../maintenance/Maintenance.php';
5 define(
'MW_PARSER_TEST',
true );
21 parent::__construct();
22 $this->
addOption(
'session-data',
'internal option, do not use',
false,
true );
24 'Use the wiki\'s Tidy configuration instead of known-good' .
40 if ( $this->
hasOption(
'session-data' ) ) {
41 $this->session = json_decode( $this->
getOption(
'session-data' ),
true );
43 $this->session = [
'options' => [] ];
45 if ( $this->
hasOption(
'use-tidy-config' ) ) {
46 $this->session[
'options'][
'use-tidy-config'] =
true;
48 $this->runner =
new ParserTestRunner( $this->recorder, $this->session[
'options'] );
52 if ( $this->numFailed === 0 ) {
53 if ( $this->numSkipped === 0 ) {
54 print
"All tests passed!\n";
56 print
"All tests passed (but skipped {$this->numSkipped})\n";
60 print
"{$this->numFailed} test(s) failed.\n";
65 $this->testFiles = [];
67 foreach ( ParserTestRunner::getParserTestFiles()
as $file ) {
69 $this->testFiles[
$file] = $fileInfo;
70 $this->testCount +=
count( $fileInfo[
'tests'] );
75 $teardown = $this->runner->staticSetup();
76 $teardown = $this->runner->setupDatabase( $teardown );
77 $teardown = $this->runner->setupUploads( $teardown );
79 print
"Running tests...\n";
81 $this->numExecuted = 0;
82 $this->numSkipped = 0;
84 foreach ( $this->testFiles
as $fileName => $fileInfo ) {
85 $this->runner->addArticles( $fileInfo[
'articles'] );
86 foreach ( $fileInfo[
'tests']
as $testInfo ) {
87 $result = $this->runner->runTest( $testInfo );
90 } elseif ( !
$result->isSuccess() ) {
91 $this->results[$fileName][$testInfo[
'desc']] =
$result;
104 $width = $this->termWidth - 9;
105 $pos = round( $width * $done / $total );
106 printf(
'│' . str_repeat(
'█', $pos ) . str_repeat(
'-', $width - $pos ) .
107 "│ %5.1f%%\r", $done / $total * 100 );
111 if ( isset( $this->session[
'startFile'] ) ) {
112 $startFile = $this->session[
'startFile'];
113 $startTest = $this->session[
'startTest'];
122 foreach ( $this->testFiles
as $fileName => $fileInfo ) {
123 if ( !isset( $this->results[$fileName] ) ) {
126 if ( !$foundStart && $startFile !==
false && $fileName !== $startFile ) {
127 $testIndex +=
count( $this->results[$fileName] );
130 foreach ( $fileInfo[
'tests']
as $testInfo ) {
131 if ( !isset( $this->results[$fileName][$testInfo[
'desc']] ) ) {
134 $result = $this->results[$fileName][$testInfo[
'desc']];
136 if ( !$foundStart && $startTest !==
false ) {
137 if ( $testInfo[
'desc'] !== $startTest ) {
147 if ( !$foundStart ) {
148 print
"Could not find the test after a restart, did you rename it?";
149 unset( $this->session[
'startFile'] );
150 unset( $this->session[
'startTest'] );
158 $heading =
"─── $text ";
159 $heading .= str_repeat(
'─', $this->termWidth - mb_strlen( $heading ) );
160 $heading =
$term->color( 34 ) . $heading .
$term->reset() .
"\n";
165 $fromLines = explode(
"\n", $left );
166 $toLines = explode(
"\n", $right );
168 return $formatter->
format(
new Diff( $fromLines, $toLines ) );
173 $div1 =
$term->color( 34 ) . str_repeat(
'━', $this->termWidth ) .
174 $term->reset() .
"\n";
175 $div2 =
$term->color( 34 ) . str_repeat(
'─', $this->termWidth ) .
176 $term->reset() .
"\n";
179 print
"Failure $index/{$this->numFailed}: {$testInfo['file']} line {$testInfo['line']}\n" .
180 "{$testInfo['desc']}\n";
182 print $this->
heading(
'Input' );
183 print
"{$testInfo['input']}\n";
185 print $this->
heading(
'Alternating expected/actual output' );
188 print $this->
heading(
'Diff' );
191 if ( $dwdiff !==
false ) {
198 if ( $testInfo[
'options'] || $testInfo[
'config'] ) {
199 print $this->
heading(
'Options / Config' );
200 if ( $testInfo[
'options'] ) {
201 print $testInfo[
'options'] .
"\n";
203 if ( $testInfo[
'config'] ) {
204 print $testInfo[
'config'] .
"\n";
209 print
"What do you want to do?\n";
211 '[R]eload code and run again',
212 '[U]pdate source file, copy actual to expected',
215 if ( strpos( $testInfo[
'options'],
' tidy' ) ===
false ) {
216 if ( empty( $testInfo[
'isSubtest'] ) ) {
217 $specs[] =
"Enable [T]idy";
220 $specs[] =
'Disable [T]idy';
223 if ( !empty( $testInfo[
'isSubtest'] ) ) {
224 $specs[] =
'Delete [s]ubtest';
226 $specs[] =
'[D]elete test';
230 foreach ( $specs
as $spec ) {
231 if ( !preg_match(
'/^(.*\[)(.)(\].*)$/', $spec, $m ) ) {
232 throw new MWException(
'Invalid option spec: ' . $spec );
234 print
'* ' . $m[1] .
$term->color( 35 ) . $m[2] .
$term->color( 0 ) . $m[3] .
"\n";
235 $options[strtoupper( $m[2] )] =
true;
247 print
"Invalid response, please enter a single letter from the list above\n";
251 switch ( strtoupper( trim(
$response ) ) ) {
253 $cmdResult = $this->
reload( $testInfo );
272 }
while ( !$cmdResult );
275 protected function dwdiff( $expected, $actual ) {
276 if ( !is_executable(
'/usr/bin/dwdiff' ) ) {
285 $markedExpected = strtr( $expected, $markers );
286 $markedActual = strtr( $actual, $markers );
287 $diff = $this->
unifiedDiff( $markedExpected, $markedActual );
289 $tempFile = tmpfile();
290 fwrite( $tempFile, $diff );
291 fseek( $tempFile, 0 );
293 $proc = proc_open(
'/usr/bin/dwdiff -Pc --diff-input',
294 [ 0 => $tempFile, 1 => [
'pipe',
'w' ], 2 => STDERR ],
301 $result = stream_get_contents( $pipes[1] );
308 $expectedLines = explode(
"\n", $expectedStr );
309 $actualLines = explode(
"\n", $actualStr );
310 $maxLines = max(
count( $expectedLines ),
count( $actualLines ) );
312 for ( $i = 0; $i < $maxLines; $i++ ) {
313 if ( $i <
count( $expectedLines ) ) {
314 $expectedLine = $expectedLines[$i];
315 $expectedChunks = str_split( $expectedLine, $this->termWidth - 3 );
317 $expectedChunks = [];
320 if ( $i <
count( $actualLines ) ) {
321 $actualLine = $actualLines[$i];
322 $actualChunks = str_split( $actualLine, $this->termWidth - 3 );
327 $maxChunks = max(
count( $expectedChunks ),
count( $actualChunks ) );
329 for ( $j = 0; $j < $maxChunks; $j++ ) {
330 if ( isset( $expectedChunks[$j] ) ) {
331 $result .=
"E: " . $expectedChunks[$j];
332 if ( $j ===
count( $expectedChunks ) - 1 ) {
341 if ( isset( $actualChunks[$j] ) ) {
343 if ( $j ===
count( $actualChunks ) - 1 ) {
355 pcntl_exec( PHP_BINARY, [
359 'startFile' => $testInfo[
'file'],
360 'startTest' => $testInfo[
'desc']
361 ] + $this->session ) ] );
363 print
"pcntl_exec() failed\n";
369 for ( $i = 1; $i < $testInfo[
'line']; $i++ ) {
371 if (
$line ===
false ) {
372 print
"Error reading from file\n";
375 $initialPart .=
$line;
379 if ( !preg_match(
'/^!!\s*test/',
$line ) ) {
380 print
"Test has moved, cannot edit\n";
386 $desc = fgets(
$file );
387 if ( trim( $desc ) !== $testInfo[
'desc'] ) {
388 print
"Description does not match, cannot edit\n";
392 return [ $initialPart, $testPart ];
396 if ( is_writable( $inputFileName ) ) {
397 $outputFileName = $inputFileName;
399 $outputFileName =
wfTempDir() .
'/' . basename( $inputFileName );
400 print
"Cannot write to input file, writing to $outputFileName instead\n";
402 return $outputFileName;
405 protected function editTest( $fileName, $deletions, $changes ) {
406 $text = file_get_contents( $fileName );
407 if ( $text ===
false ) {
408 print
"Unable to open test file!";
416 if ( is_writable( $fileName ) ) {
417 file_put_contents( $fileName,
$result );
418 print
"Wrote updated file\n";
420 print
"Cannot write updated file, here is a patch you can paste:\n\n";
421 print
"--- {$fileName}\n" .
422 "+++ {$fileName}~\n" .
432 $testInfo[
'test'] => [
433 $testInfo[
'resultSection'] => [
435 'value' =>
$result->actual .
"\n"
444 [ $testInfo[
'test'] ],
450 $resultSection = $testInfo[
'resultSection'];
451 if ( in_array( $resultSection, [
'html/php',
'html/*',
'html',
'result' ] ) ) {
452 $newSection =
'html+tidy';
453 } elseif ( in_array( $resultSection, [
'html/php+tidy',
'html+tidy' ] ) ) {
454 $newSection =
'html';
456 print
"Unrecognised result section name \"$resultSection\"";
463 $testInfo[
'test'] => [
466 'value' => $newSection
477 $testInfo[
'test'] => [
478 $testInfo[
'resultSection'] => [