Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
CompareParsers
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 6
342
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 checkOptions
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 conclusions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 stripParameters
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 processRevision
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
42
 checkParserLocally
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Take page text out of an XML dump file and render basic HTML out to files.
4 * This is *NOT* suitable for publishing or offline use; it's intended for
5 * running comparative tests of parsing behavior using real-world data.
6 *
7 * Templates etc are pulled from the local wiki database, not from the dump.
8 *
9 * Copyright © 2011 Platonides
10 * https://www.mediawiki.org/
11 *
12 * @license GPL-2.0-or-later
13 * @file
14 * @ingroup Maintenance
15 */
16
17use MediaWiki\Content\WikitextContent;
18use MediaWiki\Parser\ParserOptions;
19use MediaWiki\User\User;
20use Wikimedia\Diff\Diff;
21use Wikimedia\Diff\UnifiedDiffFormatter;
22
23// @codeCoverageIgnoreStart
24require_once __DIR__ . '/dumpIterator.php';
25// @codeCoverageIgnoreEnd
26
27/**
28 * Maintenance script to take page text out of an XML dump file and render
29 * basic HTML out to files.
30 *
31 * @ingroup Maintenance
32 */
33class CompareParsers extends DumpIterator {
34
35    /** @var int */
36    private $count = 0;
37    /** @var string|false */
38    private $saveFailed = false;
39    /** @var bool */
40    private $stripParametersEnabled;
41    /** @var bool */
42    private $showParsedOutput;
43    /** @var bool */
44    private $showDiff;
45    /** @var ParserOptions */
46    private $options;
47    /** @var int */
48    private $failed;
49
50    public function __construct() {
51        parent::__construct();
52        $this->addDescription( 'Run a file or dump with several parsers' );
53        $this->addOption( 'parser1', 'The first parser to compare.', true, true );
54        $this->addOption( 'parser2', 'The second parser to compare.', true, true );
55        $this->addOption(
56            'save-failed',
57            'Folder in which articles which differ will be stored.',
58            false,
59            true
60        );
61        $this->addOption( 'show-diff', 'Show a diff of the two renderings.', false, false );
62        $this->addOption(
63            'diff-bin',
64            'Binary to use for diffing (can also be provided by DIFF env var).',
65            false,
66            false
67        );
68        $this->addOption(
69            'strip-parameters',
70            'Remove parameters of html tags to increase readability.',
71            false,
72            false
73        );
74        $this->addOption(
75            'show-parsed-output',
76            'Show the parsed html if both Parsers give the same output.',
77            false,
78            false
79        );
80    }
81
82    public function checkOptions() {
83        if ( $this->hasOption( 'save-failed' ) ) {
84            $this->saveFailed = $this->getOption( 'save-failed' );
85        }
86
87        $this->stripParametersEnabled = $this->hasOption( 'strip-parameters' );
88        $this->showParsedOutput = $this->hasOption( 'show-parsed-output' );
89
90        $this->showDiff = $this->hasOption( 'show-diff' );
91        if ( $this->showDiff ) {
92            $bin = $this->getOption( 'diff-bin', getenv( 'DIFF' ) );
93            if ( $bin != '' ) {
94                global $wgDiff;
95                $wgDiff = $bin;
96            }
97        }
98
99        $user = new User();
100        $this->options = ParserOptions::newFromUser( $user );
101
102        $this->failed = 0;
103    }
104
105    public function conclusions() {
106        $this->error( "{$this->failed} failed revisions out of {$this->count}" );
107        if ( $this->count > 0 ) {
108            $this->output( " (" . ( $this->failed / $this->count ) . "%)\n" );
109        }
110    }
111
112    private function stripParameters( string $text ): string {
113        if ( !$this->stripParametersEnabled ) {
114            return $text;
115        }
116
117        return preg_replace( '/(<a) [^>]+>/', '$1>', $text );
118    }
119
120    /**
121     * Callback function for each revision, parse with both parsers and compare
122     */
123    public function processRevision( WikiRevision $rev ) {
124        $title = $rev->getTitle();
125
126        $parser1Name = $this->getOption( 'parser1' );
127        $parser2Name = $this->getOption( 'parser2' );
128
129        self::checkParserLocally( $parser1Name );
130        self::checkParserLocally( $parser2Name );
131
132        $parser1 = new $parser1Name();
133        $parser2 = new $parser2Name();
134
135        $content = $rev->getContent();
136
137        if ( $content->getModel() !== CONTENT_MODEL_WIKITEXT ) {
138            $this->error( "Page {$title->getPrefixedText()} does not contain wikitext "
139                . "but {$content->getModel()}\n" );
140
141            return;
142        }
143
144        /** @var WikitextContent $content */
145        '@phan-var WikitextContent $content';
146        $text = strval( $content->getText() );
147
148        $output1 = $parser1->parse( $text, $title, $this->options );
149        $output2 = $parser2->parse( $text, $title, $this->options );
150
151        if ( $output1->getText() != $output2->getText() ) {
152            $this->failed++;
153            $this->error( "Parsing for {$title->getPrefixedText()} differs\n" );
154
155            if ( $this->saveFailed ) {
156                file_put_contents(
157                    $this->saveFailed . '/' . rawurlencode( $title->getPrefixedText() ) . ".txt",
158                    $text
159                );
160            }
161            if ( $this->showDiff ) {
162                $diffs = new Diff(
163                    explode( "\n", $this->stripParameters( $output1->getText() ) ),
164                    explode( "\n", $this->stripParameters( $output2->getText() ) )
165                );
166                $formatter = new UnifiedDiffFormatter();
167                $unifiedDiff = $formatter->format( $diffs );
168
169                $this->output( $unifiedDiff );
170            }
171        } else {
172            $this->output( $title->getPrefixedText() . "\tOK\n" );
173
174            if ( $this->showParsedOutput ) {
175                $this->output( $this->stripParameters( $output1->getText() ) );
176            }
177        }
178    }
179
180    private static function checkParserLocally( string $parserName ) {
181        /* Look for the parser in a file appropriately named in the current folder */
182        if ( !class_exists( $parserName ) && file_exists( "$parserName.php" ) ) {
183            global $wgAutoloadClasses;
184            $wgAutoloadClasses[$parserName] = realpath( '.' ) . "/$parserName.php";
185        }
186    }
187}
188
189// @codeCoverageIgnoreStart
190$maintClass = CompareParsers::class;
191require_once RUN_MAINTENANCE_IF_MAIN;
192// @codeCoverageIgnoreEnd