wiki.techinc.nl/tests/phpunit/includes/HooksTest.php
Nikki Nikkhoui 0adc5f3428 Hook Container
New classes and modificatons to existing classes to support the new Hooks system. All changes are documented in RFC https://phabricator.wikimedia.org/T240307.

- HookContainer.php: Class for doing much of what Hooks.php has historically done, but enabling new-style hooks to be processed and registered. Changes include new ways of defining hook handler functions as an object with defined dependencies in extension.json, removing runWithoutAbort() and addit it to an $options parameter to be passed to HookContainer::run(), being able to decipher whether a hook handler is legacy or non-legacy style and run them in the appropriate way, etc.
- DeprecatedHooks.php: For marking hooks deprecated and verifying if one is deprecated
- DeprecatedHooksTest.php: Unit tests for DeprecatedHooks.php
- Hooks.php: register() will now additionally register hooks with handlers in new HooksContainer.php. getHandlers() will be a legacy wrapper for calling the newer HookContainer::getHandlers()
- MediaWikiServices.php: Added getHookContainer() for retrieving HookContainer singleton
- ExtensionProcessor.php: modified extractHooks() to be able to extract new style handler objects being registered in extension.json
- ServiceWiring.php: Added HookContainer to list of services to return
- HookContainerTest.php: Unit tests for HookContainer.php
- ExtensionProcessorTest.php: Moved file out of /unit folder and now extends MediaWikiTestCase instead of MediaWikiUnitTestCase (as the tests are not truly unit tests). Modified existing tests for ExtensionProcessor::extractHooks() to include a test case for new style handler

Bug: T240307
Change-Id: I432861d8995cfd7180e77e115251d8055b7eceec
2020-04-17 15:48:38 +10:00

335 lines
8.7 KiB
PHP

<?php
use MediaWiki\MediaWikiServices;
use PHPUnit\Framework\Error\Deprecated;
use Wikimedia\ScopedCallback;
class HooksTest extends MediaWikiTestCase {
public function setUp() : void {
global $wgHooks;
parent::setUp();
unset( $wgHooks['MediaWikiHooksTest001'] );
}
public static function provideHooks() {
$i = new NothingClass();
return [
[
'Object and method',
[ $i, 'someNonStatic' ],
'changed-nonstatic',
'changed-nonstatic'
],
[ 'Object and no method', [ $i ], 'changed-onevent', 'original' ],
[
'Object and method with data',
[ $i, 'someNonStaticWithData', 'data' ],
'data',
'original'
],
[ 'Object and static method', [ $i, 'someStatic' ], 'changed-static', 'original' ],
[
'Class::method static call',
[ 'NothingClass::someStatic' ],
'changed-static',
'original'
],
[
'Class::method static call as array',
[ [ 'NothingClass::someStatic' ] ],
'changed-static',
'original'
],
[ 'Global function', [ 'NothingFunction' ], 'changed-func', 'original' ],
[ 'Global function with data', [ 'NothingFunctionData', 'data' ], 'data', 'original' ],
[ 'Closure', [ function ( &$foo, $bar ) {
$foo = 'changed-closure';
return true;
} ], 'changed-closure', 'original' ],
[ 'Closure with data', [ function ( $data, &$foo, $bar ) {
$foo = $data;
return true;
}, 'data' ], 'data', 'original' ]
];
}
/**
* @dataProvider provideHooks
* @covers Hooks::register
* @covers Hooks::run
*/
public function testNewStyleHooks( $msg, $hook, $expectedFoo, $expectedBar ) {
$foo = $bar = 'original';
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', $hook );
Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
$this->assertSame( $expectedFoo, $foo, $msg );
$this->assertSame( $expectedBar, $bar, $msg );
}
/**
* @covers Hooks::getHandlers
*/
public function testGetHandlers() {
global $wgHooks;
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$this->assertSame(
[],
Hooks::getHandlers( 'MediaWikiHooksTest001' ),
'No hooks registered'
);
$a = new NothingClass();
$b = new NothingClass();
$wgHooks['MediaWikiHooksTest001'][] = $a;
$this->assertSame(
[ $a ],
Hooks::getHandlers( 'MediaWikiHooksTest001' ),
'Hook registered by $wgHooks'
);
$reset = $hookContainer->scopedRegister( 'MediaWikiHooksTest001', $b );
$this->assertSame(
[ $b, $a ],
Hooks::getHandlers( 'MediaWikiHooksTest001' ),
'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
);
ScopedCallback::consume( $reset );
unset( $wgHooks['MediaWikiHooksTest001'] );
$hookContainer->register( 'MediaWikiHooksTest001', $b );
$this->assertSame(
[ $b ],
Hooks::getHandlers( 'MediaWikiHooksTest001' ),
'Hook registered by Hook::register'
);
}
/**
* @covers Hooks::isRegistered
* @covers Hooks::register
* @covers Hooks::run
*/
public function testNewStyleHookInteraction() {
global $wgHooks;
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$a = new NothingClass();
$b = new NothingClass();
$wgHooks['MediaWikiHooksTest001'][] = $a;
$this->assertTrue(
Hooks::isRegistered( 'MediaWikiHooksTest001' ),
'Hook registered via $wgHooks should be noticed by Hooks::isRegistered'
);
$hookContainer->register( 'MediaWikiHooksTest001', $b );
$this->assertCount( 2, Hooks::getHandlers( 'MediaWikiHooksTest001' ),
'Hooks::getHandlers() should return hooks registered via wgHooks as well as Hooks::register'
);
$foo = 'quux';
$bar = 'qaax';
Hooks::run( 'MediaWikiHooksTest001', [ &$foo, &$bar ] );
$this->assertEquals(
1,
$a->calls,
'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
);
$this->assertEquals(
1,
$b->calls,
'Hooks::run() should run hooks registered via wgHooks as well as Hooks::register'
);
}
/**
* @covers Hooks::run
*/
public function testUncallableFunction() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', 'ThisFunctionDoesntExist' );
$this->expectExceptionMessage( 'Call to undefined function ThisFunctionDoesntExist' );
Hooks::run( 'MediaWikiHooksTest001', [] );
}
/**
* @covers Hooks::run
*/
public function testFalseReturn() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
return false;
} );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
$foo = 'test';
return true;
} );
$foo = 'original';
Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
$this->assertSame( 'original', $foo, 'Hooks abort after a false return.' );
}
/**
* @covers Hooks::run
*/
public function testNullReturn() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
return;
} );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
$foo = 'test';
return true;
} );
$foo = 'original';
Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
$this->assertSame( 'test', $foo, 'Hooks continue after a null return.' );
}
/**
* @covers Hooks::run
*/
public function testCallHook_FalseHook() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', false );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
$foo = 'test';
return true;
} );
$foo = 'original';
Hooks::run( 'MediaWikiHooksTest001', [ &$foo ] );
$this->assertSame( 'test', $foo, 'Hooks that are falsey are skipped.' );
}
/**
* @covers Hooks::run
*/
public function testCallHook_UnknownDatatype() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', 12345 );
$this->expectException( UnexpectedValueException::class );
Hooks::run( 'MediaWikiHooksTest001' );
}
/**
* @covers Hooks::run
*/
public function testCallHook_Deprecated() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', 'NothingClass::someStatic' );
$this->expectException( Deprecated::class );
Hooks::run( 'MediaWikiHooksTest001', [], '1.31' );
}
/**
* @covers Hooks::runWithoutAbort
*/
public function testRunWithoutAbort() {
$list = [];
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$list ) {
$list[] = 1;
return true; // Explicit true
} );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$list ) {
$list[] = 2;
return; // Implicit null
} );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$list ) {
$list[] = 3;
// No return
} );
Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$list ] );
$this->assertSame( [ 1, 2, 3 ], $list, 'All hooks ran.' );
}
/**
* @covers Hooks::runWithoutAbort
*/
public function testRunWithoutAbortWarning() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
return false;
} );
$hookContainer->register( 'MediaWikiHooksTest001', function ( &$foo ) {
$foo = 'test';
return true;
} );
$foo = 'original';
$this->expectException( UnexpectedValueException::class );
$this->expectExceptionMessage( 'Invalid return from hook-MediaWikiHooksTest001-closure for ' .
'unabortable MediaWikiHooksTest001'
);
Hooks::runWithoutAbort( 'MediaWikiHooksTest001', [ &$foo ] );
}
/**
* @covers Hooks::run
*/
public function testFatalError() {
$hookContainer = MediaWikiServices::getInstance()->getHookContainer();
$hookContainer->register( 'MediaWikiHooksTest001', function () {
return 'test';
} );
$this->expectDeprecation();
Hooks::run( 'MediaWikiHooksTest001', [] );
}
}
function NothingFunction( &$foo, &$bar ) {
$foo = 'changed-func';
return true;
}
function NothingFunctionData( $data, &$foo, &$bar ) {
$foo = $data;
return true;
}
class NothingClass {
public $calls = 0;
public static function someStatic( &$foo, &$bar ) {
$foo = 'changed-static';
return true;
}
public function someNonStatic( &$foo, &$bar ) {
$this->calls++;
$foo = 'changed-nonstatic';
$bar = 'changed-nonstatic';
return true;
}
public function onMediaWikiHooksTest001( &$foo, &$bar ) {
$this->calls++;
$foo = 'changed-onevent';
return true;
}
public function someNonStaticWithData( $data, &$foo, &$bar ) {
$this->calls++;
$foo = $data;
return true;
}
}