Update suppressWarning()/restoreWarning() calls
[lhc/web/wiklou.git] / tests / parser / fuzzTest.php
1 <?php
2
3 use Wikimedia\ScopedCallback;
4
5 require __DIR__ . '/../../maintenance/Maintenance.php';
6
7 // Make RequestContext::resetMain() happy
8 define( 'MW_PARSER_TEST', 1 );
9
10 class ParserFuzzTest extends Maintenance {
11 private $parserTest;
12 private $maxFuzzTestLength = 300;
13 private $memoryLimit = 100;
14 private $seed;
15
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 );
22
23 $this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
24 }
25
26 function finalSetup() {
27 self::requireTestsAutoloader();
28 TestSetup::applyInitialConfig();
29 }
30
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 );
38 }
39
40 /**
41 * Run a fuzz test series
42 * Draw input from a set of test files
43 * @param array $filenames
44 */
45 function fuzzTest( $filenames ) {
46 $dict = $this->getFuzzInput( $filenames );
47 $dictSize = strlen( $dict );
48 $logMaxLength = log( $this->maxFuzzTestLength );
49
50 $teardown = $this->parserTest->staticSetup();
51 $teardown = $this->parserTest->setupDatabase( $teardown );
52 $teardown = $this->parserTest->setupUploads( $teardown );
53
54 $fakeTest = [
55 'test' => '',
56 'desc' => '',
57 'input' => '',
58 'result' => '',
59 'options' => '',
60 'config' => ''
61 ];
62
63 ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
64
65 $numTotal = 0;
66 $numSuccess = 0;
67 $user = new User;
68 $opts = ParserOptions::newFromUser( $user );
69 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
70
71 while ( true ) {
72 // Generate test input
73 mt_srand( ++$this->seed );
74 $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
75 $input = '';
76
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 );
82 }
83
84 $perTestTeardown = $this->parserTest->perTestSetup( $fakeTest );
85 $parser = $this->parserTest->getParser();
86
87 // Run the test
88 try {
89 $parser->parse( $input, $title, $opts );
90 $fail = false;
91 } catch ( Exception $exception ) {
92 $fail = true;
93 }
94
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++;
102 }
103
104 $numTotal++;
105 ScopedCallback::consume( $perTestTeardown );
106
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();
113
114 foreach ( $memStats as $name => $usage ) {
115 echo "$name: $usage\n";
116 }
117 if ( function_exists( 'hphpd_break' ) ) {
118 hphpd_break();
119 }
120 return;
121 }
122 }
123 }
124 }
125
126 /**
127 * Get a memory usage breakdown
128 * @return array
129 */
130 function getMemoryBreakdown() {
131 $memStats = [];
132
133 foreach ( $GLOBALS as $name => $value ) {
134 $memStats['$' . $name] = $this->guessVarSize( $value );
135 }
136
137 $classes = get_declared_classes();
138
139 foreach ( $classes as $class ) {
140 $rc = new ReflectionClass( $class );
141 $props = $rc->getStaticProperties();
142 $memStats[$class] = $this->guessVarSize( $props );
143 $methods = $rc->getMethods();
144
145 foreach ( $methods as $method ) {
146 $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
147 }
148 }
149
150 $functions = get_defined_functions();
151
152 foreach ( $functions['user'] as $function ) {
153 $rf = new ReflectionFunction( $function );
154 $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
155 }
156
157 asort( $memStats );
158
159 return $memStats;
160 }
161
162 /**
163 * Estimate the size of the input variable
164 */
165 function guessVarSize( $var ) {
166 $length = 0;
167 try {
168 Wikimedia\suppressWarnings();
169 $length = strlen( serialize( $var ) );
170 Wikimedia\restoreWarnings();
171 } catch ( Exception $e ) {
172 }
173 return $length;
174 }
175
176 /**
177 * Get an input dictionary from a set of parser test files
178 * @param array $filenames
179 * @return string
180 */
181 function getFuzzInput( $filenames ) {
182 $dict = '';
183
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
190 );
191
192 foreach ( $matches[1] as $match ) {
193 $dict .= $match . "\n";
194 }
195 }
196
197 return $dict;
198 }
199 }
200
201 $maintClass = 'ParserFuzzTest';
202 require RUN_MAINTENANCE_IF_MAIN;