3 use Wikimedia\TestingAccessWrapper
;
6 * @covers ExtensionProcessor
8 class ExtensionProcessorTest
extends MediaWikiTestCase
{
10 private $dir, $dirname;
12 public function setUp() {
14 $this->dir
= __DIR__
. '/FooBar/extension.json';
15 $this->dirname
= dirname( $this->dir
);
19 * 'name' is absolutely required
23 public static $default = [
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' ],
34 'SpecialPages' => [ 'Foo' => 'SpecialFoo' ],
35 'callback' => 'FooBar::onRegistration',
38 $extracted = $processor->getExtractedInfo();
39 $attributes = $extracted['attributes'];
40 $this->assertArrayHasKey( 'AnAttribute', $attributes );
41 $this->assertArrayNotHasKey( '@metadata', $attributes );
42 $this->assertArrayNotHasKey( 'AutoloadClasses', $attributes );
44 [ 'FooBar' => 'FooBar::onRegistration' ],
45 $extracted['callbacks']
48 [ 'Foo' => 'SpecialFoo' ],
49 $extracted['globals']['wgSpecialPages']
53 public function testExtractNamespaces() {
54 // Test that namespace IDs can be overwritten
55 if ( !defined( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X' ) ) {
56 define( 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X', 123456 );
59 $processor = new ExtensionProcessor();
60 $processor->extractInfo( $this->dir
, self
::$default +
[
64 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
66 'defaultcontentmodel' => 'TestModel',
68 'male' => 'Male test',
69 'female' => 'Female test',
73 'protection' => 'userright',
75 [ // Test_X will use ID 123456 not 334400
77 'constant' => 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
79 'defaultcontentmodel' => 'TestModel'
84 $extracted = $processor->getExtractedInfo();
86 $this->assertArrayHasKey(
87 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A',
90 $this->assertArrayNotHasKey(
91 'MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_X',
96 $extracted['defines']['MW_EXTENSION_PROCESSOR_TEST_EXTRACT_INFO_A'],
100 $this->assertArrayHasKey( 'ExtensionNamespaces', $extracted['attributes'] );
101 $this->assertArrayHasKey( 123456, $extracted['attributes']['ExtensionNamespaces'] );
102 $this->assertArrayHasKey( 332200, $extracted['attributes']['ExtensionNamespaces'] );
103 $this->assertArrayNotHasKey( 334400, $extracted['attributes']['ExtensionNamespaces'] );
105 $this->assertSame( 'Test_X', $extracted['attributes']['ExtensionNamespaces'][123456] );
106 $this->assertSame( 'Test_A', $extracted['attributes']['ExtensionNamespaces'][332200] );
108 [ 'male' => 'Male test', 'female' => 'Female test' ],
109 $extracted['globals']['wgExtraGenderNamespaces'][332200]
111 // A has subpages, X does not
112 $this->assertTrue( $extracted['globals']['wgNamespacesWithSubpages'][332200] );
113 $this->assertArrayNotHasKey( 123456, $extracted['globals']['wgNamespacesWithSubpages'] );
116 public static function provideRegisterHooks() {
117 $merge = [ ExtensionRegistry
::MERGE_STRATEGY
=> 'array_merge_recursive' ];
120 // Content in extension.json
121 // Expected value of $wgHooks
129 // No current hooks, adding one for "FooBaz" in string format
132 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
133 [ 'FooBaz' => [ 'FooBazCallback' ] ] +
$merge,
135 // Hook for "FooBaz", adding another one
137 [ 'FooBaz' => [ 'PriorCallback' ] ],
138 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
139 [ 'FooBaz' => [ 'PriorCallback', 'FooBazCallback' ] ] +
$merge,
141 // No current hooks, adding one for "FooBaz" in verbose array format
144 [ 'Hooks' => [ 'FooBaz' => [ 'FooBazCallback' ] ] ] + self
::$default,
145 [ 'FooBaz' => [ 'FooBazCallback' ] ] +
$merge,
147 // Hook for "BarBaz", adding one for "FooBaz"
149 [ 'BarBaz' => [ 'BarBazCallback' ] ],
150 [ 'Hooks' => [ 'FooBaz' => 'FooBazCallback' ] ] + self
::$default,
152 'BarBaz' => [ 'BarBazCallback' ],
153 'FooBaz' => [ 'FooBazCallback' ],
156 // Callbacks for FooBaz wrapped in an array
159 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1' ] ] ] + self
::$default,
161 'FooBaz' => [ 'Callback1' ],
164 // Multiple callbacks for FooBaz hook
167 [ 'Hooks' => [ 'FooBaz' => [ 'Callback1', 'Callback2' ] ] ] + self
::$default,
169 'FooBaz' => [ 'Callback1', 'Callback2' ],
176 * @dataProvider provideRegisterHooks
178 public function testRegisterHooks( $pre, $info, $expected ) {
179 $processor = new MockExtensionProcessor( [ 'wgHooks' => $pre ] );
180 $processor->extractInfo( $this->dir
, $info, 1 );
181 $extracted = $processor->getExtractedInfo();
182 $this->assertEquals( $expected, $extracted['globals']['wgHooks'] );
185 public function testExtractConfig1() {
186 $processor = new ExtensionProcessor
;
189 'Bar' => 'somevalue',
201 $processor->extractInfo( $this->dir
, $info, 1 );
202 $processor->extractInfo( $this->dir
, $info2, 1 );
203 $extracted = $processor->getExtractedInfo();
204 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
205 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
206 $this->assertArrayNotHasKey( 'wg@IGNORED', $extracted['globals'] );
208 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
211 public function testExtractConfig2() {
212 $processor = new ExtensionProcessor
;
215 'Bar' => [ 'value' => 'somevalue' ],
216 'Foo' => [ 'value' => 10 ],
217 'Path' => [ 'value' => 'foo.txt', 'path' => true ],
223 'merge_strategy' => 'array_plus',
229 'Bar' => [ 'value' => 'somevalue' ],
231 'config_prefix' => 'eg',
234 $processor->extractInfo( $this->dir
, $info, 2 );
235 $processor->extractInfo( $this->dir
, $info2, 2 );
236 $extracted = $processor->getExtractedInfo();
237 $this->assertEquals( 'somevalue', $extracted['globals']['wgBar'] );
238 $this->assertEquals( 10, $extracted['globals']['wgFoo'] );
239 $this->assertEquals( "{$this->dirname}/foo.txt", $extracted['globals']['wgPath'] );
241 $this->assertEquals( 'somevalue', $extracted['globals']['egBar'] );
243 [ 10 => true, 12 => false, ExtensionRegistry
::MERGE_STRATEGY
=> 'array_plus' ],
244 $extracted['globals']['wgNamespaces']
249 * @expectedException RuntimeException
251 public function testDuplicateConfigKey1() {
252 $processor = new ExtensionProcessor
;
264 $processor->extractInfo( $this->dir
, $info, 1 );
265 $processor->extractInfo( $this->dir
, $info2, 1 );
269 * @expectedException RuntimeException
271 public function testDuplicateConfigKey2() {
272 $processor = new ExtensionProcessor
;
275 'Bar' => [ 'value' => 'somevalue' ],
280 'Bar' => [ 'value' => 'somevalue' ],
284 $processor->extractInfo( $this->dir
, $info, 2 );
285 $processor->extractInfo( $this->dir
, $info2, 2 );
288 public static function provideExtractExtensionMessagesFiles() {
289 $dir = __DIR__
. '/FooBar/';
292 [ 'ExtensionMessagesFiles' => [ 'FooBarAlias' => 'FooBar.alias.php' ] ],
293 [ 'wgExtensionMessagesFiles' => [ 'FooBarAlias' => $dir . 'FooBar.alias.php' ] ]
297 'ExtensionMessagesFiles' => [
298 'FooBarAlias' => 'FooBar.alias.php',
299 'FooBarMagic' => 'FooBar.magic.i18n.php',
303 'wgExtensionMessagesFiles' => [
304 'FooBarAlias' => $dir . 'FooBar.alias.php',
305 'FooBarMagic' => $dir . 'FooBar.magic.i18n.php',
313 * @dataProvider provideExtractExtensionMessagesFiles
315 public function testExtractExtensionMessagesFiles( $input, $expected ) {
316 $processor = new ExtensionProcessor();
317 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
318 $out = $processor->getExtractedInfo();
319 foreach ( $expected as $key => $value ) {
320 $this->assertEquals( $value, $out['globals'][$key] );
324 public static function provideExtractMessagesDirs() {
325 $dir = __DIR__
. '/FooBar/';
328 [ 'MessagesDirs' => [ 'VisualEditor' => 'i18n' ] ],
329 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n' ] ] ]
332 [ 'MessagesDirs' => [ 'VisualEditor' => [ 'i18n', 'foobar' ] ] ],
333 [ 'wgMessagesDirs' => [ 'VisualEditor' => [ $dir . 'i18n', $dir . 'foobar' ] ] ]
339 * @dataProvider provideExtractMessagesDirs
341 public function testExtractMessagesDirs( $input, $expected ) {
342 $processor = new ExtensionProcessor();
343 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
344 $out = $processor->getExtractedInfo();
345 foreach ( $expected as $key => $value ) {
346 $this->assertEquals( $value, $out['globals'][$key] );
350 public function testExtractCredits() {
351 $processor = new ExtensionProcessor();
352 $processor->extractInfo( $this->dir
, self
::$default, 1 );
353 $this->setExpectedException( Exception
::class );
354 $processor->extractInfo( $this->dir
, self
::$default, 1 );
358 * @dataProvider provideExtractResourceLoaderModules
360 public function testExtractResourceLoaderModules( $input, $expected ) {
361 $processor = new ExtensionProcessor();
362 $processor->extractInfo( $this->dir
, $input + self
::$default, 1 );
363 $out = $processor->getExtractedInfo();
364 foreach ( $expected as $key => $value ) {
365 $this->assertEquals( $value, $out['globals'][$key] );
369 public static function provideExtractResourceLoaderModules() {
370 $dir = __DIR__
. '/FooBar';
372 // Generic module with localBasePath/remoteExtPath specified
376 'ResourceModules' => [
378 'styles' => 'foobar.js',
379 'localBasePath' => '',
380 'remoteExtPath' => 'FooBar',
386 'wgResourceModules' => [
388 'styles' => 'foobar.js',
389 'localBasePath' => $dir,
390 'remoteExtPath' => 'FooBar',
395 // ResourceFileModulePaths specified:
399 'ResourceFileModulePaths' => [
400 'localBasePath' => 'modules',
401 'remoteExtPath' => 'FooBar/modules',
403 'ResourceModules' => [
406 'styles' => 'foo.js',
408 // Different paths set
410 'styles' => 'bar.js',
411 'localBasePath' => 'subdir',
412 'remoteExtPath' => 'FooBar/subdir',
414 // Custom class with no paths set
416 'class' => 'FooBarModule',
417 'extra' => 'argument',
419 // Custom class with a localBasePath
420 'test.class.with.path' => [
421 'class' => 'FooBarPathModule',
422 'extra' => 'argument',
423 'localBasePath' => '',
429 'wgResourceModules' => [
431 'styles' => 'foo.js',
432 'localBasePath' => "$dir/modules",
433 'remoteExtPath' => 'FooBar/modules',
436 'styles' => 'bar.js',
437 'localBasePath' => "$dir/subdir",
438 'remoteExtPath' => 'FooBar/subdir',
441 'class' => 'FooBarModule',
442 'extra' => 'argument',
443 'localBasePath' => "$dir/modules",
444 'remoteExtPath' => 'FooBar/modules',
446 'test.class.with.path' => [
447 'class' => 'FooBarPathModule',
448 'extra' => 'argument',
449 'localBasePath' => $dir,
450 'remoteExtPath' => 'FooBar/modules',
455 // ResourceModuleSkinStyles with file module paths
459 'ResourceFileModulePaths' => [
460 'localBasePath' => '',
461 'remoteSkinPath' => 'FooBar',
463 'ResourceModuleSkinStyles' => [
465 'test.foo' => 'foo.css',
471 'wgResourceModuleSkinStyles' => [
473 'test.foo' => 'foo.css',
474 'localBasePath' => $dir,
475 'remoteSkinPath' => 'FooBar',
480 // ResourceModuleSkinStyles with file module paths and an override
484 'ResourceFileModulePaths' => [
485 'localBasePath' => '',
486 'remoteSkinPath' => 'FooBar',
488 'ResourceModuleSkinStyles' => [
490 'test.foo' => 'foo.css',
491 'remoteSkinPath' => 'BarFoo'
497 'wgResourceModuleSkinStyles' => [
499 'test.foo' => 'foo.css',
500 'localBasePath' => $dir,
501 'remoteSkinPath' => 'BarFoo',
509 public static function provideSetToGlobal() {
512 [ 'wgAPIModules', 'wgAvailableRights' ],
515 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
516 'AvailableRights' => [ 'foobar', 'unfoobar' ],
519 'wgAPIModules' => [ 'foobar' => 'ApiFooBar' ],
520 'wgAvailableRights' => [ 'foobar', 'unfoobar' ],
524 [ 'wgAPIModules', 'wgAvailableRights' ],
526 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz' ],
527 'wgAvailableRights' => [ 'barbaz' ]
530 'APIModules' => [ 'foobar' => 'ApiFooBar' ],
531 'AvailableRights' => [ 'foobar', 'unfoobar' ],
534 'wgAPIModules' => [ 'barbaz' => 'ApiBarBaz', 'foobar' => 'ApiFooBar' ],
535 'wgAvailableRights' => [ 'barbaz', 'foobar', 'unfoobar' ],
539 [ 'wgGroupPermissions' ],
541 'wgGroupPermissions' => [
542 'sysop' => [ 'delete' ]
546 'GroupPermissions' => [
547 'sysop' => [ 'undelete' ],
552 'wgGroupPermissions' => [
553 'sysop' => [ 'delete', 'undelete' ],
562 * Attributes under manifest_version 2
564 public function testExtractAttributes() {
565 $processor = new ExtensionProcessor();
566 // Load FooBar extension
567 $processor->extractInfo( $this->dir
, [ 'name' => 'FooBar' ], 2 );
568 $processor->extractInfo(
590 $info = $processor->getExtractedInfo();
591 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
592 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
593 $this->assertArrayNotHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
597 * Attributes under manifest_version 1
599 public function testAttributes1() {
600 $processor = new ExtensionProcessor();
601 $processor->extractInfo(
608 'FizzBuzzMorePlugins' => [
614 $processor->extractInfo(
618 'FizzBuzzMorePlugins' => [
625 $info = $processor->getExtractedInfo();
626 $this->assertArrayHasKey( 'FooBarPlugins', $info['attributes'] );
627 $this->assertSame( [ 'ext.baz.foobar' ], $info['attributes']['FooBarPlugins'] );
628 $this->assertArrayHasKey( 'FizzBuzzMorePlugins', $info['attributes'] );
630 [ 'ext.baz.fizzbuzz', 'ext.bar.fizzbuzz' ],
631 $info['attributes']['FizzBuzzMorePlugins']
635 public function testAttributes1_notarray() {
636 $processor = new ExtensionProcessor();
637 $this->setExpectedException(
638 InvalidArgumentException
::class,
639 "The value for 'FooBarPlugins' should be an array (from {$this->dir})"
641 $processor->extractInfo(
644 'FooBarPlugins' => 'ext.baz.foobar',
650 public function testExtractPathBasedGlobal() {
651 $processor = new ExtensionProcessor();
652 $processor->extractInfo(
655 'ParserTestFiles' => [
656 'tests/parserTests.txt',
657 'tests/extraParserTests.txt',
659 'ServiceWiringFiles' => [
660 'includes/ServiceWiring.php'
665 $globals = $processor->getExtractedInfo()['globals'];
666 $this->assertArrayHasKey( 'wgParserTestFiles', $globals );
668 "{$this->dirname}/tests/parserTests.txt",
669 "{$this->dirname}/tests/extraParserTests.txt"
670 ], $globals['wgParserTestFiles'] );
671 $this->assertArrayHasKey( 'wgServiceWiringFiles', $globals );
673 "{$this->dirname}/includes/ServiceWiring.php"
674 ], $globals['wgServiceWiringFiles'] );
677 public function testGetRequirements() {
678 $info = self
::$default +
[
680 'MediaWiki' => '>= 1.25.0',
686 $processor = new ExtensionProcessor();
689 $processor->getRequirements( $info )
693 $processor->getRequirements( [] )
697 public function testGetExtraAutoloaderPaths() {
698 $processor = new ExtensionProcessor();
700 [ "{$this->dirname}/vendor/autoload.php" ],
701 $processor->getExtraAutoloaderPaths( $this->dirname
, [
702 'load_composer_autoloader' => true,
708 * Verify that extension.schema.json is in sync with ExtensionProcessor
712 public function testGlobalSettingsDocumentedInSchema() {
714 $globalSettings = TestingAccessWrapper
::newFromClass(
715 ExtensionProcessor
::class )->globalSettings
;
717 $version = ExtensionRegistry
::MANIFEST_VERSION
;
718 $schema = FormatJson
::decode(
719 file_get_contents( "$IP/docs/extension.schema.v$version.json" ),
723 foreach ( $globalSettings as $global ) {
724 if ( !isset( $schema['properties'][$global] ) ) {
725 $missing[] = $global;
729 $this->assertEquals( [], $missing,
730 "The following global settings are not documented in docs/extension.schema.json" );
735 * Allow overriding the default value of $this->globals
736 * so we can test merging
738 class MockExtensionProcessor
extends ExtensionProcessor
{
739 public function __construct( $globals = [] ) {
740 $this->globals
= $globals +
$this->globals
;