gplx/mediawiki/tests/phpunit/unit/includes/registration/ExtensionRegistryTest.php

564 lines
13 KiB
PHP

<?php
namespace MediaWiki\Tests\Registration;
use Exception;
use InvalidArgumentException;
use LogicException;
use MediaWiki\Registration\ExtensionRegistry;
use MediaWiki\Settings\SettingsBuilder;
use MediaWikiUnitTestCase;
use Wikimedia\ScopedCallback;
use Wikimedia\TestingAccessWrapper;
/**
* @covers \MediaWiki\Registration\ExtensionRegistry
*/
class ExtensionRegistryTest extends MediaWikiUnitTestCase {
private const DATA_DIR = __DIR__ . '/../../../data/registration';
/** @var array */
private $restoreGlobals = [];
/** @var array */
private $unsetGlobals = [];
protected function tearDown(): void {
foreach ( $this->restoreGlobals as $key => $value ) {
$GLOBALS[$key] = $value;
}
foreach ( $this->unsetGlobals as $var ) {
unset( $GLOBALS[$var] );
}
parent::tearDown();
}
private function getRegistry(): ExtensionRegistry {
$registry = new ExtensionRegistry();
// Mock the global SettingsBuilder dependencies, as this is a unit test. And because SettingsBuilder
// has a reverse dependency on the global ExtensionRegistry instance, it would throw an exception
// because access to the global ExtensionRegistry instance is forbidden in unit tests.
$registry->setSettingsBuilder( $this->createMock( SettingsBuilder::class ) );
return $registry;
}
private function setGlobal( $key, $value ) {
if ( isset( $GLOBALS[$key] ) ) {
$this->restoreGlobals[$key] = $GLOBALS[$key];
} else {
$this->unsetGlobals[] = $key;
}
$GLOBALS[$key] = $value;
}
public function testQueue_invalid() {
$this->setGlobal( 'wgExtensionInfoMTime', false );
$registry = $this->getRegistry();
$path = __DIR__ . '/doesnotexist.json';
$this->expectException( Exception::class );
$this->expectExceptionMessage( "file $path" );
$registry->queue( $path );
}
public function testQueue() {
$registry = $this->getRegistry();
$path = self::DATA_DIR . "/good.json";
$registry->queue( $path );
$this->assertArrayHasKey(
$path,
$registry->getQueue()
);
$registry->clearQueue();
$this->assertSame( [], $registry->getQueue() );
}
public function testLoadFromQueue_empty() {
$registry = $this->getRegistry();
$registry->loadFromQueue();
$this->assertSame( [], $registry->getAllThings() );
}
public function testLoadFromQueue_late() {
$registry = $this->getRegistry();
$registry->finish();
$registry->queue( self::DATA_DIR . "/good.json" );
$this->expectException( LogicException::class );
$this->expectExceptionMessage(
"The following paths tried to load late: " . self::DATA_DIR . "/good.json" );
$registry->loadFromQueue();
}
public function testLoadFromQueue() {
$registry = $this->getRegistry();
$registry->queue( self::DATA_DIR . "/good.json" );
$registry->loadFromQueue();
$this->assertArrayHasKey( 'FooBar', $registry->getAllThings() );
$this->assertTrue( $registry->isLoaded( 'FooBar' ) );
$this->assertTrue( $registry->isLoaded( 'FooBar', '*' ) );
$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
$this->assertSame( [], $registry->getAttribute( 'NotLoadedAttr' ) );
}
public function testLoadFromQueueWithConstraintWithVersion() {
$registry = $this->getRegistry();
$registry->queue( self::DATA_DIR . "/good_with_version.json" );
$registry->loadFromQueue();
$this->assertTrue( $registry->isLoaded( 'FooBar', '>= 1.2.0' ) );
$this->assertFalse( $registry->isLoaded( 'FooBar', '^1.3.0' ) );
}
public function testLoadFromQueueWithConstraintWithoutVersion() {
$registry = $this->getRegistry();
$registry->queue( self::DATA_DIR . "/good.json" );
$registry->loadFromQueue();
$this->expectException( LogicException::class );
$registry->isLoaded( 'FooBar', '>= 1.2.0' );
}
public function testReadFromQueue_nonexistent() {
$registry = $this->getRegistry();
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( 'Unable to read' );
$this->expectPHPError(
E_WARNING,
static function () use ( $registry ) {
$registry->readFromQueue( [
__DIR__ . '/doesnotexist.json' => 1
] );
}
);
}
public function testExportExtractedDataNamespaceAlreadyDefined() {
define( 'FOO_VALUE', 123 ); // Emulates overriding a namespace set in LocalSettings.php
$registry = $this->getRegistry();
$info = [ 'defines' => [ 'FOO_VALUE' => 456 ], 'globals' => [] ];
$this->expectException( Exception::class );
$this->expectExceptionMessage(
"FOO_VALUE cannot be re-defined with 456 it has already been set with 123"
);
TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info );
}
/**
* @dataProvider provideExportExtractedDataGlobals
*/
public function testExportExtractedDataGlobals( $desc, $before, $globals, $expected ) {
// Set globals for test
if ( $before ) {
foreach ( $before as $key => $value ) {
// mw prefixed globals does not exist normally
if ( str_starts_with( $key, 'mw' ) ) {
$GLOBALS[$key] = $value;
} else {
$this->setGlobal( $key, $value );
}
}
}
$info = [
'globals' => $globals,
'callbacks' => [],
'defines' => [],
'credits' => [],
'attributes' => [],
'autoloaderPaths' => []
];
$registry = $this->getRegistry();
TestingAccessWrapper::newFromObject( $registry )->exportExtractedData( $info );
$result = array_intersect_key( $GLOBALS, $expected );
$this->assertEquals( $expected, $result, $desc );
// Remove mw prefixed globals
if ( $before ) {
foreach ( $before as $key => $value ) {
if ( str_starts_with( $key, 'mw' ) ) {
unset( $GLOBALS[$key] );
}
}
}
}
public static function provideExportExtractedDataGlobals() {
// "mwtest" prefix used instead of "$wg" to avoid potential conflicts
return [
[
'Simple non-array values',
[
'mwtestFooBarConfig' => true,
'mwtestFooBarConfig2' => 'string',
],
[
'mwtestFooBarDefault' => 1234,
'mwtestFooBarConfig' => false,
],
[
'mwtestFooBarConfig' => true,
'mwtestFooBarConfig2' => 'string',
'mwtestFooBarDefault' => 1234,
],
],
[
'No global already set, simple array',
null,
[
'mwtestDefaultOptions' => [
'foobar' => true,
]
],
[
'mwtestDefaultOptions' => [
'foobar' => true,
]
],
],
[
'Global already set, simple array',
[
'mwtestDefaultOptions' => [
'foobar' => true,
'foo' => 'string'
],
],
[
'mwtestDefaultOptions' => [
'barbaz' => 12345,
'foobar' => false,
],
],
[
'mwtestDefaultOptions' => [
'barbaz' => 12345,
'foo' => 'string',
'foobar' => true,
],
]
],
[
'Global already set, 1d array that appends',
[
'mwAvailableRights' => [
'foobar',
'foo'
],
],
[
'mwAvailableRights' => [
'barbaz',
],
],
[
'mwAvailableRights' => [
'barbaz',
'foobar',
'foo',
],
]
],
[
'Global already set, array with integer keys',
[
'mwNamespacesFoo' => [
100 => true,
102 => false
],
],
[
'mwNamespacesFoo' => [
100 => false,
500 => true,
ExtensionRegistry::MERGE_STRATEGY => 'array_plus',
],
],
[
'mwNamespacesFoo' => [
100 => true,
102 => false,
500 => true,
],
]
],
[
'No global already set, $wgHooks',
[
'wgHooks' => [],
],
[
'wgHooks' => [
'FooBarEvent' => [
'FooBarClass::onFooBarEvent'
],
ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive'
],
],
[
'wgHooks' => [
'FooBarEvent' => [
'FooBarClass::onFooBarEvent'
],
],
],
],
[
'Global already set, $wgHooks',
[
'wgHooks' => [
'FooBarEvent' => [
'FooBarClass::onFooBarEvent'
],
'BazBarEvent' => [
'FooBarClass::onBazBarEvent',
],
],
],
[
'wgHooks' => [
'FooBarEvent' => [
'BazBarClass::onFooBarEvent',
],
ExtensionRegistry::MERGE_STRATEGY => 'array_merge_recursive',
],
],
[
'wgHooks' => [
'FooBarEvent' => [
'FooBarClass::onFooBarEvent',
'BazBarClass::onFooBarEvent',
],
'BazBarEvent' => [
'FooBarClass::onBazBarEvent',
],
],
],
],
[
'Global already set, $wgGroupPermissions',
[
'wgGroupPermissions' => [
'sysop' => [
'something' => true,
],
'user' => [
'somethingtwo' => true,
]
],
],
[
'wgGroupPermissions' => [
'customgroup' => [
'right' => true,
],
'user' => [
'right' => true,
'somethingtwo' => false,
'nonduplicated' => true,
],
ExtensionRegistry::MERGE_STRATEGY => 'array_plus_2d',
],
],
[
'wgGroupPermissions' => [
'customgroup' => [
'right' => true,
],
'sysop' => [
'something' => true,
],
'user' => [
'somethingtwo' => true,
'right' => true,
'nonduplicated' => true,
]
],
],
],
[
'False local setting should not be overridden (T100767)',
[
'mwtestT100767' => false,
],
[
'mwtestT100767' => true,
],
[
'mwtestT100767' => false,
],
],
[
'test array_replace_recursive',
[
'mwtestJsonConfigs' => [
'JsonZeroConfig' => [
'namespace' => 480,
'nsName' => 'Zero',
'isLocal' => false,
'remote' => [
'username' => 'foo',
],
],
],
],
[
'mwtestJsonConfigs' => [
'JsonZeroConfig' => [
'isLocal' => true,
],
ExtensionRegistry::MERGE_STRATEGY => 'array_replace_recursive',
],
],
[
'mwtestJsonConfigs' => [
'JsonZeroConfig' => [
'namespace' => 480,
'nsName' => 'Zero',
'isLocal' => false,
'remote' => [
'username' => 'foo',
],
],
],
],
],
[
'global is null before',
[
'NullGlobal' => null,
],
[
'NullGlobal' => 'not-null'
],
[
'NullGlobal' => null
],
],
[
'provide_default passive case',
[
'wgFlatArray' => [],
],
[
'wgFlatArray' => [
1,
ExtensionRegistry::MERGE_STRATEGY => 'provide_default'
],
],
[
'wgFlatArray' => []
],
],
[
'provide_default active case',
[
],
[
'wgFlatArray' => [
1,
ExtensionRegistry::MERGE_STRATEGY => 'provide_default'
],
],
[
'wgFlatArray' => [ 1 ]
],
],
[
'a configured value should not turn into a default null value',
[
'ArrayValue' => [],
'BooleanValue' => false,
'IntegerValue' => 0,
'StringValue' => '',
],
[
'ArrayValue' => null,
'BooleanValue' => null,
'IntegerValue' => null,
'StringValue' => null,
],
[
'ArrayValue' => [],
'BooleanValue' => false,
'IntegerValue' => 0,
'StringValue' => '',
],
],
[
'a configured value should not turn into a default empty array value',
[
'BooleanValue' => false,
'IntegerValue' => 0,
'NullValue' => null,
'StringValue' => '',
],
[
'BooleanValue' => [],
'IntegerValue' => [],
'NullValue' => [],
'StringValue' => [],
],
[
'BooleanValue' => false,
'IntegerValue' => 0,
'NullValue' => null,
'StringValue' => '',
],
],
];
}
public function testSetAttributeForTest() {
$registry = $this->getRegistry();
$registry->queue( self::DATA_DIR . "/good.json" );
$registry->loadFromQueue();
// Check that it worked
$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
$reset = $registry->setAttributeForTest( 'FooBarAttr', [ 'override' ] );
// overridden properly
$this->assertSame( [ 'override' ], $registry->getAttribute( 'FooBarAttr' ) );
ScopedCallback::consume( $reset );
// reset properly
$this->assertSame( [ 'test' ], $registry->getAttribute( 'FooBarAttr' ) );
}
public function testSetAttributeForTestDuplicate() {
$registry = $this->getRegistry();
$reset1 = $registry->setAttributeForTest( 'foo', [ 'val1' ] );
$this->expectException( Exception::class );
$this->expectExceptionMessage( "The attribute 'foo' has already been overridden" );
$reset2 = $registry->setAttributeForTest( 'foo', [ 'val2' ] );
}
public function testGetLazyLoadedAttribute() {
$registry = TestingAccessWrapper::newFromObject(
$this->getRegistry()
);
// Verify the registry is absolutely empty
$this->assertSame( [], $registry->getLazyLoadedAttribute( 'FooBarBaz' ) );
// Indicate what paths should be checked for the lazy attributes
$registry->queue( self::DATA_DIR . "/attribute.json" );
$registry->loadFromQueue();
// Set in attribute.json
$this->assertEquals(
[ 'buzz' => true ],
$registry->getLazyLoadedAttribute( 'FooBarBaz' )
);
// Still return an array if nothing was set
$this->assertSame(
[],
$registry->getLazyLoadedAttribute( 'NotSetAtAll' )
);
// Test test overrides
$reset = $registry->setAttributeForTest( 'FooBarBaz',
[ 'lightyear' => true ] );
$this->assertEquals(
[ 'lightyear' => true ],
$registry->getLazyLoadedAttribute( 'FooBarBaz' )
);
}
}