Merge "Improve logging of exceptions which are not thrown but attached to context"
[lhc/web/wiklou.git] / tests / phpunit / includes / registration / ExtensionProcessorTest.php
1 <?php
2
3 class ExtensionProcessorTest extends MediaWikiTestCase {
4
5 private $dir, $dirname;
6
7 public function setUp() {
8 parent::setUp();
9 $this->dir = __DIR__ . '/FooBar/extension.json';
10 $this->dirname = dirname( $this->dir );
11 }
12
13 /**
14 * 'name' is absolutely required
15 *
16 * @var array
17 */
18 public static $default = [
19 'name' => 'FooBar',
20 ];
21
22 /**
23 * @covers ExtensionProcessor::extractInfo
24 */
25 public function testExtractInfo() {
26 // Test that attributes that begin with @ are ignored
27 $processor = new ExtensionProcessor();
28 $processor->extractInfo( $this->dir, self::$default + [
29 '@metadata' => [ 'foobarbaz' ],
30 'AnAttribute' => [ 'omg' ],
31 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
32 ], 1 );
33
34 $extracted = $processor->getExtractedInfo();
35 $attributes = $extracted['attributes'];
36 $this->assertArrayHasKey( 'AnAttribute', $attributes );
37 $this->assertArrayNotHasKey( '@metadata', $attributes );
38 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
39 }
40
41 public static function provideRegisterHooks() {
42 $merge = [ ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' ];
43 // Format:
44 // Current $wgHooks
45 // Content in extension.json
46 // Expected value of $wgHooks
47 return [
48 // No hooks
49 [
50 [],
51 self::$default,
52 $merge,
53 ],
54 // No current hooks, adding one for "FooBaz" in string format
55 [
56 [],
57 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
58 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
59 ],
60 // Hook for "FooBaz", adding another one
61 [
62 [ 'FooBaz' => [ 'PriorCallback' ] ],
63 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
64 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] + $merge,
65 ],
66 // No current hooks, adding one for "FooBaz" in verbose array format
67 [
68 [],
69 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self::$default,
70 [ 'FooBaz' => [ 'FooBazCallback' ] ] + $merge,
71 ],
72 // Hook for "BarBaz", adding one for "FooBaz"
73 [
74 [ 'BarBaz' => [ 'BarBazCallback' ] ],
75 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self::$default,
76 [
77 'BarBaz' => [ 'BarBazCallback' ],
78 'FooBaz' => [ 'FooBazCallback' ],
79 ] + $merge,
80 ],
81 // Callbacks for FooBaz wrapped in an array
82 [
83 [],
84 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self::$default,
85 [
86 'FooBaz' => [ 'Callback1' ],
87 ] + $merge,
88 ],
89 // Multiple callbacks for FooBaz hook
90 [
91 [],
92 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self::$default,
93 [
94 'FooBaz' => [ 'Callback1', 'Callback2' ],
95 ] + $merge,
96 ],
97 ];
98 }
99
100 /**
101 * @covers ExtensionProcessor::extractHooks
102 * @dataProvider provideRegisterHooks
103 */
104 public function testRegisterHooks( $pre, $info, $expected ) {
105 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
106 $processor->extractInfo( $this->dir, $info, 1 );
107 $extracted = $processor->getExtractedInfo();
108 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
109 }
110
111 /**
112 * @covers ExtensionProcessor::extractConfig1
113 */
114 public function testExtractConfig1() {
115 $processor = new ExtensionProcessor;
116 $info = [
117 'config' => [
118 'Bar' => 'somevalue',
119 'Foo' => 10,
120 '@IGNORED' => 'yes',
121 ],
122 ] + self::$default;
123 $info2 = [
124 'config' => [
125 '_prefix' => 'eg',
126 'Bar' => 'somevalue'
127 ],
128 'name' => 'FooBar2',
129 ];
130 $processor->extractInfo( $this->dir, $info, 1 );
131 $processor->extractInfo( $this->dir, $info2, 1 );
132 $extracted = $processor->getExtractedInfo();
133 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
134 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
135 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
136 // Custom prefix:
137 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
138 }
139
140 /**
141 * @covers ExtensionProcessor::extractConfig2
142 */
143 public function testExtractConfig2() {
144 $processor = new ExtensionProcessor;
145 $info = [
146 'config' => [
147 'Bar' => [ 'value' => 'somevalue' ],
148 'Foo' => [ 'value' => 10 ],
149 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
150 ],
151 ] + self::$default;
152 $info2 = [
153 'config' => [
154 'Bar' => [ 'value' => 'somevalue' ],
155 ],
156 'config_prefix' => 'eg',
157 'name' => 'FooBar2',
158 ];
159 $processor->extractInfo( $this->dir, $info, 2 );
160 $processor->extractInfo( $this->dir, $info2, 2 );
161 $extracted = $processor->getExtractedInfo();
162 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
163 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
164 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
165 // Custom prefix:
166 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
167 }
168
169 public static function provideExtractExtensionMessagesFiles() {
170 $dir = __DIR__ . '/FooBar/';
171 return [
172 [
173 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
174 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
175 ],
176 [
177 [
178 'ExtensionMessagesFiles' => [
179 'FooBarAlias' => 'FooBar.alias.php',
180 'FooBarMagic' => 'FooBar.magic.i18n.php',
181 ],
182 ],
183 [
184 'wgExtensionMessagesFiles' => [
185 'FooBarAlias' => $dir . 'FooBar.alias.php',
186 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
187 ],
188 ],
189 ],
190 ];
191 }
192
193 /**
194 * @covers ExtensionProcessor::extractExtensionMessagesFiles
195 * @dataProvider provideExtractExtensionMessagesFiles
196 */
197 public function testExtractExtensionMessagesFiles( $input, $expected ) {
198 $processor = new ExtensionProcessor();
199 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
200 $out = $processor->getExtractedInfo();
201 foreach ( $expected as $key => $value ) {
202 $this->assertEquals( $value, $out['globals'][$key] );
203 }
204 }
205
206 public static function provideExtractMessagesDirs() {
207 $dir = __DIR__ . '/FooBar/';
208 return [
209 [
210 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
211 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
212 ],
213 [
214 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
215 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
216 ],
217 ];
218 }
219
220 /**
221 * @covers ExtensionProcessor::extractMessagesDirs
222 * @dataProvider provideExtractMessagesDirs
223 */
224 public function testExtractMessagesDirs( $input, $expected ) {
225 $processor = new ExtensionProcessor();
226 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
227 $out = $processor->getExtractedInfo();
228 foreach ( $expected as $key => $value ) {
229 $this->assertEquals( $value, $out['globals'][$key] );
230 }
231 }
232
233 /**
234 * @covers ExtensionProcessor::extractCredits
235 */
236 public function testExtractCredits() {
237 $processor = new ExtensionProcessor();
238 $processor->extractInfo( $this->dir, self::$default, 1 );
239 $this->setExpectedException( 'Exception' );
240 $processor->extractInfo( $this->dir, self::$default, 1 );
241 }
242
243 /**
244 * @covers ExtensionProcessor::extractResourceLoaderModules
245 * @dataProvider provideExtractResourceLoaderModules
246 */
247 public function testExtractResourceLoaderModules( $input, $expected ) {
248 $processor = new ExtensionProcessor();
249 $processor->extractInfo( $this->dir, $input + self::$default, 1 );
250 $out = $processor->getExtractedInfo();
251 foreach ( $expected as $key => $value ) {
252 $this->assertEquals( $value, $out['globals'][$key] );
253 }
254 }
255
256 public static function provideExtractResourceLoaderModules() {
257 $dir = __DIR__ . '/FooBar';
258 return [
259 // Generic module with localBasePath/remoteExtPath specified
260 [
261 // Input
262 [
263 'ResourceModules' => [
264 'test.foo' => [
265 'styles' => 'foobar.js',
266 'localBasePath' => '',
267 'remoteExtPath' => 'FooBar',
268 ],
269 ],
270 ],
271 // Expected
272 [
273 'wgResourceModules' => [
274 'test.foo' => [
275 'styles' => 'foobar.js',
276 'localBasePath' => $dir,
277 'remoteExtPath' => 'FooBar',
278 ],
279 ],
280 ],
281 ],
282 // ResourceFileModulePaths specified:
283 [
284 // Input
285 [
286 'ResourceFileModulePaths' => [
287 'localBasePath' => '',
288 'remoteExtPath' => 'FooBar',
289 ],
290 'ResourceModules' => [
291 // No paths
292 'test.foo' => [
293 'styles' => 'foo.js',
294 ],
295 // Different paths set
296 'test.bar' => [
297 'styles' => 'bar.js',
298 'localBasePath' => 'subdir',
299 'remoteExtPath' => 'FooBar/subdir',
300 ],
301 // Custom class with no paths set
302 'test.class' => [
303 'class' => 'FooBarModule',
304 'extra' => 'argument',
305 ],
306 // Custom class with a localBasePath
307 'test.class.with.path' => [
308 'class' => 'FooBarPathModule',
309 'extra' => 'argument',
310 'localBasePath' => '',
311 ]
312 ],
313 ],
314 // Expected
315 [
316 'wgResourceModules' => [
317 'test.foo' => [
318 'styles' => 'foo.js',
319 'localBasePath' => $dir,
320 'remoteExtPath' => 'FooBar',
321 ],
322 'test.bar' => [
323 'styles' => 'bar.js',
324 'localBasePath' => "$dir/subdir",
325 'remoteExtPath' => 'FooBar/subdir',
326 ],
327 'test.class' => [
328 'class' => 'FooBarModule',
329 'extra' => 'argument',
330 'localBasePath' => $dir,
331 'remoteExtPath' => 'FooBar',
332 ],
333 'test.class.with.path' => [
334 'class' => 'FooBarPathModule',
335 'extra' => 'argument',
336 'localBasePath' => $dir,
337 'remoteExtPath' => 'FooBar',
338 ]
339 ],
340 ],
341 ],
342 // ResourceModuleSkinStyles with file module paths
343 [
344 // Input
345 [
346 'ResourceFileModulePaths' => [
347 'localBasePath' => '',
348 'remoteSkinPath' => 'FooBar',
349 ],
350 'ResourceModuleSkinStyles' => [
351 'foobar' => [
352 'test.foo' => 'foo.css',
353 ]
354 ],
355 ],
356 // Expected
357 [
358 'wgResourceModuleSkinStyles' => [
359 'foobar' => [
360 'test.foo' => 'foo.css',
361 'localBasePath' => $dir,
362 'remoteSkinPath' => 'FooBar',
363 ],
364 ],
365 ],
366 ],
367 // ResourceModuleSkinStyles with file module paths and an override
368 [
369 // Input
370 [
371 'ResourceFileModulePaths' => [
372 'localBasePath' => '',
373 'remoteSkinPath' => 'FooBar',
374 ],
375 'ResourceModuleSkinStyles' => [
376 'foobar' => [
377 'test.foo' => 'foo.css',
378 'remoteSkinPath' => 'BarFoo'
379 ],
380 ],
381 ],
382 // Expected
383 [
384 'wgResourceModuleSkinStyles' => [
385 'foobar' => [
386 'test.foo' => 'foo.css',
387 'localBasePath' => $dir,
388 'remoteSkinPath' => 'BarFoo',
389 ],
390 ],
391 ],
392 ],
393 ];
394 }
395
396 public static function provideSetToGlobal() {
397 return [
398 [
399 [ 'wgAPIModules', 'wgAvailableRights' ],
400 [],
401 [
402 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
403 'AvailableRights' => [ 'foobar', 'unfoobar' ],
404 ],
405 [
406 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
407 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
408 ],
409 ],
410 [
411 [ 'wgAPIModules', 'wgAvailableRights' ],
412 [
413 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
414 'wgAvailableRights' => [ 'barbaz' ]
415 ],
416 [
417 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
418 'AvailableRights' => [ 'foobar', 'unfoobar' ],
419 ],
420 [
421 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
422 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
423 ],
424 ],
425 [
426 [ 'wgGroupPermissions' ],
427 [
428 'wgGroupPermissions' => [
429 'sysop' => [ 'delete' ]
430 ],
431 ],
432 [
433 'GroupPermissions' => [
434 'sysop' => [ 'undelete' ],
435 'user' => [ 'edit' ]
436 ],
437 ],
438 [
439 'wgGroupPermissions' => [
440 'sysop' => [ 'delete', 'undelete' ],
441 'user' => [ 'edit' ]
442 ],
443 ]
444 ]
445 ];
446 }
447
448 public function testGlobalSettingsDocumentedInSchema() {
449 global $IP;
450 $globalSettings = TestingAccessWrapper::newFromClass(
451 ExtensionProcessor::class )->globalSettings;
452
453 $version = ExtensionRegistry::MANIFEST_VERSION;
454 $schema = FormatJson::decode(
455 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
456 true
457 );
458 $missing = [];
459 foreach ( $globalSettings as $global ) {
460 if ( !isset( $schema['properties'][$global] ) ) {
461 $missing[] = $global;
462 }
463 }
464
465 $this->assertEquals( [], $missing,
466 "The following global settings are not documented in docs/extension.schema.json" );
467 }
468 }
469
470 /**
471 * Allow overriding the default value of $this->globals
472 * so we can test merging
473 */
474 class MockExtensionProcessor extends ExtensionProcessor {
475 public function __construct( $globals = [] ) {
476 $this->globals = $globals + $this->globals;
477 }
478 }