Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 163
PPFuzzTester
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 4
306
0.00% covered (danger)
0.00%
0 / 66
 execute
0.00% covered (danger)
0.00%
0 / 1
156
0.00% covered (danger)
0.00%
0 / 49
 makeInputText
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 12
 makeTitle
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 2
 pickEntryPoint
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 3
PPFuzzTest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 4
132
0.00% covered (danger)
0.00%
0 / 65
 __construct
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 9
 templateHook
0.00% covered (danger)
0.00%
0 / 1
30
0.00% covered (danger)
0.00%
0 / 21
 execute
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 19
 getReport
0.00% covered (danger)
0.00%
0 / 1
20
0.00% covered (danger)
0.00%
0 / 16
PPFuzzUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 2
30
0.00% covered (danger)
0.00%
0 / 14
 load
0.00% covered (danger)
0.00%
0 / 1
6
0.00% covered (danger)
0.00%
0 / 6
 getOption
0.00% covered (danger)
0.00%
0 / 1
12
0.00% covered (danger)
0.00%
0 / 8
<?php
/**
 * Performs fuzz-style testing of MediaWiki's preprocessor.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 * @ingroup Maintenance
 */
use MediaWiki\MediaWikiServices;
use Wikimedia\TestingAccessWrapper;
$optionsWithoutArgs = [ 'verbose' ];
require_once __DIR__ . '/commandLine.inc';
$wgHooks['BeforeParserFetchTemplateAndtitle'][] = 'PPFuzzTester::templateHook';
class PPFuzzTester {
    public $hairs = [
        '[[', ']]', '{{', '{{', '}}', '}}', '{{{', '}}}',
        '<', '>', '<nowiki', '<gallery', '</nowiki>', '</gallery>', '<nOwIkI>', '</NoWiKi>',
        '<!--', '-->',
        "\n==", "==\n",
        '|', '=', "\n", ' ', "\t", "\x7f",
        '~~', '~~~', '~~~~', 'subst:',
        'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
        'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
        // extensions
        // '<ref>', '</ref>', '<references/>',
    ];
    public $minLength = 0;
    public $maxLength = 20;
    public $maxTemplates = 5;
    // public $outputTypes = [ 'OT_HTML', 'OT_WIKI', 'OT_PREPROCESS' ];
    public $entryPoints = [ 'fuzzTestSrvus', 'fuzzTestPst', 'fuzzTestPreprocess' ];
    public $verbose = false;
    /**
     * @var bool|PPFuzzTest
     */
    private static $currentTest = false;
    public function execute() {
        if ( !file_exists( 'results' ) ) {
            mkdir( 'results' );
        }
        if ( !is_dir( 'results' ) ) {
            echo "Unable to create 'results' directory\n";
            exit( 1 );
        }
        $overallStart = microtime( true );
        $reportInterval = 1000;
        // @phan-suppress-next-line PhanInfiniteLoop
        for ( $i = 1; true; $i++ ) {
            $t = -microtime( true );
            try {
                self::$currentTest = new PPFuzzTest( $this );
                self::$currentTest->execute();
                $passed = 'passed';
            } catch ( Exception $e ) {
                $testReport = self::$currentTest->getReport();
                $exceptionReport = $e instanceof MWException ? $e->getText() : (string)$e;
                $hash = md5( $testReport );
                file_put_contents( "results/ppft-$hash.in", serialize( self::$currentTest ) );
                file_put_contents( "results/ppft-$hash.fail",
                    "Input:\n$testReport\n\nException report:\n$exceptionReport\n" );
                print "Test $hash failed\n";
                $passed = 'failed';
            }
            $t += microtime( true );
            if ( $this->verbose ) {
                printf( "Test $passed in %.3f seconds\n", $t );
                print self::$currentTest->getReport();
            }
            $reportMetric = ( microtime( true ) - $overallStart ) / $i * $reportInterval;
            if ( $reportMetric > 25 ) {
                if ( substr( $reportInterval, 0, 1 ) === '1' ) {
                    $reportInterval /= 2;
                } else {
                    $reportInterval /= 5;
                }
            } elseif ( $reportMetric < 4 ) {
                if ( substr( $reportInterval, 0, 1 ) === '1' ) {
                    $reportInterval *= 5;
                } else {
                    $reportInterval *= 2;
                }
            }
            if ( $i % $reportInterval == 0 ) {
                print "$i tests done\n";
                /*
                $testReport = self::$currentTest->getReport();
                $filename = 'results/ppft-' . md5( $testReport ) . '.pass';
                file_put_contents( $filename, "Input:\n$testReport\n" );*/
            }
        }
    }
    public function makeInputText( $max = false ) {
        if ( $max === false ) {
            $max = $this->maxLength;
        }
        $length = mt_rand( $this->minLength, $max );
        $s = '';
        for ( $i = 0; $i < $length; $i++ ) {
            $hairIndex = mt_rand( 0, count( $this->hairs ) - 1 );
            $s .= $this->hairs[$hairIndex];
        }
        // Send through the UTF-8 normaliser
        // This resolves a few differences between the old preprocessor and the
        // XML-based one, which doesn't like illegals and converts line endings.
        // It's done by the MW UI, so it's a reasonably legitimate thing to do.
        $s = MediaWikiServices::getInstance()->getContentLanguage()->normalize( $s );
        return $s;
    }
    public function makeTitle() {
        return Title::newFromText( mt_rand( 0, 1000000 ), mt_rand( 0, 10 ) );
    }
    /*
    public function pickOutputType() {
        $count = count( $this->outputTypes );
        return $this->outputTypes[ mt_rand( 0, $count - 1 ) ];
    }*/
    public function pickEntryPoint() {
        $count = count( $this->entryPoints );
        return $this->entryPoints[mt_rand( 0, $count - 1 )];
    }
}
class PPFuzzTest {
    /**
     * @var array[]
     * @phan-var array<string,array{text:string|false,finalTitle:Title}>
     */
    public $templates;
    public $mainText, $title, $entryPoint, $output;
    /** @var PPFuzzTester */
    private $parent;
    /** @var string */
    public $nickname;
    /** @var bool */
    public $fancySig;
    /**
     * @param PPFuzzTester $tester
     */
    public function __construct( $tester ) {
        global $wgMaxSigChars;
        $this->parent = $tester;
        $this->mainText = $tester->makeInputText();
        $this->title = $tester->makeTitle();
        // $this->outputType = $tester->pickOutputType();
        $this->entryPoint = $tester->pickEntryPoint();
        $this->nickname = $tester->makeInputText( $wgMaxSigChars + 10 );
        $this->fancySig = (bool)mt_rand( 0, 1 );
        $this->templates = [];
    }
    /**
     * @param Title $title
     * @return array
     */
    public function templateHook( $title ) {
        $titleText = $title->getPrefixedDBkey();
        if ( !isset( $this->templates[$titleText] ) ) {
            $finalTitle = $title;
            if ( count( $this->templates ) >= $this->parent->maxTemplates ) {
                // Too many templates
                $text = false;
            } else {
                if ( !mt_rand( 0, 1 ) ) {
                    // Redirect
                    $finalTitle = $this->parent->makeTitle();
                }
                if ( !mt_rand( 0, 5 ) ) {
                    // Doesn't exist
                    $text = false;
                } else {
                    $text = $this->parent->makeInputText();
                }
            }
            $this->templates[$titleText] = [
                'text' => $text,
                'finalTitle' => $finalTitle ];
        }
        return $this->templates[$titleText];
    }
    public function execute() {
        global $wgUser;
        $user = new PPFuzzUser;
        $user->mName = 'Fuzz';
        $user->mFrom = 'name';
        $user->ppfz_test = $this;
        $wgUser = $user;
        $options = ParserOptions::newFromUser( $user );
        $options->setTemplateCallback( [ $this, 'templateHook' ] );
        $options->setTimestamp( wfTimestampNow() );
        $this->output = call_user_func(
            [ TestingAccessWrapper::newFromObject(
                MediaWikiServices::getInstance()->getParser()
            ), $this->entryPoint ],
            $this->mainText,
            $this->title,
            $options
        );
        return $this->output;
    }
    public function getReport() {
        $s = "Title: " . $this->title->getPrefixedDBkey() . "\n" .
            // "Output type: {$this->outputType}\n" .
            "Entry point: {$this->entryPoint}\n" .
            "User: " . ( $this->fancySig ? 'fancy' : 'no-fancy' ) .
            ' ' . var_export( $this->nickname, true ) . "\n" .
            "Main text: " . var_export( $this->mainText, true ) . "\n";
        foreach ( $this->templates as $titleText => $template ) {
            $finalTitle = $template['finalTitle'];
            if ( $finalTitle != $titleText ) {
                $s .= "[[$titleText]] -> [[$finalTitle]]: " . var_export( $template['text'], true ) . "\n";
            } else {
                $s .= "[[$titleText]]: " . var_export( $template['text'], true ) . "\n";
            }
        }
        $s .= "Output: " . var_export( $this->output, true ) . "\n";
        return $s;
    }
}
class PPFuzzUser extends User {
    public $ppfz_test, $mDataLoaded;
    public function load( $flags = null ) {
        if ( $this->mDataLoaded ) {
            return;
        }
        $this->mDataLoaded = true;
        $this->loadDefaults( $this->mName );
    }
    public function getOption( $oname, $defaultOverride = null, $ignoreHidden = false ) {
        if ( $oname === 'fancysig' ) {
            return $this->ppfz_test->fancySig;
        } elseif ( $oname === 'nickname' ) {
            return $this->ppfz_test->nickname;
        } else {
            return parent::getOption( $oname, $defaultOverride, $ignoreHidden );
        }
    }
}
ini_set( 'memory_limit', '50M' );
if ( isset( $args[0] ) ) {
    $testText = file_get_contents( $args[0] );
    if ( !$testText ) {
        print "File not found\n";
        exit( 1 );
    }
    $test = unserialize( $testText );
    $result = $test->execute();
    print "Test passed.\n";
} else {
    $tester = new PPFuzzTester;
    $tester->verbose = isset( $options['verbose'] );
    $tester->execute();
}