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