Use parser test file parser from Parsoid
One (test file) parser to rule them all. Reduce a little bit of redundant code between core and Parsoid by using Parsoid's parser test file parser to run core's parser tests. This should have no effect on users of TestFileReader::read() *except* that Parsoid's test file reader is more strict about bogus lines in the test file, including duplicate test names, and we've removed support for the old v1 format (hard deprecated in 1.35). Next step will be to be able to execute parser tests on extensions using Parsoid's parser as well. Bug: T254181 Depends-On: I8ab4a8c59ed1b6837dba428f96a8ba0084b7fb68 Change-Id: I5acaf82819ae964895a831be4f28c31c77a09e84
This commit is contained in:
parent
a21774a721
commit
585cbcd77f
5 changed files with 76 additions and 414 deletions
|
|
@ -110,6 +110,8 @@ because of Phabricator reports.
|
|||
* The following User methods, deprecated and moved to BlockManager in 1.34,
|
||||
were removed: isDnsBlacklisted, inDnsBlacklist, isLocallyBlockedProxy,
|
||||
trackBlockWithCookie.
|
||||
* Support for v1 of the parser tests file format has been removed; it was
|
||||
deprecated in 1.35. (T174199)
|
||||
* …
|
||||
|
||||
=== Deprecations in 1.36 ===
|
||||
|
|
@ -134,6 +136,11 @@ because of Phabricator reports.
|
|||
=== Other changes in 1.36 ===
|
||||
* The 'tidy' key in ParserOptions (used in the parser cache) has been removed.
|
||||
It has had no effect since 1.35.
|
||||
* The implementation of TestFileReader::read has been changed to use
|
||||
Parsoid's parser test file parser. This should be compatible with
|
||||
existing code, but it only supports version 2 of the test file
|
||||
specification and may be more strict when parsing invalid input,
|
||||
including duplicate tests.
|
||||
* …
|
||||
|
||||
== Compatibility ==
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
"wikimedia/ip-utils": "1.0.0",
|
||||
"wikimedia/less.php": "3.0.0",
|
||||
"wikimedia/object-factory": "2.1.0",
|
||||
"wikimedia/parsoid": "^0.13.0@alpha",
|
||||
"wikimedia/parsoid": "^0.13.0-a4@alpha",
|
||||
"wikimedia/php-session-serializer": "1.0.7",
|
||||
"wikimedia/purtle": "1.0.7",
|
||||
"wikimedia/relpath": "2.1.1",
|
||||
|
|
|
|||
|
|
@ -820,7 +820,7 @@ class ParserTestRunner {
|
|||
*/
|
||||
public function runTest( $test ) {
|
||||
wfDebug( __METHOD__ . ": running {$test['desc']}" );
|
||||
$opts = $this->parseOptions( $test['options'] );
|
||||
$opts = $test['options'];
|
||||
if ( isset( $opts['preprocessor'] ) && $opts['preprocessor'] !== 'Preprocessor_Hash' ) {
|
||||
wfDeprecated( 'preprocessor=Preprocessor_DOM', '1.36' );
|
||||
return false; // Skip test.
|
||||
|
|
@ -974,106 +974,12 @@ class ParserTestRunner {
|
|||
return $opts[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the options string, return an associative array of options.
|
||||
* @todo Move this to TestFileReader
|
||||
*
|
||||
* @param string $instring
|
||||
* @return array
|
||||
*/
|
||||
private function parseOptions( $instring ) {
|
||||
$opts = [];
|
||||
// foo
|
||||
// foo=bar
|
||||
// foo="bar baz"
|
||||
// foo=[[bar baz]]
|
||||
// foo=bar,"baz quux"
|
||||
// foo={...json...}
|
||||
$defs = '(?(DEFINE)
|
||||
(?<qstr> # Quoted string
|
||||
"
|
||||
(?:[^\\\\"] | \\\\.)*
|
||||
"
|
||||
)
|
||||
(?<json>
|
||||
\{ # Open bracket
|
||||
(?:
|
||||
[^"{}] | # Not a quoted string or object, or
|
||||
(?&qstr) | # A quoted string, or
|
||||
(?&json) # A json object (recursively)
|
||||
)*
|
||||
\} # Close bracket
|
||||
)
|
||||
(?<value>
|
||||
(?:
|
||||
(?&qstr) # Quoted val
|
||||
|
|
||||
\[\[
|
||||
[^]]* # Link target
|
||||
\]\]
|
||||
|
|
||||
[\w-]+ # Plain word
|
||||
|
|
||||
(?&json) # JSON object
|
||||
)
|
||||
)
|
||||
)';
|
||||
$regex = '/' . $defs . '\b
|
||||
(?<k>[\w-]+) # Key
|
||||
\b
|
||||
(?:\s*
|
||||
= # First sub-value
|
||||
\s*
|
||||
(?<v>
|
||||
(?&value)
|
||||
(?:\s*
|
||||
, # Sub-vals 1..N
|
||||
\s*
|
||||
(?&value)
|
||||
)*
|
||||
)
|
||||
)?
|
||||
/x';
|
||||
$valueregex = '/' . $defs . '(?&value)/x';
|
||||
|
||||
if ( preg_match_all( $regex, $instring, $matches, PREG_SET_ORDER ) ) {
|
||||
foreach ( $matches as $bits ) {
|
||||
$key = strtolower( $bits['k'] );
|
||||
if ( !isset( $bits['v'] ) ) {
|
||||
$opts[$key] = true;
|
||||
} else {
|
||||
preg_match_all( $valueregex, $bits['v'], $vmatches );
|
||||
$opts[$key] = array_map( [ $this, 'cleanupOption' ], $vmatches[0] );
|
||||
if ( count( $opts[$key] ) == 1 ) {
|
||||
$opts[$key] = $opts[$key][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $opts;
|
||||
}
|
||||
|
||||
private function cleanupOption( $opt ) {
|
||||
if ( substr( $opt, 0, 1 ) == '"' ) {
|
||||
return stripcslashes( substr( $opt, 1, -1 ) );
|
||||
}
|
||||
|
||||
if ( substr( $opt, 0, 2 ) == '[[' ) {
|
||||
return substr( $opt, 2, -2 );
|
||||
}
|
||||
|
||||
if ( substr( $opt, 0, 1 ) == '{' ) {
|
||||
return FormatJson::decode( $opt, true );
|
||||
}
|
||||
return $opt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Do any required setup which is dependent on test options.
|
||||
*
|
||||
* @see staticSetup() for more information about setup/teardown
|
||||
*
|
||||
* @param array $test Test info supplied by TestFileReader
|
||||
* @param array|ParserTest $test Test info supplied by TestFileReader
|
||||
* @param callable|null $nextTeardown
|
||||
* @return ScopedCallback
|
||||
*/
|
||||
|
|
@ -1083,8 +989,8 @@ class ParserTestRunner {
|
|||
$this->checkSetupDone( 'setupDatabase', 'setDatabase' );
|
||||
$teardown[] = $this->markSetupDone( 'perTestSetup' );
|
||||
|
||||
$opts = $this->parseOptions( $test['options'] );
|
||||
$config = $test['config'];
|
||||
$opts = is_array( $test ) ? $test['options'] : $test->options;
|
||||
$config = is_array( $test ) ? $test['config'] : $test->config;
|
||||
|
||||
// Find out values for some special options.
|
||||
$langCode =
|
||||
|
|
|
|||
|
|
@ -19,343 +19,85 @@
|
|||
* @ingroup Testing
|
||||
*/
|
||||
|
||||
use Wikimedia\Parsoid\ParserTests\TestFileReader as ParsoidTestFileReader;
|
||||
|
||||
class TestFileReader {
|
||||
private $file;
|
||||
private $fh;
|
||||
private $section = null;
|
||||
/** String|null: current test section being analyzed */
|
||||
private $sectionData = [];
|
||||
private $sectionLineNum = [];
|
||||
private $lineNum = 0;
|
||||
private $runDisabled;
|
||||
private $regex;
|
||||
private $format = 1;
|
||||
|
||||
private $articles = [];
|
||||
private $requirements = [];
|
||||
private $tests = [];
|
||||
|
||||
/**
|
||||
* Read and parse the parser test file named by $file.
|
||||
* @internal
|
||||
* @param string $file
|
||||
* @param array $options
|
||||
* @return array
|
||||
*/
|
||||
public static function read( $file, array $options = [] ) {
|
||||
$reader = new self( $file, $options );
|
||||
$reader->execute();
|
||||
|
||||
$requirements = [];
|
||||
foreach ( $reader->requirements as $type => $reqsOfType ) {
|
||||
foreach ( $reqsOfType as $name => $unused ) {
|
||||
$requirements[] = [
|
||||
'type' => $type,
|
||||
'name' => $name
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'requirements' => $requirements,
|
||||
'tests' => $reader->tests,
|
||||
'articles' => $reader->articles
|
||||
];
|
||||
}
|
||||
|
||||
private function __construct( $file, $options ) {
|
||||
$this->file = $file;
|
||||
$this->fh = fopen( $this->file, "rt" );
|
||||
|
||||
if ( !$this->fh ) {
|
||||
throw new MWException( "Couldn't open file '$file'\n" );
|
||||
}
|
||||
|
||||
$options = $options + [
|
||||
'runDisabled' => false,
|
||||
'regex' => '//',
|
||||
'filter' => false,
|
||||
];
|
||||
if ( isset( $options['regex'] ) ) {
|
||||
$options['filter'] = [ 'regex' => $options['regex'] ];
|
||||
}
|
||||
$parsoidReader = ParsoidTestFileReader::read( $file, function ( $msg ) {
|
||||
wfDeprecatedMsg( $msg, '1.35', false, false );
|
||||
} );
|
||||
if ( $parsoidReader->testFormat < 2 ) {
|
||||
throw new MWException(
|
||||
"Support for the parserTest v1 file format was removed in MediaWiki 1.36"
|
||||
);
|
||||
}
|
||||
$tests = [];
|
||||
foreach ( $parsoidReader->testCases as $t ) {
|
||||
self::addTest( $tests, $t, $options );
|
||||
}
|
||||
$articles = [];
|
||||
foreach ( $parsoidReader->articles as $a ) {
|
||||
$articles[] = [
|
||||
'name' => $a->title,
|
||||
'text' => $a->text,
|
||||
'line' => $a->lineNumStart,
|
||||
'file' => $a->filename,
|
||||
];
|
||||
}
|
||||
return [
|
||||
'requirements' => $parsoidReader->requirements,
|
||||
'tests' => $tests,
|
||||
'articles' => $articles,
|
||||
];
|
||||
$this->runDisabled = $options['runDisabled'];
|
||||
$this->regex = $options['regex'];
|
||||
}
|
||||
|
||||
private function addCurrentTest() {
|
||||
// "input" and "result" are old section names allowed
|
||||
// for backwards-compatibility.
|
||||
$input = $this->checkSection(
|
||||
[ 'wikitext', 'input' ], false );
|
||||
$output = $this->checkSection(
|
||||
[ 'html/php', 'html/*', 'html', 'result' ], false );
|
||||
$tidySection = $this->checkSection(
|
||||
[ 'html/php+tidy', 'html+tidy' ], false );
|
||||
$nonTidySection = $this->checkSection(
|
||||
[ 'html/php+untidy', 'html+untidy' ], false );
|
||||
if ( $this->format < 2 ) {
|
||||
wfDeprecatedMsg(
|
||||
"The parserTest v1 file format was deprecated in MediaWiki 1.35 " .
|
||||
"(used in {$this->file})", '1.35', false, false );
|
||||
if ( $nonTidySection === false ) {
|
||||
// untidy by default
|
||||
$nonTidySection = $output;
|
||||
}
|
||||
} else {
|
||||
if ( $this->checkSection( [ 'input' ], false ) ) {
|
||||
wfDeprecatedMsg(
|
||||
"The input section in parserTest files was deprecated in MediaWiki 1.35 " .
|
||||
"(used in {$this->file})",
|
||||
'1.35', false, false );
|
||||
}
|
||||
if ( $this->checkSection( [ 'result' ], false ) ) {
|
||||
wfDeprecatedMsg(
|
||||
"The result section in parserTest files was deprecated in MediaWiki 1.35 " .
|
||||
"(used in {$this->file})",
|
||||
'1.35', false, false );
|
||||
}
|
||||
if ( $output && $tidySection ) {
|
||||
wfDeprecatedMsg(
|
||||
'The untidy section of parserTest files was deprecated in MediaWiki 1.35, ' .
|
||||
"it should be renamed at {$this->file}:{$this->sectionLineNum['test']}",
|
||||
'1.35', false, false
|
||||
);
|
||||
}
|
||||
if ( $tidySection === false ) {
|
||||
// tidy by default
|
||||
$tidySection = $output;
|
||||
}
|
||||
if ( $nonTidySection && !$tidySection ) {
|
||||
// Tests with a "without tidy" section but no "with tidy" section
|
||||
// are deprecated!
|
||||
wfDeprecatedMsg(
|
||||
'Parser tests with a "without tidy" section but no "with tidy" ' .
|
||||
' section were deprecated in MediaWiki 1.35. Found at ' .
|
||||
"{$this->file}:{$this->sectionLineNum['test']}",
|
||||
'1.35', false, false );
|
||||
}
|
||||
private static function addTest( array &$tests, Wikimedia\Parsoid\ParserTests\Test $t, array $options ) {
|
||||
if ( $t->wikitext === false ) {
|
||||
$t->error( "Test lacks wikitext section", $t->testName );
|
||||
}
|
||||
|
||||
// Remove trailing newline
|
||||
$data = array_map( 'ParserTestRunner::chomp', $this->sectionData );
|
||||
|
||||
// Apply defaults
|
||||
$data += [
|
||||
'options' => '',
|
||||
'config' => ''
|
||||
];
|
||||
|
||||
if ( $input === false ) {
|
||||
throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
|
||||
"lacks input section" );
|
||||
}
|
||||
if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) && !$this->runDisabled ) {
|
||||
if ( isset( $t->options['disabled'] ) && !$options['runDisabled'] ) {
|
||||
// Disabled
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $tidySection === false && $nonTidySection === false ) {
|
||||
if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) {
|
||||
if ( $t->legacyHtml === null ) {
|
||||
if ( isset( $t->sections['html/parsoid'] ) || isset( $t->sections['wikitext/edited'] ) ) {
|
||||
// Parsoid only
|
||||
return;
|
||||
} else {
|
||||
throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
|
||||
"lacks result section" );
|
||||
$t->error( "Test lacks html section", $t->testName );
|
||||
}
|
||||
}
|
||||
|
||||
if ( !preg_match( $this->regex, $data['test'] ) ) {
|
||||
if ( !$t->matchesFilter( $options['filter'] ) ) {
|
||||
// Filtered test
|
||||
return;
|
||||
}
|
||||
|
||||
$commonInfo = [
|
||||
'test' => $data['test'],
|
||||
'desc' => $data['test'],
|
||||
'input' => $data[$input],
|
||||
'options' => $data['options'],
|
||||
'config' => $data['config'],
|
||||
'line' => $this->sectionLineNum['test'],
|
||||
'file' => $this->file
|
||||
$tests[] = [
|
||||
'test' => $t->testName,
|
||||
'desc' => ( $t->comment ?? '' ) . $t->testName,
|
||||
'input' => $t->wikitext,
|
||||
'result' => $t->legacyHtml,
|
||||
'options' => $t->options,
|
||||
'config' => $t->config ?? '',
|
||||
'line' => $t->lineNumStart,
|
||||
'file' => $t->filename,
|
||||
];
|
||||
|
||||
if ( $nonTidySection !== false ) {
|
||||
if ( $tidySection !== false ) {
|
||||
// Add tidy subtest
|
||||
$this->tests[] = [
|
||||
'desc' => $data['test'] . ' (with tidy)',
|
||||
'result' => $data[$tidySection],
|
||||
'resultSection' => $tidySection,
|
||||
'options' => $data['options'] . ' tidy',
|
||||
'isSubtest' => true,
|
||||
] + $commonInfo;
|
||||
} else {
|
||||
// We can no longer run the non-tidy test, and we don't have
|
||||
// a tidy alternative.
|
||||
wfDeprecatedMsg( "Skipping non-tidy test {$data['test']} " .
|
||||
"since it was removed in MediaWiki 1.35, and there is no tidy subtest",
|
||||
'1.35', false, false );
|
||||
}
|
||||
} elseif ( $tidySection !== false ) {
|
||||
// No need to override desc when there is no subtest
|
||||
$this->tests[] = [
|
||||
'result' => $data[$tidySection],
|
||||
'resultSection' => $tidySection,
|
||||
'options' => $data['options'] . ' tidy'
|
||||
] + $commonInfo;
|
||||
} else {
|
||||
throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
|
||||
"lacks result section" );
|
||||
}
|
||||
}
|
||||
|
||||
private function execute() {
|
||||
while ( ( $line = fgets( $this->fh ) ) !== false ) {
|
||||
$this->lineNum++;
|
||||
$matches = [];
|
||||
|
||||
// Support test file format versioning.
|
||||
if ( $this->lineNum === 1 &&
|
||||
preg_match( '/^!!\s*VERSION\s+(\d+)/i', $line, $matches ) ) {
|
||||
$this->format = intval( $matches[1] );
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
|
||||
$this->section = strtolower( $matches[1] );
|
||||
|
||||
if ( $this->section == 'endarticle' ) {
|
||||
$this->checkSection( 'text' );
|
||||
$this->checkSection( 'article' );
|
||||
|
||||
$this->addArticle(
|
||||
ParserTestRunner::chomp( $this->sectionData['article'] ),
|
||||
$this->sectionData['text'], $this->lineNum );
|
||||
|
||||
$this->clearSection();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->section == 'endhooks' ) {
|
||||
$this->checkSection( 'hooks' );
|
||||
|
||||
foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
|
||||
$line = trim( $line );
|
||||
|
||||
if ( $line ) {
|
||||
$this->addRequirement( 'hook', $line );
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearSection();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->section == 'endfunctionhooks' ) {
|
||||
$this->checkSection( 'functionhooks' );
|
||||
|
||||
foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
|
||||
$line = trim( $line );
|
||||
|
||||
if ( $line ) {
|
||||
$this->addRequirement( 'functionHook', $line );
|
||||
}
|
||||
}
|
||||
|
||||
$this->clearSection();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->section == 'end' ) {
|
||||
$this->checkSection( 'test' );
|
||||
$this->addCurrentTest();
|
||||
$this->clearSection();
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( isset( $this->sectionData[$this->section] ) ) {
|
||||
throw new MWException( "duplicate section '$this->section' "
|
||||
. "at line {$this->lineNum} of $this->file\n" );
|
||||
}
|
||||
|
||||
$this->sectionLineNum[$this->section] = $this->lineNum;
|
||||
$this->sectionData[$this->section] = '';
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $this->section ) {
|
||||
$this->sectionData[$this->section] .= $line;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear section name and its data
|
||||
*/
|
||||
private function clearSection() {
|
||||
$this->sectionLineNum = [];
|
||||
$this->sectionData = [];
|
||||
$this->section = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the current section data has some value for the given token
|
||||
* name(s) (first parameter).
|
||||
* Throw an exception if it is not set, referencing current section
|
||||
* and adding the current file name and line number
|
||||
*
|
||||
* @param string|array $tokens Expected token(s) that should have been
|
||||
* mentioned before closing this section
|
||||
* @param bool $fatal True iff an exception should be thrown if
|
||||
* the section is not found.
|
||||
* @return bool|string
|
||||
* @throws MWException
|
||||
*/
|
||||
private function checkSection( $tokens, $fatal = true ) {
|
||||
if ( $this->section === null ) {
|
||||
throw new MWException( __METHOD__ . " can not verify a null section!\n" );
|
||||
}
|
||||
if ( !is_array( $tokens ) ) {
|
||||
$tokens = [ $tokens ];
|
||||
}
|
||||
if ( count( $tokens ) == 0 ) {
|
||||
throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
|
||||
}
|
||||
|
||||
$data = $this->sectionData;
|
||||
$tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
|
||||
return isset( $data[$token] );
|
||||
} );
|
||||
|
||||
if ( count( $tokens ) == 0 ) {
|
||||
if ( !$fatal ) {
|
||||
return false;
|
||||
}
|
||||
throw new MWException( sprintf(
|
||||
"'%s' without '%s' at line %s of %s\n",
|
||||
$this->section,
|
||||
implode( ',', $tokens ),
|
||||
$this->lineNum,
|
||||
$this->file
|
||||
) );
|
||||
}
|
||||
if ( count( $tokens ) > 1 ) {
|
||||
throw new MWException( sprintf(
|
||||
"'%s' with unexpected tokens '%s' at line %s of %s\n",
|
||||
$this->section,
|
||||
implode( ',', $tokens ),
|
||||
$this->lineNum,
|
||||
$this->file
|
||||
) );
|
||||
}
|
||||
|
||||
return array_values( $tokens )[0];
|
||||
}
|
||||
|
||||
private function addArticle( $name, $text, $line ) {
|
||||
$this->articles[] = [
|
||||
'name' => $name,
|
||||
'text' => $text,
|
||||
'line' => $line,
|
||||
'file' => $this->file
|
||||
];
|
||||
}
|
||||
|
||||
private function addRequirement( $type, $name ) {
|
||||
$this->requirements[$type][$name] = true;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,14 @@ class ParserTestFileSuite extends TestSuite {
|
|||
parent::__construct( $name );
|
||||
$this->ptRunner = $runner;
|
||||
$this->ptFileName = $fileName;
|
||||
$this->ptFileInfo = TestFileReader::read( $this->ptFileName );
|
||||
try {
|
||||
$this->ptFileInfo = TestFileReader::read( $this->ptFileName );
|
||||
} catch ( \Exception $e ) {
|
||||
// Friendlier wrapping for any syntax errors that might occur.
|
||||
throw new MWException(
|
||||
$fileName . ': ' . $e->getMessage()
|
||||
);
|
||||
}
|
||||
if ( !$this->ptRunner->meetsRequirements( $this->ptFileInfo['requirements'] ) ) {
|
||||
$skipMessage = 'required extension not enabled';
|
||||
} else {
|
||||
|
|
|
|||
Loading…
Reference in a new issue