3 use Wikimedia\TestingAccessWrapper
;
5 class ExtensionProcessorTest
extends MediaWikiTestCase
{
7 private $dir, $dirname;
9 public function setUp() {
11 $this->dir
= __DIR__
. '/FooBar/extension.json';
12 $this->dirname
= dirname( $this->dir
);
16 * 'name' is absolutely required
20 public static $default = [
25 * @covers ExtensionProcessor::extractInfo
27 public function testExtractInfo() {
28 // Test that attributes that begin with @ are ignored
29 $processor = new ExtensionProcessor();
30 $processor->extractInfo( $this->dir
, self
::$default +
[
31 '@metadata' => [ 'foobarbaz' ],
32 'AnAttribute' => [ 'omg' ],
33 'AutoloadClasses' => [ 'FooBar' => 'includes/FooBar.php' ],
36 $extracted = $processor->getExtractedInfo();
37 $attributes = $extracted['attributes'];
38 $this->assertArrayHasKey( 'AnAttribute', $attributes );
39 $this->assertArrayNotHasKey( '@metadata', $attributes );
40 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
44 * @covers ExtensionProcessor::extractInfo
46 public function testExtractInfo_namespaces() {
47 // Test that namespace IDs can be overwritten
48 if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
49 define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
52 $processor = new ExtensionProcessor();
53 $processor->extractInfo( $this->dir
, self
::$default +
[
57 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
59 'content' => 'TestModel'
61 [ // Test_X will use ID 123456 not 334400
63 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
65 'content' => 'TestModel'
70 $extracted = $processor->getExtractedInfo();
72 $this->assertArrayHasKey(
73 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
76 $this->assertArrayNotHasKey(
77 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
82 $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
86 $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
87 $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
88 $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
89 $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
91 $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
92 $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
95 public static function provideRegisterHooks() {
96 $merge = [ ExtensionRegistry
::MERGE_STRATEGY
=> 'array_merge_recursive' ];
99 // Content in extension.json
100 // Expected value of $wgHooks
108 // No current hooks, adding one for "FooBaz" in string format
111 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
112 [ 'FooBaz' => [ 'FooBazCallback' ] ] +
$merge,
114 // Hook for "FooBaz", adding another one
116 [ 'FooBaz' => [ 'PriorCallback' ] ],
117 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
118 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] +
$merge,
120 // No current hooks, adding one for "FooBaz" in verbose array format
123 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self
::$default,
124 [ 'FooBaz' => [ 'FooBazCallback' ] ] +
$merge,
126 // Hook for "BarBaz", adding one for "FooBaz"
128 [ 'BarBaz' => [ 'BarBazCallback' ] ],
129 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
131 'BarBaz' => [ 'BarBazCallback' ],
132 'FooBaz' => [ 'FooBazCallback' ],
135 // Callbacks for FooBaz wrapped in an array
138 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self
::$default,
140 'FooBaz' => [ 'Callback1' ],
143 // Multiple callbacks for FooBaz hook
146 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self
::$default,
148 'FooBaz' => [ 'Callback1', 'Callback2' ],
155 * @covers ExtensionProcessor::extractHooks
156 * @dataProvider provideRegisterHooks
158 public function testRegisterHooks( $pre, $info, $expected ) {
159 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
160 $processor->extractInfo( $this->dir
, $info, 1 );
161 $extracted = $processor->getExtractedInfo();
162 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
166 * @covers ExtensionProcessor::extractConfig1
168 public function testExtractConfig1() {
169 $processor = new ExtensionProcessor
;
172 'Bar' => 'somevalue',
184 $processor->extractInfo( $this->dir
, $info, 1 );
185 $processor->extractInfo( $this->dir
, $info2, 1 );
186 $extracted = $processor->getExtractedInfo();
187 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
188 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
189 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
191 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
195 * @covers ExtensionProcessor::extractConfig2
197 public function testExtractConfig2() {
198 $processor = new ExtensionProcessor
;
201 'Bar' => [ 'value' => 'somevalue' ],
202 'Foo' => [ 'value' => 10 ],
203 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
208 'Bar' => [ 'value' => 'somevalue' ],
210 'config_prefix' => 'eg',
213 $processor->extractInfo( $this->dir
, $info, 2 );
214 $processor->extractInfo( $this->dir
, $info2, 2 );
215 $extracted = $processor->getExtractedInfo();
216 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
217 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
218 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
220 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
224 * @covers ExtensionProcessor::addConfigGlobal()
225 * @expectedException RuntimeException
227 public function testDuplicateConfigKey1() {
228 $processor = new ExtensionProcessor
;
240 $processor->extractInfo( $this->dir
, $info, 1 );
241 $processor->extractInfo( $this->dir
, $info2, 1 );
245 * @covers ExtensionProcessor::addConfigGlobal()
246 * @expectedException RuntimeException
248 public function testDuplicateConfigKey2() {
249 $processor = new ExtensionProcessor
;
252 'Bar' => [ 'value' => 'somevalue' ],
257 'Bar' => [ 'value' => 'somevalue' ],
261 $processor->extractInfo( $this->dir
, $info, 2 );
262 $processor->extractInfo( $this->dir
, $info2, 2 );
265 public static function provideExtractExtensionMessagesFiles() {
266 $dir = __DIR__
. '/FooBar/';
269 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
270 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
274 'ExtensionMessagesFiles' => [
275 'FooBarAlias' => 'FooBar.alias.php',
276 'FooBarMagic' => 'FooBar.magic.i18n.php',
280 'wgExtensionMessagesFiles' => [
281 'FooBarAlias' => $dir . 'FooBar.alias.php',
282 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
290 * @covers ExtensionProcessor::extractExtensionMessagesFiles
291 * @dataProvider provideExtractExtensionMessagesFiles
293 public function testExtractExtensionMessagesFiles( $input, $expected ) {
294 $processor = new ExtensionProcessor();
295 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
296 $out = $processor->getExtractedInfo();
297 foreach ( $expected as $key => $value ) {
298 $this->assertEquals( $value, $out['globals'][$key] );
302 public static function provideExtractMessagesDirs() {
303 $dir = __DIR__
. '/FooBar/';
306 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
307 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
310 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
311 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
317 * @covers ExtensionProcessor::extractMessagesDirs
318 * @dataProvider provideExtractMessagesDirs
320 public function testExtractMessagesDirs( $input, $expected ) {
321 $processor = new ExtensionProcessor();
322 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
323 $out = $processor->getExtractedInfo();
324 foreach ( $expected as $key => $value ) {
325 $this->assertEquals( $value, $out['globals'][$key] );
330 * @covers ExtensionProcessor::extractCredits
332 public function testExtractCredits() {
333 $processor = new ExtensionProcessor();
334 $processor->extractInfo( $this->dir
, self
::$default, 1 );
335 $this->setExpectedException( 'Exception' );
336 $processor->extractInfo( $this->dir
, self
::$default, 1 );
340 * @covers ExtensionProcessor::extractResourceLoaderModules
341 * @dataProvider provideExtractResourceLoaderModules
343 public function testExtractResourceLoaderModules( $input, $expected ) {
344 $processor = new ExtensionProcessor();
345 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
346 $out = $processor->getExtractedInfo();
347 foreach ( $expected as $key => $value ) {
348 $this->assertEquals( $value, $out['globals'][$key] );
352 public static function provideExtractResourceLoaderModules() {
353 $dir = __DIR__
. '/FooBar';
355 // Generic module with localBasePath/remoteExtPath specified
359 'ResourceModules' => [
361 'styles' => 'foobar.js',
362 'localBasePath' => '',
363 'remoteExtPath' => 'FooBar',
369 'wgResourceModules' => [
371 'styles' => 'foobar.js',
372 'localBasePath' => $dir,
373 'remoteExtPath' => 'FooBar',
378 // ResourceFileModulePaths specified:
382 'ResourceFileModulePaths' => [
383 'localBasePath' => '',
384 'remoteExtPath' => 'FooBar',
386 'ResourceModules' => [
389 'styles' => 'foo.js',
391 // Different paths set
393 'styles' => 'bar.js',
394 'localBasePath' => 'subdir',
395 'remoteExtPath' => 'FooBar/subdir',
397 // Custom class with no paths set
399 'class' => 'FooBarModule',
400 'extra' => 'argument',
402 // Custom class with a localBasePath
403 'test.class.with.path' => [
404 'class' => 'FooBarPathModule',
405 'extra' => 'argument',
406 'localBasePath' => '',
412 'wgResourceModules' => [
414 'styles' => 'foo.js',
415 'localBasePath' => $dir,
416 'remoteExtPath' => 'FooBar',
419 'styles' => 'bar.js',
420 'localBasePath' => "$dir/subdir",
421 'remoteExtPath' => 'FooBar/subdir',
424 'class' => 'FooBarModule',
425 'extra' => 'argument',
426 'localBasePath' => $dir,
427 'remoteExtPath' => 'FooBar',
429 'test.class.with.path' => [
430 'class' => 'FooBarPathModule',
431 'extra' => 'argument',
432 'localBasePath' => $dir,
433 'remoteExtPath' => 'FooBar',
438 // ResourceModuleSkinStyles with file module paths
442 'ResourceFileModulePaths' => [
443 'localBasePath' => '',
444 'remoteSkinPath' => 'FooBar',
446 'ResourceModuleSkinStyles' => [
448 'test.foo' => 'foo.css',
454 'wgResourceModuleSkinStyles' => [
456 'test.foo' => 'foo.css',
457 'localBasePath' => $dir,
458 'remoteSkinPath' => 'FooBar',
463 // ResourceModuleSkinStyles with file module paths and an override
467 'ResourceFileModulePaths' => [
468 'localBasePath' => '',
469 'remoteSkinPath' => 'FooBar',
471 'ResourceModuleSkinStyles' => [
473 'test.foo' => 'foo.css',
474 'remoteSkinPath' => 'BarFoo'
480 'wgResourceModuleSkinStyles' => [
482 'test.foo' => 'foo.css',
483 'localBasePath' => $dir,
484 'remoteSkinPath' => 'BarFoo',
492 public static function provideSetToGlobal() {
495 [ 'wgAPIModules', 'wgAvailableRights' ],
498 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
499 'AvailableRights' => [ 'foobar', 'unfoobar' ],
502 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
503 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
507 [ 'wgAPIModules', 'wgAvailableRights' ],
509 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
510 'wgAvailableRights' => [ 'barbaz' ]
513 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
514 'AvailableRights' => [ 'foobar', 'unfoobar' ],
517 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
518 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
522 [ 'wgGroupPermissions' ],
524 'wgGroupPermissions' => [
525 'sysop' => [ 'delete' ]
529 'GroupPermissions' => [
530 'sysop' => [ 'undelete' ],
535 'wgGroupPermissions' => [
536 'sysop' => [ 'delete', 'undelete' ],
545 * Attributes under manifest_version 2
547 * @covers ExtensionProcessor::extractAttributes
548 * @covers ExtensionProcessor::getExtractedInfo
550 public function testExtractAttributes() {
551 $processor = new ExtensionProcessor();
552 // Load FooBar extension
553 $processor->extractInfo( $this->dir
, [ 'name' => 'FooBar' ], 2 );
554 $processor->extractInfo(
576 $info = $processor->getExtractedInfo();
577 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
578 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
579 $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
583 * Attributes under manifest_version 1
585 * @covers ExtensionProcessor::extractInfo
587 public function testAttributes1() {
588 $processor = new ExtensionProcessor();
589 $processor->extractInfo(
596 'FizzBuzzMorePlugins' => [
603 $info = $processor->getExtractedInfo();
604 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
605 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
606 $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
607 $this->assertSame( [ 'ext.baz.fizzbuzz' ], $info['attributes']['FizzBuzzMorePlugins'] );
610 public function testGlobalSettingsDocumentedInSchema() {
612 $globalSettings = TestingAccessWrapper
::newFromClass(
613 ExtensionProcessor
::class )->globalSettings
;
615 $version = ExtensionRegistry
::MANIFEST_VERSION
;
616 $schema = FormatJson
::decode(
617 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
621 foreach ( $globalSettings as $global ) {
622 if ( !isset( $schema['properties'][$global] ) ) {
623 $missing[] = $global;
627 $this->assertEquals( [], $missing,
628 "The following global settings are not documented in docs/extension.schema.json" );
633 * Allow overriding the default value of $this->globals
634 * so we can test merging
636 class MockExtensionProcessor
extends ExtensionProcessor
{
637 public function __construct( $globals = [] ) {
638 $this->globals
= $globals +
$this->globals
;