Merge "Added release notes for 'ContentHandler::runLegacyHooks' removal"
[mediawiki.git] / tests / parser / fuzzTest.php
blob9a2a9c9370840d7bbc2e8c90b46d35592bd3d986
1 <?php
3 use Wikimedia\ScopedCallback;
5 require __DIR__ . '/../../maintenance/Maintenance.php';
7 // Make RequestContext::resetMain() happy
8 define( 'MW_PARSER_TEST', 1 );
10 class ParserFuzzTest extends Maintenance {
11 private $parserTest;
12 private $maxFuzzTestLength = 300;
13 private $memoryLimit = 100;
14 private $seed;
16 function __construct() {
17 parent::__construct();
18 $this->addDescription( 'Run a fuzz test on the parser, until it segfaults ' .
19 'or throws an exception' );
20 $this->addOption( 'file', 'Use the specified file as a dictionary, ' .
21 ' or leave blank to use parserTests.txt', false, true, true );
23 $this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
26 function finalSetup() {
27 self::requireTestsAutoloader();
28 TestSetup::applyInitialConfig();
31 function execute() {
32 $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
33 $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
34 $this->parserTest = new ParserTestRunner(
35 new MultiTestRecorder,
36 [] );
37 $this->fuzzTest( $files );
40 /**
41 * Run a fuzz test series
42 * Draw input from a set of test files
43 * @param array $filenames
45 function fuzzTest( $filenames ) {
46 $dict = $this->getFuzzInput( $filenames );
47 $dictSize = strlen( $dict );
48 $logMaxLength = log( $this->maxFuzzTestLength );
50 $teardown = $this->parserTest->staticSetup();
51 $teardown = $this->parserTest->setupDatabase( $teardown );
52 $teardown = $this->parserTest->setupUploads( $teardown );
54 $fakeTest = [
55 'test' => '',
56 'desc' => '',
57 'input' => '',
58 'result' => '',
59 'options' => '',
60 'config' => ''
63 ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
65 $numTotal = 0;
66 $numSuccess = 0;
67 $user = new User;
68 $opts = ParserOptions::newFromUser( $user );
69 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
71 while ( true ) {
72 // Generate test input
73 mt_srand( ++$this->seed );
74 $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
75 $input = '';
77 while ( strlen( $input ) < $totalLength ) {
78 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
79 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
80 $offset = mt_rand( 0, $dictSize - $hairLength );
81 $input .= substr( $dict, $offset, $hairLength );
84 $perTestTeardown = $this->parserTest->perTestSetup( $fakeTest );
85 $parser = $this->parserTest->getParser();
87 // Run the test
88 try {
89 $parser->parse( $input, $title, $opts );
90 $fail = false;
91 } catch ( Exception $exception ) {
92 $fail = true;
95 if ( $fail ) {
96 echo "Test failed with seed {$this->seed}\n";
97 echo "Input:\n";
98 printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
99 echo "$exception\n";
100 } else {
101 $numSuccess++;
104 $numTotal++;
105 ScopedCallback::consume( $perTestTeardown );
107 if ( $numTotal % 100 == 0 ) {
108 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
109 echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
110 if ( $usage >= 100 ) {
111 echo "Out of memory:\n";
112 $memStats = $this->getMemoryBreakdown();
114 foreach ( $memStats as $name => $usage ) {
115 echo "$name: $usage\n";
117 if ( function_exists( 'hphpd_break' ) ) {
118 hphpd_break();
120 return;
127 * Get a memory usage breakdown
128 * @return array
130 function getMemoryBreakdown() {
131 $memStats = [];
133 foreach ( $GLOBALS as $name => $value ) {
134 $memStats['$' . $name] = $this->guessVarSize( $value );
137 $classes = get_declared_classes();
139 foreach ( $classes as $class ) {
140 $rc = new ReflectionClass( $class );
141 $props = $rc->getStaticProperties();
142 $memStats[$class] = $this->guessVarSize( $props );
143 $methods = $rc->getMethods();
145 foreach ( $methods as $method ) {
146 $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
150 $functions = get_defined_functions();
152 foreach ( $functions['user'] as $function ) {
153 $rf = new ReflectionFunction( $function );
154 $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
157 asort( $memStats );
159 return $memStats;
163 * Estimate the size of the input variable
165 function guessVarSize( $var ) {
166 $length = 0;
167 try {
168 MediaWiki\suppressWarnings();
169 $length = strlen( serialize( $var ) );
170 MediaWiki\restoreWarnings();
171 } catch ( Exception $e ) {
173 return $length;
177 * Get an input dictionary from a set of parser test files
178 * @param array $filenames
179 * @return string
181 function getFuzzInput( $filenames ) {
182 $dict = '';
184 foreach ( $filenames as $filename ) {
185 $contents = file_get_contents( $filename );
186 preg_match_all(
187 '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
188 $contents,
189 $matches
192 foreach ( $matches[1] as $match ) {
193 $dict .= $match . "\n";
197 return $dict;
201 $maintClass = 'ParserFuzzTest';
202 require RUN_MAINTENANCE_IF_MAIN;