X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/tests/phpunit/includes/filebackend/FileBackendTest.php diff --git a/tests/phpunit/includes/filebackend/FileBackendTest.php b/tests/phpunit/includes/filebackend/FileBackendTest.php new file mode 100644 index 00000000..ddcf19bd --- /dev/null +++ b/tests/phpunit/includes/filebackend/FileBackendTest.php @@ -0,0 +1,2641 @@ +getNewTempDirectory(); + if ( $this->getCliArg( 'use-filebackend' ) ) { + if ( self::$backendToUse ) { + $this->singleBackend = self::$backendToUse; + } else { + $name = $this->getCliArg( 'use-filebackend' ); + $useConfig = []; + foreach ( $wgFileBackends as $conf ) { + if ( $conf['name'] == $name ) { + $useConfig = $conf; + break; + } + } + $useConfig['name'] = 'localtesting'; // swap name + $useConfig['shardViaHashLevels'] = [ // test sharding + 'unittest-cont1' => [ 'levels' => 1, 'base' => 16, 'repeat' => 1 ] + ]; + if ( isset( $useConfig['fileJournal'] ) ) { + $useConfig['fileJournal'] = FileJournal::factory( $useConfig['fileJournal'], $name ); + } + $useConfig['lockManager'] = LockManagerGroup::singleton()->get( $useConfig['lockManager'] ); + $class = $useConfig['class']; + self::$backendToUse = new $class( $useConfig ); + $this->singleBackend = self::$backendToUse; + } + } else { + $this->singleBackend = new FSFileBackend( [ + 'name' => 'localtesting', + 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ), + 'wikiId' => wfWikiID(), + 'containerPaths' => [ + 'unittest-cont1' => "{$tmpDir}/localtesting-cont1", + 'unittest-cont2' => "{$tmpDir}/localtesting-cont2" ] + ] ); + } + $this->multiBackend = new FileBackendMultiWrite( [ + 'name' => 'localtesting', + 'lockManager' => LockManagerGroup::singleton()->get( 'fsLockManager' ), + 'parallelize' => 'implicit', + 'wikiId' => wfWikiID() . wfRandomString(), + 'backends' => [ + [ + 'name' => 'localmultitesting1', + 'class' => 'FSFileBackend', + 'containerPaths' => [ + 'unittest-cont1' => "{$tmpDir}/localtestingmulti1-cont1", + 'unittest-cont2' => "{$tmpDir}/localtestingmulti1-cont2" ], + 'isMultiMaster' => false + ], + [ + 'name' => 'localmultitesting2', + 'class' => 'FSFileBackend', + 'containerPaths' => [ + 'unittest-cont1' => "{$tmpDir}/localtestingmulti2-cont1", + 'unittest-cont2' => "{$tmpDir}/localtestingmulti2-cont2" ], + 'isMultiMaster' => true + ] + ] + ] ); + } + + private static function baseStorePath() { + return 'mwstore://localtesting'; + } + + private function backendClass() { + return get_class( $this->backend ); + } + + /** + * @dataProvider provider_testIsStoragePath + */ + public function testIsStoragePath( $path, $isStorePath ) { + $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ), + "FileBackend::isStoragePath on path '$path'" ); + } + + public static function provider_testIsStoragePath() { + return [ + [ 'mwstore://', true ], + [ 'mwstore://backend', true ], + [ 'mwstore://backend/container', true ], + [ 'mwstore://backend/container/', true ], + [ 'mwstore://backend/container/path', true ], + [ 'mwstore://backend//container/', true ], + [ 'mwstore://backend//container//', true ], + [ 'mwstore://backend//container//path', true ], + [ 'mwstore:///', true ], + [ 'mwstore:/', false ], + [ 'mwstore:', false ], + ]; + } + + /** + * @dataProvider provider_testSplitStoragePath + */ + public function testSplitStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::splitStoragePath( $path ), + "FileBackend::splitStoragePath on path '$path'" ); + } + + public static function provider_testSplitStoragePath() { + return [ + [ 'mwstore://backend/container', [ 'backend', 'container', '' ] ], + [ 'mwstore://backend/container/', [ 'backend', 'container', '' ] ], + [ 'mwstore://backend/container/path', [ 'backend', 'container', 'path' ] ], + [ 'mwstore://backend/container//path', [ 'backend', 'container', '/path' ] ], + [ 'mwstore://backend//container/path', [ null, null, null ] ], + [ 'mwstore://backend//container//path', [ null, null, null ] ], + [ 'mwstore://', [ null, null, null ] ], + [ 'mwstore://backend', [ null, null, null ] ], + [ 'mwstore:///', [ null, null, null ] ], + [ 'mwstore:/', [ null, null, null ] ], + [ 'mwstore:', [ null, null, null ] ] + ]; + } + + /** + * @dataProvider provider_normalizeStoragePath + */ + public function testNormalizeStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ), + "FileBackend::normalizeStoragePath on path '$path'" ); + } + + public static function provider_normalizeStoragePath() { + return [ + [ 'mwstore://backend/container', 'mwstore://backend/container' ], + [ 'mwstore://backend/container/', 'mwstore://backend/container' ], + [ 'mwstore://backend/container/path', 'mwstore://backend/container/path' ], + [ 'mwstore://backend/container//path', 'mwstore://backend/container/path' ], + [ 'mwstore://backend/container///path', 'mwstore://backend/container/path' ], + [ + 'mwstore://backend/container///path//to///obj', + 'mwstore://backend/container/path/to/obj' + ], + [ 'mwstore://', null ], + [ 'mwstore://backend', null ], + [ 'mwstore://backend//container/path', null ], + [ 'mwstore://backend//container//path', null ], + [ 'mwstore:///', null ], + [ 'mwstore:/', null ], + [ 'mwstore:', null ], + ]; + } + + /** + * @dataProvider provider_testParentStoragePath + */ + public function testParentStoragePath( $path, $res ) { + $this->assertEquals( $res, FileBackend::parentStoragePath( $path ), + "FileBackend::parentStoragePath on path '$path'" ); + } + + public static function provider_testParentStoragePath() { + return [ + [ 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ], + [ 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ], + [ 'mwstore://backend/container/path', 'mwstore://backend/container' ], + [ 'mwstore://backend/container', null ], + [ 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ], + [ 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ], + [ 'mwstore://backend/container/path/', 'mwstore://backend/container' ], + [ 'mwstore://backend/container/', null ], + ]; + } + + /** + * @dataProvider provider_testExtensionFromPath + */ + public function testExtensionFromPath( $path, $res ) { + $this->assertEquals( $res, FileBackend::extensionFromPath( $path ), + "FileBackend::extensionFromPath on path '$path'" ); + } + + public static function provider_testExtensionFromPath() { + return [ + [ 'mwstore://backend/container/path.txt', 'txt' ], + [ 'mwstore://backend/container/path.svg.png', 'png' ], + [ 'mwstore://backend/container/path', '' ], + [ 'mwstore://backend/container/path.', '' ], + ]; + } + + /** + * @dataProvider provider_testStore + */ + public function testStore( $op ) { + $this->addTmpFiles( $op['src'] ); + + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestStore( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestStore( $op ); + $this->tearDownFiles(); + } + + private function doTestStore( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( [ 'dir' => dirname( $dest ) ] ); + + file_put_contents( $source, "Unit test file" ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->store( $op ); + } + + $status = $this->backend->doOperation( $op ); + + $this->assertGoodStatus( $status, + "Store from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Store from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Store from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( true, file_exists( $source ), + "Source file $source still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest exists ($backendName)." ); + + $this->assertEquals( filesize( $source ), + $this->backend->getFileSize( [ 'src' => $dest ] ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = FSFile::getPropsFromPath( $source ); + $props2 = $this->backend->getFileProps( [ 'src' => $dest ] ); + $this->assertEquals( $props1, $props2, + "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( [ $dest ] ); + } + + public static function provider_testStore() { + $cases = []; + + $tmpName = TempFSFile::factory( "unittests_", 'txt', wfTempDir() )->getPath(); + $toPath = self::baseStorePath() . '/unittest-cont1/e/fun/obj1.txt'; + $op = [ 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath ]; + $cases[] = [ $op ]; + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = [ $op2 ]; + + $op3 = $op; + $op3['overwriteSame'] = true; + $cases[] = [ $op3 ]; + + return $cases; + } + + /** + * @dataProvider provider_testCopy + */ + public function testCopy( $op ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestCopy( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestCopy( $op ); + $this->tearDownFiles(); + } + + private function doTestCopy( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( [ 'dir' => dirname( $source ) ] ); + $this->prepare( [ 'dir' => dirname( $dest ) ] ); + + if ( isset( $op['ignoreMissingSource'] ) ) { + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ), + "Source file $source does not exist ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest does not exist ($backendName)." ); + + return; // done + } + + $status = $this->backend->doOperation( + [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->copy( $op ); + } + + $status = $this->backend->doOperation( $op ); + + $this->assertGoodStatus( $status, + "Copy from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Copy from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Copy from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $source ] ), + "Source file $source still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest exists after copy ($backendName)." ); + + $this->assertEquals( + $this->backend->getFileSize( [ 'src' => $source ] ), + $this->backend->getFileSize( [ 'src' => $dest ] ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = $this->backend->getFileProps( [ 'src' => $source ] ); + $props2 = $this->backend->getFileProps( [ 'src' => $dest ] ); + $this->assertEquals( $props1, $props2, + "Source and destination have the same props ($backendName)." ); + + $this->assertBackendPathsConsistent( [ $source, $dest ] ); + } + + public static function provider_testCopy() { + $cases = []; + + $source = self::baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; + + $op = [ 'op' => 'copy', 'src' => $source, 'dst' => $dest ]; + $cases[] = [ + $op, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['overwriteSame'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = [ + $op2, // operation + self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source + $dest, // dest + ]; + + return $cases; + } + + /** + * @dataProvider provider_testMove + */ + public function testMove( $op ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestMove( $op ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestMove( $op ); + $this->tearDownFiles(); + } + + private function doTestMove( $op ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $dest = $op['dst']; + $this->prepare( [ 'dir' => dirname( $source ) ] ); + $this->prepare( [ 'dir' => dirname( $dest ) ] ); + + if ( isset( $op['ignoreMissingSource'] ) ) { + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ), + "Source file $source does not exist ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest does not exist ($backendName)." ); + + return; // done + } + + $status = $this->backend->doOperation( + [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + + if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) { + $this->backend->copy( $op ); + } + + $status = $this->backend->doOperation( $op ); + $this->assertGoodStatus( $status, + "Move from $source to $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Move from $source to $dest succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Move from $source to $dest has proper 'success' field in Status ($backendName)." ); + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ), + "Source file $source does not still exists ($backendName)." ); + $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest exists after move ($backendName)." ); + + $this->assertNotEquals( + $this->backend->getFileSize( [ 'src' => $source ] ), + $this->backend->getFileSize( [ 'src' => $dest ] ), + "Destination file $dest has correct size ($backendName)." ); + + $props1 = $this->backend->getFileProps( [ 'src' => $source ] ); + $props2 = $this->backend->getFileProps( [ 'src' => $dest ] ); + $this->assertEquals( false, $props1['fileExists'], + "Source file does not exist accourding to props ($backendName)." ); + $this->assertEquals( true, $props2['fileExists'], + "Destination file exists accourding to props ($backendName)." ); + + $this->assertBackendPathsConsistent( [ $source, $dest ] ); + } + + public static function provider_testMove() { + $cases = []; + + $source = self::baseStorePath() . '/unittest-cont1/e/file.txt'; + $dest = self::baseStorePath() . '/unittest-cont2/a/fileMoved.txt'; + + $op = [ 'op' => 'move', 'src' => $source, 'dst' => $dest ]; + $cases[] = [ + $op, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['overwrite'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['overwriteSame'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = [ + $op2, // operation + $source, // source + $dest, // dest + ]; + + $op2 = $op; + $op2['ignoreMissingSource'] = true; + $cases[] = [ + $op2, // operation + self::baseStorePath() . '/unittest-cont-bad/e/file.txt', // source + $dest, // dest + ]; + + return $cases; + } + + /** + * @dataProvider provider_testDelete + */ + public function testDelete( $op, $withSource, $okStatus ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDelete( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDelete( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + } + + private function doTestDelete( $op, $withSource, $okStatus ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $this->prepare( [ 'dir' => dirname( $source ) ] ); + + if ( $withSource ) { + $status = $this->backend->doOperation( + [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ] ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Deletion of file at $source succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Deletion of file at $source succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Deletion of file at $source has proper 'success' field in Status ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Deletion of file at $source failed ($backendName)." ); + } + + $this->assertEquals( false, $this->backend->fileExists( [ 'src' => $source ] ), + "Source file $source does not exist after move ($backendName)." ); + + $this->assertFalse( + $this->backend->getFileSize( [ 'src' => $source ] ), + "Source file $source has correct size (false) ($backendName)." ); + + $props1 = $this->backend->getFileProps( [ 'src' => $source ] ); + $this->assertFalse( $props1['fileExists'], + "Source file $source does not exist according to props ($backendName)." ); + + $this->assertBackendPathsConsistent( [ $source ] ); + } + + public static function provider_testDelete() { + $cases = []; + + $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; + + $op = [ 'op' => 'delete', 'src' => $source ]; + $cases[] = [ + $op, // operation + true, // with source + true // succeeds + ]; + + $cases[] = [ + $op, // operation + false, // without source + false // fails + ]; + + $op['ignoreMissingSource'] = true; + $cases[] = [ + $op, // operation + false, // without source + true // succeeds + ]; + + $op['ignoreMissingSource'] = true; + $op['src'] = self::baseStorePath() . '/unittest-cont-bad/e/file.txt'; + $cases[] = [ + $op, // operation + false, // without source + true // succeeds + ]; + + return $cases; + } + + /** + * @dataProvider provider_testDescribe + */ + public function testDescribe( $op, $withSource, $okStatus ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestDescribe( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestDescribe( $op, $withSource, $okStatus ); + $this->tearDownFiles(); + } + + private function doTestDescribe( $op, $withSource, $okStatus ) { + $backendName = $this->backendClass(); + + $source = $op['src']; + $this->prepare( [ 'dir' => dirname( $source ) ] ); + + if ( $withSource ) { + $status = $this->backend->doOperation( + [ 'op' => 'create', 'content' => 'blahblah', 'dst' => $source, + 'headers' => [ 'Content-Disposition' => 'xxx' ] ] ); + $this->assertGoodStatus( $status, + "Creation of file at $source succeeded ($backendName)." ); + if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) { + $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] ); + $this->assertHasHeaders( [ 'Content-Disposition' => 'xxx' ], $attr ); + } + + $status = $this->backend->describe( [ 'src' => $source, + 'headers' => [ 'Content-Disposition' => '' ] ] ); // remove + $this->assertGoodStatus( $status, + "Removal of header for $source succeeded ($backendName)." ); + + if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) { + $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] ); + $this->assertFalse( isset( $attr['headers']['content-disposition'] ), + "File 'Content-Disposition' header removed." ); + } + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Describe of file at $source succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Describe of file at $source succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Describe of file at $source has proper 'success' field in Status ($backendName)." ); + if ( $this->backend->hasFeatures( FileBackend::ATTR_HEADERS ) ) { + $attr = $this->backend->getFileXAttributes( [ 'src' => $source ] ); + $this->assertHasHeaders( $op['headers'], $attr ); + } + } else { + $this->assertEquals( false, $status->isOK(), + "Describe of file at $source failed ($backendName)." ); + } + + $this->assertBackendPathsConsistent( [ $source ] ); + } + + private function assertHasHeaders( array $headers, array $attr ) { + foreach ( $headers as $n => $v ) { + if ( $n !== '' ) { + $this->assertTrue( isset( $attr['headers'][strtolower( $n )] ), + "File has '$n' header." ); + $this->assertEquals( $v, $attr['headers'][strtolower( $n )], + "File has '$n' header value." ); + } else { + $this->assertFalse( isset( $attr['headers'][strtolower( $n )] ), + "File does not have '$n' header." ); + } + } + } + + public static function provider_testDescribe() { + $cases = []; + + $source = self::baseStorePath() . '/unittest-cont1/e/myfacefile.txt'; + + $op = [ 'op' => 'describe', 'src' => $source, + 'headers' => [ 'Content-Disposition' => 'inline' ], ]; + $cases[] = [ + $op, // operation + true, // with source + true // succeeds + ]; + + $cases[] = [ + $op, // operation + false, // without source + false // fails + ]; + + return $cases; + } + + /** + * @dataProvider provider_testCreate + */ + public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize ); + $this->tearDownFiles(); + } + + private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) { + $backendName = $this->backendClass(); + + $dest = $op['dst']; + $this->prepare( [ 'dir' => dirname( $dest ) ] ); + + $oldText = 'blah...blah...waahwaah'; + if ( $alreadyExists ) { + $status = $this->backend->doOperation( + [ 'op' => 'create', 'content' => $oldText, 'dst' => $dest ] ); + $this->assertGoodStatus( $status, + "Creation of file at $dest succeeded ($backendName)." ); + } + + $status = $this->backend->doOperation( $op ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Creation of file at $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of file at $dest succeeded ($backendName)." ); + $this->assertEquals( [ 0 => true ], $status->success, + "Creation of file at $dest has proper 'success' field in Status ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Creation of file at $dest failed ($backendName)." ); + } + + $this->assertEquals( true, $this->backend->fileExists( [ 'src' => $dest ] ), + "Destination file $dest exists after creation ($backendName)." ); + + $props1 = $this->backend->getFileProps( [ 'src' => $dest ] ); + $this->assertEquals( true, $props1['fileExists'], + "Destination file $dest exists according to props ($backendName)." ); + if ( $okStatus ) { // file content is what we saved + $this->assertEquals( $newSize, $props1['size'], + "Destination file $dest has expected size according to props ($backendName)." ); + $this->assertEquals( $newSize, + $this->backend->getFileSize( [ 'src' => $dest ] ), + "Destination file $dest has correct size ($backendName)." ); + } else { // file content is some other previous text + $this->assertEquals( strlen( $oldText ), $props1['size'], + "Destination file $dest has original size according to props ($backendName)." ); + $this->assertEquals( strlen( $oldText ), + $this->backend->getFileSize( [ 'src' => $dest ] ), + "Destination file $dest has original size according to props ($backendName)." ); + } + + $this->assertBackendPathsConsistent( [ $dest ] ); + } + + /** + * @dataProvider provider_testCreate + */ + public static function provider_testCreate() { + $cases = []; + + $dest = self::baseStorePath() . '/unittest-cont2/a/myspacefile.txt'; + + $op = [ 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest ]; + $cases[] = [ + $op, // operation + false, // no dest already exists + true, // succeeds + strlen( $op['content'] ) + ]; + + $op2 = $op; + $op2['content'] = "\n"; + $cases[] = [ + $op2, // operation + false, // no dest already exists + true, // succeeds + strlen( $op2['content'] ) + ]; + + $op2 = $op; + $op2['content'] = "fsf\n waf 3kt"; + $cases[] = [ + $op2, // operation + true, // dest already exists + false, // fails + strlen( $op2['content'] ) + ]; + + $op2 = $op; + $op2['content'] = "egm'g gkpe gpqg eqwgwqg"; + $op2['overwrite'] = true; + $cases[] = [ + $op2, // operation + true, // dest already exists + true, // succeeds + strlen( $op2['content'] ) + ]; + + $op2 = $op; + $op2['content'] = "39qjmg3-qg"; + $op2['overwriteSame'] = true; + $cases[] = [ + $op2, // operation + true, // dest already exists + false, // succeeds + strlen( $op2['content'] ) + ]; + + return $cases; + } + + public function testDoQuickOperations() { + $this->backend = $this->singleBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->doTestDoQuickOperations(); + $this->tearDownFiles(); + } + + private function doTestDoQuickOperations() { + $backendName = $this->backendClass(); + + $base = self::baseStorePath(); + $files = [ + "$base/unittest-cont1/e/fileA.a", + "$base/unittest-cont1/e/fileB.a", + "$base/unittest-cont1/e/fileC.a" + ]; + $createOps = []; + $purgeOps = []; + foreach ( $files as $path ) { + $status = $this->prepare( [ 'dir' => dirname( $path ) ] ); + $this->assertGoodStatus( $status, + "Preparing $path succeeded without warnings ($backendName)." ); + $createOps[] = [ 'op' => 'create', 'dst' => $path, 'content' => mt_rand( 0, 50000 ) ]; + $copyOps[] = [ 'op' => 'copy', 'src' => $path, 'dst' => "$path-2" ]; + $moveOps[] = [ 'op' => 'move', 'src' => "$path-2", 'dst' => "$path-3" ]; + $purgeOps[] = [ 'op' => 'delete', 'src' => $path ]; + $purgeOps[] = [ 'op' => 'delete', 'src' => "$path-3" ]; + } + $purgeOps[] = [ 'op' => 'null' ]; + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $createOps ), + "Creation of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( [ 'src' => $file ] ), + "File $file exists." ); + } + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $copyOps ), + "Quick copy of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( [ 'src' => "$file-2" ] ), + "File $file-2 exists." ); + } + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $moveOps ), + "Quick move of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertTrue( $this->backend->fileExists( [ 'src' => "$file-3" ] ), + "File $file-3 move in." ); + $this->assertFalse( $this->backend->fileExists( [ 'src' => "$file-2" ] ), + "File $file-2 moved away." ); + } + + $this->assertGoodStatus( + $this->backend->quickCopy( [ 'src' => $files[0], 'dst' => $files[0] ] ), + "Copy of file {$files[0]} over itself succeeded ($backendName)." ); + $this->assertTrue( $this->backend->fileExists( [ 'src' => $files[0] ] ), + "File {$files[0]} still exists." ); + + $this->assertGoodStatus( + $this->backend->quickMove( [ 'src' => $files[0], 'dst' => $files[0] ] ), + "Move of file {$files[0]} over itself succeeded ($backendName)." ); + $this->assertTrue( $this->backend->fileExists( [ 'src' => $files[0] ] ), + "File {$files[0]} still exists." ); + + $this->assertGoodStatus( + $this->backend->doQuickOperations( $purgeOps ), + "Quick deletion of source files succeeded ($backendName)." ); + foreach ( $files as $file ) { + $this->assertFalse( $this->backend->fileExists( [ 'src' => $file ] ), + "File $file purged." ); + $this->assertFalse( $this->backend->fileExists( [ 'src' => "$file-3" ] ), + "File $file-3 purged." ); + } + } + + /** + * @dataProvider provider_testConcatenate + */ + public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ); + $this->tearDownFiles(); + } + + private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) { + $backendName = $this->backendClass(); + + $expContent = ''; + // Create sources + $ops = []; + foreach ( $srcs as $i => $source ) { + $this->prepare( [ 'dir' => dirname( $source ) ] ); + $ops[] = [ + 'op' => 'create', // operation + 'dst' => $source, // source + 'content' => $srcsContent[$i] + ]; + $expContent .= $srcsContent[$i]; + } + $status = $this->backend->doOperations( $ops ); + + $this->assertGoodStatus( $status, + "Creation of source files succeeded ($backendName)." ); + + $dest = $params['dst'] = $this->getNewTempFile(); + if ( $alreadyExists ) { + $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false; + $this->assertEquals( true, $ok, + "Creation of file at $dest succeeded ($backendName)." ); + } else { + $ok = file_put_contents( $dest, '' ) !== false; + $this->assertEquals( true, $ok, + "Creation of 0-byte file at $dest succeeded ($backendName)." ); + } + + // Combine the files into one + $status = $this->backend->concatenate( $params ); + if ( $okStatus ) { + $this->assertGoodStatus( $status, + "Creation of concat file at $dest succeeded without warnings ($backendName)." ); + $this->assertEquals( true, $status->isOK(), + "Creation of concat file at $dest succeeded ($backendName)." ); + } else { + $this->assertEquals( false, $status->isOK(), + "Creation of concat file at $dest failed ($backendName)." ); + } + + if ( $okStatus ) { + $this->assertEquals( true, is_file( $dest ), + "Dest concat file $dest exists after creation ($backendName)." ); + } else { + $this->assertEquals( true, is_file( $dest ), + "Dest concat file $dest exists after failed creation ($backendName)." ); + } + + $contents = file_get_contents( $dest ); + $this->assertNotEquals( false, $contents, "File at $dest exists ($backendName)." ); + + if ( $okStatus ) { + $this->assertEquals( $expContent, $contents, + "Concat file at $dest has correct contents ($backendName)." ); + } else { + $this->assertNotEquals( $expContent, $contents, + "Concat file at $dest has correct contents ($backendName)." ); + } + } + + public static function provider_testConcatenate() { + $cases = []; + + $srcs = [ + self::baseStorePath() . '/unittest-cont1/e/file1.txt', + self::baseStorePath() . '/unittest-cont1/e/file2.txt', + self::baseStorePath() . '/unittest-cont1/e/file3.txt', + self::baseStorePath() . '/unittest-cont1/e/file4.txt', + self::baseStorePath() . '/unittest-cont1/e/file5.txt', + self::baseStorePath() . '/unittest-cont1/e/file6.txt', + self::baseStorePath() . '/unittest-cont1/e/file7.txt', + self::baseStorePath() . '/unittest-cont1/e/file8.txt', + self::baseStorePath() . '/unittest-cont1/e/file9.txt', + self::baseStorePath() . '/unittest-cont1/e/file10.txt' + ]; + $content = [ + 'egfage', + 'ageageag', + 'rhokohlr', + 'shgmslkg', + 'kenga', + 'owagmal', + 'kgmae', + 'g eak;g', + 'lkaem;a', + 'legma' + ]; + $params = [ 'srcs' => $srcs ]; + + $cases[] = [ + $params, // operation + $srcs, // sources + $content, // content for each source + false, // no dest already exists + true, // succeeds + ]; + + $cases[] = [ + $params, // operation + $srcs, // sources + $content, // content for each source + true, // dest already exists + false, // succeeds + ]; + + return $cases; + } + + /** + * @dataProvider provider_testGetFileStat + */ + public function testGetFileStat( $path, $content, $alreadyExists ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestGetFileStat( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestGetFileStat( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + } + + private function doTestGetFileStat( $path, $content, $alreadyExists ) { + $backendName = $this->backendClass(); + + if ( $alreadyExists ) { + $this->prepare( [ 'dir' => dirname( $path ) ] ); + $status = $this->create( [ 'dst' => $path, 'content' => $content ] ); + $this->assertGoodStatus( $status, + "Creation of file at $path succeeded ($backendName)." ); + + $size = $this->backend->getFileSize( [ 'src' => $path ] ); + $time = $this->backend->getFileTimestamp( [ 'src' => $path ] ); + $stat = $this->backend->getFileStat( [ 'src' => $path ] ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, + "Correct file timestamp of '$path'" ); + + $size = $stat['size']; + $time = $stat['mtime']; + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 10, + "Correct file timestamp of '$path'" ); + + $this->backend->clearCache( [ $path ] ); + + $size = $this->backend->getFileSize( [ 'src' => $path ] ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + + $this->backend->preloadCache( [ $path ] ); + + $size = $this->backend->getFileSize( [ 'src' => $path ] ); + + $this->assertEquals( strlen( $content ), $size, + "Correct file size of '$path'" ); + } else { + $size = $this->backend->getFileSize( [ 'src' => $path ] ); + $time = $this->backend->getFileTimestamp( [ 'src' => $path ] ); + $stat = $this->backend->getFileStat( [ 'src' => $path ] ); + + $this->assertFalse( $size, "Correct file size of '$path'" ); + $this->assertFalse( $time, "Correct file timestamp of '$path'" ); + $this->assertFalse( $stat, "Correct file stat of '$path'" ); + } + } + + public static function provider_testGetFileStat() { + $cases = []; + + $base = self::baseStorePath(); + $cases[] = [ "$base/unittest-cont1/e/b/z/some_file.txt", "some file contents", true ]; + $cases[] = [ "$base/unittest-cont1/e/b/some-other_file.txt", "", true ]; + $cases[] = [ "$base/unittest-cont1/e/b/some-diff_file.txt", null, false ]; + + return $cases; + } + + /** + * @dataProvider provider_testGetFileStat + */ + public function testStreamFile( $path, $content, $alreadyExists ) { + $this->backend = $this->singleBackend; + $this->tearDownFiles(); + $this->doTestStreamFile( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + + $this->backend = $this->multiBackend; + $this->tearDownFiles(); + $this->doTestStreamFile( $path, $content, $alreadyExists ); + $this->tearDownFiles(); + } + + private function doTestStreamFile( $path, $content ) { + $backendName = $this->backendClass(); + + if ( $content !== null ) { + $this->prepare( [ 'dir' => dirname( $path ) ] ); + $status = $this->create( [ 'dst' => $path, 'content' => $content ] ); + $this->assertGoodStatus( $status, + "Creation of file at $path succeeded ($backendName)." ); + + ob_start(); + $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] ); + $data = ob_get_contents(); + ob_end_clean(); + + $this->assertEquals( $content, $data, "Correct content streamed from '$path'" ); + } else { // 404 case + ob_start(); + $this->backend->streamFile( [ 'src' => $path, 'headless' => 1, 'allowOB' => 1 ] ); + $data = ob_get_contents(); + ob_end_clean(); + + $this->assertRegExp( '#