gplx/mediawiki/tests/phpunit/unit/includes/HookContainer/HookContainerTest.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;
}
}
}