wiki.techinc.nl/tests/phpunit/includes/MediaWikiServicesTest.php
daniel db729e46f6 Treat $wgHooks as a regular setting
$wgHooks should be treated like a regular setting, which cannot be
manipulated after bootstrapping is complete. This will allow us to
greatly simplify the logic in HookContainer.

Replacing $wgHooks with a fake array after bootstrapping allows us to
detect any remaining live access to $wgHooks without breaking
functionality.

The plan is to have the fake array emit deprecation warnings in the 1.40
release, and make it throw exceptions in later releases.
See Iddcb760cf8961316d6527e81b9aa968657d8354c for the deprecation
warnings.

Bug: T331602
Change-Id: I0ebba9a29f81b0d86ad8fd84d478fb244f9e9c15
2023-03-09 10:20:44 +01:00

420 lines
12 KiB
PHP

<?php
use MediaWiki\Hook\MediaWikiServicesHook;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\StaticHookRegistry;
use MediaWiki\MediaWikiServices;
use Wikimedia\Services\DestructibleService;
use Wikimedia\Services\SalvageableService;
/**
* @covers MediaWiki\MediaWikiServices
*/
class MediaWikiServicesTest extends MediaWikiIntegrationTestCase {
private $deprecatedServices = [];
public static $mockServiceWiring = [];
/**
* @return Config
*/
private function newTestConfig() {
$globalConfig = new GlobalVarConfig();
$testConfig = new HashConfig();
$testConfig->set( 'ServiceWiringFiles', $globalConfig->get( 'ServiceWiringFiles' ) );
$testConfig->set( 'ConfigRegistry', $globalConfig->get( 'ConfigRegistry' ) );
$testConfig->set( 'Hooks', [] );
return $testConfig;
}
/**
* @return MediaWikiServices
*/
private function newMediaWikiServices() {
$config = $this->newTestConfig();
$instance = new MediaWikiServices( $config );
// Load the default wiring from the specified files.
$wiringFiles = $config->get( 'ServiceWiringFiles' );
$instance->loadWiringFiles( $wiringFiles );
return $instance;
}
private function newConfigWithMockWiring() {
$config = new HashConfig;
$config->set( 'ServiceWiringFiles', [ __DIR__ . '/MockServiceWiring.php' ] );
return $config;
}
public function testGetInstance() {
$services = MediaWikiServices::getInstance();
$this->assertInstanceOf( MediaWikiServices::class, $services );
}
public function testForceGlobalInstance() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
$this->assertInstanceOf( MediaWikiServices::class, $oldServices );
$this->assertNotSame( $oldServices, $newServices );
$theServices = MediaWikiServices::getInstance();
$this->assertSame( $theServices, $newServices );
MediaWikiServices::forceGlobalInstance( $oldServices );
$theServices = MediaWikiServices::getInstance();
$this->assertSame( $theServices, $oldServices );
}
public function testResetGlobalInstance() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
$service1 = $this->createMock( SalvageableService::class );
$service1->expects( $this->never() )
->method( 'salvage' );
$newServices->defineService(
'Test',
static function () use ( $service1 ) {
return $service1;
}
);
// force instantiation
$newServices->getService( 'Test' );
MediaWikiServices::resetGlobalInstance( $this->newTestConfig() );
$theServices = MediaWikiServices::getInstance();
$this->assertSame(
$service1,
$theServices->getService( 'Test' ),
'service definition should survive reset'
);
$this->assertNotSame( $theServices, $newServices );
$this->assertNotSame( $theServices, $oldServices );
MediaWikiServices::forceGlobalInstance( $oldServices );
}
public function testResetGlobalInstance_quick() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
$service1 = $this->createMock( SalvageableService::class );
$service1->expects( $this->never() )
->method( 'salvage' );
$service2 = $this->createMock( SalvageableService::class );
$service2->expects( $this->once() )
->method( 'salvage' )
->with( $service1 );
// sequence of values the instantiator will return
$instantiatorReturnValues = [
$service1,
$service2,
];
$newServices->defineService(
'Test',
static function () use ( &$instantiatorReturnValues ) {
return array_shift( $instantiatorReturnValues );
}
);
// force instantiation
$newServices->getService( 'Test' );
MediaWikiServices::resetGlobalInstance( $this->newTestConfig(), 'quick' );
$theServices = MediaWikiServices::getInstance();
$this->assertSame( $service2, $theServices->getService( 'Test' ) );
$this->assertNotSame( $theServices, $newServices );
$this->assertNotSame( $theServices, $oldServices );
MediaWikiServices::forceGlobalInstance( $oldServices );
}
public function testResetGlobalInstance_T263925() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
self::$mockServiceWiring = [
'HookContainer' => function ( MediaWikiServices $services ) {
return new HookContainer(
new StaticHookRegistry(
[],
[
'MediaWikiServices' => [
[
'handler' => [
'name' => 'test',
'factory' => static function () {
return new class implements MediaWikiServicesHook {
public function onMediaWikiServices( $services ) {
}
};
}
],
'deprecated' => false,
'extensionPath' => 'path'
],
]
],
[]
),
$this->createSimpleObjectFactory()
);
}
];
$newServices->redefineService( 'HookContainer',
self::$mockServiceWiring['HookContainer'] );
$newServices->getHookContainer()->run( 'MediaWikiServices', [ $newServices ] );
MediaWikiServices::resetGlobalInstance( $this->newConfigWithMockWiring(), 'quick' );
$this->assertTrue( true, 'expected no exception from above' );
self::$mockServiceWiring = [];
MediaWikiServices::forceGlobalInstance( $oldServices );
}
public function testDisableStorage() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
$lbFactory = $this->createMock( \Wikimedia\Rdbms\LBFactorySimple::class );
$newServices->redefineService(
'DBLoadBalancerFactory',
static function () use ( $lbFactory ) {
return $lbFactory;
}
);
$this->assertFalse( $newServices->isStorageDisabled() );
$newServices->disableStorage(); // should destroy DBLoadBalancerFactory
$this->assertTrue( $newServices->isStorageDisabled() );
try {
$newServices->getDBLoadBalancer()->getConnection( DB_REPLICA );
} catch ( RuntimeException $ex ) {
// ok, as expected
}
MediaWikiServices::forceGlobalInstance( $oldServices );
$newServices->destroy();
// This should work now.
MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection( DB_REPLICA );
// No exception was thrown, avoid being risky
$this->assertTrue( true );
}
public function testResetChildProcessServices() {
$newServices = $this->newMediaWikiServices();
$oldServices = MediaWikiServices::forceGlobalInstance( $newServices );
$service1 = $this->createMock( DestructibleService::class );
$service1->expects( $this->once() )
->method( 'destroy' );
$service2 = $this->createMock( DestructibleService::class );
$service2->expects( $this->never() )
->method( 'destroy' );
// sequence of values the instantiator will return
$instantiatorReturnValues = [
$service1,
$service2,
];
$newServices->defineService(
'Test',
static function () use ( &$instantiatorReturnValues ) {
return array_shift( $instantiatorReturnValues );
}
);
// force the service to become active, so we can check that it does get destroyed
$oldTestService = $newServices->getService( 'Test' );
MediaWikiServices::resetChildProcessServices();
$finalServices = MediaWikiServices::getInstance();
$newTestService = $finalServices->getService( 'Test' );
$this->assertNotSame( $oldTestService, $newTestService );
MediaWikiServices::forceGlobalInstance( $oldServices );
}
public function testResetServiceForTesting() {
$services = $this->newMediaWikiServices();
$serviceCounter = 0;
$services->defineService(
'Test',
function () use ( &$serviceCounter ) {
$serviceCounter++;
$service = $this->createMock( Wikimedia\Services\DestructibleService::class );
$service->expects( $this->once() )->method( 'destroy' );
return $service;
}
);
// This should do nothing. In particular, it should not create a service instance.
$services->resetServiceForTesting( 'Test' );
$this->assertSame( 0, $serviceCounter, 'No service instance should be created yet.' );
$oldInstance = $services->getService( 'Test' );
$this->assertSame( 1, $serviceCounter, 'A service instance should exit now.' );
// The old instance should be detached, and destroy() called.
$services->resetServiceForTesting( 'Test' );
$newInstance = $services->getService( 'Test' );
$this->assertNotSame( $oldInstance, $newInstance );
// Satisfy the expectation that destroy() is called also for the second service instance.
$newInstance->destroy();
}
public function testResetServiceForTesting_noDestroy() {
$services = $this->newMediaWikiServices();
$services->defineService(
'Test',
function () {
$service = $this->createMock( Wikimedia\Services\DestructibleService::class );
$service->expects( $this->never() )->method( 'destroy' );
return $service;
}
);
$oldInstance = $services->getService( 'Test' );
// The old instance should be detached, but destroy() not called.
$services->resetServiceForTesting( 'Test', false );
$newInstance = $services->getService( 'Test' );
$this->assertNotSame( $oldInstance, $newInstance );
}
public function provideGetters() {
$getServiceCases = $this->provideGetService();
$getterCases = [];
// All getters should be named just like the service, with "get" added.
foreach ( $getServiceCases as $name => $case ) {
if ( $name[0] === '_' ) {
// Internal service, no getter
continue;
}
[ $service, $class ] = $case;
$getterCases[$name] = [
'get' . $service,
$class,
in_array( $service, $this->deprecatedServices )
];
}
return $getterCases;
}
/**
* @dataProvider provideGetters
*/
public function testGetters( $getter, $type, $isDeprecated = false ) {
if ( $isDeprecated ) {
$this->hideDeprecated( MediaWikiServices::class . "::$getter" );
}
// Test against the default instance, since the dummy will not know the default services.
$services = MediaWikiServices::getInstance();
$service = $services->$getter();
$this->assertInstanceOf( $type, $service );
}
public function provideGetService() {
global $IP;
$serviceList = require "$IP/includes/ServiceWiring.php";
$ret = [];
foreach ( $serviceList as $name => $callback ) {
$fun = new ReflectionFunction( $callback );
if ( !$fun->hasReturnType() ) {
throw new MWException( 'All service callbacks must have a return type defined, ' .
"none found for $name" );
}
$returnType = $fun->getReturnType();
$ret[$name] = [ $name, $returnType->getName() ];
}
return $ret;
}
/**
* @dataProvider provideGetService
*/
public function testGetService( $name, $type ) {
// Test against the default instance, since the dummy will not know the default services.
$services = MediaWikiServices::getInstance();
$service = $services->getService( $name );
$this->assertInstanceOf( $type, $service );
}
public function testDefaultServiceInstantiation() {
// Check all services in the default instance, not a dummy instance!
// Note that we instantiate all services here, including any that
// were registered by extensions.
$services = MediaWikiServices::getInstance();
$names = $services->getServiceNames();
foreach ( $names as $name ) {
$this->assertTrue( $services->hasService( $name ) );
$service = $services->getService( $name );
$this->assertIsObject( $service );
}
}
public function testDefaultServiceWiringServicesHaveTests() {
global $IP;
$testedServices = array_keys( $this->provideGetService() );
$allServices = array_keys( require "$IP/includes/ServiceWiring.php" );
$this->assertEquals(
[],
array_diff( $allServices, $testedServices ),
'The following services have not been added to MediaWikiServicesTest::provideGetService'
);
}
public function testGettersAreSorted() {
$methods = ( new ReflectionClass( MediaWikiServices::class ) )
->getMethods( ReflectionMethod::IS_STATIC | ReflectionMethod::IS_PUBLIC );
$names = array_map( static function ( $method ) {
return $method->getName();
}, $methods );
$serviceNames = array_map( static function ( $name ) {
return "get$name";
}, array_keys( $this->provideGetService() ) );
$names = array_values( array_filter( $names, static function ( $name ) use ( $serviceNames ) {
return in_array( $name, $serviceNames );
} ) );
$sortedNames = $names;
natcasesort( $sortedNames );
$this->assertSame( $sortedNames, $names,
'Please keep service getters sorted alphabetically' );
}
}