Merge "docs: mw.widgets.CategorySelector: Fix example code"
[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 {
23 private $file;
24 private $fh;
25 private $section = null;
26 /** String|null: current test section being analyzed */
27 private $sectionData = [];
28 private $lineNum = 0;
29 private $runDisabled;
30 private $runParsoid;
31 private $regex;
32
33 private $articles = [];
34 private $requirements = [];
35 private $tests = [];
36
37 public static function read( $file, array $options = [] ) {
38 $reader = new self( $file, $options );
39 $reader->execute();
40
41 $requirements = [];
42 foreach ( $reader->requirements as $type => $reqsOfType ) {
43 foreach ( $reqsOfType as $name => $unused ) {
44 $requirements[] = [
45 'type' => $type,
46 'name' => $name
47 ];
48 }
49 }
50
51 return [
52 'requirements' => $requirements,
53 'tests' => $reader->tests,
54 'articles' => $reader->articles
55 ];
56 }
57
58 private function __construct( $file, $options ) {
59 $this->file = $file;
60 $this->fh = fopen( $this->file, "rt" );
61
62 if ( !$this->fh ) {
63 throw new MWException( "Couldn't open file '$file'\n" );
64 }
65
66 $options = $options + [
67 'runDisabled' => false,
68 'runParsoid' => false,
69 'regex' => '//',
70 ];
71 $this->runDisabled = $options['runDisabled'];
72 $this->runParsoid = $options['runParsoid'];
73 $this->regex = $options['regex'];
74 }
75
76 private function addCurrentTest() {
77 // "input" and "result" are old section names allowed
78 // for backwards-compatibility.
79 $input = $this->checkSection( [ 'wikitext', 'input' ], false );
80 $result = $this->checkSection( [ 'html/php', 'html/*', 'html', 'result' ], false );
81 // Some tests have "with tidy" and "without tidy" variants
82 $tidy = $this->checkSection( [ 'html/php+tidy', 'html+tidy' ], false );
83
84 if ( !isset( $this->sectionData['options'] ) ) {
85 $this->sectionData['options'] = '';
86 }
87
88 if ( !isset( $this->sectionData['config'] ) ) {
89 $this->sectionData['config'] = '';
90 }
91
92 $isDisabled = preg_match( '/\\bdisabled\\b/i', $this->sectionData['options'] ) &&
93 !$this->runDisabled;
94 $isParsoidOnly = preg_match( '/\\bparsoid\\b/i', $this->sectionData['options'] ) &&
95 $result == 'html' &&
96 !$this->runParsoid;
97 $isFiltered = !preg_match( $this->regex, $this->sectionData['test'] );
98 if ( $input == false || $result == false || $isDisabled || $isParsoidOnly || $isFiltered ) {
99 // Disabled test
100 return;
101 }
102
103 $test = [
104 'test' => ParserTestRunner::chomp( $this->sectionData['test'] ),
105 'input' => ParserTestRunner::chomp( $this->sectionData[$input] ),
106 'result' => ParserTestRunner::chomp( $this->sectionData[$result] ),
107 'options' => ParserTestRunner::chomp( $this->sectionData['options'] ),
108 'config' => ParserTestRunner::chomp( $this->sectionData['config'] ),
109 ];
110 $test['desc'] = $test['test'];
111 $this->tests[] = $test;
112
113 if ( $tidy !== false ) {
114 $test['options'] .= " tidy";
115 $test['desc'] .= ' (with tidy)';
116 $test['result'] = ParserTestRunner::chomp( $this->sectionData[$tidy] );
117 $this->tests[] = $test;
118 }
119 }
120
121 private function execute() {
122 while ( false !== ( $line = fgets( $this->fh ) ) ) {
123 $this->lineNum++;
124 $matches = [];
125
126 if ( preg_match( '/^!!\s*(\S+)/', $line, $matches ) ) {
127 $this->section = strtolower( $matches[1] );
128
129 if ( $this->section == 'endarticle' ) {
130 $this->checkSection( 'text' );
131 $this->checkSection( 'article' );
132
133 $this->addArticle(
134 ParserTestRunner::chomp( $this->sectionData['article'] ),
135 $this->sectionData['text'], $this->lineNum );
136
137 $this->clearSection();
138
139 continue;
140 }
141
142 if ( $this->section == 'endhooks' ) {
143 $this->checkSection( 'hooks' );
144
145 foreach ( explode( "\n", $this->sectionData['hooks'] ) as $line ) {
146 $line = trim( $line );
147
148 if ( $line ) {
149 $this->addRequirement( 'hook', $line );
150 }
151 }
152
153 $this->clearSection();
154
155 continue;
156 }
157
158 if ( $this->section == 'endfunctionhooks' ) {
159 $this->checkSection( 'functionhooks' );
160
161 foreach ( explode( "\n", $this->sectionData['functionhooks'] ) as $line ) {
162 $line = trim( $line );
163
164 if ( $line ) {
165 $this->addRequirement( 'functionHook', $line );
166 }
167 }
168
169 $this->clearSection();
170
171 continue;
172 }
173
174 if ( $this->section == 'endtransparenthooks' ) {
175 $this->checkSection( 'transparenthooks' );
176
177 foreach ( explode( "\n", $this->sectionData['transparenthooks'] ) as $line ) {
178 $line = trim( $line );
179
180 if ( $line ) {
181 $this->addRequirement( 'transparentHook', $line );
182 }
183 }
184
185 $this->clearSection();
186
187 continue;
188 }
189
190 if ( $this->section == 'end' ) {
191 $this->checkSection( 'test' );
192 $this->addCurrentTest();
193 $this->clearSection();
194 continue;
195 }
196
197 if ( isset( $this->sectionData[$this->section] ) ) {
198 throw new MWException( "duplicate section '$this->section' "
199 . "at line {$this->lineNum} of $this->file\n" );
200 }
201
202 $this->sectionData[$this->section] = '';
203
204 continue;
205 }
206
207 if ( $this->section ) {
208 $this->sectionData[$this->section] .= $line;
209 }
210 }
211 }
212
213 /**
214 * Clear section name and its data
215 */
216 private function clearSection() {
217 $this->sectionData = [];
218 $this->section = null;
219
220 }
221
222 /**
223 * Verify the current section data has some value for the given token
224 * name(s) (first parameter).
225 * Throw an exception if it is not set, referencing current section
226 * and adding the current file name and line number
227 *
228 * @param string|array $tokens Expected token(s) that should have been
229 * mentioned before closing this section
230 * @param bool $fatal True iff an exception should be thrown if
231 * the section is not found.
232 * @return bool|string
233 * @throws MWException
234 */
235 private function checkSection( $tokens, $fatal = true ) {
236 if ( is_null( $this->section ) ) {
237 throw new MWException( __METHOD__ . " can not verify a null section!\n" );
238 }
239 if ( !is_array( $tokens ) ) {
240 $tokens = [ $tokens ];
241 }
242 if ( count( $tokens ) == 0 ) {
243 throw new MWException( __METHOD__ . " can not verify zero sections!\n" );
244 }
245
246 $data = $this->sectionData;
247 $tokens = array_filter( $tokens, function ( $token ) use ( $data ) {
248 return isset( $data[$token] );
249 } );
250
251 if ( count( $tokens ) == 0 ) {
252 if ( !$fatal ) {
253 return false;
254 }
255 throw new MWException( sprintf(
256 "'%s' without '%s' at line %s of %s\n",
257 $this->section,
258 implode( ',', $tokens ),
259 $this->lineNum,
260 $this->file
261 ) );
262 }
263 if ( count( $tokens ) > 1 ) {
264 throw new MWException( sprintf(
265 "'%s' with unexpected tokens '%s' at line %s of %s\n",
266 $this->section,
267 implode( ',', $tokens ),
268 $this->lineNum,
269 $this->file
270 ) );
271 }
272
273 return array_values( $tokens )[0];
274 }
275
276 private function addArticle( $name, $text, $line ) {
277 $this->articles[] = [
278 'name' => $name,
279 'text' => $text,
280 'line' => $line,
281 'file' => $this->file
282 ];
283 }
284
285 private function addRequirement( $type, $name ) {
286 $this->requirements[$type][$name] = true;
287 }
288 }
289