X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/tests/parser/TestFileReader.php diff --git a/tests/parser/TestFileReader.php b/tests/parser/TestFileReader.php new file mode 100644 index 00000000..a96485d4 --- /dev/null +++ b/tests/parser/TestFileReader.php @@ -0,0 +1,335 @@ +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; + } +}