1183 lines
35 KiB
PHP
1183 lines
35 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\HookContainer {
|
|
|
|
use Error;
|
|
use InvalidArgumentException;
|
|
use LogicException;
|
|
use MediaWiki\HookContainer\HookContainer;
|
|
use MediaWiki\HookContainer\StaticHookRegistry;
|
|
use MediaWiki\Tests\Unit\DummyServicesTrait;
|
|
use MediaWikiUnitTestCase;
|
|
use stdClass;
|
|
use UnexpectedValueException;
|
|
use Wikimedia\ScopedCallback;
|
|
use Wikimedia\TestingAccessWrapper;
|
|
|
|
class HookContainerTest extends MediaWikiUnitTestCase {
|
|
use DummyServicesTrait;
|
|
|
|
private const HANDLER_FUNCTION = FooClass::class . '::fooStaticMethod';
|
|
|
|
private const HANDLER_REGISTRATION = [
|
|
'extensionPath' => __DIR__,
|
|
'handler' => [
|
|
'name' => 'TestHookHandler',
|
|
'class' => 'FooExtension\Hooks'
|
|
]
|
|
];
|
|
|
|
/**
|
|
* Creates a new hook container with StaticHookRegistry and empty ObjectFactory
|
|
*
|
|
* @param null|array $oldHooks
|
|
* @param null|array $newHooks
|
|
* @param array $deprecatedHooksArray
|
|
*
|
|
* @return HookContainer
|
|
*/
|
|
private function newHookContainer(
|
|
$oldHooks = null, $newHooks = null, $deprecatedHooksArray = []
|
|
) {
|
|
if ( $oldHooks === null ) {
|
|
$oldHooks[ 'FoobarActionComplete' ][] = static function ( &$called ) {
|
|
$called[] = 11;
|
|
};
|
|
}
|
|
if ( $newHooks === null ) {
|
|
$handler = [ 'handler' => [
|
|
'name' => 'FooExtension-FooActionHandler',
|
|
'class' => 'FooExtension\\Hooks',
|
|
'services' => [] ]
|
|
];
|
|
$newHooks = [ 'FooActionComplete' => [ $handler ] ];
|
|
}
|
|
|
|
// fake object factory
|
|
$objectFactory = $this->getDummyObjectFactory(
|
|
[
|
|
'SomeService' => static function () {
|
|
return new stdClass();
|
|
}
|
|
]
|
|
);
|
|
|
|
$registry = new StaticHookRegistry( $oldHooks, $newHooks, $deprecatedHooksArray );
|
|
$hookContainer = new HookContainer( $registry, $objectFactory );
|
|
return $hookContainer;
|
|
}
|
|
|
|
public static function provideRegister() {
|
|
return [
|
|
'function' => [ 'strtoupper', 'strtoupper' ],
|
|
'object' => [ new \FooExtension\Hooks(), 'FooExtension\Hooks::onFooActionComplete' ],
|
|
'object and method' => [ [ new FooClass(), 'fooMethod' ], 'MediaWiki\Tests\HookContainer\FooClass::fooMethod' ],
|
|
'extension' => [
|
|
self::HANDLER_REGISTRATION,
|
|
'FooExtension\Hooks::onFooActionComplete'
|
|
],
|
|
'callable referencing a class that extends an unknown class' => [
|
|
[ 'MediaWiki\Tests\BrokenClass', 'aMethod' ],
|
|
'MediaWiki\Tests\BrokenClass::aMethod'
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::register
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRegister
|
|
*/
|
|
public function testRegister( $handler, $expected ) {
|
|
$hookContainer = $this->newHookContainer( [], [
|
|
'FooActionComplete' => [ $handler ]
|
|
], [] );
|
|
|
|
$handlers = $hookContainer->getHandlerDescriptions( 'FooActionComplete' );
|
|
|
|
$this->assertSame( $expected, $handlers[0] );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerDescriptions
|
|
*/
|
|
public function testGetHandlerDescriptions() {
|
|
$handler = 'MediaWiki\Tests\HookContainer\FooClass::fooStaticMethod';
|
|
$expected = [ $handler ];
|
|
|
|
$hookContainer = $this->newHookContainer(
|
|
[
|
|
'BarActionComplete' => [ $handler ]
|
|
],
|
|
[
|
|
'FooActionComplete' => [ $handler ]
|
|
], [] );
|
|
|
|
$this->assertSame( $expected, $hookContainer->getHandlerDescriptions( 'FooActionComplete' ) );
|
|
$this->assertSame( $expected, $hookContainer->getHandlerDescriptions( 'BarActionComplete' ) );
|
|
|
|
// Fire the hooks, then check again
|
|
$hookContainer->run( 'FooActionComplete', [ 1 ] );
|
|
$hookContainer->run( 'BarActionComplete', [ 1 ] );
|
|
|
|
$this->assertSame( $expected, $hookContainer->getHandlerDescriptions( 'FooActionComplete' ) );
|
|
$this->assertSame( $expected, $hookContainer->getHandlerDescriptions( 'BarActionComplete' ) );
|
|
}
|
|
|
|
/**
|
|
* Values returned: hook, handlersToRegister, expectedReturn
|
|
*/
|
|
public static function provideGetHandlerDescriptions() {
|
|
return [
|
|
'NoHandlersExist' => [
|
|
'MWTestHook',
|
|
null,
|
|
0,
|
|
0
|
|
],
|
|
'SuccessfulHandlerReturn' => [
|
|
'FooActionComplete',
|
|
[
|
|
'handler' => [
|
|
'name' => 'FooExtension-FooActionHandler',
|
|
'class' => 'FooExtension\\Hooks',
|
|
'services' => [],
|
|
],
|
|
],
|
|
1,
|
|
1
|
|
],
|
|
'SkipDeprecated' => [
|
|
'FooActionCompleteDeprecated',
|
|
[
|
|
'handler' => [
|
|
'name' => 'FooExtension-FooActionHandler',
|
|
'class' => 'FooExtension\\Hooks',
|
|
'services' => [],
|
|
],
|
|
'deprecated' => true,
|
|
],
|
|
1,
|
|
0
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::salvage
|
|
*/
|
|
public function testSalvage() {
|
|
$firstHookContainer = $this->newHookContainer( [], [] );
|
|
$secondHookContainer = $this->newHookContainer();
|
|
|
|
$firstHookContainer->register( 'TestHook', self::HANDLER_FUNCTION );
|
|
|
|
$secondHookContainer->salvage( $firstHookContainer );
|
|
|
|
$this->assertTrue( $secondHookContainer->isRegistered( 'TestHook' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::salvage
|
|
*/
|
|
public function testSalvageThrows() {
|
|
$firstHookContainer = $this->newHookContainer( [], [] );
|
|
$secondHookContainer = $this->newHookContainer();
|
|
|
|
$secondHookContainer->register( 'TestHook', self::HANDLER_FUNCTION );
|
|
|
|
$this->expectException( LogicException::class );
|
|
$secondHookContainer->salvage( $firstHookContainer );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
|
|
* @covers \MediaWiki\HookContainer\HookContainer::register
|
|
* @covers \MediaWiki\HookContainer\HookContainer::clear
|
|
*/
|
|
public function testIsRegistered() {
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'XyzHook' => [ self::HANDLER_FUNCTION ] ],
|
|
[ 'MWTestHook' => [ self::HANDLER_REGISTRATION ] ],
|
|
);
|
|
|
|
$hookContainer->register( 'AbcHook', self::HANDLER_FUNCTION );
|
|
|
|
$this->assertFalse( $hookContainer->isRegistered( 'XyzzyHook' ) );
|
|
|
|
$this->assertTrue( $hookContainer->isRegistered( 'XyzHook' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'MWTestHook' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'AbcHook' ) );
|
|
|
|
$hookContainer->clear( 'AbcHook' );
|
|
$hookContainer->clear( 'XyzHook' );
|
|
$hookContainer->clear( 'MWTestHook' );
|
|
|
|
$this->assertFalse( $hookContainer->isRegistered( 'XyzHook' ) );
|
|
$this->assertFalse( $hookContainer->isRegistered( 'MWTestHook' ) );
|
|
$this->assertFalse( $hookContainer->isRegistered( 'AbcHook' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
|
|
*/
|
|
public function testScopedRegister() {
|
|
$hookContainer = $this->newHookContainer();
|
|
$reset = $hookContainer->scopedRegister( 'MWTestHook', [ new FooClass(),
|
|
'fooMethod'
|
|
] );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'MWTestHook' ) );
|
|
ScopedCallback::consume( $reset );
|
|
$this->assertFalse( $hookContainer->isRegistered( 'MWTestHook' ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
|
|
*/
|
|
public function testScopedRegisterTwoHandlers() {
|
|
$hookContainer = $this->newHookContainer();
|
|
$called1 = $called2 = false;
|
|
$reset1 = $hookContainer->scopedRegister( 'MWTestHook',
|
|
static function () use ( &$called1 ) {
|
|
$called1 = true;
|
|
}
|
|
);
|
|
$reset2 = $hookContainer->scopedRegister( 'MWTestHook',
|
|
static function () use ( &$called2 ) {
|
|
$called2 = true;
|
|
}
|
|
);
|
|
$hookContainer->run( 'MWTestHook' );
|
|
$this->assertTrue( $called1 );
|
|
$this->assertTrue( $called2 );
|
|
|
|
$called1 = $called2 = false;
|
|
ScopedCallback::consume( $reset1 );
|
|
$hookContainer->run( 'MWTestHook' );
|
|
$this->assertFalse( $called1 );
|
|
$this->assertTrue( $called2 );
|
|
|
|
$called1 = $called2 = false;
|
|
ScopedCallback::consume( $reset2 );
|
|
$hookContainer->run( 'MWTestHook' );
|
|
$this->assertFalse( $called1 );
|
|
$this->assertFalse( $called2 );
|
|
}
|
|
|
|
/**
|
|
* Register handlers with scopedRegister() and register()
|
|
* @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
|
|
*/
|
|
public function testHandlersRegisteredWithScopedRegisterAndRegister() {
|
|
$hookContainer = $this->newHookContainer();
|
|
$numCalls = 0;
|
|
$hookContainer->register( 'MWTestHook', static function () use ( &$numCalls ) {
|
|
$numCalls++;
|
|
} );
|
|
$reset = $hookContainer->scopedRegister( 'MWTestHook', static function () use ( &$numCalls ) {
|
|
$numCalls++;
|
|
} );
|
|
|
|
// handlers registered in 2 different ways
|
|
$this->assertCount( 2, $hookContainer->getHandlerDescriptions( 'MWTestHook' ) );
|
|
$hookContainer->run( 'MWTestHook' );
|
|
$this->assertEquals( 2, $numCalls );
|
|
|
|
// Remove one of the handlers that increments $called
|
|
ScopedCallback::consume( $reset );
|
|
$this->assertCount( 1, $hookContainer->getHandlerDescriptions( 'MWTestHook' ) );
|
|
|
|
$numCalls = 0;
|
|
$hookContainer->run( 'MWTestHook' );
|
|
$this->assertSame( 1, $numCalls );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerDescriptions
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
|
* @dataProvider provideGetHandlerDescriptions
|
|
*/
|
|
public function testGetHandlers(
|
|
string $hook,
|
|
?array $handlerToRegister,
|
|
int $expectedDescriptions,
|
|
int $expectedCallbacks
|
|
) {
|
|
if ( $handlerToRegister ) {
|
|
$hooks = [ $hook => [ $handlerToRegister ] ];
|
|
} else {
|
|
$hooks = [];
|
|
}
|
|
$fakeDeprecatedHooks = [
|
|
'FooActionCompleteDeprecated' => [ 'deprecatedVersion' => '1.35' ]
|
|
];
|
|
$hookContainer = $this->newHookContainer( [], $hooks, $fakeDeprecatedHooks );
|
|
|
|
$descriptions = $hookContainer->getHandlerDescriptions( $hook );
|
|
$this->assertCount(
|
|
$expectedDescriptions,
|
|
$descriptions,
|
|
'getHandlerDescriptions()'
|
|
);
|
|
|
|
$this->expectDeprecationAndContinue( '/getHandlerCallbacks/' );
|
|
$callbacks = $hookContainer->getHandlerCallbacks( $hook );
|
|
$this->assertCount(
|
|
$expectedCallbacks,
|
|
$callbacks,
|
|
'getHandlerCallbacks()'
|
|
);
|
|
|
|
foreach ( $callbacks as $clbk ) {
|
|
$this->assertIsCallable( $clbk );
|
|
}
|
|
}
|
|
|
|
public static function provideRunConfigured() {
|
|
$fooObj = new FooClass();
|
|
$closure = static function ( &$count ) {
|
|
$count++;
|
|
};
|
|
$extra = 10;
|
|
return [
|
|
// Callables
|
|
'Function' => [ 'fooGlobalFunction' ],
|
|
'Object and method' => [ [ $fooObj, 'fooMethod' ] ],
|
|
'Class name and static method' => [ [ 'MediaWiki\Tests\HookContainer\FooClass', 'fooStaticMethod' ] ],
|
|
'static method' => [ 'MediaWiki\Tests\HookContainer\FooClass::fooStaticMethod' ],
|
|
'Closure' => [ $closure ],
|
|
|
|
// Shorthand
|
|
'Object' => [ $fooObj ],
|
|
|
|
// No-ops
|
|
'NOOP' => [ HookContainer::NOOP, 1 ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunConfigured
|
|
*/
|
|
public function testRunConfigured( $handler, $expectedCount = 2 ) {
|
|
$hookContainer = $this->newHookContainer( [ 'Increment' => [ $handler ] ] );
|
|
|
|
$count = 1;
|
|
$hookValue = $hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertTrue( $hookValue );
|
|
$this->assertSame( $expectedCount, $count );
|
|
}
|
|
|
|
public static function provideRunDeprecatedStyle() {
|
|
$fooObj = new FooClass();
|
|
$closure = static function ( &$count ) {
|
|
$count++;
|
|
};
|
|
$extra = 10;
|
|
return [
|
|
// Handlers with extra data attached
|
|
'static method with extra data' => [
|
|
[ 'MediaWiki\Tests\HookContainer\FooClass::fooStaticMethodWithExtra', $extra ],
|
|
11
|
|
],
|
|
'Object and method with extra data' => [ [ [ $fooObj, 'fooMethodWithExtra' ], $extra ], 11 ],
|
|
'Function extra data' => [ [ 'fooGlobalFunctionWithExtra', $extra ], 11 ],
|
|
'Closure with extra data' => [
|
|
[
|
|
static function ( int $inc, &$count ) {
|
|
$count += $inc;
|
|
},
|
|
10
|
|
],
|
|
11
|
|
],
|
|
|
|
// No-ops
|
|
'empty array' => [ [], 1 ],
|
|
'null' => [ null, 1 ],
|
|
'false' => [ false, 1 ],
|
|
|
|
// Strange edge cases
|
|
'Object in array without method' => [ [ $fooObj ] ],
|
|
'Callable in array' => [ [ [ $fooObj, 'fooMethod' ] ] ],
|
|
'Closure in array with no extra data' => [ [ $closure ] ],
|
|
'Function in array' => [ [ 'fooGlobalFunction' ] ],
|
|
'Function in array in array' => [ [ [ 'fooGlobalFunction' ] ] ],
|
|
'static method as array in array' => [
|
|
[ [ 'MediaWiki\Tests\HookContainer\FooClass', 'fooStaticMethod' ] ]
|
|
],
|
|
'Object and fully-qualified non-static method' => [
|
|
[ $fooObj, 'MediaWiki\Tests\HookContainer\FooClass::fooMethod' ]
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunDeprecatedStyle
|
|
*/
|
|
public function testRunDeprecatedStyle( $handler, $expectedCount = 2 ) {
|
|
$hookContainer = $this->newHookContainer( [ 'Increment' => [ $handler ] ] );
|
|
|
|
$this->expectDeprecationAndContinue( '/Deprecated handler style/' );
|
|
|
|
$count = 1;
|
|
$hookValue = $hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertTrue( $hookValue );
|
|
$this->assertSame( $expectedCount, $count );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunConfigured
|
|
* @dataProvider provideRunExtensionHook
|
|
*/
|
|
public function testRegisterAndRun( $handler, $expectedCount = 2 ) {
|
|
$hookContainer = $this->newHookContainer( [], [] );
|
|
$hookContainer->register( 'Increment', $handler );
|
|
|
|
$count = 1;
|
|
$hookValue = $hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertTrue( $hookValue );
|
|
$this->assertSame( $expectedCount, $count );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunDeprecatedStyle
|
|
*/
|
|
public function testRegisterDeprecatedStyle( $handler ) {
|
|
$hookContainer = $this->newHookContainer( [], [] );
|
|
|
|
// Force the handler list to be initialized, so register() will normalize the handler immediately.
|
|
$hookContainer->run( 'Increment' );
|
|
|
|
$this->expectDeprecationAndContinue( '/Deprecated handler style for hook/' );
|
|
$hookContainer->register( 'Increment', $handler );
|
|
}
|
|
|
|
/**
|
|
* Values returned: hook, handler, handler arguments, options
|
|
*/
|
|
public static function provideRegisterAndRunCallback() {
|
|
$fooObj = new FooClass();
|
|
return [
|
|
// Callables
|
|
'Function' => [ 'fooGlobalFunction' ],
|
|
'Object and method' => [ [ $fooObj, 'fooMethod' ] ],
|
|
'Class name and static method' => [ [ 'MediaWiki\Tests\HookContainer\FooClass', 'fooStaticMethod' ] ],
|
|
'static method' => [ 'MediaWiki\Tests\HookContainer\FooClass::fooStaticMethod' ],
|
|
'Closure' => [
|
|
static function ( &$count ) {
|
|
$count++;
|
|
}
|
|
],
|
|
|
|
// Extension-style handler
|
|
'Extension handler' => [ self::HANDLER_REGISTRATION ],
|
|
|
|
// NOTE: hook handlers with extra data are not supported for callbacks!
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
|
* @dataProvider provideRegisterAndRunCallback
|
|
*/
|
|
public function testRegisterAndRunCallback( $handler, $expectedCount = 2 ) {
|
|
$hookContainer = $this->newHookContainer( [], [] );
|
|
$hookContainer->register( 'Increment', $handler );
|
|
|
|
$this->expectDeprecationAndContinue( '/getHandlerCallbacks/' );
|
|
|
|
$count = 1;
|
|
foreach ( $hookContainer->getHandlerCallbacks( 'Increment' ) as $callback ) {
|
|
$callback( $count );
|
|
}
|
|
$this->assertSame( $expectedCount, $count );
|
|
}
|
|
|
|
/**
|
|
* Values returned: hook, handler, handler arguments, options
|
|
*/
|
|
public static function provideRunExtensionHook() {
|
|
return [
|
|
[ self::HANDLER_REGISTRATION ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunExtensionHook
|
|
*/
|
|
public function testRunExtensionHook( array $handler, $expectedCount = 1 ) {
|
|
$hookContainer = $this->newHookContainer( [], [ 'X\\Y::Increment' => [ $handler ] ] );
|
|
|
|
$count = 0;
|
|
$hookValue = $hookContainer->run( 'X\\Y::Increment', [ &$count ] );
|
|
$this->assertTrue( $hookValue );
|
|
$this->assertSame( $expectedCount, $count );
|
|
}
|
|
|
|
public static function provideRunFailsWithNoService() {
|
|
$handler = self::HANDLER_REGISTRATION;
|
|
$handler['handler']['services'] = [ 'SomeService' ];
|
|
|
|
yield [ $handler ];
|
|
|
|
$handler = self::HANDLER_REGISTRATION;
|
|
$handler['handler']['optional_services'] = [ 'SomeService' ];
|
|
|
|
yield [ $handler ];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @dataProvider provideRunFailsWithNoService
|
|
*/
|
|
public function testRunFailsWithNoService( array $handler ) {
|
|
$hookContainer = $this->newHookContainer( [], [ 'Increment' => [ $handler ] ] );
|
|
|
|
$this->expectException( UnexpectedValueException::class );
|
|
|
|
$count = 0;
|
|
$options = [ 'noServices' => true ];
|
|
$hookContainer->run( 'Increment', [ &$count ], $options );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
*/
|
|
public function testRunOrder() {
|
|
$configured1 = static function ( &$seq ) {
|
|
$seq[] = 'configured1';
|
|
};
|
|
|
|
$configured2 = static function ( &$seq ) {
|
|
$seq[] = 'configured2';
|
|
};
|
|
|
|
$registered = static function ( &$seq ) {
|
|
$seq[] = 'registered';
|
|
};
|
|
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'Append' => [ $configured1, $configured2 ] ],
|
|
[ 'Append' => [ self::HANDLER_REGISTRATION ] ]
|
|
);
|
|
|
|
$hookContainer->register( 'Append', $registered );
|
|
|
|
$seq = [ 'start' ];
|
|
$hookContainer->run( 'Append', [ &$seq ] );
|
|
|
|
$expected = [ 'start', 'configured1', 'configured2', 'FooExtension', 'registered' ];
|
|
$this->assertSame( $expected, $seq );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* Test HookContainer::run() when the handler returns false
|
|
*/
|
|
public function testRunAbort() {
|
|
$handler1 = [ 'handler' => [
|
|
'name' => 'FooExtension-Abort1',
|
|
'class' => 'FooExtension\\AbortHooks1'
|
|
] ];
|
|
$handler2 = [ 'handler' => [
|
|
'name' => 'FooExtension-Abort2',
|
|
'class' => 'FooExtension\\AbortHooks2'
|
|
] ];
|
|
$handler3 = [ 'handler' => [
|
|
'name' => 'FooExtension-Abort3',
|
|
'class' => 'FooExtension\\AbortHooks3'
|
|
] ];
|
|
$hooks = [
|
|
'Abort' => [
|
|
$handler1,
|
|
$handler2,
|
|
$handler3
|
|
]
|
|
];
|
|
$hookContainer = $this->newHookContainer( [], $hooks );
|
|
$called = [];
|
|
$ret = $hookContainer->run( 'Abort', [ &$called ] );
|
|
$this->assertFalse( $ret );
|
|
$this->assertArrayEquals( [ 1, 2 ], $called );
|
|
}
|
|
|
|
public static function provideRegisterDeprecated() {
|
|
// registering a deprecated hook should trigger a warning
|
|
yield [ [ 'deprecatedVersion' => '1.0' ], true ];
|
|
|
|
// the silent flag should suppress the warning
|
|
yield [ [ 'deprecatedVersion' => '1.0', 'silent' => true ], false ];
|
|
}
|
|
|
|
/**
|
|
* Test HookContainer::register() successfully registers even when hook is deprecated.
|
|
* @covers \MediaWiki\HookContainer\HookContainer::register
|
|
* @dataProvider provideRegisterDeprecated
|
|
*/
|
|
public function testRegisterDeprecated( array $deprecationInfo, bool $expectWarning ) {
|
|
$deprecations = [ 'FooActionComplete' => $deprecationInfo ];
|
|
|
|
// Assert we don't get any deprecation warnings during initialization!
|
|
$this->newHookContainer(
|
|
[ 'FooActionComplete' => [ self::HANDLER_FUNCTION ] ],
|
|
[ 'FooActionComplete' => [ self::HANDLER_REGISTRATION ] ],
|
|
$deprecations
|
|
);
|
|
|
|
// Make a hook container with no hooks registered yet
|
|
$hookContainer = $this->newHookContainer( [], [], $deprecations );
|
|
|
|
// Expected deprecation?
|
|
if ( $expectWarning ) {
|
|
$this->expectDeprecationAndContinue( '/FooActionComplete hook/' );
|
|
}
|
|
|
|
$hookContainer->register( 'FooActionComplete', self::HANDLER_FUNCTION );
|
|
|
|
// Deprecated hooks should still be functional!
|
|
$this->assertTrue( $hookContainer->isRegistered( 'FooActionComplete' ) );
|
|
}
|
|
|
|
/**
|
|
* Test running deprecated hooks from $wgHooks with the deprecation declared in HookContainer.
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @dataProvider provideRegisterDeprecated
|
|
*/
|
|
public function testRunConfiguredDeprecated( array $deprecationInfo ) {
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'Increment' => [ self::HANDLER_FUNCTION ] ],
|
|
[],
|
|
[ 'Increment' => $deprecationInfo ]
|
|
);
|
|
|
|
// No warning expected when running the hook!
|
|
// Deprecated hooks should still be functional!
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertSame( 1, $count );
|
|
}
|
|
|
|
/**
|
|
* Test running deprecated hooks from $wgHooks with the deprecation passed in the options parameter.
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
* @dataProvider provideRegisterDeprecated
|
|
*/
|
|
public function testRunConfiguredDeprecatedWithOption( array $deprecationInfo, bool $expectWarning ) {
|
|
// Assert we don't get any deprecation warnings during initialization!
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'Increment' => [ self::HANDLER_FUNCTION ] ],
|
|
);
|
|
|
|
// Expected deprecation?
|
|
if ( $expectWarning ) {
|
|
$this->expectDeprecationAndContinue( '/Use of Increment hook/' );
|
|
}
|
|
|
|
// Deprecated hooks should still be functional!
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ], $deprecationInfo );
|
|
$this->assertSame( 1, $count );
|
|
}
|
|
|
|
/**
|
|
* Test running deprecated hooks from extensions.
|
|
*
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
*/
|
|
public function testRunHandlerObjectDeprecated() {
|
|
$deprecationInfo = [ 'deprecatedVersion' => '1.0' ];
|
|
|
|
// If the handler acknowledges deprecation, it should be skipped
|
|
$knownDeprecated = self::HANDLER_REGISTRATION + [ 'deprecated' => true ];
|
|
|
|
$hookContainer = $this->newHookContainer(
|
|
[],
|
|
[ 'Increment' => [ self::HANDLER_REGISTRATION, $knownDeprecated ] ],
|
|
[ 'Increment' => $deprecationInfo ]
|
|
);
|
|
|
|
// Deprecated hooks should be functional, the handle that acknowledges deprecation should be skipped.
|
|
// We do not expect deprecation warnings here. They are covered by emitDeprecationWarnings()
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertSame( 1, $count );
|
|
}
|
|
|
|
/**
|
|
* Test running deprecated hooks from extensions.
|
|
*
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
*/
|
|
public function testRunHandlerObjectDeprecatedWithOption() {
|
|
$deprecationInfo = [ 'deprecatedVersion' => '1.0' ];
|
|
|
|
// If the handler acknowledges deprecation, it should be skipped
|
|
$knownDeprecated = self::HANDLER_REGISTRATION + [ 'deprecated' => true ];
|
|
|
|
$hookContainer = $this->newHookContainer(
|
|
[],
|
|
[ 'Increment' => [ self::HANDLER_REGISTRATION, $knownDeprecated ] ],
|
|
[ 'Increment' => $deprecationInfo ]
|
|
);
|
|
|
|
// We do expect deprecation warnings when the 'deprecationVersion' key is provided in the $options parameter.
|
|
$this->expectDeprecationAndContinue( '/Use of Increment hook/' );
|
|
|
|
// Deprecated hooks should be functional, the handle that acknowledges deprecation should be skipped.
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ], $deprecationInfo );
|
|
$this->assertSame( 1, $count );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHookNames
|
|
*/
|
|
public function testGetHookNames() {
|
|
$fooHandler = [ 'handler' => [
|
|
'name' => 'FooHookHandler',
|
|
'class' => 'FooExtension\\Hooks'
|
|
] ];
|
|
|
|
$noop = static function () {
|
|
// noop
|
|
};
|
|
|
|
$container = $this->newHookContainer(
|
|
[
|
|
'A' => [ $noop ]
|
|
],
|
|
[
|
|
'B' => [ $fooHandler ]
|
|
]
|
|
);
|
|
|
|
$container->register( 'C', 'strtoupper' );
|
|
|
|
// Ask for a few hooks that have no handlers.
|
|
// Negative caching inside HookHandler should not cause them to be returned from getHookNames
|
|
$container->isRegistered( 'X' );
|
|
|
|
$this->expectDeprecationAndContinue( '/getHandlerCallbacks/' );
|
|
$container->getHandlerCallbacks( 'Y' );
|
|
|
|
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getHookNames() );
|
|
|
|
// make sure we are getting each hook name only once
|
|
$container->register( 'B', 'strtoupper' );
|
|
$container->register( 'A', 'strtoupper' );
|
|
|
|
$this->assertArrayEquals( [ 'A', 'B', 'C' ], $container->getHookNames() );
|
|
}
|
|
|
|
/**
|
|
* Values returned: hook, handlersToRegister, options
|
|
*/
|
|
public static function provideRunErrors() {
|
|
// XXX: should also fail: non-function string, empty array
|
|
return [
|
|
'return a string' => [
|
|
static function () {
|
|
return 'string';
|
|
},
|
|
[]
|
|
],
|
|
'abort even though not abortable' => [
|
|
static function () {
|
|
return false;
|
|
},
|
|
[ 'abortable' => false ]
|
|
],
|
|
'callable referencing a class that extends an unknown class' => [
|
|
[ 'MediaWiki\\Tests\\BrokenClass2', 'aMethod' ],
|
|
[],
|
|
Error::class
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRunErrors
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* Test errors thrown with invalid handlers
|
|
*/
|
|
public function testRunErrors( $handler, $options, $expected = UnexpectedValueException::class ) {
|
|
$hookContainer = $this->newHookContainer();
|
|
$hookContainer->register( 'MWTestHook', $handler );
|
|
|
|
$this->filterDeprecated( '/^Returning a string from a hook handler/' );
|
|
$this->expectException( $expected );
|
|
$hookContainer->run( 'MWTestHook', [], $options );
|
|
}
|
|
|
|
/**
|
|
* Values returned: hook, handlersToRegister, options
|
|
*/
|
|
public static function provideRegisterErrors() {
|
|
// XXX: should also fail: non-function string, empty array
|
|
return [
|
|
'a number' => [ 123 ],
|
|
'non-callable string' => [ 'a, b, c' ],
|
|
'array referencing an unknown method' => [ [ self::class, 'thisMethodDoesNotExist' ] ],
|
|
'empty string' => [ '' ],
|
|
'zero' => [ 0 ],
|
|
'true' => [ true ],
|
|
'callable referencing an unknown class' => [
|
|
[ 'FooExtension\DoesNotExist', 'onFoo' ],
|
|
'FooExtension\DoesNotExist::onFoo'
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRegisterErrors
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @covers \MediaWiki\HookContainer\HookContainer::register
|
|
*/
|
|
public function testRegisterErrors( $badHandler ) {
|
|
$hookContainer = $this->newHookContainer();
|
|
|
|
// Force the handler list to be initialized, so register() will normalize the handler immediately.
|
|
$hookContainer->run( 'MWTestHook' );
|
|
|
|
$this->expectException( InvalidArgumentException::class );
|
|
$hookContainer->register( 'MWTestHook', $badHandler );
|
|
}
|
|
|
|
/**
|
|
* @dataProvider provideRegisterErrors
|
|
* @covers \MediaWiki\HookContainer\HookContainer::normalizeHandler
|
|
* @covers \MediaWiki\HookContainer\HookContainer::run
|
|
*/
|
|
public function testRunWithBadHandlers( $badHandler ) {
|
|
$goodHandler = self::HANDLER_FUNCTION;
|
|
$hookContainer = $this->newHookContainer( [ 'MWTestHook' => [ $badHandler, $goodHandler ] ] );
|
|
|
|
// Bad handlers from the constructor should fail silently
|
|
$count = 0;
|
|
$hookContainer->run( 'MWTestHook', [ &$count ] );
|
|
|
|
$this->assertSame( 1, $count );
|
|
}
|
|
|
|
public static function provideEmitDeprecationWarnings() {
|
|
yield 'Deprecated extension hook' => [
|
|
'$oldHooks' => [],
|
|
'$newHooks' => [ self::HANDLER_REGISTRATION ],
|
|
'$deprecationInfo' => [ 'deprecatedVersion' => '1.35' ],
|
|
'$expectWarning' => true,
|
|
];
|
|
|
|
yield 'Deprecated extension hook, silent' => [
|
|
'$oldHooks' => [],
|
|
'$newHooks' => [ self::HANDLER_REGISTRATION ],
|
|
'$deprecationInfo' => [ 'deprecatedVersion' => '1.35', 'silent' => true ],
|
|
'$expectWarning' => false,
|
|
];
|
|
|
|
yield 'Deprecated extension hook, acknowledged' => [
|
|
'$oldHooks' => [],
|
|
'$newHooks' => [ self::HANDLER_REGISTRATION + [ 'deprecated' => true ] ],
|
|
'$deprecationInfo' => [ 'deprecatedVersion' => '1.35' ],
|
|
'$expectWarning' => false,
|
|
];
|
|
|
|
yield 'Deprecated configured hook' => [
|
|
'$oldHooks' => [ self::HANDLER_FUNCTION ],
|
|
'$newHooks' => [],
|
|
'$deprecationInfo' => [ 'deprecatedVersion' => '1.35' ],
|
|
'$expectWarning' => false, // NOTE: Currently expected to be ignored. This may change.
|
|
];
|
|
|
|
yield 'Deprecated configured hook, silent' => [
|
|
'$oldHooks' => [ self::HANDLER_FUNCTION ],
|
|
'$newHooks' => [],
|
|
'$deprecationInfo' => [ 'deprecatedVersion' => '1.35', 'silent' => true ],
|
|
'$expectWarning' => false,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::emitDeprecationWarnings
|
|
* @dataProvider provideEmitDeprecationWarnings
|
|
*/
|
|
public function testEmitDeprecationWarnings( $oldHandlers, $newHandlers, $deprecationInfo, $expectWarning ) {
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'FooActionComplete' => $oldHandlers ],
|
|
[ 'FooActionComplete' => $newHandlers ],
|
|
[ 'FooActionComplete' => $deprecationInfo ]
|
|
);
|
|
|
|
if ( $expectWarning ) {
|
|
$this->expectDeprecationAndContinue( '/Hook FooActionComplete was deprecated/' );
|
|
}
|
|
|
|
$hookContainer->emitDeprecationWarnings();
|
|
$this->addToAssertionCount( 1 );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::emitDeprecationWarnings
|
|
*/
|
|
public function testEmitDeprecationWarningsSilent() {
|
|
$hooks = [
|
|
'FooActionComplete' => [
|
|
[
|
|
'handler' => 'fooGlobalFunction',
|
|
'extensionPath' => 'fake-extension.json'
|
|
]
|
|
]
|
|
];
|
|
$deprecatedHooksArray = [
|
|
'FooActionComplete' => [
|
|
'deprecatedVersion' => '1.35',
|
|
'silent' => true
|
|
]
|
|
];
|
|
$hookContainer = $this->newHookContainer( [], $hooks, $deprecatedHooksArray );
|
|
$hookContainer->emitDeprecationWarnings();
|
|
$this->assertTrue( true );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::isRegistered
|
|
* @covers \MediaWiki\HookContainer\HookContainer::getHandlerCallbacks
|
|
* @covers \MediaWiki\HookContainer\HookContainer::register
|
|
* @covers \MediaWiki\HookContainer\HookContainer::clear
|
|
*/
|
|
public function testClear() {
|
|
$increment = [ new \FooExtension\Hooks(), 'onIncrement' ];
|
|
|
|
$hookContainer = $this->newHookContainer(
|
|
[ 'Increment' => [ $increment ], 'XyzHook' => [ self::HANDLER_FUNCTION ], ],
|
|
[ 'Increment' => [ self::HANDLER_REGISTRATION ], 'FooActionComplete' => [ self::HANDLER_REGISTRATION ] ],
|
|
);
|
|
|
|
$hookContainer->register( 'AbcHook', self::HANDLER_FUNCTION );
|
|
$hookContainer->register( 'Increment', static function ( &$count ) {
|
|
$count++;
|
|
} );
|
|
|
|
// Check: all three handlers should be called initially.
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertSame( 3, $count );
|
|
|
|
$hookContainer->clear( 'Increment' );
|
|
|
|
$this->assertFalse( $hookContainer->isRegistered( 'Increment' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'AbcHook' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'XyzHook' ) );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'FooActionComplete' ) );
|
|
|
|
$this->assertCount( 0, $hookContainer->getHandlerDescriptions( 'Increment' ) );
|
|
$this->assertNotEmpty( $hookContainer->getHandlerDescriptions( 'AbcHook' ) );
|
|
$this->assertNotEmpty( $hookContainer->getHandlerDescriptions( 'FooActionComplete' ) );
|
|
$this->assertNotEmpty( $hookContainer->getHandlerDescriptions( 'XyzHook' ) );
|
|
|
|
// No more increment!
|
|
$hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertSame( 3, $count );
|
|
|
|
// When adding a handler again...
|
|
$hookContainer->register( 'Increment', static function ( &$count ) {
|
|
$count = 11;
|
|
} );
|
|
|
|
// ...the new handler should be called, but not the old ones.
|
|
$count = 0;
|
|
$hookContainer->run( 'Increment', [ &$count ] );
|
|
$this->assertSame( 11, $count );
|
|
$this->assertTrue( $hookContainer->isRegistered( 'Increment' ) );
|
|
}
|
|
|
|
public static function provideMayBeCallable() {
|
|
yield 'function' => [
|
|
'strtoupper',
|
|
];
|
|
yield 'closure' => [
|
|
static function () {
|
|
// noop
|
|
},
|
|
];
|
|
yield 'object and method' => [
|
|
[ new FooClass(), 'fooMethod' ],
|
|
];
|
|
yield 'static method as array' => [
|
|
[ FooClass::class, 'fooStaticMethod', ],
|
|
];
|
|
yield 'static method as string' => [
|
|
'MediaWiki\Tests\HookContainer\FooClass::fooStaticMethod',
|
|
];
|
|
yield 'callable referencing a class that extends an unknown class' => [
|
|
[ 'MediaWiki\Tests\BrokenClass3', 'aMethod' ],
|
|
];
|
|
}
|
|
|
|
public static function provideNotCallable() {
|
|
yield 'object' => [
|
|
new \FooExtension\Hooks(),
|
|
];
|
|
yield 'object and non-existing method' => [
|
|
[ new FooClass(), 'noSuchMethod' ],
|
|
];
|
|
yield 'object and method and extra stuff' => [
|
|
[ new FooClass(), 'fooMethod', 'extra', 'stuff' ],
|
|
];
|
|
yield 'object and method assoc' => [
|
|
[ 'a' => new FooClass(), 'b' => 'fooMethod' ],
|
|
];
|
|
yield 'object and method nested in array' => [
|
|
[ [ new FooClass(), 'fooMethod' ], 'whatever' ],
|
|
];
|
|
yield 'non-existing static method on existing class' => [
|
|
'MediaWiki\Tests\HookContainer\FooClass::noSuchMethod',
|
|
];
|
|
yield 'global function with extra data in array' => [
|
|
[ 'strtoupper', 'extra' ],
|
|
];
|
|
yield 'non-existing static method on existing class as array' => [
|
|
[ FooClass::class, 'noSuchMethod' ],
|
|
];
|
|
yield 'non-function text' => [
|
|
'just some text',
|
|
];
|
|
yield 'object in array with no method' => [
|
|
[ new \FooExtension\Hooks() ],
|
|
];
|
|
yield 'callable referencing an unknown class' => [
|
|
[ 'FooExtension\DoesNotExist', 'onFoo' ],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::mayBeCallable
|
|
* @dataProvider provideMayBeCallable
|
|
*/
|
|
public function testMayBeCallable_true( $v ) {
|
|
$access = TestingAccessWrapper::newFromClass( HookContainer::class );
|
|
$this->assertTrue( $access->mayBeCallable( $v ) );
|
|
}
|
|
|
|
/**
|
|
* @covers \MediaWiki\HookContainer\HookContainer::mayBeCallable
|
|
* @dataProvider provideNotCallable
|
|
*/
|
|
public function testMayBeCallable_false( $v ) {
|
|
$access = TestingAccessWrapper::newFromClass( HookContainer::class );
|
|
$this->assertFalse( $access->mayBeCallable( $v ) );
|
|
}
|
|
}
|
|
|
|
// Mock class for different types of handler functions
|
|
class FooClass {
|
|
|
|
public function fooMethod( &$count ) {
|
|
$count++;
|
|
return true;
|
|
}
|
|
|
|
public function onIncrement( &$count ) {
|
|
$count++;
|
|
}
|
|
|
|
public static function fooStaticMethod( &$count ) {
|
|
$count++;
|
|
return null;
|
|
}
|
|
|
|
public function fooMethodWithExtra( int $inc, &$count ) {
|
|
$count += $inc;
|
|
return true;
|
|
}
|
|
|
|
public static function fooStaticMethodWithExtra( int $inc, &$count ) {
|
|
$count += $inc;
|
|
return null;
|
|
}
|
|
|
|
public static function fooMethodReturnValueError() {
|
|
return 'a string';
|
|
}
|
|
|
|
public static function onMWTestHook() {
|
|
// noop
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Function in global namespace
|
|
namespace {
|
|
|
|
function fooGlobalFunction( &$count ) {
|
|
$count++;
|
|
return true;
|
|
}
|
|
|
|
function fooGlobalFunctionWithExtra( $inc, &$count ) {
|
|
$count += $inc;
|
|
}
|
|
|
|
}
|
|
|
|
// Mock Extension
|
|
namespace FooExtension {
|
|
|
|
class Hooks {
|
|
|
|
public function onFooActionComplete() {
|
|
return true;
|
|
}
|
|
|
|
public function onMWTest() {
|
|
// noop
|
|
}
|
|
|
|
public function onIncrement( &$count ) {
|
|
$count++;
|
|
}
|
|
|
|
public function onX_Y__Increment( &$count ) {
|
|
$count++;
|
|
}
|
|
|
|
public function onAppend( &$list ) {
|
|
$list[] = 'FooExtension';
|
|
}
|
|
}
|
|
|
|
class AbortHooks1 {
|
|
public function onAbort( &$called ) {
|
|
$called[] = 1;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class AbortHooks2 {
|
|
public function onAbort( &$called ) {
|
|
$called[] = 2;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
class AbortHooks3 {
|
|
public function onAbort( &$called ) {
|
|
$called[] = 3;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
}
|