Merge "Revert "Clean up user handling in UploadStash""
[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 require_once __DIR__ . '/../TestsAutoLoader.php';
26 }
27
28 function execute() {
29 $files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
30 $this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
31 $this->parserTest = new ParserTest;
32 $this->fuzzTest( $files );
33 }
34
35 /**
36 * Run a fuzz test series
37 * Draw input from a set of test files
38 * @param array $filenames
39 */
40 function fuzzTest( $filenames ) {
41 $GLOBALS['wgContLang'] = Language::factory( 'en' );
42 $dict = $this->getFuzzInput( $filenames );
43 $dictSize = strlen( $dict );
44 $logMaxLength = log( $this->maxFuzzTestLength );
45 $this->parserTest->setupDatabase();
46 ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
47
48 $numTotal = 0;
49 $numSuccess = 0;
50 $user = new User;
51 $opts = ParserOptions::newFromUser( $user );
52 $title = Title::makeTitle( NS_MAIN, 'Parser_test' );
53
54 while ( true ) {
55 // Generate test input
56 mt_srand( ++$this->seed );
57 $totalLength = mt_rand( 1, $this->maxFuzzTestLength );
58 $input = '';
59
60 while ( strlen( $input ) < $totalLength ) {
61 $logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
62 $hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
63 $offset = mt_rand( 0, $dictSize - $hairLength );
64 $input .= substr( $dict, $offset, $hairLength );
65 }
66
67 $this->parserTest->setupGlobals();
68 $parser = $this->parserTest->getParser();
69
70 // Run the test
71 try {
72 $parser->parse( $input, $title, $opts );
73 $fail = false;
74 } catch ( Exception $exception ) {
75 $fail = true;
76 }
77
78 if ( $fail ) {
79 echo "Test failed with seed {$this->seed}\n";
80 echo "Input:\n";
81 printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
82 echo "$exception\n";
83 } else {
84 $numSuccess++;
85 }
86
87 $numTotal++;
88 $this->parserTest->teardownGlobals();
89 $parser->__destruct();
90
91 if ( $numTotal % 100 == 0 ) {
92 $usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
93 echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
94 if ( $usage >= 100 ) {
95 echo "Out of memory:\n";
96 $memStats = $this->getMemoryBreakdown();
97
98 foreach ( $memStats as $name => $usage ) {
99 echo "$name: $usage\n";
100 }
101 if ( function_exists( 'hphpd_break' ) ) {
102 hphpd_break();
103 }
104 return;
105 }
106 }
107 }
108 }
109
110 /**
111 * Get a memory usage breakdown
112 * @return array
113 */
114 function getMemoryBreakdown() {
115 $memStats = [];
116
117 foreach ( $GLOBALS as $name => $value ) {
118 $memStats['$' . $name] = $this->guessVarSize( $value );
119 }
120
121 $classes = get_declared_classes();
122
123 foreach ( $classes as $class ) {
124 $rc = new ReflectionClass( $class );
125 $props = $rc->getStaticProperties();
126 $memStats[$class] = $this->guessVarSize( $props );
127 $methods = $rc->getMethods();
128
129 foreach ( $methods as $method ) {
130 $memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
131 }
132 }
133
134 $functions = get_defined_functions();
135
136 foreach ( $functions['user'] as $function ) {
137 $rf = new ReflectionFunction( $function );
138 $memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
139 }
140
141 asort( $memStats );
142
143 return $memStats;
144 }
145
146 /**
147 * Estimate the size of the input variable
148 */
149 function guessVarSize( $var ) {
150 $length = 0;
151 try {
152 MediaWiki\suppressWarnings();
153 $length = strlen( serialize( $var ) );
154 MediaWiki\restoreWarnings();
155 } catch ( Exception $e ) {
156 }
157 return $length;
158 }
159
160 /**
161 * Get an input dictionary from a set of parser test files
162 * @param array $filenames
163 * @return string
164 */
165 function getFuzzInput( $filenames ) {
166 $dict = '';
167
168 foreach ( $filenames as $filename ) {
169 $contents = file_get_contents( $filename );
170 preg_match_all(
171 '/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
172 $contents,
173 $matches
174 );
175
176 foreach ( $matches[1] as $match ) {
177 $dict .= $match . "\n";
178 }
179 }
180
181 return $dict;
182 }
183 }
184
185 $maintClass = 'ParserFuzzTest';
186 require RUN_MAINTENANCE_IF_MAIN;