3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
22 class TestFileReader
{
25 private $section = null;
26 /** String|null: current test section being analyzed */
27 private $sectionData = [];
28 private $sectionLineNum = [];
34 private $articles = [];
35 private $requirements = [];
38 public static function read( $file, array $options = [] ) {
39 $reader = new self( $file, $options );
43 foreach ( $reader->requirements
as $type => $reqsOfType ) {
44 foreach ( $reqsOfType as $name => $unused ) {
53 'requirements' => $requirements,
54 'tests' => $reader->tests
,
55 'articles' => $reader->articles
59 private function __construct( $file, $options ) {
61 $this->fh
= fopen( $this->file
, "rt" );
64 throw new MWException( "Couldn't open file '$file'\n" );
67 $options = $options +
[
68 'runDisabled' => false,
69 'runParsoid' => false,
72 $this->runDisabled
= $options['runDisabled'];
73 $this->runParsoid
= $options['runParsoid'];
74 $this->regex
= $options['regex'];
77 private function addCurrentTest() {
78 // "input" and "result" are old section names allowed
79 // for backwards-compatibility.
80 $input = $this->checkSection( [ 'wikitext', 'input' ], false );
81 $nonTidySection = $this->checkSection(
82 [ 'html/php', 'html/*', 'html', 'result' ], false );
83 // Some tests have "with tidy" and "without tidy" variants
84 $tidySection = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
86 // Remove trailing newline
87 $data = array_map( 'ParserTestRunner::chomp', $this->sectionData
);
95 if ( $input === false ) {
96 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
97 "lacks input section" );
100 if ( preg_match( '/\\bdisabled\\b/i', $data['options'] ) && !$this->runDisabled
) {
105 if ( $tidySection === false && $nonTidySection === false ) {
106 if ( isset( $data['html/parsoid'] ) ||
isset( $data['wikitext/edited'] ) ) {
110 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
111 "lacks result section" );
115 if ( preg_match( '/\\bparsoid\\b/i', $data['options'] ) && $nonTidySection === 'html'
116 && !$this->runParsoid
118 // A test which normally runs on Parsoid but can optionally be run with MW
122 if ( !preg_match( $this->regex
, $data['test'] ) ) {
128 'test' => $data['test'],
129 'desc' => $data['test'],
130 'input' => $data[$input],
131 'options' => $data['options'],
132 'config' => $data['config'],
133 'line' => $this->sectionLineNum
['test'],
134 'file' => $this->file
137 if ( $nonTidySection !== false ) {
140 'result' => $data[$nonTidySection],
141 'resultSection' => $nonTidySection
144 if ( $tidySection !== false ) {
147 'desc' => $data['test'] . ' (with tidy)',
148 'result' => $data[$tidySection],
149 'resultSection' => $tidySection,
150 'options' => $data['options'] . ' tidy',
154 } elseif ( $tidySection !== false ) {
155 // No need to override desc when there is no subtest
157 'result' => $data[$tidySection],
158 'resultSection' => $tidySection,
159 'options' => $data['options'] . ' tidy'
162 throw new MWException( "Test at {$this->file}:{$this->sectionLineNum['test']} " .
163 "lacks result section" );
167 private function execute() {
168 while ( false !== ( $line = fgets( $this->fh
) ) ) {
172 if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
173 $this->section
= strtolower( $matches[1] );
175 if ( $this->section
== 'endarticle' ) {
176 $this->checkSection( 'text' );
177 $this->checkSection( 'article' );
180 ParserTestRunner
::chomp( $this->sectionData
['article'] ),
181 $this->sectionData
['text'], $this->lineNum
);
183 $this->clearSection();
188 if ( $this->section
== 'endhooks' ) {
189 $this->checkSection( 'hooks' );
191 foreach ( explode( "\n", $this->sectionData
['hooks'] ) as $line ) {
192 $line = trim( $line );
195 $this->addRequirement( 'hook', $line );
199 $this->clearSection();
204 if ( $this->section
== 'endfunctionhooks' ) {
205 $this->checkSection( 'functionhooks' );
207 foreach ( explode( "\n", $this->sectionData
['functionhooks'] ) as $line ) {
208 $line = trim( $line );
211 $this->addRequirement( 'functionHook', $line );
215 $this->clearSection();
220 if ( $this->section
== 'endtransparenthooks' ) {
221 $this->checkSection( 'transparenthooks' );
223 foreach ( explode( "\n", $this->sectionData
['transparenthooks'] ) as $line ) {
224 $line = trim( $line );
227 $this->addRequirement( 'transparentHook', $line );
231 $this->clearSection();
236 if ( $this->section
== 'end' ) {
237 $this->checkSection( 'test' );
238 $this->addCurrentTest();
239 $this->clearSection();
243 if ( isset( $this->sectionData
[$this->section
] ) ) {
244 throw new MWException( "duplicate section '$this->section' "
245 . "at line {$this->lineNum} of $this->file\n" );
248 $this->sectionLineNum
[$this->section
] = $this->lineNum
;
249 $this->sectionData
[$this->section
] = '';
254 if ( $this->section
) {
255 $this->sectionData
[$this->section
] .= $line;
261 * Clear section name and its data
263 private function clearSection() {
264 $this->sectionLineNum
= [];
265 $this->sectionData
= [];
266 $this->section
= null;
270 * Verify the current section data has some value for the given token
271 * name(s) (first parameter).
272 * Throw an exception if it is not set, referencing current section
273 * and adding the current file name and line number
275 * @param string|array $tokens Expected token(s) that should have been
276 * mentioned before closing this section
277 * @param bool $fatal True iff an exception should be thrown if
278 * the section is not found.
279 * @return bool|string
280 * @throws MWException
282 private function checkSection( $tokens, $fatal = true ) {
283 if ( is_null( $this->section
) ) {
284 throw new MWException( __METHOD__
. " can not verify a null section!\n" );
286 if ( !is_array( $tokens ) ) {
287 $tokens = [ $tokens ];
289 if ( count( $tokens ) == 0 ) {
290 throw new MWException( __METHOD__
. " can not verify zero sections!\n" );
293 $data = $this->sectionData
;
294 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
295 return isset( $data[$token] );
298 if ( count( $tokens ) == 0 ) {
302 throw new MWException( sprintf(
303 "'%s' without '%s' at line %s of %s\n",
305 implode( ',', $tokens ),
310 if ( count( $tokens ) > 1 ) {
311 throw new MWException( sprintf(
312 "'%s' with unexpected tokens '%s' at line %s of %s\n",
314 implode( ',', $tokens ),
320 return array_values( $tokens )[0];
323 private function addArticle( $name, $text, $line ) {
324 $this->articles
[] = [
328 'file' => $this->file
332 private function addRequirement( $type, $name ) {
333 $this->requirements
[$type][$name] = true;