Unit tests for FileBackend

Currently 62.79% coverage, 108/172 lines.

One oddity discovered during testing was that the "quick" variants of
most methods don't have an $opts parameter. It seems like just an
oversight, so I added it.

Bug: T234227
Change-Id: If2978065392cd6dcf693a588bb1ce6b5d43828f2
This commit is contained in:
Aryeh Gregor 2019-10-28 19:41:39 +02:00
parent cd7e60a535
commit c250d07bac
8 changed files with 829 additions and 24 deletions

View file

@ -150,8 +150,8 @@ class FileBackendDBRepoWrapper extends FileBackend {
return $this->backend->doOperationsInternal( $this->mungeOpPaths( $ops ), $opts );
}
protected function doQuickOperationsInternal( array $ops ) {
return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ) );
protected function doQuickOperationsInternal( array $ops, array $opts ) {
return $this->backend->doQuickOperationsInternal( $this->mungeOpPaths( $ops ), $opts );
}
protected function doPrepare( array $params ) {

View file

@ -185,6 +185,7 @@ abstract class FileBackend implements LoggerAwareInterface {
* - logger : Optional PSR logger object.
* - profiler : Optional callback that takes a section name argument and returns
* a ScopedCallback instance that ends the profile section in its destructor.
* - statusWrapper : Optional callback that is used to wrap returned StatusValues
* @throws InvalidArgumentException
*/
public function __construct( array $config ) {
@ -198,8 +199,7 @@ abstract class FileBackend implements LoggerAwareInterface {
"Backend domain ID not provided for '{$this->name}'." );
}
$this->lockManager = $config['lockManager'] ?? new NullLockManager( [] );
$this->fileJournal = $config['fileJournal']
?? FileJournal::factory( [ 'class' => NullFileJournal::class ], $this->name );
$this->fileJournal = $config['fileJournal'] ?? new NullFileJournal;
$this->readOnly = isset( $config['readOnly'] )
? (string)$config['readOnly']
: '';
@ -712,16 +712,17 @@ abstract class FileBackend implements LoggerAwareInterface {
/** @noinspection PhpUnusedLocalVariableInspection */
$scope = ScopedCallback::newScopedIgnoreUserAbort(); // try to ignore client aborts
return $this->doQuickOperationsInternal( $ops );
return $this->doQuickOperationsInternal( $ops, $opts );
}
/**
* @see FileBackend::doQuickOperations()
* @param array $ops
* @param array $opts
* @return StatusValue
* @since 1.20
*/
abstract protected function doQuickOperationsInternal( array $ops );
abstract protected function doQuickOperationsInternal( array $ops, array $opts );
/**
* Same as doQuickOperations() except it takes a single operation.
@ -730,11 +731,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperations()
*
* @param array $op Operation
* @param array $opts Batch operation options
* @return StatusValue
* @since 1.20
*/
final public function doQuickOperation( array $op ) {
return $this->doQuickOperations( [ $op ] );
final public function doQuickOperation( array $op, array $opts = [] ) {
return $this->doQuickOperations( [ $op ], $opts );
}
/**
@ -744,11 +746,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.20
*/
final public function quickCreate( array $params ) {
return $this->doQuickOperation( [ 'op' => 'create' ] + $params );
final public function quickCreate( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'create' ] + $params, $opts );
}
/**
@ -758,11 +761,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.20
*/
final public function quickStore( array $params ) {
return $this->doQuickOperation( [ 'op' => 'store' ] + $params );
final public function quickStore( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'store' ] + $params, $opts );
}
/**
@ -772,11 +776,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.20
*/
final public function quickCopy( array $params ) {
return $this->doQuickOperation( [ 'op' => 'copy' ] + $params );
final public function quickCopy( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'copy' ] + $params, $opts );
}
/**
@ -786,11 +791,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.20
*/
final public function quickMove( array $params ) {
return $this->doQuickOperation( [ 'op' => 'move' ] + $params );
final public function quickMove( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'move' ] + $params, $opts );
}
/**
@ -800,11 +806,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.20
*/
final public function quickDelete( array $params ) {
return $this->doQuickOperation( [ 'op' => 'delete' ] + $params );
final public function quickDelete( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'delete' ] + $params, $opts );
}
/**
@ -814,11 +821,12 @@ abstract class FileBackend implements LoggerAwareInterface {
* @see FileBackend::doQuickOperation()
*
* @param array $params Operation parameters
* @param array $opts Operation options
* @return StatusValue
* @since 1.21
*/
final public function quickDescribe( array $params ) {
return $this->doQuickOperation( [ 'op' => 'describe' ] + $params );
final public function quickDescribe( array $params, array $opts = [] ) {
return $this->doQuickOperation( [ 'op' => 'describe' ] + $params, $opts );
}
/**

View file

@ -541,7 +541,7 @@ class FileBackendMultiWrite extends FileBackend {
return false;
}
protected function doQuickOperationsInternal( array $ops ) {
protected function doQuickOperationsInternal( array $ops, array $opts ) {
$status = $this->newStatus();
// Do the operations on the master backend; setting StatusValue fields
$realOps = $this->substOpBatchPaths( $ops, $this->backends[$this->masterIndex] );

View file

@ -1311,7 +1311,7 @@ abstract class FileBackendStore extends FileBackend {
return $status;
}
final protected function doQuickOperationsInternal( array $ops ) {
final protected function doQuickOperationsInternal( array $ops, array $opts ) {
/** @noinspection PhpUnusedLocalVariableInspection */
$ps = $this->scopedProfileSection( __METHOD__ . "-{$this->name}" );
$status = $this->newStatus();

View file

@ -4,6 +4,10 @@
* @since 1.20
*/
class NullFileJournal extends FileJournal {
public function __construct() {
// No-op
}
/**
* @see FileJournal::doLogChangeBatch()
* @param array $entries

View file

@ -44,7 +44,7 @@ use Wikimedia\TestingAccessWrapper;
* @covers LockManager
* @covers NullLockManager
*/
class FileBackendTest extends MediaWikiTestCase {
class FileBackendIntegrationTest extends MediaWikiIntegrationTestCase {
/** @var FileBackend */
private $backend;

View file

@ -0,0 +1,793 @@
<?php
use MediaWiki\FileBackend\FSFile\TempFSFileFactory;
use Wikimedia\TestingAccessWrapper;
/**
* @coversDefaultClass FileBackend
*/
class FileBackendTest extends MediaWikiUnitTestCase {
/**
* createMock() stubs out all methods, which isn't desirable for testing an abstract base class,
* since we often want to test that the base class calls certain methods that the derived class
* is meant to override. getMockBuilder() can be set to override only certain methods, but then
* you have to manually specify all abstract methods or else it doesn't work.
* getMockForAbstractClass() automatically fills in stubs for the abstract methods, but by
* default doesn't allow overriding any other methods. So we have to write our own.
*
* @param string|array ...$args Zero or more of the following:
* - A nonempty associative array, interpreted as $config to be passed to the constructor. The
* 'name' and 'domainId' will be given default values if not present.
* - A nonempty indexed array or a string, interpreted as a list of methods to override.
* - An empty array, which is ignored.
* @return FileBackend A mock with no methods overridden except those specified in
* $methodsToMock, and all abstract methods.
*/
private function newMockFileBackend( ...$args ) : FileBackend {
$methodsToMock = [];
$config = [];
foreach ( $args as $arg ) {
if ( is_string( $arg ) ) {
$methodsToMock = [ $arg ];
} elseif ( is_array( $arg ) ) {
if ( isset( $arg[0] ) ) {
$methodsToMock = $arg;
} elseif ( $arg ) {
$config = $arg;
}
} else {
throw new InvalidArgumentException(
'Arguments must be strings or nonempty arrays' );
}
}
$config += [ 'name' => 'test_name' ];
if ( !array_key_exists( 'wikiId', $config ) ) {
$config += [ 'domainId' => '' ];
}
// getMockForAbstractClass has a lot of undocumented parameters that we need to set
// https://github.com/sebastianbergmann/phpunit-mock-objects/blob/5.0.10/src/Generator.php#L268
// TODO Would be better to use getMockBuilder and replace the un-overridden abstract methods
// with something that throws.
return $this->getMockForAbstractClass( FileBackend::class,
/* $arguments */ [ $config ],
/* $mockClassName */ '',
/* $callOriginalConstructor */ true,
/* $callOriginalClone */ false,
/* $callAutoload */ true,
/* $mockedMethods */ $methodsToMock,
/* $cloneArguments */ false
);
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_validName
* @param mixed $name
*/
public function testConstruct_validName( $name ) : void {
$this->newMockFileBackend( [ 'name' => $name ] );
// No exception
$this->assertTrue( true );
}
public static function provideConstruct_validName() : array {
return [
'True' => [ true ],
'Positive integer' => [ 7 ],
'Zero integer' => [ 0 ],
'Zero float' => [ 0.0 ],
'Negative integer' => [ -7 ],
'Negative float' => [ -7.0 ],
'255 chars is allowed' => [ str_repeat( 'a', 255 ) ],
];
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_invalidName
* @param mixed $name
*/
public function testConstruct_invalidName( $name ) : void {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( "Backend name '$name' is invalid." );
$this->newMockFileBackend( [ 'name' => $name, 'domainId' => false ] );
}
public static function provideConstruct_invalidName() : array {
return [
'Empty string' => [ '' ],
'256 chars is too long' => [ str_repeat( 'a', 256 ) ],
'!' => [ '!' ],
'With space' => [ 'a b' ],
'False' => [ false ],
'Null' => [ null ],
'Positive float' => [ 13.402 ],
'Negative float' => [ -13.402 ],
];
}
/**
* @covers ::__construct
*/
public function testConstruct_noName() : void {
$this->expectException( PHPUnit\Framework\Error\Notice::class );
$this->expectExceptionMessage( 'Undefined index: name' );
$this->getMockBuilder( FileBackend::class )
->setConstructorArgs( [ [] ] )
->getMock();
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_validDomainId
* @param string $domainId
*/
public function testConstruct_validDomainId( string $domainId ) : void {
$this->newMockFileBackend( [ 'domainId' => $domainId ] );
// No exception
$this->assertTrue( true );
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_validDomainId
* @param string $wikiId
*/
public function testConstruct_validWikiId( string $wikiId ) : void {
$this->newMockFileBackend( [ 'wikiId' => $wikiId ] );
// No exception
$this->assertTrue( true );
}
public static function provideConstruct_validDomainId() : array {
return [
'Empty string' => [ '' ],
'1000 chars' => [ str_repeat( 'a', 1000 ) ],
'Null character' => [ "\0" ],
'Invalid UTF-8' => [ "\xff" ],
];
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_invalidDomainId
* @param mixed $domainId
*/
public function testConstruct_invalidDomainId( $domainId ) : void {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( "Backend domain ID not provided for 'test_name'." );
$this->newMockFileBackend( [ 'domainId' => $domainId ] );
}
public static function provideConstruct_invalidDomainId() : array {
return [
// We don't include null because that will fall back to wikiId
'False' => [ false ],
'True' => [ true ],
'Integer' => [ 7 ],
'Function' => [ function () {
} ],
'Float' => [ -13.402 ],
'Object' => [ new stdclass ],
'Array' => [ [] ],
];
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_invalidWikiId
* @param mixed $wikiId
*/
public function testConstruct_invalidWikiId( $wikiId ) : void {
$this->expectException( InvalidArgumentException::class );
$this->expectExceptionMessage( "Backend domain ID not provided for 'test_name'." );
$this->newMockFileBackend( [ 'wikiId' => $wikiId ] );
}
public static function provideConstruct_invalidWikiId() : array {
return [
'Null' => [ null ],
] + self::provideConstruct_invalidDomainId();
}
/**
* @covers ::__construct
*/
public function testConstruct_noDomainId() : void {
$this->expectException( PHPUnit\Framework\Error\Notice::class );
$this->expectExceptionMessage( 'Undefined index: wikiId' );
$this->getMockBuilder( FileBackend::class )
->setConstructorArgs( [ [ 'name' => 'test_name' ] ] )
->getMock();
}
/**
* @covers ::__construct
* @dataProvider provideConstruct_properties
* @param string $property
* @param mixed $expected
* @param array $config Can also include the key 'inexact' to tell us to not check equality
* strictly.
*/
public function testConstruct_properties(
string $property, $expected, array $config = []
) : void {
$backend = $this->newMockFileBackend( $config );
if ( $expected instanceof Closure ) {
$expected = $expected( $backend );
}
$assertMethod = isset( $config['inexact'] ) ? 'assertEquals' : 'assertSame';
unset( $config['inexact'] );
// We need to test this for the sake of subclasses that actually use the property. There
// doesn't seem to be any better way to do it. It shouldn't be tested in the subclasses,
// because we're testing the behavior of this class' constructor. We could make our own
// subclass, but we'd have to stub 26 abstract methods.
$this->$assertMethod( $expected,
TestingAccessWrapper::newFromObject( $backend )->$property );
}
public static function provideConstruct_properties() : array {
$tmpFileFactory = new TempFSFileFactory( 'some_unique_path' );
return [
'parallelize default value' => [ 'parallelize', 'off' ],
'parallelize null' => [ 'parallelize', 'off', [ 'parallelize' => null ] ],
'parallelize cast to string' => [ 'parallelize', '1', [ 'parallelize' => true ] ],
'parallelize case-preserving' =>
[ 'parallelize', 'iMpLiCiT', [ 'parallelize' => 'iMpLiCiT' ] ],
'concurrency default value' => [ 'concurrency', 50 ],
'concurrency null' => [ 'concurrency', 50, [ 'concurrency' => null ] ],
'concurrency cast to int' => [ 'concurrency', 51, [ 'concurrency' => '51x' ] ],
'obResetFunc default value' => [ 'obResetFunc',
// I'd've thought the return type should be 'callable', but apparently protected
// methods aren't callable.
function ( FileBackend $backend ) : array {
return [ $backend, 'resetOutputBuffer' ];
} ],
'obResetFunc null' => [ 'obResetFunc',
function ( FileBackend $backend ) : array {
return [ $backend, 'resetOutputBuffer' ];
} ],
'obResetFunc set' => [ 'obResetFunc', 'wfSomeImaginaryFunction',
[ 'obResetFunc' => 'wfSomeImaginaryFunction' ] ],
'streamMimeFunc default value' => [ 'streamMimeFunc', null ],
'streamMimeFunc set' => [ 'streamMimeFunc', 'smf', [ 'streamMimeFunc' => 'smf' ] ],
'profiler default value' => [ 'profiler', null ],
'profiler callable' => [ 'profiler', 'strtr', [ 'profiler' => 'strtr' ] ],
'profiler not callable' => [ 'profiler', null, [ 'profiler' => '!' ] ],
'logger default value' => [ 'logger', new Psr\Log\NullLogger, [ 'inexact' => true ] ],
'logger set' => [ 'logger', 'abcd', [ 'logger' => 'abcd' ] ],
'statusWrapper default value' => [ 'statusWrapper', null ],
'statusWrapper set' => [ 'statusWrapper', 'stat', [ 'statusWrapper' => 'stat' ] ],
'tmpFileFactory default value' =>
[ 'tmpFileFactory', new TempFSFileFactory, [ 'inexact' => true ] ],
'tmpDirectory null' => [ 'tmpFileFactory', new TempFSFileFactory,
[ 'tmpDirectory' => null, 'inexact' => true ] ],
'tmpDirectory set' => [ 'tmpFileFactory', new TempFSFileFactory( 'dir' ),
[ 'tmpDirectory' => 'dir', 'inexact' => true ] ],
'tmpFileFactory null' => [ 'tmpFileFactory', new TempFSFileFactory,
[ 'tmpFileFactory' => null, 'inexact' => true ] ],
'tmpFileFactory set' => [ 'tmpFileFactory', $tmpFileFactory,
[ 'tmpFileFactory' => $tmpFileFactory ] ],
'tmpDirectory and tmpFileFactory set' => [
'tmpFileFactory',
new TempFSFileFactory( 'dir' ),
[ 'tmpDirectory' => 'dir', 'tmpFileFactory' => $tmpFileFactory, 'inexact' => true ],
],
'tmpDirectory null and tmpFileFactory set' => [ 'tmpFileFactory', $tmpFileFactory,
[ 'tmpDirectory' => null, 'tmpFileFactory' => $tmpFileFactory ] ],
];
}
/**
* @covers ::__construct
* @covers ::getName
*/
public function testGetName() : void {
$backend = $this->newMockFileBackend();
$this->assertSame( 'test_name', $backend->getName() );
}
/**
* @covers ::__construct
* @covers ::getDomainId
* @dataProvider provideGetDomainId
* @param array $config
*/
public function testGetDomainId( array $config ) : void {
$backend = $this->newMockFileBackend( $config );
$this->assertSame( 'test_domain', $backend->getDomainId() );
}
/**
* @covers ::__construct
* @covers ::getWikiId
* @dataProvider provideGetDomainId
* @param array $config
*/
public function testGetWikiId( array $config ) : void {
$backend = $this->newMockFileBackend( $config );
$this->assertSame( 'test_domain', $backend->getWikiId() );
}
public static function provideGetDomainId() : array {
return [
'Only domainId' => [ [ 'domainId' => 'test_domain' ] ],
'Only wikiId' => [ [ 'wikiId' => 'test_domain' ] ],
'null domainId' => [ [ 'domainId' => null, 'wikiId' => 'test_domain' ] ],
'wikiId is ignored if domainId is present' =>
[ [ 'domainId' => 'test_domain', 'wikiId' => 'other_domain' ] ],
];
}
/**
* @covers ::__construct
* @covers ::isReadOnly
* @covers ::getReadOnlyReason
*/
public function testIsReadOnly_default() : void {
$backend = $this->newMockFileBackend();
$this->assertFalse( $backend->isReadOnly() );
$this->assertFalse( $backend->getReadOnlyReason() );
}
/**
* @covers ::__construct
* @covers ::isReadOnly
* @covers ::getReadOnlyReason
*/
public function testIsReadOnly() : void {
$backend = $this->newMockFileBackend( [ 'readOnly' => '.' ] );
$this->assertTrue( $backend->isReadOnly() );
$this->assertSame( '.', $backend->getReadOnlyReason() );
}
/**
* @covers ::getFeatures
*/
public function testGetFeatures() : void {
$backend = $this->newMockFileBackend();
$this->assertSame( FileBackend::ATTR_UNICODE_PATHS, $backend->getFeatures() );
}
/**
* @covers ::hasFeatures
* @dataProvider provideHasFeatures
* @param bool $expected
* @param int $testedFeatures
* @param int $actualFeatures
*/
public function testHasFeatures(
bool $expected, int $actualFeatures, int $testedFeatures
) : void {
$backend = $this->createMock( FileBackend::class );
$backend->method( 'getFeatures' )->willReturn( $actualFeatures );
$this->assertSame( $expected, $backend->hasFeatures( $testedFeatures ) );
}
public static function provideHasFeatures() : array {
return [
'Nothing has nothing' => [ true, 0, 0 ],
"Nothing doesn't have something" => [ false, 0, 1 ],
'Something has nothing' => [ true, 1, 0 ],
'Something has itself' => [ true, 1, 1 ],
"Something doesn't have something else" => [ false, 0b01, 0b10 ],
"Something doesn't have itself and something else" => [ false, 0b01, 0b11 ],
'Two things have the first one' => [ true, 0b11, 0b01 ],
'Two things have the second one' => [ true, 0b11, 0b10 ],
'Two things have both' => [ true, 0b11, 0b11 ],
"Two things don't have a third" => [ false, 0b11, 0b100 ],
];
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @covers ::doQuickOperations
* @covers ::doQuickOperation
* @covers ::prepare
* @covers ::secure
* @covers ::publish
* @covers ::clean
* @dataProvider provideReadOnly
* @param string $method
*/
public function testReadOnly( string $method ) : void {
$backend = $this->newMockFileBackend( [ 'readOnly' => '.' ] );
$status = $backend->$method( [] );
$this->assertSame( [ [
'type' => 'error',
'message' => 'backend-fail-readonly',
'params' => [ 'test_name', '.' ],
] ], $status->getErrors() );
$this->assertFalse( $status->isOK() );
}
public static function provideReadOnly() : array {
return [
'doOperations' => [ 'doOperations', 'doOperationsInternal', [ [ [] ] ] ],
'doOperation' => [ 'doOperation', 'doOperationsInternal', [ [ 'op' => '' ] ] ],
'doQuickOperations' => [ 'doQuickOperations', 'doQuickOperationsInternal', [ [ [] ] ] ],
'doQuickOperation' => [
'doQuickOperation',
'doQuickOperationsInternal',
[ [ 'op' => '' ] ]
],
'prepare' => [ 'prepare', 'doPrepare' ],
'secure' => [ 'secure', 'doSecure' ],
'publish' => [ 'publish', 'doPublish' ],
'clean' => [ 'clean', 'doClean' ],
];
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @covers ::doQuickOperations
* @covers ::doQuickOperation
* @covers ::prepare
* @covers ::secure
* @covers ::publish
* @covers ::clean
* @dataProvider provideReadOnly
* @param string $method Method to call
* @param string $internalMethod Internal method the call will be forwarded to
* @param array $args To be passed to $method before a final argument of
* [ 'bypassReadOnly' => true ]
*/
public function testDoOperations_bypassReadOnly(
string $method, string $internalMethod, array $args = []
) : void {
$backend = $this->newMockFileBackend( [ 'readOnly' => '.' ], $internalMethod );
$backend->expects( $this->once() )->method( $internalMethod )
->willReturn( StatusValue::newGood( 'myvalue' ) );
$status = $backend->$method( ...array_merge( $args, [ [ 'bypassReadOnly' => true ] ] ) );
$this->assertTrue( $status->isOK() );
$this->assertEmpty( $status->getErrors() );
$this->assertSame( 'myvalue', $status->getValue() );
}
/**
* @covers ::doOperations
* @covers ::doQuickOperations
* @dataProvider provideDoMultipleOperations
* @param string $method
*/
public function testDoOperations_noOp( string $method ) : void {
$backend = $this->newMockFileBackend(
[ 'doOperationsInternal', 'doQuickOperationsInternal' ] );
$backend->expects( $this->never() )->method( 'doOperationsInternal' );
$backend->expects( $this->never() )->method( 'doQuickOperationsInternal' );
$status = $backend->$method( [] );
$this->assertTrue( $status->isOK() );
$this->assertEmpty( $status->getErrors() );
}
public static function provideDoMultipleOperations() : array {
return [
'doOperations' => [ 'doOperations' ],
'doQuickOperations' => [ 'doQuickOperations' ],
];
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @dataProvider provideDoOperations
* @param string $method 'doOperation' or 'doOperations'
*/
public function testDoOperations_nonLockingNoForce( string $method ) : void {
$backend = $this->newMockFileBackend( [ 'doOperationsInternal' ] );
$backend->expects( $this->once() )->method( 'doOperationsInternal' )
->with( [ [] ], [] );
$backend->$method( $method === 'doOperation' ? [] : [ [] ], [ 'nonLocking' => true ] );
}
public static function provideDoOperations() : array {
return [
'doOperations' => [ 'doOperations' ],
'doOperation' => [ 'doOperation' ],
];
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @dataProvider provideDoOperations
* @param string $method 'doOperation' or 'doOperations'
*/
public function testDoOperations_nonLockingForce( string $method ) : void {
$backend = $this->newMockFileBackend( [ 'doOperationsInternal' ] );
$backend->expects( $this->once() )->method( 'doOperationsInternal' )
->with( [ [] ], [ 'nonLocking' => true, 'force' => true ] );
$backend->$method(
$method === 'doOperation' ? [] : [ [] ],
[ 'nonLocking' => true, 'force' => true ]
);
}
// XXX Can't test newScopedIgnoreUserAbort() because it's a no-op in CLI
/**
* @covers ::create
* @covers ::store
* @covers ::copy
* @covers ::move
* @covers ::delete
* @covers ::describe
* @covers ::quickCreate
* @covers ::quickStore
* @covers ::quickCopy
* @covers ::quickMove
* @covers ::quickDelete
* @covers ::quickDescribe
* @dataProvider provideAction
* @param string $prefix '' or 'quick'
* @param string $action
*/
public function testAction( string $prefix, string $action ) : void {
$backend = $this->newMockFileBackend( 'do' . ucfirst( $prefix ) . 'OperationsInternal' );
$expectedOp = [ 'op' => $action, 'foo' => 'bar' ];
if ( $prefix === 'quick' ) {
$expectedOp['overwrite'] = true;
}
$backend->expects( $this->once() )
->method( 'do' . ucfirst( $prefix ) . 'OperationsInternal' )
->with( [ $expectedOp ], [ 'baz' => 'quuz' ] )
->willReturn( StatusValue::newGood( 'myvalue' ) );
$method = $prefix ? $prefix . ucfirst( $action ) : $action;
$status = $backend->$method( [ 'op' => 'ignored', 'foo' => 'bar' ], [ 'baz' => 'quuz' ] );
$this->assertTrue( $status->isOK() );
$this->assertSame( 'myvalue', $status->getValue() );
}
public static function provideAction() : array {
$ret = [];
foreach ( [ '', 'quick' ] as $prefix ) {
foreach ( [ 'create', 'store', 'copy', 'move', 'delete', 'describe' ] as $action ) {
$key = $prefix ? $prefix . ucfirst( $action ) : $action;
$ret[$key] = [ $prefix, $action ];
}
}
return $ret;
}
/**
* @covers ::prepare
* @covers ::secure
* @covers ::publish
* @covers ::clean
* @dataProvider provideForwardToDo
* @param string $method
*/
public function testForwardToDo( string $method ) : void {
$backend = $this->newMockFileBackend( 'do' . ucfirst( $method ) );
$backend->expects( $this->once() )->method( 'do' . ucfirst( $method ) )
->with( [ 'foo' => 'bar' ] )
->willReturn( StatusValue::newGood( 'myvalue' ) );
$status = $backend->$method( [ 'foo' => 'bar' ] );
$this->assertTrue( $status->isOK() );
$this->assertEmpty( $status->getErrors() );
$this->assertSame( 'myvalue', $status->getValue() );
}
public static function provideForwardToDo() : array {
return [
'prepare' => [ 'prepare' ],
'secure' => [ 'secure' ],
'publish' => [ 'publish' ],
'clean' => [ 'clean' ],
];
}
/**
* @covers ::getFileContents
* @covers ::getLocalReference
* @covers ::getLocalCopy
* @dataProvider provideForwardToMulti
* @param string $method
*/
public function testForwardToMulti( string $method ) : void {
$backend = $this->newMockFileBackend( "{$method}Multi" );
$backend->expects( $this->once() )->method( "{$method}Multi" )
->with( [ 'srcs' => [ 'mysrc' ], 'foo' => 'bar', 'src' => 'mysrc' ] )
->willReturn( [ 'mysrc' => 'mycontents' ] );
$result = $backend->$method( [ 'srcs' => 'ignored', 'foo' => 'bar', 'src' => 'mysrc' ] );
$this->assertSame( 'mycontents', $result );
}
public static function provideForwardToMulti() : array {
return [
'getFileContents' => [ 'getFileContents' ],
'getLocalReference' => [ 'getLocalReference' ],
'getLocalCopy' => [ 'getLocalCopy' ],
];
}
/**
* @covers ::getTopDirectoryList
* @covers ::getTopFileList
* @dataProvider provideForwardFromTop
* @param string $methodSuffix
*/
public function testForwardFromTop( string $methodSuffix ) : void {
$backend = $this->newMockFileBackend( "get$methodSuffix" );
$backend->expects( $this->once() )->method( "get$methodSuffix" )
->with( [ 'topOnly' => true, 'foo' => 'bar' ] )
->willReturn( [ 'something' ] );
$method = "getTop$methodSuffix";
$result = $backend->$method( [ 'topOnly' => 'ignored', 'foo' => 'bar' ] );
$this->assertSame( [ 'something' ], $result );
}
public static function provideForwardFromTop() : array {
return [
'getTopDirectoryList' => [ 'DirectoryList' ],
'getTopFileList' => [ 'FileList' ],
];
}
/**
* @covers ::__construct
* @covers ::lockFiles
* @covers ::unlockFiles
* @dataProvider provideLockUnlockFiles
* @param string $method
* @param int $timeout Only relevant for lockFiles
*/
public function testLockUnlockFiles( string $method, ?int $timeout = null ) : void {
// TODO Test that normalizeStoragePath is being called
$args = [ [ 'mwstore://a/b', 'mwstore://c/d/e' ], LockManager::LOCK_SH ];
$mockLm = $this->getMockBuilder( LockManager::class )
->disableOriginalConstructor()
->setMethods( [ 'do' . ucfirst( $method ) . 'ByType', 'doLock', 'doUnlock' ] )
->getMock();
// XXX PHPUnit can't override final methods (T231419)
//$mockLm->expects( $this->once() )->method( $method )
// ->with( ...array_merge( $args, [ $timeout ?? 0 ] ) )
// ->willReturn( StatusValue::newGood( 'myvalue' ) );
//$mockLm->expects( $this->never() )->method( $this->anythingBut( $method ) );
$mockLm->expects( $this->once() )->method( 'do' . ucfirst( $method ) . 'ByType' )
->with( [ LockManager::LOCK_SH => [ 'mwstore://a/b', 'mwstore://c/d/e' ] ] )
->willReturn( StatusValue::newGood( 'myvalue' ) );
$backend = $this->newMockFileBackend( [ 'lockManager' => $mockLm ] );
$backendMethod = "{$method}Files";
$status = $backend->$backendMethod( ...array_merge( $args, (array)$timeout ) );
$this->assertTrue( $status->isOK() );
$this->assertEmpty( $status->getErrors() );
$this->assertSame( 'myvalue', $status->getValue() );
}
public static function provideLockUnlockFiles() : array {
return [
[ 'lock' ],
[ 'lock', 731 ],
[ 'unlock' ],
];
}
/**
* @covers ::__construct
* @covers ::getRootStoragePath
* @dataProvider provideConstruct_validName
* @param mixed $name
*/
public function testGetRootStoragePath( $name ) : void {
$backend = $this->newMockFileBackend( [ 'name' => $name ] );
$this->assertSame( "mwstore://$name", $backend->getRootStoragePath() );
}
/**
* @covers ::__construct
* @covers ::getContainerStoragePath
* @dataProvider provideConstruct_validName
* @param mixed $name
*/
public function testGetContainerStoragePath( $name ) : void {
$backend = $this->newMockFileBackend( [ 'name' => $name ] );
$this->assertSame( "mwstore://$name/mycontainer",
$backend->getContainerStoragePath( 'mycontainer' ) );
}
/**
* @covers ::__construct
* @covers ::getJournal
*/
public function testGetFileJournal_default() : void {
$backend = $this->newMockFileBackend();
$this->assertEquals( new NullFileJournal, $backend->getJournal() );
}
/**
* @covers ::__construct
* @covers ::getJournal
*/
public function testGetJournal() : void {
$mockJournal = $this->createNoOpMock( FileJournal::class );
$backend = $this->newMockFileBackend( [ 'fileJournal' => $mockJournal ] );
$this->assertSame( $mockJournal, $backend->getJournal() );
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @covers ::resolveFSFileObjects
* @dataProvider provideDoOperations
* @param string $method 'doOperation' or 'doOperations'
*/
public function testResolveFSFileObjects( string $method ) : void {
$tmpFile = ( new TempFSFileFactory )->newTempFSFile( 'a' );
$backend = $this->newMockFileBackend( 'doOperationsInternal' );
$backend->expects( $this->once() )->method( 'doOperationsInternal' )
->with( [ [ 'src' => $tmpFile->getPath(), 'srcRef' => $tmpFile ] ] )
->willReturn( StatusValue::newGood() );
$op = [ 'src' => $tmpFile ];
if ( $method === 'doOperations' ) {
$op = [ $op ];
}
$status = $backend->$method( $op );
$this->assertTrue( $status->isOK() );
$this->assertEmpty( $status->getErrors() );
}
/**
* @covers ::doOperations
* @covers ::doOperation
* @covers ::resolveFSFileObjects
* @dataProvider provideDoOperations
* @param string $method 'doOperation' or 'doOperations'
*/
public function testResolveFSFileObjects_preservesTempFiles( string $method ) : void {
$tmpFile = ( new TempFSFileFactory )->newTempFSFile( 'a' );
$path = $tmpFile->getPath();
$backend = $this->newMockFileBackend();
$op = [ 'src' => $tmpFile ];
if ( $method === 'doOperations' ) {
$op = [ $op ];
}
$status = $backend->$method( $op );
$this->assertTrue( file_exists( $path ) );
}
}

View file

@ -1,6 +1,6 @@
<?php
class TestFileJournal extends NullFileJournal {
class TestFileJournal extends FileJournal {
/** @var bool */
private $purged = false;