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. 7 * 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. 12 * 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 17 * 18 * @file 19 * @ingroup Testing 20 */ 21 22use MediaWiki\Shell\Shell; 23 24/** 25 * This is a TestRecorder responsible for printing information about progress, 26 * success and failure to the console. It is specific to the parserTests.php 27 * frontend. 28 */ 29class ParserTestPrinter extends TestRecorder { 30 private $total; 31 private $success; 32 private $skipped; 33 private $term; 34 private $showDiffs; 35 private $showProgress; 36 private $showFailure; 37 private $showOutput; 38 private $useDwdiff; 39 private $markWhitespace; 40 private $xmlError; 41 42 public function __construct( $term, $options ) { 43 $this->term = $term; 44 $options += [ 45 'showDiffs' => true, 46 'showProgress' => true, 47 'showFailure' => true, 48 'showOutput' => false, 49 'useDwdiff' => false, 50 'markWhitespace' => false, 51 ]; 52 $this->showDiffs = $options['showDiffs']; 53 $this->showProgress = $options['showProgress']; 54 $this->showFailure = $options['showFailure']; 55 $this->showOutput = $options['showOutput']; 56 $this->useDwdiff = $options['useDwdiff']; 57 $this->markWhitespace = $options['markWhitespace']; 58 } 59 60 public function start() { 61 $this->total = 0; 62 $this->success = 0; 63 $this->skipped = 0; 64 } 65 66 public function startTest( $test ) { 67 if ( $this->showProgress ) { 68 $this->showTesting( $test['desc'] ); 69 } 70 } 71 72 private function showTesting( $desc ) { 73 print "Running test $desc... "; 74 } 75 76 /** 77 * Show "Reading tests from ..." 78 * 79 * @param string $path 80 */ 81 public function startSuite( $path ) { 82 print $this->term->color( 1 ) . 83 "Running parser tests from \"$path\"..." . 84 $this->term->reset() . 85 "\n"; 86 } 87 88 public function endSuite( $path ) { 89 print "\n"; 90 } 91 92 public function record( $test, ParserTestResult $result ) { 93 $this->total++; 94 $this->success += ( $result->isSuccess() ? 1 : 0 ); 95 96 if ( $result->isSuccess() ) { 97 $this->showSuccess( $result ); 98 } else { 99 $this->showFailure( $result ); 100 } 101 } 102 103 /** 104 * Print a happy success message. 105 * 106 * @param ParserTestResult $testResult 107 */ 108 private function showSuccess( ParserTestResult $testResult ) { 109 if ( $this->showProgress ) { 110 print $this->term->color( '1;32' ) . 'PASSED' . $this->term->reset() . "\n"; 111 } 112 } 113 114 /** 115 * Print a failure message and provide some explanatory output 116 * about what went wrong if so configured. 117 * 118 * @param ParserTestResult $testResult 119 * @return bool 120 */ 121 private function showFailure( ParserTestResult $testResult ) { 122 if ( $this->showFailure ) { 123 if ( !$this->showProgress ) { 124 # In quiet mode we didn't show the 'Testing' message before the 125 # test, in case it succeeded. Show it now: 126 $this->showTesting( $testResult->getDescription() ); 127 } 128 129 print $this->term->color( '31' ) . 'FAILED!' . $this->term->reset() . "\n"; 130 131 print "{$testResult->test['file']}:{$testResult->test['line']}\n"; 132 133 if ( $this->showOutput ) { 134 print "--- Expected ---\n{$testResult->expected}\n"; 135 print "--- Actual ---\n{$testResult->actual}\n"; 136 } 137 138 if ( $this->showDiffs ) { 139 print $this->quickDiff( $testResult->expected, $testResult->actual ); 140 if ( !$this->wellFormed( $testResult->actual ) ) { 141 print "XML error: $this->xmlError\n"; 142 } 143 } 144 } 145 146 return false; 147 } 148 149 /** 150 * Run given strings through a diff and return the (colorized) output. 151 * Requires writable /tmp directory and a 'diff' command in the PATH. 152 * 153 * @param string $input 154 * @param string $output 155 * @param string $inFileTail Tailing for the input file name 156 * @param string $outFileTail Tailing for the output file name 157 * @return string 158 */ 159 private function quickDiff( $input, $output, 160 $inFileTail = 'expected', $outFileTail = 'actual' 161 ) { 162 if ( $this->markWhitespace ) { 163 $pairs = [ 164 "\n" => '¶', 165 ' ' => '·', 166 "\t" => '→' 167 ]; 168 $input = strtr( $input, $pairs ); 169 $output = strtr( $output, $pairs ); 170 } 171 172 $infile = tempnam( wfTempDir(), "mwParser-$inFileTail" ); 173 $this->dumpToFile( $input, $infile ); 174 175 $outfile = tempnam( wfTempDir(), "mwParser-$outFileTail" ); 176 $this->dumpToFile( $output, $outfile ); 177 178 global $wgDiff3; 179 // we assume that people with diff3 also have usual diff 180 if ( $this->useDwdiff ) { 181 $shellCommand = 'dwdiff -Pc'; 182 } else { 183 $shellCommand = ( wfIsWindows() && !$wgDiff3 ) ? 'fc' : 'diff -au'; 184 } 185 186 $result = Shell::command() 187 ->unsafeParams( $shellCommand ) 188 ->params( $infile, $outfile ) 189 ->execute(); 190 $diff = $result->getStdout(); 191 192 unlink( $infile ); 193 unlink( $outfile ); 194 195 if ( $this->useDwdiff ) { 196 return $diff; 197 } else { 198 return $this->colorDiff( $diff ); 199 } 200 } 201 202 /** 203 * Write the given string to a file, adding a final newline. 204 * 205 * @param string $data 206 * @param string $filename 207 */ 208 private function dumpToFile( $data, $filename ) { 209 $file = fopen( $filename, "wt" ); 210 fwrite( $file, $data . "\n" ); 211 fclose( $file ); 212 } 213 214 /** 215 * Colorize unified diff output if set for ANSI color output. 216 * Subtractions are colored blue, additions red. 217 * 218 * @param string $text 219 * @return string 220 */ 221 private function colorDiff( $text ) { 222 return preg_replace( 223 [ '/^(-.*)$/m', '/^(\+.*)$/m' ], 224 [ $this->term->color( 34 ) . '$1' . $this->term->reset(), 225 $this->term->color( 31 ) . '$1' . $this->term->reset() ], 226 $text ); 227 } 228 229 private function wellFormed( $text ) { 230 $html = 231 Sanitizer::hackDocType() . 232 '<html>' . 233 $text . 234 '</html>'; 235 236 $parser = xml_parser_create( "UTF-8" ); 237 238 # case folding violates XML standard, turn it off 239 xml_parser_set_option( $parser, XML_OPTION_CASE_FOLDING, false ); 240 241 if ( !xml_parse( $parser, $html, true ) ) { 242 $err = xml_error_string( xml_get_error_code( $parser ) ); 243 $position = xml_get_current_byte_index( $parser ); 244 $fragment = $this->extractFragment( $html, $position ); 245 $this->xmlError = "$err at byte $position:\n$fragment"; 246 xml_parser_free( $parser ); 247 248 return false; 249 } 250 251 xml_parser_free( $parser ); 252 253 return true; 254 } 255 256 private function extractFragment( $text, $position ) { 257 $start = max( 0, $position - 10 ); 258 $before = $position - $start; 259 $fragment = '...' . 260 $this->term->color( 34 ) . 261 substr( $text, $start, $before ) . 262 $this->term->color( 0 ) . 263 $this->term->color( 31 ) . 264 $this->term->color( 1 ) . 265 substr( $text, $position, 1 ) . 266 $this->term->color( 0 ) . 267 $this->term->color( 34 ) . 268 substr( $text, $position + 1, 9 ) . 269 $this->term->color( 0 ) . 270 '...'; 271 $display = str_replace( "\n", ' ', $fragment ); 272 $caret = ' ' . 273 str_repeat( ' ', $before ) . 274 $this->term->color( 31 ) . 275 '^' . 276 $this->term->color( 0 ); 277 278 return "$display\n$caret"; 279 } 280 281 /** 282 * Show a warning to the user 283 * @param string $message 284 */ 285 public function warning( $message ) { 286 echo "$message\n"; 287 } 288 289 /** 290 * Mark a test skipped 291 * @param string $test 292 * @param string $subtest 293 */ 294 public function skipped( $test, $subtest ) { 295 if ( $this->showProgress ) { 296 print $this->term->color( '1;33' ) . 'SKIPPED' . $this->term->reset() . "\n"; 297 } 298 $this->skipped++; 299 } 300 301 public function report() { 302 if ( $this->total > 0 ) { 303 $this->reportPercentage( $this->success, $this->total ); 304 } else { 305 print $this->term->color( 31 ) . "No tests found." . $this->term->reset() . "\n"; 306 } 307 } 308 309 private function reportPercentage( $success, $total ) { 310 $ratio = wfPercent( 100 * $success / $total ); 311 print $this->term->color( 1 ) . "Passed $success of $total tests ($ratio)"; 312 if ( $this->skipped ) { 313 print ", skipped {$this->skipped}"; 314 } 315 print "... "; 316 317 if ( $success == $total ) { 318 print $this->term->color( 32 ) . "ALL TESTS PASSED!"; 319 } else { 320 $failed = $total - $success; 321 print $this->term->color( 31 ) . "$failed tests failed!"; 322 } 323 324 print $this->term->reset() . "\n"; 325 326 return ( $success == $total ); 327 } 328} 329