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, 'runParsoid' => false, 'regex' => '//', ]; $this->runDisabled = $options['runDisabled']; $this->runParsoid = $options['runParsoid']; $this->regex = $options['regex']; } private function addCurrentTest() { // "input" and "result" are old section names allowed // for backwards-compatibility. $input = $this->checkSection( [ 'wikitext', 'input' ], false ); $nonTidySection = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false ); // Some tests have "with tidy" and "without tidy" variants $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false ); // 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 ) { // Disabled return; } if ( $tidySection === false && $nonTidySection === false ) { if ( isset( $data['html/parsoid'] ) || isset( $data['wikitext/edited'] ) ) { // Parsoid only return; } else { throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " . "lacks result section" ); } } if ( preg_match( '/\\bparsoid\\b/i', $data['options'] ) && $nonTidySection === 'html' && !$this->runParsoid ) { // A test which normally runs on Parsoid but can optionally be run with MW return; } if ( !preg_match( $this->regex, $data['test'] ) ) { // 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 ]; if ( $nonTidySection !== false ) { // Add non-tidy test $this->tests[] = [ 'result' => $data[$nonTidySection], 'resultSection' => $nonTidySection ] + $commonInfo; 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; } } 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 ( false !== ( $line = fgets( $this->fh ) ) ) { $this->lineNum++; $matches = []; 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 == 'endtransparenthooks' ) { $this->checkSection( 'transparenthooks' ); foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) { $line = trim( $line ); if ( $line ) { $this->addRequirement( 'transparentHook', $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 ( is_null( $this->section ) ) { 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; } }