term = $term; $options += [ 'showDiffs' => true, 'showProgress' => true, 'showFailure' => true, 'showOutput' => false, 'useDwdiff' => false, 'markWhitespace' => false, ]; $this->showDiffs = $options['showDiffs']; $this->showProgress = $options['showProgress']; $this->showFailure = $options['showFailure']; $this->showOutput = $options['showOutput']; $this->useDwdiff = $options['useDwdiff']; $this->markWhitespace = $options['markWhitespace']; } public function start() { $this->total = 0; $this->success = 0; $this->skipped = 0; } public function startTest( $test ) { if ( $this->showProgress ) { $this->showTesting( $test['desc'] ); } } private function showTesting( $desc ) { print "Running test $desc... "; } /** * Show "Reading tests from ..." * * @param string $path */ public function startSuite( $path ) { print $this->term->color( 1 ) . "Running parser tests from \"$path\"..." . $this->term->reset() . "\n"; } public function endSuite( $path ) { print "\n"; } public function record( $test, ParserTestResult $result ) { $this->total++; $this->success += ( $result->isSuccess() ? 1 : 0 ); if ( $result->isSuccess() ) { $this->showSuccess( $result ); } else { $this->showFailure( $result ); } } /** * Print a happy success message. * * @param ParserTestResult $testResult * @return bool */ private function showSuccess( ParserTestResult $testResult ) { if ( $this->showProgress ) { print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; } } /** * Print a failure message and provide some explanatory output * about what went wrong if so configured. * * @param ParserTestResult $testResult * @return bool */ private function showFailure( ParserTestResult $testResult ) { if ( $this->showFailure ) { if ( !$this->showProgress ) { # In quiet mode we didn't show the 'Testing' message before the # test, in case it succeeded. Show it now: $this->showTesting( $testResult->getDescription() ); } print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; if ( $this->showOutput ) { print "--- Expected ---\n{$testResult->expected}\n"; print "--- Actual ---\n{$testResult->actual}\n"; } if ( $this->showDiffs ) { print $this->quickDiff( $testResult->expected, $testResult->actual ); if ( !$this->wellFormed( $testResult->actual ) ) { print "XML error: $this->xmlError\n"; } } } return false; } /** * Run given strings through a diff and return the (colorized) output. * Requires writable /tmp directory and a 'diff' command in the PATH. * * @param string $input * @param string $output * @param string $inFileTail Tailing for the input file name * @param string $outFileTail Tailing for the output file name * @return string */ private function quickDiff( $input, $output, $inFileTail = 'expected', $outFileTail = 'actual' ) { if ( $this->markWhitespace ) { $pairs = [ "\n" => '¶', ' ' => '·', "\t" => '→' ]; $input = strtr( $input, $pairs ); $output = strtr( $output, $pairs ); } # Windows, or at least the fc utility, is retarded $slash = wfIsWindows() ? '\\' : '/'; $prefix = wfTempDir() . "{$slash}mwParser-" . mt_rand(); $infile = "$prefix-$inFileTail"; $this->dumpToFile( $input, $infile ); $outfile = "$prefix-$outFileTail"; $this->dumpToFile( $output, $outfile ); $shellInfile = wfEscapeShellArg( $infile ); $shellOutfile = wfEscapeShellArg( $outfile ); global $wgDiff3; // we assume that people with diff3 also have usual diff if ( $this->useDwdiff ) { $shellCommand = 'dwdiff -Pc'; } else { $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au'; } $diff = wfShellExec( "$shellCommand $shellInfile $shellOutfile" ); unlink( $infile ); unlink( $outfile ); if ( $this->useDwdiff ) { return $diff; } else { return $this->colorDiff( $diff ); } } /** * Write the given string to a file, adding a final newline. * * @param string $data * @param string $filename */ private function dumpToFile( $data, $filename ) { $file = fopen( $filename, "wt" ); fwrite( $file, $data . "\n" ); fclose( $file ); } /** * Colorize unified diff output if set for ANSI color output. * Subtractions are colored blue, additions red. * * @param string $text * @return string */ private function colorDiff( $text ) { return preg_replace( [ '/^(-.*)$/m', '/^(\+.*)$/m' ], [ $this->term->color( 34 ) . '$1' . $this->term->reset(), $this->term->color( 31 ) . '$1' . $this->term->reset() ], $text ); } private function wellFormed( $text ) { $html = Sanitizer::hackDocType() . '' . $text . ''; $parser = xml_parser_create( "UTF-8" ); # case folding violates XML standard, turn it off xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); if ( !xml_parse( $parser, $html, true ) ) { $err = xml_error_string( xml_get_error_code( $parser ) ); $position = xml_get_current_byte_index( $parser ); $fragment = $this->extractFragment( $html, $position ); $this->xmlError = "$err at byte $position:\n$fragment"; xml_parser_free( $parser ); return false; } xml_parser_free( $parser ); return true; } private function extractFragment( $text, $position ) { $start = max( 0, $position - 10 ); $before = $position - $start; $fragment = '...' . $this->term->color( 34 ) . substr( $text, $start, $before ) . $this->term->color( 0 ) . $this->term->color( 31 ) . $this->term->color( 1 ) . substr( $text, $position, 1 ) . $this->term->color( 0 ) . $this->term->color( 34 ) . substr( $text, $position + 1, 9 ) . $this->term->color( 0 ) . '...'; $display = str_replace( "\n", ' ', $fragment ); $caret = ' ' . str_repeat( ' ', $before ) . $this->term->color( 31 ) . '^' . $this->term->color( 0 ); return "$display\n$caret"; } /** * Show a warning to the user * @param string $message */ public function warning( $message ) { echo "$message\n"; } /** * Mark a test skipped * @param string $test * @param string $subtest */ public function skipped( $test, $subtest ) { if ( $this->showProgress ) { print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n"; } $this->skipped++; } public function report() { if ( $this->total > 0 ) { $this->reportPercentage( $this->success, $this->total ); } else { print $this->term->color( 31 ) . "No tests found." . $this->term->reset() . "\n"; } } private function reportPercentage( $success, $total ) { $ratio = wfPercent( 100 * $success / $total ); print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)"; if ( $this->skipped ) { print ", skipped {$this->skipped}"; } print "... "; if ( $success == $total ) { print $this->term->color( 32 ) . "ALL TESTS PASSED!"; } else { $failed = $total - $success; print $this->term->color( 31 ) . "$failed tests failed!"; } print $this->term->reset() . "\n"; return ( $success == $total ); } }