Follows-up I361fde0de7f4406bce6ed075ed397effa5be3359. Per T253461, not mass-changing source code, but the use of the native error silencing operator (@) is especially useful in tests because: 1. It requires any/all statements to be explicitly marked. The suppressWarnings/restoreWarnings sections encourage developers to be "lazy" and thus encapsulate more than needed if there are multiple ones near each other, which would ignore potentially important warnings in a test case, which is generally exactly the time when it is really useful to get warnings etc. 2. It avoids leaking state, for example in LBFactoryTest the assertFalse call would throw a PHPUnit assertion error (not meant to be caught by the local catch), and thus won't reach AtEase::restoreWarnings. This then causes later code to end up in a mismatching state and creates a confusing error_reporting state. See .phpcs.xml, where the at operator is allowed for all test code. Change-Id: I68d1725d685e0a7586468bc9de6dc29ceea31b8a
200 lines
5 KiB
PHP
200 lines
5 KiB
PHP
<?php
|
|
|
|
use MediaWiki\Settings\SettingsBuilder;
|
|
use Wikimedia\ScopedCallback;
|
|
|
|
require_once __DIR__ . '/../../maintenance/Maintenance.php';
|
|
|
|
class ParserFuzzTest extends Maintenance {
|
|
private $parserTest;
|
|
private $maxFuzzTestLength = 300;
|
|
private $memoryLimit = 100;
|
|
private $seed;
|
|
|
|
public function __construct() {
|
|
parent::__construct();
|
|
$this->addDescription( 'Run a fuzz test on the parser, until it segfaults ' .
|
|
'or throws an exception' );
|
|
$this->addOption( 'file', 'Use the specified file as a dictionary, ' .
|
|
' or leave blank to use parserTests.txt', false, true, true );
|
|
|
|
$this->addOption( 'seed', 'Start the fuzz test from the specified seed', false, true );
|
|
}
|
|
|
|
public function finalSetup( SettingsBuilder $settingsBuilder = null ) {
|
|
// Make RequestContext::resetMain() happy
|
|
define( 'MW_PARSER_TEST', 1 );
|
|
|
|
self::requireTestsAutoloader();
|
|
TestSetup::applyInitialConfig();
|
|
}
|
|
|
|
public function execute() {
|
|
$files = $this->getOption( 'file', [ __DIR__ . '/parserTests.txt' ] );
|
|
$this->seed = intval( $this->getOption( 'seed', 1 ) ) - 1;
|
|
$this->parserTest = new ParserTestRunner(
|
|
new MultiTestRecorder,
|
|
[] );
|
|
$this->fuzzTest( $files );
|
|
}
|
|
|
|
/**
|
|
* Run a fuzz test series
|
|
* Draw input from a set of test files
|
|
* @param array $filenames
|
|
*/
|
|
public function fuzzTest( $filenames ) {
|
|
$dict = $this->getFuzzInput( $filenames );
|
|
$dictSize = strlen( $dict );
|
|
$logMaxLength = log( $this->maxFuzzTestLength );
|
|
|
|
$teardown = $this->parserTest->staticSetup();
|
|
$teardown = $this->parserTest->setupDatabase( $teardown );
|
|
$teardown = $this->parserTest->setupUploads( $teardown );
|
|
|
|
$fakeTest = [
|
|
'test' => '',
|
|
'desc' => '',
|
|
'input' => '',
|
|
'result' => '',
|
|
'options' => '',
|
|
'config' => ''
|
|
];
|
|
|
|
ini_set( 'memory_limit', $this->memoryLimit * 1048576 * 2 );
|
|
|
|
$numTotal = 0;
|
|
$numSuccess = 0;
|
|
$user = new User;
|
|
$opts = ParserOptions::newFromUser( $user );
|
|
$title = Title::makeTitle( NS_MAIN, 'Parser_test' );
|
|
|
|
while ( true ) {
|
|
// Generate test input
|
|
mt_srand( ++$this->seed );
|
|
$totalLength = mt_rand( 1, $this->maxFuzzTestLength );
|
|
$input = '';
|
|
|
|
while ( strlen( $input ) < $totalLength ) {
|
|
$logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
|
|
$hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
|
|
$offset = mt_rand( 0, $dictSize - $hairLength );
|
|
$input .= substr( $dict, $offset, $hairLength );
|
|
}
|
|
|
|
$perTestTeardown = $this->parserTest->perTestSetup( $fakeTest );
|
|
$parser = $this->parserTest->getParser();
|
|
|
|
// Run the test
|
|
try {
|
|
$parser->parse( $input, $title, $opts );
|
|
$fail = false;
|
|
} catch ( Exception $exception ) {
|
|
$fail = true;
|
|
}
|
|
|
|
if ( $fail ) {
|
|
echo "Test failed with seed {$this->seed}\n";
|
|
echo "Input:\n";
|
|
printf( "string(%d) \"%s\"\n\n", strlen( $input ), $input );
|
|
echo "$exception\n";
|
|
} else {
|
|
$numSuccess++;
|
|
}
|
|
|
|
$numTotal++;
|
|
ScopedCallback::consume( $perTestTeardown );
|
|
|
|
if ( $numTotal % 100 == 0 ) {
|
|
$usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
|
|
echo "{$this->seed}: $numSuccess/$numTotal (mem: $usage%)\n";
|
|
if ( $usage >= 100 ) {
|
|
echo "Out of memory:\n";
|
|
$memStats = $this->getMemoryBreakdown();
|
|
|
|
foreach ( $memStats as $name => $usage ) {
|
|
echo "$name: $usage\n";
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a memory usage breakdown
|
|
* @return array
|
|
*/
|
|
private function getMemoryBreakdown() {
|
|
$memStats = [];
|
|
|
|
foreach ( $GLOBALS as $name => $value ) {
|
|
$memStats['$' . $name] = $this->guessVarSize( $value );
|
|
}
|
|
|
|
$classes = get_declared_classes();
|
|
|
|
foreach ( $classes as $class ) {
|
|
$rc = new ReflectionClass( $class );
|
|
$props = $rc->getStaticProperties();
|
|
$memStats[$class] = $this->guessVarSize( $props );
|
|
$methods = $rc->getMethods();
|
|
|
|
foreach ( $methods as $method ) {
|
|
$memStats[$class] += $this->guessVarSize( $method->getStaticVariables() );
|
|
}
|
|
}
|
|
|
|
$functions = get_defined_functions();
|
|
|
|
foreach ( $functions['user'] as $function ) {
|
|
$rf = new ReflectionFunction( $function );
|
|
$memStats["$function()"] = $this->guessVarSize( $rf->getStaticVariables() );
|
|
}
|
|
|
|
asort( $memStats );
|
|
|
|
return $memStats;
|
|
}
|
|
|
|
/**
|
|
* Estimate the size of the input variable
|
|
* @param mixed $var
|
|
* @return int
|
|
*/
|
|
public function guessVarSize( $var ) {
|
|
$length = 0;
|
|
try {
|
|
$length = strlen( @serialize( $var ) );
|
|
} catch ( Exception $e ) {
|
|
}
|
|
return $length;
|
|
}
|
|
|
|
/**
|
|
* Get an input dictionary from a set of parser test files
|
|
* @param array $filenames
|
|
* @return string
|
|
*/
|
|
public function getFuzzInput( $filenames ) {
|
|
$dict = '';
|
|
|
|
foreach ( $filenames as $filename ) {
|
|
$contents = file_get_contents( $filename );
|
|
preg_match_all(
|
|
'/!!\s*(input|wikitext)\n(.*?)\n!!\s*(result|html|html\/\*|html\/php)/s',
|
|
$contents,
|
|
$matches
|
|
);
|
|
|
|
foreach ( $matches[1] as $match ) {
|
|
$dict .= $match . "\n";
|
|
}
|
|
}
|
|
|
|
return $dict;
|
|
}
|
|
}
|
|
|
|
$maintClass = ParserFuzzTest::class;
|
|
require_once RUN_MAINTENANCE_IF_MAIN;
|