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
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
;
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
34 class ParserTestPrinter
extends TestRecorder
{
41 /** @var AnsiTermColorer|DummyTermColorer */
46 private $showProgress;
54 private $markWhitespace;
58 public function __construct( $term, $options ) {
62 'showProgress' => true,
63 'showFailure' => true,
64 'showOutput' => 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() {
82 public function startTest( ParserTest
$test, ParserTestMode
$mode ) {
83 if ( $this->showProgress
) {
84 $fake = new ParserTestResult( $test, $mode, '', '' );
85 $this->showTesting( $fake->getDescription() );
90 * @param string $desc Test description
92 private function showTesting( string $desc ) {
93 self
::print( "Running test $desc... " );
97 * Show "Reading tests from ..."
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() .
109 public function endSuite( string $path ) {
113 public function record( ParserTestResult
$result ): void
{
115 $this->success +
= ( $result->isSuccess() ?
1 : 0 );
117 if ( $result->isSuccess() ) {
118 $this->showSuccess();
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 ) {
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
186 private function quickDiff( $input, $output,
187 $inFileTail = 'expected', $outFileTail = 'actual'
189 if ( $this->markWhitespace
) {
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 );
206 // we assume that people with diff3 also have usual diff
207 if ( $this->useDwdiff
) {
208 $shellCommand = 'dwdiff -Pc';
210 $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ?
'fc' : 'diff -au';
213 $result = Shell
::command()
214 ->unsafeParams( $shellCommand )
215 ->params( $infile, $outfile )
217 $diff = $result->getStdout();
222 if ( $this->useDwdiff
) {
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" );
242 * Colorize unified diff output if set for ANSI color output.
243 * Subtractions are colored blue, additions red.
245 * @param string $text
248 private function colorDiff( $text ) {
250 [ '/^(-.*)$/m', '/^(\+.*)$/m' ],
251 [ $this->term
->color( '34' ) . '$1' . $this->term
->reset(),
252 $this->term
->color( '31' ) . '$1' . $this->term
->reset() ],
256 private function wellFormed( $text ) {
258 Sanitizer
::hackDocType() .
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 );
278 xml_parser_free( $parser );
283 private function extractFragment( $text, $position ) {
284 $start = max( 0, $position - 10 );
285 $before = $position - $start;
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' ) .
298 $display = str_replace( "\n", ' ', $fragment );
300 str_repeat( ' ', $before ) .
301 $this->term
->color( '31' ) .
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 ) {
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";
329 public function report() {
330 if ( $this->total
> 0 ) {
331 $this->reportPercentage( $this->success
, $this->total
);
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}" );
350 if ( $success == $total ) {
351 print $this->term
->color( '32' ) . "ALL TESTS PASSED!";
353 $failed = $total - $success;
354 self
::print( $this->term
->color( '31' ) . "$failed tests failed!" );
357 print $this->term
->reset() . "\n";
359 return ( $success == $total );