123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489 |
- <?php
- require __DIR__ . '/../../maintenance/Maintenance.php';
- define( 'MW_PARSER_TEST', true );
- /**
- * Interactive parser test runner and test file editor
- */
- class ParserEditTests extends Maintenance {
- private $termWidth;
- private $testFiles;
- private $testCount;
- private $recorder;
- private $runner;
- private $numExecuted;
- private $numSkipped;
- private $numFailed;
- function __construct() {
- parent::__construct();
- $this->addOption( 'session-data', 'internal option, do not use', false, true );
- $this->addOption( 'use-tidy-config',
- 'Use the wiki\'s Tidy configuration instead of known-good' .
- 'defaults.' );
- }
- public function finalSetup() {
- parent::finalSetup();
- self::requireTestsAutoloader();
- TestSetup::applyInitialConfig();
- }
- public function execute() {
- $this->termWidth = $this->getTermSize()[0] - 1;
- $this->recorder = new TestRecorder();
- $this->setupFileData();
- if ( $this->hasOption( 'session-data' ) ) {
- $this->session = json_decode( $this->getOption( 'session-data' ), true );
- } else {
- $this->session = [ 'options' => [] ];
- }
- if ( $this->hasOption( 'use-tidy-config' ) ) {
- $this->session['options']['use-tidy-config'] = true;
- }
- $this->runner = new ParserTestRunner( $this->recorder, $this->session['options'] );
- $this->runTests();
- if ( $this->numFailed === 0 ) {
- if ( $this->numSkipped === 0 ) {
- print "All tests passed!\n";
- } else {
- print "All tests passed (but skipped {$this->numSkipped})\n";
- }
- return;
- }
- print "{$this->numFailed} test(s) failed.\n";
- $this->showResults();
- }
- protected function setupFileData() {
- $this->testFiles = [];
- $this->testCount = 0;
- foreach ( ParserTestRunner::getParserTestFiles() as $file ) {
- $fileInfo = TestFileReader::read( $file );
- $this->testFiles[$file] = $fileInfo;
- $this->testCount += count( $fileInfo['tests'] );
- }
- }
- protected function runTests() {
- $teardown = $this->runner->staticSetup();
- $teardown = $this->runner->setupDatabase( $teardown );
- $teardown = $this->runner->setupUploads( $teardown );
- print "Running tests...\n";
- $this->results = [];
- $this->numExecuted = 0;
- $this->numSkipped = 0;
- $this->numFailed = 0;
- foreach ( $this->testFiles as $fileName => $fileInfo ) {
- $this->runner->addArticles( $fileInfo['articles'] );
- foreach ( $fileInfo['tests'] as $testInfo ) {
- $result = $this->runner->runTest( $testInfo );
- if ( $result === false ) {
- $this->numSkipped++;
- } elseif ( !$result->isSuccess() ) {
- $this->results[$fileName][$testInfo['desc']] = $result;
- $this->numFailed++;
- }
- $this->numExecuted++;
- $this->showProgress();
- }
- }
- print "\n";
- }
- protected function showProgress() {
- $done = $this->numExecuted;
- $total = $this->testCount;
- $width = $this->termWidth - 9;
- $pos = round( $width * $done / $total );
- printf( '│' . str_repeat( '█', $pos ) . str_repeat( '-', $width - $pos ) .
- "│ %5.1f%%\r", $done / $total * 100 );
- }
- protected function showResults() {
- if ( isset( $this->session['startFile'] ) ) {
- $startFile = $this->session['startFile'];
- $startTest = $this->session['startTest'];
- $foundStart = false;
- } else {
- $startFile = false;
- $startTest = false;
- $foundStart = true;
- }
- $testIndex = 0;
- foreach ( $this->testFiles as $fileName => $fileInfo ) {
- if ( !isset( $this->results[$fileName] ) ) {
- continue;
- }
- if ( !$foundStart && $startFile !== false && $fileName !== $startFile ) {
- $testIndex += count( $this->results[$fileName] );
- continue;
- }
- foreach ( $fileInfo['tests'] as $testInfo ) {
- if ( !isset( $this->results[$fileName][$testInfo['desc']] ) ) {
- continue;
- }
- $result = $this->results[$fileName][$testInfo['desc']];
- $testIndex++;
- if ( !$foundStart && $startTest !== false ) {
- if ( $testInfo['desc'] !== $startTest ) {
- continue;
- }
- $foundStart = true;
- }
- $this->handleFailure( $testIndex, $testInfo, $result );
- }
- }
- if ( !$foundStart ) {
- print "Could not find the test after a restart, did you rename it?";
- unset( $this->session['startFile'] );
- unset( $this->session['startTest'] );
- $this->showResults();
- }
- print "All done\n";
- }
- protected function heading( $text ) {
- $term = new AnsiTermColorer;
- $heading = "─── $text ";
- $heading .= str_repeat( '─', $this->termWidth - mb_strlen( $heading ) );
- $heading = $term->color( 34 ) . $heading . $term->reset() . "\n";
- return $heading;
- }
- protected function unifiedDiff( $left, $right ) {
- $fromLines = explode( "\n", $left );
- $toLines = explode( "\n", $right );
- $formatter = new UnifiedDiffFormatter;
- return $formatter->format( new Diff( $fromLines, $toLines ) );
- }
- protected function handleFailure( $index, $testInfo, $result ) {
- $term = new AnsiTermColorer;
- $div1 = $term->color( 34 ) . str_repeat( '━', $this->termWidth ) .
- $term->reset() . "\n";
- $div2 = $term->color( 34 ) . str_repeat( '─', $this->termWidth ) .
- $term->reset() . "\n";
- print $div1;
- print "Failure $index/{$this->numFailed}: {$testInfo['file']} line {$testInfo['line']}\n" .
- "{$testInfo['desc']}\n";
- print $this->heading( 'Input' );
- print "{$testInfo['input']}\n";
- print $this->heading( 'Alternating expected/actual output' );
- print $this->alternatingAligned( $result->expected, $result->actual );
- print $this->heading( 'Diff' );
- $dwdiff = $this->dwdiff( $result->expected, $result->actual );
- if ( $dwdiff !== false ) {
- $diff = $dwdiff;
- } else {
- $diff = $this->unifiedDiff( $result->expected, $result->actual );
- }
- print $diff;
- if ( $testInfo['options'] || $testInfo['config'] ) {
- print $this->heading( 'Options / Config' );
- if ( $testInfo['options'] ) {
- print $testInfo['options'] . "\n";
- }
- if ( $testInfo['config'] ) {
- print $testInfo['config'] . "\n";
- }
- }
- print $div2;
- print "What do you want to do?\n";
- $specs = [
- '[R]eload code and run again',
- '[U]pdate source file, copy actual to expected',
- '[I]gnore' ];
- if ( strpos( $testInfo['options'], ' tidy' ) === false ) {
- if ( empty( $testInfo['isSubtest'] ) ) {
- $specs[] = "Enable [T]idy";
- }
- } else {
- $specs[] = 'Disable [T]idy';
- }
- if ( !empty( $testInfo['isSubtest'] ) ) {
- $specs[] = 'Delete [s]ubtest';
- }
- $specs[] = '[D]elete test';
- $specs[] = '[Q]uit';
- $options = [];
- foreach ( $specs as $spec ) {
- if ( !preg_match( '/^(.*\[)(.)(\].*)$/', $spec, $m ) ) {
- throw new MWException( 'Invalid option spec: ' . $spec );
- }
- print '* ' . $m[1] . $term->color( 35 ) . $m[2] . $term->color( 0 ) . $m[3] . "\n";
- $options[strtoupper( $m[2] )] = true;
- }
- do {
- $response = $this->readconsole();
- $cmdResult = false;
- if ( $response === false ) {
- exit( 0 );
- }
- $response = strtoupper( trim( $response ) );
- if ( !isset( $options[$response] ) ) {
- print "Invalid response, please enter a single letter from the list above\n";
- continue;
- }
- switch ( strtoupper( trim( $response ) ) ) {
- case 'R':
- $cmdResult = $this->reload( $testInfo );
- break;
- case 'U':
- $cmdResult = $this->update( $testInfo, $result );
- break;
- case 'I':
- return;
- case 'T':
- $cmdResult = $this->switchTidy( $testInfo );
- break;
- case 'S':
- $cmdResult = $this->deleteSubtest( $testInfo );
- break;
- case 'D':
- $cmdResult = $this->deleteTest( $testInfo );
- break;
- case 'Q':
- exit( 0 );
- }
- } while ( !$cmdResult );
- }
- protected function dwdiff( $expected, $actual ) {
- if ( !is_executable( '/usr/bin/dwdiff' ) ) {
- return false;
- }
- $markers = [
- "\n" => '¶',
- ' ' => '·',
- "\t" => '→'
- ];
- $markedExpected = strtr( $expected, $markers );
- $markedActual = strtr( $actual, $markers );
- $diff = $this->unifiedDiff( $markedExpected, $markedActual );
- $tempFile = tmpfile();
- fwrite( $tempFile, $diff );
- fseek( $tempFile, 0 );
- $pipes = [];
- $proc = proc_open( '/usr/bin/dwdiff -Pc --diff-input',
- [ 0 => $tempFile, 1 => [ 'pipe', 'w' ], 2 => STDERR ],
- $pipes );
- if ( !$proc ) {
- return false;
- }
- $result = stream_get_contents( $pipes[1] );
- proc_close( $proc );
- fclose( $tempFile );
- return $result;
- }
- protected function alternatingAligned( $expectedStr, $actualStr ) {
- $expectedLines = explode( "\n", $expectedStr );
- $actualLines = explode( "\n", $actualStr );
- $maxLines = max( count( $expectedLines ), count( $actualLines ) );
- $result = '';
- for ( $i = 0; $i < $maxLines; $i++ ) {
- if ( $i < count( $expectedLines ) ) {
- $expectedLine = $expectedLines[$i];
- $expectedChunks = str_split( $expectedLine, $this->termWidth - 3 );
- } else {
- $expectedChunks = [];
- }
- if ( $i < count( $actualLines ) ) {
- $actualLine = $actualLines[$i];
- $actualChunks = str_split( $actualLine, $this->termWidth - 3 );
- } else {
- $actualChunks = [];
- }
- $maxChunks = max( count( $expectedChunks ), count( $actualChunks ) );
- for ( $j = 0; $j < $maxChunks; $j++ ) {
- if ( isset( $expectedChunks[$j] ) ) {
- $result .= "E: " . $expectedChunks[$j];
- if ( $j === count( $expectedChunks ) - 1 ) {
- $result .= "¶";
- }
- $result .= "\n";
- } else {
- $result .= "E:\n";
- }
- $result .= "\33[4m" . // underline
- "A: ";
- if ( isset( $actualChunks[$j] ) ) {
- $result .= $actualChunks[$j];
- if ( $j === count( $actualChunks ) - 1 ) {
- $result .= "¶";
- }
- }
- $result .= "\33[0m\n"; // reset
- }
- }
- return $result;
- }
- protected function reload( $testInfo ) {
- global $argv;
- pcntl_exec( PHP_BINARY, [
- $argv[0],
- '--session-data',
- json_encode( [
- 'startFile' => $testInfo['file'],
- 'startTest' => $testInfo['desc']
- ] + $this->session ) ] );
- print "pcntl_exec() failed\n";
- return false;
- }
- protected function findTest( $file, $testInfo ) {
- $initialPart = '';
- for ( $i = 1; $i < $testInfo['line']; $i++ ) {
- $line = fgets( $file );
- if ( $line === false ) {
- print "Error reading from file\n";
- return false;
- }
- $initialPart .= $line;
- }
- $line = fgets( $file );
- if ( !preg_match( '/^!!\s*test/', $line ) ) {
- print "Test has moved, cannot edit\n";
- return false;
- }
- $testPart = $line;
- $desc = fgets( $file );
- if ( trim( $desc ) !== $testInfo['desc'] ) {
- print "Description does not match, cannot edit\n";
- return false;
- }
- $testPart .= $desc;
- return [ $initialPart, $testPart ];
- }
- protected function getOutputFileName( $inputFileName ) {
- if ( is_writable( $inputFileName ) ) {
- $outputFileName = $inputFileName;
- } else {
- $outputFileName = wfTempDir() . '/' . basename( $inputFileName );
- print "Cannot write to input file, writing to $outputFileName instead\n";
- }
- return $outputFileName;
- }
- protected function editTest( $fileName, $deletions, $changes ) {
- $text = file_get_contents( $fileName );
- if ( $text === false ) {
- print "Unable to open test file!";
- return false;
- }
- $result = TestFileEditor::edit( $text, $deletions, $changes,
- function ( $msg ) {
- print "$msg\n";
- }
- );
- if ( is_writable( $fileName ) ) {
- file_put_contents( $fileName, $result );
- print "Wrote updated file\n";
- } else {
- print "Cannot write updated file, here is a patch you can paste:\n\n";
- print "--- {$fileName}\n" .
- "+++ {$fileName}~\n" .
- $this->unifiedDiff( $text, $result ) .
- "\n";
- }
- }
- protected function update( $testInfo, $result ) {
- $this->editTest( $testInfo['file'],
- [], // deletions
- [ // changes
- $testInfo['test'] => [
- $testInfo['resultSection'] => [
- 'op' => 'update',
- 'value' => $result->actual . "\n"
- ]
- ]
- ]
- );
- }
- protected function deleteTest( $testInfo ) {
- $this->editTest( $testInfo['file'],
- [ $testInfo['test'] ], // deletions
- [] // changes
- );
- }
- protected function switchTidy( $testInfo ) {
- $resultSection = $testInfo['resultSection'];
- if ( in_array( $resultSection, [ 'html/php', 'html/*', 'html', 'result' ] ) ) {
- $newSection = 'html+tidy';
- } elseif ( in_array( $resultSection, [ 'html/php+tidy', 'html+tidy' ] ) ) {
- $newSection = 'html';
- } else {
- print "Unrecognised result section name \"$resultSection\"";
- return;
- }
- $this->editTest( $testInfo['file'],
- [], // deletions
- [ // changes
- $testInfo['test'] => [
- $resultSection => [
- 'op' => 'rename',
- 'value' => $newSection
- ]
- ]
- ]
- );
- }
- protected function deleteSubtest( $testInfo ) {
- $this->editTest( $testInfo['file'],
- [], // deletions
- [ // changes
- $testInfo['test'] => [
- $testInfo['resultSection'] => [
- 'op' => 'delete'
- ]
- ]
- ]
- );
- }
- }
- $maintClass = 'ParserEditTests';
- require RUN_MAINTENANCE_IF_MAIN;
|