Index: trunk/phase3/tests/phpunit/includes/parser/NewParserTest.php |
— | — | @@ -16,6 +16,11 @@ |
17 | 17 | public $hooks = array(); |
18 | 18 | public $functionHooks = array(); |
19 | 19 | |
| 20 | + //Fuzz test |
| 21 | + public $maxFuzzTestLength = 300; |
| 22 | + public $fuzzSeed = 0; |
| 23 | + public $memoryLimit = 50; |
| 24 | + |
20 | 25 | function setUp() { |
21 | 26 | global $wgContLang; |
22 | 27 | $wgContLang = Language::factory( 'en' ); |
— | — | @@ -311,12 +316,145 @@ |
312 | 317 | } |
313 | 318 | |
314 | 319 | } |
| 320 | + |
| 321 | + } |
| 322 | + |
| 323 | + /** |
| 324 | + * Run a fuzz test series |
| 325 | + * Draw input from a set of test files |
| 326 | + */ |
| 327 | + function testFuzzTests() { |
315 | 328 | |
| 329 | + global $wgParserTestFiles; |
| 330 | + |
| 331 | + $files = $wgParserTestFiles; |
| 332 | + |
| 333 | + if( $this->getCliArg( 'file=' ) ) { |
| 334 | + $files = array( $this->getCliArg( 'file=' ) ); |
| 335 | + } |
| 336 | + |
| 337 | + $dict = $this->getFuzzInput( $files ); |
| 338 | + $dictSize = strlen( $dict ); |
| 339 | + $logMaxLength = log( $this->maxFuzzTestLength ); |
| 340 | + |
| 341 | + ini_set( 'memory_limit', $this->memoryLimit * 1048576 ); |
| 342 | + |
| 343 | + $user = new User; |
| 344 | + $opts = ParserOptions::newFromUser( $user ); |
| 345 | + $title = Title::makeTitle( NS_MAIN, 'Parser_test' ); |
| 346 | + |
| 347 | + $id = 1; |
| 348 | + |
| 349 | + while ( true ) { |
316 | 350 | |
| 351 | + // Generate test input |
| 352 | + mt_srand( ++$this->fuzzSeed ); |
| 353 | + $totalLength = mt_rand( 1, $this->maxFuzzTestLength ); |
| 354 | + $input = ''; |
| 355 | + |
| 356 | + while ( strlen( $input ) < $totalLength ) { |
| 357 | + $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength; |
| 358 | + $hairLength = min( intval( exp( $logHairLength ) ), $dictSize ); |
| 359 | + $offset = mt_rand( 0, $dictSize - $hairLength ); |
| 360 | + $input .= substr( $dict, $offset, $hairLength ); |
| 361 | + } |
| 362 | + |
| 363 | + $this->setupGlobals(); |
| 364 | + $parser = $this->getParser(); |
| 365 | + |
| 366 | + // Run the test |
| 367 | + try { |
| 368 | + $parser->parse( $input, $title, $opts ); |
| 369 | + $this->assertTrue( true, "Test $id, fuzz seed {$this->fuzzSeed}" ); |
| 370 | + } catch ( Exception $exception ) { |
| 371 | + |
| 372 | + ob_start(); |
| 373 | + var_dump( $input ); |
| 374 | + $input_dump = ob_get_contents(); |
| 375 | + ob_end_clean(); |
| 376 | + |
| 377 | + $this->assertTrue( false, "Test $id, fuzz seed {$this->fuzzSeed}. \n\nInput: $input_dump\n\nError: {$exception->getMessage()}\n\nBacktrace: {$exception->getTraceAsString()}" ); |
| 378 | + } |
| 379 | + |
| 380 | + $this->teardownGlobals(); |
| 381 | + $parser->__destruct(); |
| 382 | + |
| 383 | + if ( $id % 100 == 0 ) { |
| 384 | + $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 ); |
| 385 | + //echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n"; |
| 386 | + if ( $usage > 90 ) { |
| 387 | + $ret = "Out of memory:\n"; |
| 388 | + $memStats = $this->getMemoryBreakdown(); |
| 389 | + |
| 390 | + foreach ( $memStats as $name => $usage ) { |
| 391 | + $ret .= "$name: $usage\n"; |
| 392 | + } |
| 393 | + |
| 394 | + throw new MWException( $ret ); |
| 395 | + return; |
| 396 | + } |
| 397 | + } |
| 398 | + |
| 399 | + $id++; |
| 400 | + |
| 401 | + } |
317 | 402 | } |
318 | 403 | |
| 404 | + /** |
| 405 | + * Get an input dictionary from a set of parser test files |
| 406 | + */ |
| 407 | + function getFuzzInput( $filenames ) { |
| 408 | + $dict = ''; |
| 409 | + |
| 410 | + foreach ( $filenames as $filename ) { |
| 411 | + $contents = file_get_contents( $filename ); |
| 412 | + preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches ); |
| 413 | + |
| 414 | + foreach ( $matches[1] as $match ) { |
| 415 | + $dict .= $match . "\n"; |
| 416 | + } |
| 417 | + } |
| 418 | + |
| 419 | + return $dict; |
| 420 | + } |
319 | 421 | |
320 | 422 | /** |
| 423 | + * Get a memory usage breakdown |
| 424 | + */ |
| 425 | + function getMemoryBreakdown() { |
| 426 | + $memStats = array(); |
| 427 | + |
| 428 | + foreach ( $GLOBALS as $name => $value ) { |
| 429 | + $memStats['$' . $name] = strlen( serialize( $value ) ); |
| 430 | + } |
| 431 | + |
| 432 | + $classes = get_declared_classes(); |
| 433 | + |
| 434 | + foreach ( $classes as $class ) { |
| 435 | + $rc = new ReflectionClass( $class ); |
| 436 | + $props = $rc->getStaticProperties(); |
| 437 | + $memStats[$class] = strlen( serialize( $props ) ); |
| 438 | + $methods = $rc->getMethods(); |
| 439 | + |
| 440 | + foreach ( $methods as $method ) { |
| 441 | + $memStats[$class] += strlen( serialize( $method->getStaticVariables() ) ); |
| 442 | + } |
| 443 | + } |
| 444 | + |
| 445 | + $functions = get_defined_functions(); |
| 446 | + |
| 447 | + foreach ( $functions['user'] as $function ) { |
| 448 | + $rf = new ReflectionFunction( $function ); |
| 449 | + $memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) ); |
| 450 | + } |
| 451 | + |
| 452 | + asort( $memStats ); |
| 453 | + |
| 454 | + return $memStats; |
| 455 | + } |
| 456 | + |
| 457 | + |
| 458 | + /** |
321 | 459 | * Run a given wikitext input through a freshly-constructed wiki parser, |
322 | 460 | * and compare the output against the expected results. |
323 | 461 | * Prints status and explanatory messages to stdout. |