Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / parser / ParserTestPrinter.php
blobffe4c0fc4db8e6f138c94a24d50e6cc7502a636b
1 <?php
2 /**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
18 * @file
19 * @ingroup Testing
22 use MediaWiki\Parser\Sanitizer;
23 use MediaWiki\Shell\Shell;
24 use MediaWiki\Tests\AnsiTermColorer;
25 use MediaWiki\Tests\DummyTermColorer;
26 use Wikimedia\Parsoid\ParserTests\Test as ParserTest;
27 use Wikimedia\Parsoid\ParserTests\TestMode as ParserTestMode;
29 /**
30 * This is a TestRecorder responsible for printing information about progress,
31 * success and failure to the console. It is specific to the parserTests.php
32 * frontend.
34 class ParserTestPrinter extends TestRecorder {
35 /** @var int */
36 private $total;
37 /** @var int */
38 private $success;
39 /** @var int */
40 private $skipped;
41 /** @var AnsiTermColorer|DummyTermColorer */
42 private $term;
43 /** @var bool */
44 private $showDiffs;
45 /** @var bool */
46 private $showProgress;
47 /** @var bool */
48 private $showFailure;
49 /** @var bool */
50 private $showOutput;
51 /** @var bool */
52 private $useDwdiff;
53 /** @var bool */
54 private $markWhitespace;
55 /** @var string */
56 private $xmlError;
58 public function __construct( $term, $options ) {
59 $this->term = $term;
60 $options += [
61 'showDiffs' => true,
62 'showProgress' => true,
63 'showFailure' => true,
64 'showOutput' => false,
65 'useDwdiff' => false,
66 'markWhitespace' => false,
68 $this->showDiffs = $options['showDiffs'];
69 $this->showProgress = $options['showProgress'];
70 $this->showFailure = $options['showFailure'];
71 $this->showOutput = $options['showOutput'];
72 $this->useDwdiff = $options['useDwdiff'];
73 $this->markWhitespace = $options['markWhitespace'];
76 public function start() {
77 $this->total = 0;
78 $this->success = 0;
79 $this->skipped = 0;
82 public function startTest( ParserTest $test, ParserTestMode $mode ) {
83 if ( $this->showProgress ) {
84 $fake = new ParserTestResult( $test, $mode, '', '' );
85 $this->showTesting( $fake->getDescription() );
89 /**
90 * @param string $desc Test description
92 private function showTesting( string $desc ) {
93 self::print( "Running test $desc... " );
96 /**
97 * Show "Reading tests from ..."
99 * @param string $path
100 * @param-taint $path none
102 public function startSuite( string $path ) {
103 print $this->term->color( 1 ) .
104 "Running parser tests from \"$path\"..." .
105 $this->term->reset() .
106 "\n";
109 public function endSuite( string $path ) {
110 print "\n";
113 public function record( ParserTestResult $result ): void {
114 $this->total++;
115 $this->success += ( $result->isSuccess() ? 1 : 0 );
117 if ( $result->isSuccess() ) {
118 $this->showSuccess();
119 } else {
120 $this->showFailure( $result );
125 * Print a happy success message.
127 private function showSuccess(): void {
128 if ( $this->showProgress ) {
129 print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n";
134 * Helper function to ensure the phan SecurityCheckPlugin does not register
135 * false "XSS" positives on parser test output, as this is a CLI tool.
137 * @param string $str Output string
138 * @param-taint $str none
140 private static function print( string $str ) {
141 print $str;
145 * Print a failure message and provide some explanatory output
146 * about what went wrong if so configured.
148 * @param ParserTestResult $testResult
150 private function showFailure( ParserTestResult $testResult ): void {
151 if ( $this->showFailure ) {
152 if ( !$this->showProgress ) {
153 # In quiet mode we didn't show the 'Testing' message before the
154 # test, in case it succeeded. Show it now:
155 $this->showTesting( $testResult->getDescription() );
158 print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n";
160 print "{$testResult->test->filename}:{$testResult->test->lineNumStart}\n";
162 if ( $this->showOutput ) {
163 self::print( "--- Expected ---\n{$testResult->expected}\n" );
164 self::print( "--- Actual ---\n{$testResult->actual}\n" );
167 if ( $this->showDiffs ) {
168 self::print( $this->quickDiff( $testResult->expected, $testResult->actual ) );
169 if ( !$this->wellFormed( $testResult->actual ) ) {
170 self::print( "XML error: $this->xmlError\n" );
177 * Run given strings through a diff and return the (colorized) output.
178 * Requires writable /tmp directory and a 'diff' command in the PATH.
180 * @param string $input
181 * @param string $output
182 * @param string $inFileTail Tailing for the input file name
183 * @param string $outFileTail Tailing for the output file name
184 * @return string
186 private function quickDiff( $input, $output,
187 $inFileTail = 'expected', $outFileTail = 'actual'
189 if ( $this->markWhitespace ) {
190 $pairs = [
191 "\n" => '¶',
192 ' ' => '·',
193 "\t" => '→'
195 $input = strtr( $input, $pairs );
196 $output = strtr( $output, $pairs );
199 $infile = tempnam( wfTempDir(), "mwParser-$inFileTail" );
200 $this->dumpToFile( $input, $infile );
202 $outfile = tempnam( wfTempDir(), "mwParser-$outFileTail" );
203 $this->dumpToFile( $output, $outfile );
205 global $wgDiff3;
206 // we assume that people with diff3 also have usual diff
207 if ( $this->useDwdiff ) {
208 $shellCommand = 'dwdiff -Pc';
209 } else {
210 $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au';
213 $result = Shell::command()
214 ->unsafeParams( $shellCommand )
215 ->params( $infile, $outfile )
216 ->execute();
217 $diff = $result->getStdout();
219 unlink( $infile );
220 unlink( $outfile );
222 if ( $this->useDwdiff ) {
223 return $diff;
224 } else {
225 return $this->colorDiff( $diff );
230 * Write the given string to a file, adding a final newline.
232 * @param string $data
233 * @param string $filename
235 private function dumpToFile( $data, $filename ) {
236 $file = fopen( $filename, "wt" );
237 fwrite( $file, $data . "\n" );
238 fclose( $file );
242 * Colorize unified diff output if set for ANSI color output.
243 * Subtractions are colored blue, additions red.
245 * @param string $text
246 * @return string
248 private function colorDiff( $text ) {
249 return preg_replace(
250 [ '/^(-.*)$/m', '/^(\+.*)$/m' ],
251 [ $this->term->color( '34' ) . '$1' . $this->term->reset(),
252 $this->term->color( '31' ) . '$1' . $this->term->reset() ],
253 $text );
256 private function wellFormed( $text ) {
257 $html =
258 Sanitizer::hackDocType() .
259 '<html>' .
260 $text .
261 '</html>';
263 $parser = xml_parser_create( "UTF-8" );
265 # case folding violates XML standard, turn it off
266 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, 0 );
268 if ( !xml_parse( $parser, $html, true ) ) {
269 $err = xml_error_string( xml_get_error_code( $parser ) );
270 $position = xml_get_current_byte_index( $parser );
271 $fragment = $this->extractFragment( $html, $position );
272 $this->xmlError = "$err at byte $position:\n$fragment";
273 xml_parser_free( $parser );
275 return false;
278 xml_parser_free( $parser );
280 return true;
283 private function extractFragment( $text, $position ) {
284 $start = max( 0, $position - 10 );
285 $before = $position - $start;
286 $fragment = '...' .
287 $this->term->color( '34' ) .
288 substr( $text, $start, $before ) .
289 $this->term->color( '0' ) .
290 $this->term->color( '31' ) .
291 $this->term->color( '1' ) .
292 substr( $text, $position, 1 ) .
293 $this->term->color( '0' ) .
294 $this->term->color( '34' ) .
295 substr( $text, $position + 1, 9 ) .
296 $this->term->color( '0' ) .
297 '...';
298 $display = str_replace( "\n", ' ', $fragment );
299 $caret = ' ' .
300 str_repeat( ' ', $before ) .
301 $this->term->color( '31' ) .
302 '^' .
303 $this->term->color( '0' );
305 return "$display\n$caret";
309 * Show a warning to the user
310 * @param string $message
312 public function warning( string $message ) {
313 echo "$message\n";
317 * Mark a test skipped
318 * @param ParserTest $test
319 * @param ParserTestMode $mode
320 * @param string $subtest
322 public function skipped( ParserTest $test, ParserTestMode $mode, string $subtest ) {
323 if ( $this->showProgress ) {
324 print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n";
326 $this->skipped++;
329 public function report() {
330 if ( $this->total > 0 ) {
331 $this->reportPercentage( $this->success, $this->total );
332 } else {
333 print $this->term->color( '31' ) . "No tests found." . $this->term->reset() . "\n";
338 * @param int $success Number of passed tests
339 * @param int $total Number of total tests
340 * @return bool True if and only if all tests passed
342 private function reportPercentage( $success, $total ) {
343 $ratio = wfPercent( 100 * $success / $total );
344 self::print( $this->term->color( '1' ) . "Passed $success of $total tests ($ratio)" );
345 if ( $this->skipped ) {
346 self::print( ", skipped {$this->skipped}" );
348 print "... ";
350 if ( $success == $total ) {
351 print $this->term->color( '32' ) . "ALL TESTS PASSED!";
352 } else {
353 $failed = $total - $success;
354 self::print( $this->term->color( '31' ) . "$failed tests failed!" );
357 print $this->term->reset() . "\n";
359 return ( $success == $total );