1<?php 2 3use Wikimedia\ScopedCallback; 4use Wikimedia\TestingAccessWrapper; 5 6/** 7 * @covers ExtensionRegistry 8 */ 9class ExtensionRegistryTest extends MediaWikiIntegrationTestCase { 10 11 private $dataDir; 12 13 protected function setUp() : void { 14 parent::setUp(); 15 $this->dataDir = __DIR__ . '/../../data/registration'; 16 } 17 18 public function testQueue_invalid() { 19 $registry = new ExtensionRegistry(); 20 $path = __DIR__ . '/doesnotexist.json'; 21 $this->expectException( Exception::class ); 22 $this->expectExceptionMessage( "file $path" ); 23 $registry->queue( $path ); 24 } 25 26 public function testQueue() { 27 $registry = new ExtensionRegistry(); 28 $path = "{$this->dataDir}/good.json"; 29 $registry->queue( $path ); 30 $this->assertArrayHasKey( 31 $path, 32 $registry->getQueue() 33 ); 34 $registry->clearQueue(); 35 $this->assertSame( [], $registry->getQueue() ); 36 } 37 38 public function testLoadFromQueue_empty() { 39 $registry = new ExtensionRegistry(); 40 $registry->loadFromQueue(); 41 $this->assertSame( [], $registry->getAllThings() ); 42 } 43 44 public function testLoadFromQueue_late() { 45 $registry = new ExtensionRegistry(); 46 $registry->finish(); 47 $registry->queue( "{$this->dataDir}/good.json" ); 48 $this->expectException( MWException::class ); 49 $this->expectExceptionMessage( 50 "The following paths tried to load late: {$this->dataDir}/good.json" ); 51 $registry->loadFromQueue(); 52 } 53 54 public function testLoadFromQueue() { 55 $registry = new ExtensionRegistry(); 56 $registry->queue( "{$this->dataDir}/good.json" ); 57 $registry->loadFromQueue(); 58 $this->assertArrayHasKey( 'FooBar', $registry->getAllThings() ); 59 $this->assertTrue( $registry->isLoaded( 'FooBar' ) ); 60 $this->assertTrue( $registry->isLoaded( 'FooBar', '*' ) ); 61 $this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) ); 62 $this->assertSame( [], $registry->getAttribute( 'NotLoadedAttr' ) ); 63 } 64 65 public function testLoadFromQueueWithConstraintWithVersion() { 66 $registry = new ExtensionRegistry(); 67 $registry->queue( "{$this->dataDir}/good_with_version.json" ); 68 $registry->loadFromQueue(); 69 $this->assertTrue( $registry->isLoaded( 'FooBar', '>= 1.2.0' ) ); 70 $this->assertFalse( $registry->isLoaded( 'FooBar', '^1.3.0' ) ); 71 } 72 73 public function testLoadFromQueueWithConstraintWithoutVersion() { 74 $registry = new ExtensionRegistry(); 75 $registry->queue( "{$this->dataDir}/good.json" ); 76 $registry->loadFromQueue(); 77 $this->expectException( LogicException::class ); 78 $registry->isLoaded( 'FooBar', '>= 1.2.0' ); 79 } 80 81 public function testReadFromQueue_nonexistent() { 82 $registry = new ExtensionRegistry(); 83 $this->expectError(); 84 $registry->readFromQueue( [ 85 __DIR__ . '/doesnotexist.json' => 1 86 ] ); 87 } 88 89 public function testReadFromQueueInitializeAutoloaderWithPsr4Namespaces() { 90 $registry = new ExtensionRegistry(); 91 $registry->readFromQueue( [ 92 "{$this->dataDir}/autoload_namespaces.json" => 1 93 ] ); 94 $this->assertTrue( 95 class_exists( 'Test\\MediaWiki\\AutoLoader\\TestFooBar' ), 96 "Registry initializes Autoloader from AutoloadNamespaces" 97 ); 98 } 99 100 public function testExportExtractedDataNamespaceAlreadyDefined() { 101 define( 'FOO_VALUE', 123 ); // Emulates overriding a namespace set in LocalSettings.php 102 $registry = new ExtensionRegistry(); 103 $info = [ 'defines' => [ 'FOO_VALUE' => 456 ], 'globals' => [] ]; 104 $this->expectException( Exception::class ); 105 $this->expectExceptionMessage( 106 "FOO_VALUE cannot be re-defined with 456 it has already been set with 123" 107 ); 108 TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info ); 109 } 110 111 /** 112 * @dataProvider provideExportExtractedDataGlobals 113 */ 114 public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) { 115 // Set globals for test 116 if ( $before ) { 117 foreach ( $before as $key => $value ) { 118 // mw prefixed globals does not exist normally 119 if ( substr( $key, 0, 2 ) == 'mw' ) { 120 $GLOBALS[$key] = $value; 121 } else { 122 $this->setMwGlobals( $key, $value ); 123 } 124 } 125 } 126 127 $info = [ 128 'globals' => $globals, 129 'callbacks' => [], 130 'defines' => [], 131 'credits' => [], 132 'attributes' => [], 133 'autoloaderPaths' => [] 134 ]; 135 $registry = new ExtensionRegistry(); 136 TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info ); 137 foreach ( $expected as $name => $value ) { 138 $this->assertArrayHasKey( $name, $GLOBALS, $desc ); 139 $this->assertEquals( $value, $GLOBALS[$name], $desc ); 140 } 141 142 // Remove mw prefixed globals 143 if ( $before ) { 144 foreach ( $before as $key => $value ) { 145 if ( substr( $key, 0, 2 ) == 'mw' ) { 146 unset( $GLOBALS[$key] ); 147 } 148 } 149 } 150 } 151 152 public static function provideExportExtractedDataGlobals() { 153 // "mwtest" prefix used instead of "$wg" to avoid potential conflicts 154 return [ 155 [ 156 'Simple non-array values', 157 [ 158 'mwtestFooBarConfig' => true, 159 'mwtestFooBarConfig2' => 'string', 160 ], 161 [ 162 'mwtestFooBarDefault' => 1234, 163 'mwtestFooBarConfig' => false, 164 ], 165 [ 166 'mwtestFooBarConfig' => true, 167 'mwtestFooBarConfig2' => 'string', 168 'mwtestFooBarDefault' => 1234, 169 ], 170 ], 171 [ 172 'No global already set, simple array', 173 null, 174 [ 175 'mwtestDefaultOptions' => [ 176 'foobar' => true, 177 ] 178 ], 179 [ 180 'mwtestDefaultOptions' => [ 181 'foobar' => true, 182 ] 183 ], 184 ], 185 [ 186 'Global already set, simple array', 187 [ 188 'mwtestDefaultOptions' => [ 189 'foobar' => true, 190 'foo' => 'string' 191 ], 192 ], 193 [ 194 'mwtestDefaultOptions' => [ 195 'barbaz' => 12345, 196 'foobar' => false, 197 ], 198 ], 199 [ 200 'mwtestDefaultOptions' => [ 201 'barbaz' => 12345, 202 'foo' => 'string', 203 'foobar' => true, 204 ], 205 ] 206 ], 207 [ 208 'Global already set, 1d array that appends', 209 [ 210 'mwAvailableRights' => [ 211 'foobar', 212 'foo' 213 ], 214 ], 215 [ 216 'mwAvailableRights' => [ 217 'barbaz', 218 ], 219 ], 220 [ 221 'mwAvailableRights' => [ 222 'barbaz', 223 'foobar', 224 'foo', 225 ], 226 ] 227 ], 228 [ 229 'Global already set, array with integer keys', 230 [ 231 'mwNamespacesFoo' => [ 232 100 => true, 233 102 => false 234 ], 235 ], 236 [ 237 'mwNamespacesFoo' => [ 238 100 => false, 239 500 => true, 240 ExtensionRegistry::MERGE_STRATEGY => 'array_plus', 241 ], 242 ], 243 [ 244 'mwNamespacesFoo' => [ 245 100 => true, 246 102 => false, 247 500 => true, 248 ], 249 ] 250 ], 251 [ 252 'No global already set, $wgHooks', 253 [ 254 'wgHooks' => [], 255 ], 256 [ 257 'wgHooks' => [ 258 'FooBarEvent' => [ 259 'FooBarClass::onFooBarEvent' 260 ], 261 ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive' 262 ], 263 ], 264 [ 265 'wgHooks' => [ 266 'FooBarEvent' => [ 267 'FooBarClass::onFooBarEvent' 268 ], 269 ], 270 ], 271 ], 272 [ 273 'Global already set, $wgHooks', 274 [ 275 'wgHooks' => [ 276 'FooBarEvent' => [ 277 'FooBarClass::onFooBarEvent' 278 ], 279 'BazBarEvent' => [ 280 'FooBarClass::onBazBarEvent', 281 ], 282 ], 283 ], 284 [ 285 'wgHooks' => [ 286 'FooBarEvent' => [ 287 'BazBarClass::onFooBarEvent', 288 ], 289 ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive', 290 ], 291 ], 292 [ 293 'wgHooks' => [ 294 'FooBarEvent' => [ 295 'FooBarClass::onFooBarEvent', 296 'BazBarClass::onFooBarEvent', 297 ], 298 'BazBarEvent' => [ 299 'FooBarClass::onBazBarEvent', 300 ], 301 ], 302 ], 303 ], 304 [ 305 'Global already set, $wgGroupPermissions', 306 [ 307 'wgGroupPermissions' => [ 308 'sysop' => [ 309 'something' => true, 310 ], 311 'user' => [ 312 'somethingtwo' => true, 313 ] 314 ], 315 ], 316 [ 317 'wgGroupPermissions' => [ 318 'customgroup' => [ 319 'right' => true, 320 ], 321 'user' => [ 322 'right' => true, 323 'somethingtwo' => false, 324 'nonduplicated' => true, 325 ], 326 ExtensionRegistry::MERGE_STRATEGY => 'array_plus_2d', 327 ], 328 ], 329 [ 330 'wgGroupPermissions' => [ 331 'customgroup' => [ 332 'right' => true, 333 ], 334 'sysop' => [ 335 'something' => true, 336 ], 337 'user' => [ 338 'somethingtwo' => true, 339 'right' => true, 340 'nonduplicated' => true, 341 ] 342 ], 343 ], 344 ], 345 [ 346 'False local setting should not be overridden (T100767)', 347 [ 348 'mwtestT100767' => false, 349 ], 350 [ 351 'mwtestT100767' => true, 352 ], 353 [ 354 'mwtestT100767' => false, 355 ], 356 ], 357 [ 358 'test array_replace_recursive', 359 [ 360 'mwtestJsonConfigs' => [ 361 'JsonZeroConfig' => [ 362 'namespace' => 480, 363 'nsName' => 'Zero', 364 'isLocal' => false, 365 'remote' => [ 366 'username' => 'foo', 367 ], 368 ], 369 ], 370 ], 371 [ 372 'mwtestJsonConfigs' => [ 373 'JsonZeroConfig' => [ 374 'isLocal' => true, 375 ], 376 ExtensionRegistry::MERGE_STRATEGY => 'array_replace_recursive', 377 ], 378 ], 379 [ 380 'mwtestJsonConfigs' => [ 381 'JsonZeroConfig' => [ 382 'namespace' => 480, 383 'nsName' => 'Zero', 384 'isLocal' => false, 385 'remote' => [ 386 'username' => 'foo', 387 ], 388 ], 389 ], 390 ], 391 ], 392 [ 393 'global is null before', 394 [ 395 'NullGlobal' => null, 396 ], 397 [ 398 'NullGlobal' => 'not-null' 399 ], 400 [ 401 'NullGlobal' => null 402 ], 403 ], 404 [ 405 'provide_default passive case', 406 [ 407 'wgFlatArray' => [], 408 ], 409 [ 410 'wgFlatArray' => [ 411 1, 412 ExtensionRegistry::MERGE_STRATEGY => 'provide_default' 413 ], 414 ], 415 [ 416 'wgFlatArray' => [] 417 ], 418 ], 419 [ 420 'provide_default active case', 421 [ 422 ], 423 [ 424 'wgFlatArray' => [ 425 1, 426 ExtensionRegistry::MERGE_STRATEGY => 'provide_default' 427 ], 428 ], 429 [ 430 'wgFlatArray' => [ 1 ] 431 ], 432 ] 433 ]; 434 } 435 436 public function testSetAttributeForTest() { 437 $registry = new ExtensionRegistry(); 438 $registry->queue( "{$this->dataDir}/good.json" ); 439 $registry->loadFromQueue(); 440 // Sanity check that it worked 441 $this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) ); 442 $reset = $registry->setAttributeForTest( 'FooBarAttr', [ 'override' ] ); 443 // overridden properly 444 $this->assertSame( [ 'override' ], $registry->getAttribute( 'FooBarAttr' ) ); 445 ScopedCallback::consume( $reset ); 446 // reset properly 447 $this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) ); 448 } 449 450 public function testSetAttributeForTestDuplicate() { 451 $registry = new ExtensionRegistry(); 452 $reset1 = $registry->setAttributeForTest( 'foo', [ 'val1' ] ); 453 $this->expectException( Exception::class ); 454 $this->expectExceptionMessage( "The attribute 'foo' has already been overridden" ); 455 $reset2 = $registry->setAttributeForTest( 'foo', [ 'val2' ] ); 456 } 457 458 public function testGetLazyLoadedAttribute() { 459 $registry = TestingAccessWrapper::newFromObject( 460 new ExtensionRegistry() 461 ); 462 // Verify the registry is absolutely empty 463 $this->assertSame( [], $registry->getLazyLoadedAttribute( 'FooBarBaz' ) ); 464 // Indicate what paths should be checked for the lazy attributes 465 $registry->loaded = [ 466 'FooBar' => [ 467 'path' => "{$this->dataDir}/attribute.json", 468 ] 469 ]; 470 // Set in attribute.json 471 $this->assertEquals( 472 [ 'buzz' => true ], 473 $registry->getLazyLoadedAttribute( 'FooBarBaz' ) 474 ); 475 // Still return an array if nothing was set 476 $this->assertSame( 477 [], 478 $registry->getLazyLoadedAttribute( 'NotSetAtAll' ) 479 ); 480 481 // Test test overrides 482 $reset = $registry->setAttributeForTest( 'FooBarBaz', 483 [ 'lightyear' => true ] ); 484 $this->assertEquals( 485 [ 'lightyear' => true ], 486 $registry->getLazyLoadedAttribute( 'FooBarBaz' ) 487 ); 488 } 489} 490