expects( $this->never() )->method( $this->anythingBut( 'foo', 'bar' ) ); * which will throw if any unexpected method is called. * * @param mixed ...$values Values that are not matched * @return Constraint */ protected function anythingBut( ...$values ) { if ( !in_array( '__destruct', $values, true ) ) { // Ensure that __destruct is always included. PHPUnit will fail very hard with no // useful output if __destruct ends up being called (T280780). $values[] = '__destruct'; } return $this->logicalNot( $this->logicalOr( ...array_map( [ $this, 'identicalTo' ], $values ) ) ); } /** * Return a PHPUnit mock that is expected to never have any methods called on it. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $type * @psalm-param list $allow Methods to allow * * @param string $type * @param string[] $allow Methods to allow * * @return MockObject&RealInstanceType */ protected function createNoOpMock( $type, $allow = [] ) { $mock = $this->createMock( $type ); $mock->expects( $this->never() )->method( $this->anythingBut( '__debugInfo', '__destruct', ...$allow ) ); return $mock; } /** * Return a PHPUnit mock that is expected to never have any methods called on it. * * @psalm-template RealInstanceType of object * * @psalm-param class-string $type * @psalm-param list $allow Methods to allow * * @param string $type * @param string[] $allow methods to allow * * @return MockObject&RealInstanceType */ protected function createNoOpAbstractMock( $type, $allow = [] ) { $mock = $this->getMockBuilder( $type ) ->disableOriginalConstructor() ->disableOriginalClone() ->disableArgumentCloning() ->disallowMockingUnknownTypes() ->getMockForAbstractClass(); $mock->expects( $this->never() )->method( $this->anythingBut( '__destruct', ...$allow ) ); return $mock; } /** * Create an ObjectFactory with no dependencies and no services * * @return ObjectFactory */ protected function createSimpleObjectFactory() { $serviceContainer = $this->createMock( ContainerInterface::class ); $serviceContainer->method( 'has' )->willReturn( false ); $serviceContainer->method( 'get' )->willReturnCallback( static function ( $serviceName ) { throw new NoSuchServiceException( $serviceName ); } ); return new ObjectFactory( $serviceContainer ); } /** * Create an initially empty HookContainer with an empty service container * attached. Register only the hooks specified in the parameter. * * @param callable[] $hooks * @return HookContainer */ protected function createHookContainer( $hooks = [] ) { $hookContainer = new HookContainer( new StaticHookRegistry(), $this->createSimpleObjectFactory() ); foreach ( $hooks as $name => $callback ) { $hookContainer->register( $name, $callback ); } return $hookContainer; } /** * Skip the test if not running the necessary php version * * @since 1.42 (also backported to 1.39.8, 1.40.4 and 1.41.2) * * @param string $op * @param string $version */ protected function markTestSkippedIfPhp( $op, $version ) { if ( version_compare( PHP_VERSION, $version, $op ) ) { $this->markTestSkipped( "PHP $version isn't supported for this test" ); } } /** * Don't throw a warning if $function is deprecated and called later * * @since 1.19 * * @param string $function */ public function hideDeprecated( $function ) { // Construct a regex that will match the message generated by // wfDeprecated() if it is called for the specified function. $this->filterDeprecated( '/Use of ' . preg_quote( $function, '/' ) . ' /' ); } /** * Don't throw a warning for deprecation messages matching a regex. * * @since 1.35 * * @param string $regex */ public function filterDeprecated( $regex ) { MWDebug::filterDeprecationForTest( $regex ); } /** * Expect a deprecation notice, but suppress it and continue operation so we can test that the * deprecated functionality works as intended for compatibility. * * @since 1.39 * * @param string $regex Deprecation message that must be triggered. */ public function expectDeprecationAndContinue( string $regex ): void { $this->expectedDeprecations[] = $regex; MWDebug::filterDeprecationForTest( $regex, function () use ( $regex ): void { $this->actualDeprecations[] = $regex; } ); } /** * @after */ public function checkExpectedDeprecationsOnTearDown(): void { if ( $this->expectedDeprecations ) { $this->assertSame( [], array_diff( $this->expectedDeprecations, $this->actualDeprecations ), 'Expected deprecation warning(s) were not emitted' ); } } /** * Check whether file contains given data. * @param string $fileName * @param string $actualData * @param bool $createIfMissing If true, and file does not exist, create it with given data * and skip the test. * @param string $msg * @since 1.30 */ protected function assertFileContains( $fileName, $actualData, $createIfMissing = false, $msg = '' ) { if ( $createIfMissing ) { if ( !is_file( $fileName ) ) { file_put_contents( $fileName, $actualData ); $this->markTestSkipped( "Data file $fileName does not exist" ); } } else { $this->assertFileExists( $fileName ); } $this->assertEquals( file_get_contents( $fileName ), $actualData, $msg ); } /** * Assert that an associative array contains the subset of an expected array. * * The internal key order does not matter. * Values are compared with strict equality. * * @since 1.41 * @param array $expected * @param array $actual * @param string $message */ protected function assertArrayContains( array $expected, array $actual, $message = '' ) { $patched = array_replace_recursive( $actual, $expected ); ksort( $patched ); ksort( $actual ); $result = ( $actual === $patched ); if ( !$result ) { $comparisonFailure = new ComparisonFailure( $patched, $actual, var_export( $patched, true ), var_export( $actual, true ) ); $failureDescription = 'Failed asserting that array contains the expected submap.'; if ( $message != '' ) { $failureDescription = $message . "\n" . $failureDescription; } throw new ExpectationFailedException( $failureDescription, $comparisonFailure ); } else { $this->assertTrue( true, $message ); } } /** * Assert that two arrays are equal. By default, this means that both arrays need to hold * the same set of values. Using additional arguments, order and associated key can also * be set as relevant. * * @since 1.20 * * @param array $expected * @param array $actual * @param bool $ordered If the order of the values should match * @param bool $named If the keys should match * @param string $message */ public function assertArrayEquals( array $expected, array $actual, $ordered = false, $named = false, string $message = '' ) { if ( !$ordered ) { $this->objectAssociativeSort( $expected ); $this->objectAssociativeSort( $actual ); } if ( !$named ) { $expected = array_values( $expected ); $actual = array_values( $actual ); } $this->assertEquals( $expected, $actual, $message ); } /** * Does an associative sort that works for objects. * * @since 1.20 * * @param array &$array */ protected function objectAssociativeSort( array &$array ) { uasort( $array, static function ( $a, $b ) { return serialize( $a ) <=> serialize( $b ); } ); } /** * @before */ protected function phpErrorFilterSetUp() { $this->originalPhpErrorFilter = error_reporting(); } /** * @after */ protected function phpErrorFilterTearDown() { $phpErrorFilter = error_reporting(); if ( $phpErrorFilter !== $this->originalPhpErrorFilter ) { error_reporting( $this->originalPhpErrorFilter ); $message = "PHP error_reporting setting found dirty." . " Did you forget AtEase::restoreWarnings?"; $this->fail( $message ); } } /** * Re-enable any disabled deprecation warnings and allow same deprecations to be thrown * multiple times in different tests, so the PHPUnit expectDeprecation() works. * * @after */ protected function mwDebugTearDown() { MWDebug::clearLog(); MWDebug::clearDeprecationFilters(); } /** * Reset any fake timestamps so that they don't mess with any other tests. * * @since 1.37 before that, integration tests had it reset in * MediaWikiIntegrationTestCase::mediaWikiTearDown, and unit tests didn't at all * * @after */ protected function fakeTimestampTearDown() { ConvertibleTimestamp::setFakeTime( null ); } /** * @param string $text * @param array $params * @return Message|MockObject * @since 1.35 */ protected function getMockMessage( string $text = '', array $params = [] ) { $msg = $this->createMock( Message::class ); $msg->method( $this->logicalOr( '__toString', 'escaped', 'getKey', 'parse', 'parseAsBlock', 'plain', 'text', 'toString' ) )->willReturn( $text ); $msg->method( 'getParams' )->willReturn( $params ); $msg->method( $this->logicalOr( 'inContentLanguage', 'inLanguage', 'numParams', 'params', 'rawParams', 'setContext', 'title', 'useDatabase' ) )->willReturnSelf(); $msg->method( 'exists' )->willReturn( true ); return $msg; } private function failStatus( StatusValue $status, $reason, $message = '' ) { $reason = $message === '' ? $reason : "$message\n$reason"; $this->fail( "$reason\n$status" ); } protected function assertStatusOK( StatusValue $status, $message = '' ) { if ( !$status->isOK() ) { $errors = $status->splitByErrorType()[0]; $this->failStatus( $errors, 'Status should be OK', $message ); } else { $this->addToAssertionCount( 1 ); } } protected function assertStatusGood( StatusValue $status, $message = '' ) { if ( !$status->isGood() ) { $this->failStatus( $status, 'Status should be Good', $message ); } else { $this->addToAssertionCount( 1 ); } } protected function assertStatusNotOK( StatusValue $status, $message = '' ) { if ( $status->isOK() ) { $this->failStatus( $status, 'Status should not be OK', $message ); } else { $this->addToAssertionCount( 1 ); } } protected function assertStatusNotGood( StatusValue $status, $message = '' ) { if ( $status->isGood() ) { $this->failStatus( $status, 'Status should not be Good', $message ); } else { $this->addToAssertionCount( 1 ); } } protected function assertStatusMessage( string $messageKey, StatusValue $status, $message = '' ) { if ( !$status->hasMessage( $messageKey ) ) { $this->failStatus( $status, "Status should have message $messageKey", $message ); } else { $this->addToAssertionCount( 1 ); } } /** * Check if the status contains exactly the same messages as the expected status. * * Prefer using assertStatusError / assertStatusWarning unless you really need to check the * parameters, count and order of the messages too. * * This method does not compare isGood() vs isOK() or the values of the statuses, use dedicated * assertion methods for that. * * Note that some differences between the internals of the objects are allowed (such as their own * class, use of MessageSpecifier vs string keys, use of strings vs other scalars for parameters). * * @param StatusValue $expected * @param StatusValue $actual * @param string $message */ protected function assertStatusMessagesExactly( StatusValue $expected, StatusValue $actual, $message = '' ) { $localizer = new FakeQqxMessageLocalizer(); foreach ( [ 'error', 'warning' ] as $type ) { foreach ( array_map( null, $expected->getMessages( $type ), $actual->getMessages( $type ) ) as [ $expectedMsg, $actualMsg ] ) { if ( $expectedMsg === null || $actualMsg === null || $localizer->msg( $expectedMsg )->text() !== $localizer->msg( $actualMsg )->text() ) { $this->failStatus( $actual, "Status messages should be exactly like: $expected\nActual:", $message ); } } } $this->addToAssertionCount( 1 ); } protected function assertStatusValue( $expected, StatusValue $status, $message = 'Status value' ) { $this->assertEquals( $expected, $status->getValue(), $message ); } protected function assertStatusError( string $messageKey, StatusValue $status, $message = '' ) { $this->assertStatusNotOK( $status, $message ); $this->assertStatusMessage( $messageKey, $status, $message ); } protected function assertStatusWarning( string $messageKey, StatusValue $status, $message = '' ) { $this->assertStatusNotGood( $status, $message ); $this->assertStatusOK( $status, $message ); $this->assertStatusMessage( $messageKey, $status, $message ); } /** * Put each HTML element on its own line and then equals() the results * * Use for nicely formatting of PHPUnit diff output when comparing very * simple HTML * * @since 1.20 * @since 1.39 available in MediaWikiUnitTestCase * * @param string $expected HTML on oneline * @param string $actual HTML on oneline * @param string $msg Optional message */ protected function assertHTMLEquals( $expected, $actual, $msg = '' ) { $expected = str_replace( '>', ">\n", $expected ); $actual = str_replace( '>', ">\n", $actual ); $this->assertEquals( $expected, $actual, $msg ); } /** * This method allows you to assert that the given callback emits a PHP error. It is a PHPUnit 10 compatible * replacement for expectNotice(), expectWarning(), expectError(), and the other methods deprecated in * https://github.com/sebastianbergmann/phpunit/issues/5062. * * @param int $errorLevel * @param callable $callback * @param string|null $msg String that must be contained in the error message */ protected function expectPHPError( int $errorLevel, callable $callback, string $msg = null ): void { try { $errorEmitted = false; set_error_handler( function ( $_, $actualMsg ) use ( $msg, &$errorEmitted ) { if ( $msg !== null ) { $this->assertStringContainsString( $msg, $actualMsg ); } $errorEmitted = true; }, $errorLevel ); $callback(); $this->assertTrue( $errorEmitted, "No PHP error was emitted." ); } finally { restore_error_handler(); } } }