PK       ! 6k      DeprecatedHooksTest.phpnu Iw        <?php

namespace MediaWiki\Tests\HookContainer;

use MediaWiki\HookContainer\DeprecatedHooks;
use MediaWikiUnitTestCase;
use Wikimedia\TestingAccessWrapper;

class DeprecatedHooksTest extends MediaWikiUnitTestCase {

	/**
	 * @covers \MediaWiki\HookContainer\DeprecatedHooks::__construct
	 * @covers \MediaWiki\HookContainer\DeprecatedHooks::isHookDeprecated
	 */
	public function testIsHookDeprecated() {
		$extDeprecatedHooks = [
			'FooBaz' => [ 'deprecatedVersion' => '1.35', 'component' => 'ComponentFooBaz' ]
		];
		$deprecatedHooks = new DeprecatedHooks( $extDeprecatedHooks );
		$this->assertTrue( $deprecatedHooks->isHookDeprecated( 'FooBaz' ) );
		$this->assertFalse( $deprecatedHooks->isHookDeprecated( 'FooBazBar' ) );
	}

	/**
	 * @covers \MediaWiki\HookContainer\DeprecatedHooks::markDeprecated
	 */
	public function testMarkDeprecatedException() {
		$extDeprecatedHooks = [
			'FooBaz' => [ 'deprecatedVersion' => '1.35', 'component' => 'ComponentFooBaz' ]
		];
		$deprecatedHooks = new DeprecatedHooks( $extDeprecatedHooks );
		$this->expectExceptionMessage(
			"Cannot mark hook 'FooBaz' deprecated with version 1.31. " .
			"It is already marked deprecated with version 1.35"
		);
		$deprecatedHooks->markDeprecated( 'FooBaz', '1.31' );
	}

	/**
	 * @covers \MediaWiki\HookContainer\DeprecatedHooks::markDeprecated
	 */
	public function testMarkDeprecated() {
		$deprecatedHooks = new DeprecatedHooks();
		$deprecatedHooks->markDeprecated( 'FooBaz', '1.31', 'ComponentFooBaz' );
		$allDeprecated = $deprecatedHooks->getDeprecationInfo();
		$this->assertArrayHasKey( 'FooBaz', $allDeprecated );
		$this->assertEquals(
			[
				'deprecatedVersion' => '1.31',
				'component' => 'ComponentFooBaz',
				'silent' => false
			],
			$allDeprecated['FooBaz']
		);
	}

	/**
	 * @covers \MediaWiki\HookContainer\DeprecatedHooks::getDeprecationInfo
	 */
	public function testGetDeprecationInfo() {
		$extDeprecatedHooks = [
			'FooBar' => [ 'deprecatedVersion' => '1.21', 'component' => 'ComponentFooBar' ],
			'FooBarBaz' => [ 'deprecatedVersion' => '1.21' ],
			'SoftlyDeprecated' => [
				'deprecatedVersion' => '1.21',
				'component' => 'ComponentFooBar',
				'silent' => true
			]
		];
		$deprecatedHooksWrapper = TestingAccessWrapper::newFromObject( new DeprecatedHooks() );
		$preRegisteredDeprecated = $deprecatedHooksWrapper->deprecatedHooks;
		$deprecatedHooks = new DeprecatedHooks( $extDeprecatedHooks );
		$hookDeprecationInfo = $deprecatedHooks->getDeprecationInfo( 'FooBar' );

		$this->assertNull( $deprecatedHooks->getDeprecationInfo( 'FooBazBaz' ) );
		$this->assertEquals(
			[
				'deprecatedVersion' => '1.21',
				'component' => 'ComponentFooBar',
				'silent' => false,
			],
			$hookDeprecationInfo
		);

		$this->assertEquals(
			[
				'deprecatedVersion' => '1.21',
				'component' => 'ComponentFooBar',
				'silent' => true,
			],
			$deprecatedHooks->getDeprecationInfo( 'SoftlyDeprecated' )
		);

		$this->assertCount(
			count( $preRegisteredDeprecated ) + count( $extDeprecatedHooks ),
			$deprecatedHooks->getDeprecationInfo()
		);
	}
}
PK       ! Y5`  `    HookRunnerTestBase.phpnu Iw        <?php

namespace MediaWiki\Tests\HookContainer;

use Generator;
use MediaWiki\HookContainer\HookContainer;
use MediaWikiUnitTestCase;
use ReflectionClass;
use ReflectionMethod;
use ReflectionParameter;

/**
 * Tests that all arguments passed into HookRunner are passed along to HookContainer.
 * @stable to extend
 * @since 1.36
 */
abstract class HookRunnerTestBase extends MediaWikiUnitTestCase {
	/**
	 * @return Generator|array
	 */
	// abstract public static function provideHookRunners();

	/**
	 * Temporary override to make provideHookRunners static.
	 * See T332865.
	 *
	 * @return Generator|array
	 */
	final public static function provideHookRunnersStatically() {
		$reflectionMethod = new ReflectionMethod( static::class, 'provideHookRunners' );
		if ( $reflectionMethod->isStatic() ) {
			return $reflectionMethod->invoke( null );
		}

		trigger_error(
			'overriding provideHookRunners as an instance method is deprecated. (' .
			$reflectionMethod->getFileName() . ':' . $reflectionMethod->getEndLine() . ')',
			E_USER_DEPRECATED
		);

		return $reflectionMethod->invoke( new static() );
	}

	/**
	 * @dataProvider provideHookRunnersStatically
	 */
	public function testAllMethodsInheritedFromInterface( string $hookRunnerClass ) {
		$hookRunnerReflectionClass = new ReflectionClass( $hookRunnerClass );
		$hookMethods = $hookRunnerReflectionClass->getMethods();
		$hookInterfaces = $hookRunnerReflectionClass->getInterfaces();
		foreach ( $hookMethods as $method ) {
			if ( $method->isConstructor() ) {
				continue;
			}
			$interfacesWithMethod = array_filter(
				$hookInterfaces,
				static function ( ReflectionClass $interface ) use ( $method ) {
					return $interface->hasMethod( $method->getName() );
				}
			);
			$this->assertCount( 1, $interfacesWithMethod,
				'Exactly one hook interface must have method ' . $method->getName() );
		}
	}

	/**
	 * @dataProvider provideHookRunnersStatically
	 */
	public function testHookInterfacesConvention( string $hookRunnerClass ) {
		$hookRunnerReflectionClass = new ReflectionClass( $hookRunnerClass );
		$hookInterfaces = $hookRunnerReflectionClass->getInterfaces();
		$hookMethods = [];
		foreach ( $hookInterfaces as $interface ) {
			$name = $interface->getName();

			$this->assertStringEndsWith( 'Hook', $name,
				"Interface name '$name' must have the suffix 'Hook'." );

			$methods = $interface->getMethods();
			$this->assertCount( 1, $methods,
				'Hook interface should have one method' );

			$method = $methods[0];
			$methodName = $method->getName();
			$this->assertStringStartsWith( 'on', $methodName,
				"Interface method '$methodName' must have prefix 'on'." );
			$this->assertTrue( $method->isPublic(), "Interface method '$methodName' should public" );
			$this->assertFalse( $method->isStatic(), "Interface method '$methodName' should not static." );

			$hookMethods[] = $methodName;
		}
		$this->assertArrayEquals( $hookMethods, array_unique( $hookMethods ) );
	}

	public static function provideHookMethods() {
		foreach ( self::provideHookRunnersStatically() as $name => [ $hookRunnerClass ] ) {
			$hookRunnerReflectionClass = new ReflectionClass( $hookRunnerClass );
			foreach ( $hookRunnerReflectionClass->getInterfaces() as $hookInterface ) {
				yield $name . ':' . $hookInterface->getName()
					=> [ $hookRunnerClass, $hookInterface->getMethods()[0] ];
			}
		}
	}

	/**
	 * @dataProvider provideHookMethods
	 */
	public function testHookContainerArguments(
		string $hookRunnerClass,
		ReflectionMethod $hookMethod
	) {
		$params = [];
		foreach ( $hookMethod->getParameters() as $param ) {
			$bogusValue = $this->getMockedParamValue( $param );
			if ( $param->isPassedByReference() ) {
				$params[] = &$bogusValue;
				unset( $bogusValue );
			} else {
				$params[] = $bogusValue;
			}
		}
		$hookMethodName = $hookMethod->getName();
		$mockContainer = $this->createNoOpMock( HookContainer::class, [ 'run' ] );
		$mockContainer
			->expects( $this->once() )
			->method( 'run' )
			->willReturnCallback( function ( string $hookName, array $hookCallParams ) use ( $hookMethodName, $params ) {
				// HookContainer builds the method from the hook name with some normalisation,
				// so the passed hook name and the method must be equal
				// This is not a function in HookContainer as hooks are hot path
				// and just avoid the extra call for performance
				$expectedFuncName = 'on' . strtr( ucfirst( $hookName ), ':-', '__' );
				$this->assertSame( $expectedFuncName, $hookMethodName,
					'Interface function must named "on<hook name>" with : or - replaced by _' );
				$this->assertSame( $params, $hookCallParams );
				return true;
			} );
		$hookRunner = new $hookRunnerClass( $mockContainer );
		$hookRunner->$hookMethodName( ...$params );
	}

	protected function getMockedParamValue( ReflectionParameter $param ) {
		$paramType = $param->getType();
		if ( !$paramType ) {
			// Return a string for all the untyped parameters, good enough for our purposes.
			return $param->getName();
		}
		$paramName = $paramType->getName();
		if ( $paramName === 'string' ) {
			return $param->getName();
		}
		if ( $paramName === 'array' ) {
			return [];
		}
		if ( $paramName === 'bool' ) {
			return false;
		}
		if ( $paramName === 'int' ) {
			return 42;
		}
		if ( $paramName === 'float' ) {
			return 42.0;
		}
		if ( $paramName === 'callable' ) {
			return static function () {
				// No-op
			};
		}
		return $this->createNoOpMock( $paramName );
	}
}
PK       ! O6      HookRunnerTest.phpnu Iw        <?php

namespace MediaWiki\Tests\HookContainer;

use MediaWiki\Api\ApiHookRunner;
use MediaWiki\HookContainer\HookRunner;
use MediaWiki\ResourceLoader as RL;

/**
 * Tests that all arguments passed into HookRunner are passed along to HookContainer.
 * @covers \MediaWiki\HookContainer\HookRunner
 * @covers \MediaWiki\Api\ApiHookRunner
 * @covers \MediaWiki\ResourceLoader\HookRunner
 */
class HookRunnerTest extends HookRunnerTestBase {

	public static function provideHookRunners() {
		yield ApiHookRunner::class => [ ApiHookRunner::class ];
		yield HookRunner::class => [ HookRunner::class ];
		yield RL\HookRunner::class => [ RL\HookRunner::class ];
	}
}
PK       ! cl      FauxGlobalHookArrayTest.phpnu Iw        <?php

namespace MediaWiki\Tests\HookContainer;

use MediaWiki\HookContainer\FauxGlobalHookArray;
use MediaWiki\HookContainer\HookContainer;
use MediaWiki\HookContainer\StaticHookRegistry;
use MediaWikiUnitTestCase;
use Wikimedia\ObjectFactory\ObjectFactory;

/**
 * Tests that all arguments passed into FauxGlobalHookArray are passed along to HookContainer.
 * @covers \MediaWiki\HookContainer\FauxGlobalHookArray
 * @covers \MediaWiki\HookContainer\FauxHookHandlerArray
 */
class FauxGlobalHookArrayTest extends MediaWikiUnitTestCase {

	public function testRegisterHandler() {
		$this->expectDeprecationAndContinue( '/Accessing \$wgHooks directly is deprecated/' );

		$counter = 0;

		$handler = static function () use ( &$counter ) {
			$counter++;
		};

		$registry = new StaticHookRegistry();
		$factory = $this->createNoOpMock( ObjectFactory::class );
		$container = new HookContainer( $registry, $factory );
		$hooks = new FauxGlobalHookArray( $container );

		$this->expectDeprecationAndContinue( '/getHandlerCallbacks/' );

		// Register a handler via the array
		$hooks['FirstHook'][] = $handler;

		$this->assertTrue( $container->isRegistered( 'FirstHook' ) );
		$this->assertCount( 1, $container->getHandlerCallbacks( 'FirstHook' ) );

		$this->assertTrue( isset( $hooks['FirstHook'] ) );
		$this->assertCount( 1, $hooks['FirstHook'] );

		$first = $hooks['FirstHook'][0];
		$this->assertSame( $handler, $first );

		$container->run( 'FirstHook' );
		$this->assertSame( 1, $counter );

		// Register a handler via the HookContainer
		$container->register( 'SecondHook', $handler );

		$this->assertTrue( isset( $hooks['SecondHook'] ) );
		$this->assertCount( 1, $hooks['SecondHook'] );

		$first = $hooks['SecondHook'][0];
		$this->assertSame( $handler, $first );
	}

}
PK       ! &)R      HookContainerTest.phpnu Iw        <?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;
		}
	}

}
PK       ! {F      StaticHookRegistryTest.phpnu Iw        <?php
/**
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 * http://www.gnu.org/copyleft/gpl.html
 *
 * @file
 */

use MediaWiki\HookContainer\DeprecatedHooks;
use MediaWiki\HookContainer\StaticHookRegistry;

/**
 * @author DannyS712
 *
 * @covers \MediaWiki\HookContainer\StaticHookRegistry
 */
class StaticHookRegistryTest extends MediaWikiUnitTestCase {

	public function testStaticHookRegistry() {
		// Since the actual format for the first two isn't checked in the StaticHookRegistry
		// code, no need to follow it
		$globalHooks = [ 'global hooks' ];
		$extensionHooks = [ 'extension hooks' ];
		$deprecatedHooks = [
			'ExampleDeprecatedHook' => [ 'deprecatedVersion' => '1.36' ]
		];
		$staticHookRegistry = new StaticHookRegistry(
			$globalHooks,
			$extensionHooks,
			$deprecatedHooks
		);
		$this->assertEquals( $globalHooks, $staticHookRegistry->getGlobalHooks() );
		$this->assertEquals( $extensionHooks, $staticHookRegistry->getExtensionHooks() );
		$this->assertInstanceOf( DeprecatedHooks::class, $staticHookRegistry->getDeprecatedHooks() );
	}

}
PK       ! e       HookContainerIntegrationTest.phpnu Iw        <?php

namespace MediaWiki\Tests\HookContainer {

	use MediaWiki\Registration\ExtensionRegistry;
	use Wikimedia\ScopedCallback;

	class HookContainerIntegrationTest extends \MediaWikiIntegrationTestCase {

		/**
		 * @covers \MediaWiki\HookContainer\HookContainer::run
		 */
		public function testHookRunsWhenExtensionRegistered() {
			$extensionRegistry = ExtensionRegistry::getInstance();
			$numHandlersExecuted = 0;
			$handlers = [ 'FooHook' => [ [
				'handler' => [
					'class' => 'FooExtension\\FooExtensionHooks',
					'name' => 'FooExtension-FooHandler',
				] ] ]
			];
			$reset = $extensionRegistry->setAttributeForTest( 'Hooks', $handlers );
			$this->assertSame( 0, $numHandlersExecuted );

			$this->resetServices();
			$hookContainer = $this->getServiceContainer()->getHookContainer();
			$hookContainer->run( 'FooHook', [ &$numHandlersExecuted ] );
			$this->assertSame( 1, $numHandlersExecuted );
			ScopedCallback::consume( $reset );
		}

		/**
		 * @covers \MediaWiki\HookContainer\HookContainer::run
		 * @covers \MediaWiki\HookContainer\HookContainer::scopedRegister
		 */
		public function testHookRunsWithMultipleMixedHandlerTypes() {
			$handlerExt = [
				'FooHook' => [
					[ 'handler' => [
						'class' => 'FooExtension\\FooExtensionHooks',
						'name' => 'FooExtension-FooHandler',
					]
					]
				]
			];
			$resetExt = ExtensionRegistry::getInstance()->setAttributeForTest( 'Hooks', $handlerExt );

			$this->resetServices();
			$hookContainer = $this->getServiceContainer()->getHookContainer();

			$numHandlersExecuted = 0;
			$reset = $hookContainer->scopedRegister( 'FooHook', static function ( &$numHandlersRun ) {
				$numHandlersRun++;
			} );
			$reset2 = $hookContainer->scopedRegister( 'FooHook', static function ( &$numHandlersRun ) {
				$numHandlersRun++;
			} );

			$hookContainer->run( 'FooHook', [ &$numHandlersExecuted ] );
			$this->assertEquals( 3, $numHandlersExecuted );

			ScopedCallback::consume( $reset );
			ScopedCallback::consume( $reset2 );
			ScopedCallback::consume( $resetExt );
		}

		/**
		 * @covers \MediaWiki\HookContainer\HookContainer
		 */
		public function testValidServiceInjection() {
			$handler = [
				'handler' => [
					'name' => 'FooExtension-Mash',
					'class' => 'FooExtension\\ServiceHooks',
					'services' => [ 'ReadOnlyMode' ]
				],
				'extensionPath' => '/path/to/extension.json'
			];
			$hooks = [ 'Mash' => [ $handler ] ];
			$reset = ExtensionRegistry::getInstance()->setAttributeForTest( 'Hooks', $hooks );

			$this->resetServices();
			$hookContainer = $this->getServiceContainer()->getHookContainer();

			$arg = 0;
			$ret = $hookContainer->run( 'Mash', [ &$arg ] );
			$this->assertTrue( $ret );
			$this->assertSame( 1, $arg );
		}

		/**
		 * @covers \MediaWiki\HookContainer\HookContainer
		 */
		public function testInvalidServiceInjection() {
			$handler = [
				'handler' => [
					'name' => 'FooExtension-Mash',
					'class' => 'FooExtension\\ServiceHooks',
					'services' => [ 'ReadOnlyMode' ]
				],
				'extensionPath' => '/path/to/extension.json'
			];
			$hooks = [ 'Mash' => [ $handler ] ];
			$reset = ExtensionRegistry::getInstance()->setAttributeForTest( 'Hooks', $hooks );

			$this->resetServices();
			$hookContainer = $this->getServiceContainer()->getHookContainer();

			$this->expectException( \UnexpectedValueException::class );
			$arg = 0;
			$hookContainer->run( 'Mash', [ &$arg ], [ 'noServices' => true ] );
		}
	}
}

namespace FooExtension {

	class FooExtensionHooks {

		public function onFooHook( &$numHandlersRun ) {
			$numHandlersRun++;
		}
	}

	class ServiceHooks {
		public function __construct( \Wikimedia\Rdbms\ReadOnlyMode $readOnlyMode ) {
		}

		public function onMash( &$arg ) {
			$arg++;
			return true;
		}
	}

}
PK         ! 6k                    DeprecatedHooksTest.phpnu Iw        PK         ! Y5`  `              L  HookRunnerTestBase.phpnu Iw        PK         ! O6                !  HookRunnerTest.phpnu Iw        PK         ! cl                $  FauxGlobalHookArrayTest.phpnu Iw        PK         ! &)R                
,  HookContainerTest.phpnu Iw        PK         ! {F                [  StaticHookRegistryTest.phpnu Iw        PK         ! e                 m  HookContainerIntegrationTest.phpnu Iw        PK      i  u    