Renames preparatory to parser tests refactor
[lhc/web/wiklou.git] / tests / parser / TestFileReader.php
1 <?php
2 /**
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.
7 *
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.
12 *
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
17 *
18 * @file
19 * @ingroup Testing
20 */
21
22 class TestFileReader implements Iterator {
23 private $file;
24 private $fh;
25 /**
26 * @var ParserTestRunner|ParserTestTopLevelSuite An instance of ParserTestRunner
27 * (parserTests.php) or ParserTestTopLevelSuite (phpunit)
28 */
29 private $parserTest;
30 private $index = 0;
31 private $test;
32 private $section = null;
33 /** String|null: current test section being analyzed */
34 private $sectionData = [];
35 private $lineNum;
36 private $eof;
37 # Create a fake parser tests which never run anything unless
38 # asked to do so. This will avoid running hooks for a disabled test
39 private $delayedParserTest;
40 private $nextSubTest = 0;
41
42 function __construct( $file, $parserTest ) {
43 $this->file = $file;
44 $this->fh = fopen( $this->file, "rt" );
45
46 if ( !$this->fh ) {
47 throw new MWException( "Couldn't open file '$file'\n" );
48 }
49
50 $this->parserTest = $parserTest;
51 $this->delayedParserTest = new DelayedParserTest();
52
53 $this->lineNum = $this->index = 0;
54 }
55
56 function rewind() {
57 if ( fseek( $this->fh, 0 ) ) {
58 throw new MWException( "Couldn't fseek to the start of '$this->file'\n" );
59 }
60
61 $this->index = -1;
62 $this->lineNum = 0;
63 $this->eof = false;
64 $this->next();
65
66 return true;
67 }
68
69 function current() {
70 return $this->test;
71 }
72
73 function key() {
74 return $this->index;
75 }
76
77 function next() {
78 if ( $this->readNextTest() ) {
79 $this->index++;
80 return true;
81 } else {
82 $this->eof = true;
83 }
84 }
85
86 function valid() {
87 return $this->eof != true;
88 }
89
90 function setupCurrentTest() {
91 // "input" and "result" are old section names allowed
92 // for backwards-compatibility.
93 $input = $this->checkSection( [ 'wikitext', 'input' ], false );
94 $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false );
95 // some tests have "with tidy" and "without tidy" variants
96 $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
97 if ( $tidy != false ) {
98 if ( $this->nextSubTest == 0 ) {
99 if ( $result != false ) {
100 $this->nextSubTest = 1; // rerun non-tidy variant later
101 }
102 $result = $tidy;
103 } else {
104 $this->nextSubTest = 0; // go on to next test after this
105 $tidy = false;
106 }
107 }
108
109 if ( !isset( $this->sectionData['options'] ) ) {
110 $this->sectionData['options'] = '';
111 }
112
113 if ( !isset( $this->sectionData['config'] ) ) {
114 $this->sectionData['config'] = '';
115 }
116
117 $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) &&
118 !$this->parserTest->runDisabled;
119 $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) &&
120 $result == 'html' &&
121 !$this->parserTest->runParsoid;
122 $isFiltered = !preg_match( "/" . $this->parserTest->regex . "/i", $this->sectionData['test'] );
123 if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
124 # disabled test
125 return false;
126 }
127
128 # We are really going to run the test, run pending hooks and hooks function
129 wfDebug( __METHOD__ . " unleashing delayed test for: {$this->sectionData['test']}" );
130 $hooksResult = $this->delayedParserTest->unleash( $this->parserTest );
131 if ( !$hooksResult ) {
132 # Some hook reported an issue. Abort.
133 throw new MWException( "Problem running requested parser hook from the test file" );
134 }
135
136 $this->test = [
137 'test' => ParserTestRunner::chomp( $this->sectionData['test'] ),
138 'subtest' => $this->nextSubTest,
139 'input' => ParserTestRunner::chomp( $this->sectionData[$input] ),
140 'result' => ParserTestRunner::chomp( $this->sectionData[$result] ),
141 'options' => ParserTestRunner::chomp( $this->sectionData['options'] ),
142 'config' => ParserTestRunner::chomp( $this->sectionData['config'] ),
143 ];
144 if ( $tidy != false ) {
145 $this->test['options'] .= " tidy";
146 }
147 return true;
148 }
149
150 function readNextTest() {
151 # Run additional subtests of previous test
152 while ( $this->nextSubTest > 0 ) {
153 if ( $this->setupCurrentTest() ) {
154 return true;
155 }
156 }
157
158 $this->clearSection();
159 # Reset hooks for the delayed test object
160 $this->delayedParserTest->reset();
161
162 while ( false !== ( $line = fgets( $this->fh ) ) ) {
163 $this->lineNum++;
164 $matches = [];
165
166 if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
167 $this->section = strtolower( $matches[1] );
168
169 if ( $this->section == 'endarticle' ) {
170 $this->checkSection( 'text' );
171 $this->checkSection( 'article' );
172
173 $this->parserTest->addArticle(
174 ParserTestRunner::chomp( $this->sectionData['article'] ),
175 $this->sectionData['text'], $this->lineNum );
176
177 $this->clearSection();
178
179 continue;
180 }
181
182 if ( $this->section == 'endhooks' ) {
183 $this->checkSection( 'hooks' );
184
185 foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
186 $line = trim( $line );
187
188 if ( $line ) {
189 $this->delayedParserTest->requireHook( $line );
190 }
191 }
192
193 $this->clearSection();
194
195 continue;
196 }
197
198 if ( $this->section == 'endfunctionhooks' ) {
199 $this->checkSection( 'functionhooks' );
200
201 foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
202 $line = trim( $line );
203
204 if ( $line ) {
205 $this->delayedParserTest->requireFunctionHook( $line );
206 }
207 }
208
209 $this->clearSection();
210
211 continue;
212 }
213
214 if ( $this->section == 'endtransparenthooks' ) {
215 $this->checkSection( 'transparenthooks' );
216
217 foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
218 $line = trim( $line );
219
220 if ( $line ) {
221 $this->delayedParserTest->requireTransparentHook( $line );
222 }
223 }
224
225 $this->clearSection();
226
227 continue;
228 }
229
230 if ( $this->section == 'end' ) {
231 $this->checkSection( 'test' );
232 do {
233 if ( $this->setupCurrentTest() ) {
234 return true;
235 }
236 } while ( $this->nextSubTest > 0 );
237 # go on to next test (since this was disabled)
238 $this->clearSection();
239 $this->delayedParserTest->reset();
240 continue;
241 }
242
243 if ( isset( $this->sectionData[$this->section] ) ) {
244 throw new MWException( "duplicate section '$this->section' "
245 . "at line {$this->lineNum} of $this->file\n" );
246 }
247
248 $this->sectionData[$this->section] = '';
249
250 continue;
251 }
252
253 if ( $this->section ) {
254 $this->sectionData[$this->section] .= $line;
255 }
256 }
257
258 return false;
259 }
260
261 /**
262 * Clear section name and its data
263 */
264 private function clearSection() {
265 $this->sectionData = [];
266 $this->section = null;
267
268 }
269
270 /**
271 * Verify the current section data has some value for the given token
272 * name(s) (first parameter).
273 * Throw an exception if it is not set, referencing current section
274 * and adding the current file name and line number
275 *
276 * @param string|array $tokens Expected token(s) that should have been
277 * mentioned before closing this section
278 * @param bool $fatal True iff an exception should be thrown if
279 * the section is not found.
280 * @return bool|string
281 * @throws MWException
282 */
283 private function checkSection( $tokens, $fatal = true ) {
284 if ( is_null( $this->section ) ) {
285 throw new MWException( __METHOD__ . " can not verify a null section!\n" );
286 }
287 if ( !is_array( $tokens ) ) {
288 $tokens = [ $tokens ];
289 }
290 if ( count( $tokens ) == 0 ) {
291 throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
292 }
293
294 $data = $this->sectionData;
295 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
296 return isset( $data[$token] );
297 } );
298
299 if ( count( $tokens ) == 0 ) {
300 if ( !$fatal ) {
301 return false;
302 }
303 throw new MWException( sprintf(
304 "'%s' without '%s' at line %s of %s\n",
305 $this->section,
306 implode( ',', $tokens ),
307 $this->lineNum,
308 $this->file
309 ) );
310 }
311 if ( count( $tokens ) > 1 ) {
312 throw new MWException( sprintf(
313 "'%s' with unexpected tokens '%s' at line %s of %s\n",
314 $this->section,
315 implode( ',', $tokens ),
316 $this->lineNum,
317 $this->file
318 ) );
319 }
320
321 return array_values( $tokens )[0];
322 }
323 }
324