fuzzTest.php 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. <?php
  2. use Wikimedia\ScopedCallback;
  3. require __DIR__ . '/../../maintenance/Maintenance.php';
  4. // Make RequestContext::resetMain() happy
  5. define( 'MW_PARSER_TEST', 1 );
  6. class ParserFuzzTest extends Maintenance {
  7. private $parserTest;
  8. private $maxFuzzTestLength = 300;
  9. private $memoryLimit = 100;
  10. private $seed;
  11. function __construct() {
  12. parent::__construct();
  13. $this->addDescription( 'Run a fuzz test on the parser, until it segfaults ' .
  14. 'or throws an exception' );
  15. $this->addOption( 'file', 'Use the specified file as a dictionary, ' .
  16. ' or leave blank to use parserTests.txt', false, true, true );
  17. $this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
  18. }
  19. function finalSetup() {
  20. self::requireTestsAutoloader();
  21. TestSetup::applyInitialConfig();
  22. }
  23. function execute() {
  24. $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
  25. $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
  26. $this->parserTest = new ParserTestRunner(
  27. new MultiTestRecorder,
  28. [] );
  29. $this->fuzzTest( $files );
  30. }
  31. /**
  32. * Run a fuzz test series
  33. * Draw input from a set of test files
  34. * @param array $filenames
  35. */
  36. function fuzzTest( $filenames ) {
  37. $dict = $this->getFuzzInput( $filenames );
  38. $dictSize = strlen( $dict );
  39. $logMaxLength = log( $this->maxFuzzTestLength );
  40. $teardown = $this->parserTest->staticSetup();
  41. $teardown = $this->parserTest->setupDatabase( $teardown );
  42. $teardown = $this->parserTest->setupUploads( $teardown );
  43. $fakeTest = [
  44. 'test' => '',
  45. 'desc' => '',
  46. 'input' => '',
  47. 'result' => '',
  48. 'options' => '',
  49. 'config' => ''
  50. ];
  51. ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
  52. $numTotal = 0;
  53. $numSuccess = 0;
  54. $user = new User;
  55. $opts = ParserOptions::newFromUser( $user );
  56. $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
  57. while ( true ) {
  58. // Generate test input
  59. mt_srand( ++$this->seed );
  60. $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
  61. $input = '';
  62. while ( strlen( $input ) < $totalLength ) {
  63. $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
  64. $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
  65. $offset = mt_rand( 0, $dictSize - $hairLength );
  66. $input .= substr( $dict, $offset, $hairLength );
  67. }
  68. $perTestTeardown = $this->parserTest->perTestSetup( $fakeTest );
  69. $parser = $this->parserTest->getParser();
  70. // Run the test
  71. try {
  72. $parser->parse( $input, $title, $opts );
  73. $fail = false;
  74. } catch ( Exception $exception ) {
  75. $fail = true;
  76. }
  77. if ( $fail ) {
  78. echo "Test failed with seed {$this->seed}\n";
  79. echo "Input:\n";
  80. printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
  81. echo "$exception\n";
  82. } else {
  83. $numSuccess++;
  84. }
  85. $numTotal++;
  86. ScopedCallback::consume( $perTestTeardown );
  87. if ( $numTotal % 100 == 0 ) {
  88. $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
  89. echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
  90. if ( $usage >= 100 ) {
  91. echo "Out of memory:\n";
  92. $memStats = $this->getMemoryBreakdown();
  93. foreach ( $memStats as $name => $usage ) {
  94. echo "$name: $usage\n";
  95. }
  96. if ( function_exists( 'hphpd_break' ) ) {
  97. hphpd_break();
  98. }
  99. return;
  100. }
  101. }
  102. }
  103. }
  104. /**
  105. * Get a memory usage breakdown
  106. * @return array
  107. */
  108. function getMemoryBreakdown() {
  109. $memStats = [];
  110. foreach ( $GLOBALS as $name => $value ) {
  111. $memStats['$' . $name] = $this->guessVarSize( $value );
  112. }
  113. $classes = get_declared_classes();
  114. foreach ( $classes as $class ) {
  115. $rc = new ReflectionClass( $class );
  116. $props = $rc->getStaticProperties();
  117. $memStats[$class] = $this->guessVarSize( $props );
  118. $methods = $rc->getMethods();
  119. foreach ( $methods as $method ) {
  120. $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
  121. }
  122. }
  123. $functions = get_defined_functions();
  124. foreach ( $functions['user'] as $function ) {
  125. $rf = new ReflectionFunction( $function );
  126. $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
  127. }
  128. asort( $memStats );
  129. return $memStats;
  130. }
  131. /**
  132. * Estimate the size of the input variable
  133. */
  134. function guessVarSize( $var ) {
  135. $length = 0;
  136. try {
  137. Wikimedia\suppressWarnings();
  138. $length = strlen( serialize( $var ) );
  139. Wikimedia\restoreWarnings();
  140. } catch ( Exception $e ) {
  141. }
  142. return $length;
  143. }
  144. /**
  145. * Get an input dictionary from a set of parser test files
  146. * @param array $filenames
  147. * @return string
  148. */
  149. function getFuzzInput( $filenames ) {
  150. $dict = '';
  151. foreach ( $filenames as $filename ) {
  152. $contents = file_get_contents( $filename );
  153. preg_match_all(
  154. '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
  155. $contents,
  156. $matches
  157. );
  158. foreach ( $matches[1] as $match ) {
  159. $dict .= $match . "\n";
  160. }
  161. }
  162. return $dict;
  163. }
  164. }
  165. $maintClass = 'ParserFuzzTest';
  166. require RUN_MAINTENANCE_IF_MAIN;