[FileBackend] File locking fixes.
[lhc/web/wiklou.git] / tests / phpunit / includes / filerepo / FileBackendTest.php
index 3f32891..9c36a57 100644 (file)
@@ -1,20 +1,45 @@
 <?php
 
-// @TODO: fix empty dir leakage
+/**
+ * @group FileRepo
+ * @group FileBackend
+ */
 class FileBackendTest extends MediaWikiTestCase {
        private $backend, $multiBackend;
-       private $filesToPrune, $pathsToPrune;
+       private $filesToPrune = array();
+       private $dirsToPrune = array();
+       private static $backendToUse;
 
        function setUp() {
+               global $wgFileBackends;
                parent::setUp();
-               $tmpDir = wfTempDir() . '/' . time() . '-' . mt_rand();
-               $this->singleBackend = new FSFileBackend( array(
-                       'name'        => 'localtesting',
-                       'lockManager' => 'fsLockManager',
-                       'containerPaths' => array(
-                               'cont1' => "$tmpDir/localtesting/cont1",
-                               'cont2' => "$tmpDir/localtesting/cont2" )
-               ) );
+               $tmpPrefix = wfTempDir() . '/filebackend-unittest-' . time() . '-' . mt_rand();
+               if ( $this->getCliArg( 'use-filebackend=' ) ) {
+                       if ( self::$backendToUse ) {
+                               $this->singleBackend = self::$backendToUse;
+                       } else {
+                               $name = $this->getCliArg( 'use-filebackend=' );
+                               $useConfig = array();
+                               foreach ( $wgFileBackends as $conf ) {
+                                       if ( $conf['name'] == $name ) {
+                                               $useConfig = $conf;
+                                               break;
+                                       }
+                               }
+                               $useConfig['name'] = 'localtesting'; // swap name
+                               $class = $useConfig['class'];
+                               self::$backendToUse = new $class( $useConfig );
+                               $this->singleBackend = self::$backendToUse;
+                       }
+               } else {
+                       $this->singleBackend = new FSFileBackend( array(
+                               'name'        => 'localtesting',
+                               'lockManager' => 'fsLockManager',
+                               'containerPaths' => array(
+                                       'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
+                                       'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
+                       ) );
+               }
                $this->multiBackend = new FileBackendMultiWrite( array(
                        'name'        => 'localtesting',
                        'lockManager' => 'fsLockManager',
@@ -24,8 +49,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
-                                               'cont1' => "$tmpDir/localtestingmulti1/cont1",
-                                               'cont2' => "$tmpDir/localtestingmulti1/cont2" ),
+                                               'unittest-cont1' => "{$tmpPrefix}-localtestingmulti1-cont1",
+                                               'unittest-cont2' => "{$tmpPrefix}-localtestingmulti1-cont2" ),
                                        'isMultiMaster' => false
                                ),
                                array(
@@ -33,13 +58,13 @@ class FileBackendTest extends MediaWikiTestCase {
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
-                                               'cont1' => "$tmpDir/localtestingmulti2/cont1",
-                                               'cont2' => "$tmpDir/localtestingmulti2/cont2" ),
+                                               'unittest-cont1' => "{$tmpPrefix}-localtestingmulti2-cont1",
+                                               'unittest-cont2' => "{$tmpPrefix}-localtestingmulti2-cont2" ),
                                        'isMultiMaster' => true
                                )
                        )
                ) );
-               $this->filesToPrune = $this->pathsToPrune = array();
+               $this->filesToPrune = array();
        }
 
        private function baseStorePath() {
@@ -50,32 +75,157 @@ class FileBackendTest extends MediaWikiTestCase {
                return get_class( $this->backend );
        }
 
+       /**
+        * @dataProvider provider_testIsStoragePath
+        */
+       public function testIsStoragePath( $path, $isStorePath ) {
+               $this->assertEquals( $isStorePath, FileBackend::isStoragePath( $path ),
+                       "FileBackend::isStoragePath on path '$path'" );
+       }
+
+       function provider_testIsStoragePath() {
+               return array(
+                       array( 'mwstore://', true ),
+                       array( 'mwstore://backend', true ),
+                       array( 'mwstore://backend/container', true ),
+                       array( 'mwstore://backend/container/', true ),
+                       array( 'mwstore://backend/container/path', true ),
+                       array( 'mwstore://backend//container/', true ),
+                       array( 'mwstore://backend//container//', true ),
+                       array( 'mwstore://backend//container//path', true ),
+                       array( 'mwstore:///', true ),
+                       array( 'mwstore:/', false ),
+                       array( 'mwstore:', false ),
+               );
+       }
+
+       /**
+        * @dataProvider provider_testSplitStoragePath
+        */
+       public function testSplitStoragePath( $path, $res ) {
+               $this->assertEquals( $res, FileBackend::splitStoragePath( $path ),
+                       "FileBackend::splitStoragePath on path '$path'" );
+       }
+
+       function provider_testSplitStoragePath() {
+               return array(
+                       array( 'mwstore://backend/container', array( 'backend', 'container', '' ) ),
+                       array( 'mwstore://backend/container/', array( 'backend', 'container', '' ) ),
+                       array( 'mwstore://backend/container/path', array( 'backend', 'container', 'path' ) ),
+                       array( 'mwstore://backend/container//path', array( 'backend', 'container', '/path' ) ),
+                       array( 'mwstore://backend//container/path', array( null, null, null ) ),
+                       array( 'mwstore://backend//container//path', array( null, null, null ) ),
+                       array( 'mwstore://', array( null, null, null ) ),
+                       array( 'mwstore://backend', array( null, null, null ) ),
+                       array( 'mwstore:///', array( null, null, null ) ),
+                       array( 'mwstore:/', array( null, null, null ) ),
+                       array( 'mwstore:', array( null, null, null ) )
+               );
+       }
+
+       /**
+        * @dataProvider provider_normalizeStoragePath
+        */
+       public function testNormalizeStoragePath( $path, $res ) {
+               $this->assertEquals( $res, FileBackend::normalizeStoragePath( $path ),
+                       "FileBackend::normalizeStoragePath on path '$path'" );
+       }
+
+       function provider_normalizeStoragePath() {
+               return array(
+                       array( 'mwstore://backend/container', 'mwstore://backend/container' ),
+                       array( 'mwstore://backend/container/', 'mwstore://backend/container' ),
+                       array( 'mwstore://backend/container/path', 'mwstore://backend/container/path' ),
+                       array( 'mwstore://backend/container//path', 'mwstore://backend/container/path' ),
+                       array( 'mwstore://backend/container///path', 'mwstore://backend/container/path' ),
+                       array( 'mwstore://backend/container///path//to///obj', 'mwstore://backend/container/path/to/obj',
+                       array( 'mwstore://', null ),
+                       array( 'mwstore://backend', null ),
+                       array( 'mwstore://backend//container/path', null ),
+                       array( 'mwstore://backend//container//path', null ),
+                       array( 'mwstore:///', null ),
+                       array( 'mwstore:/', null ),
+                       array( 'mwstore:', null ), )
+               );
+       }
+
+       /**
+        * @dataProvider provider_testParentStoragePath
+        */
+       public function testParentStoragePath( $path, $res ) {
+               $this->assertEquals( $res, FileBackend::parentStoragePath( $path ),
+                       "FileBackend::parentStoragePath on path '$path'" );
+       }
+
+       function provider_testParentStoragePath() {
+               return array(
+                       array( 'mwstore://backend/container/path/to/obj', 'mwstore://backend/container/path/to' ),
+                       array( 'mwstore://backend/container/path/to', 'mwstore://backend/container/path' ),
+                       array( 'mwstore://backend/container/path', 'mwstore://backend/container' ),
+                       array( 'mwstore://backend/container', null ),
+                       array( 'mwstore://backend/container/path/to/obj/', 'mwstore://backend/container/path/to' ),
+                       array( 'mwstore://backend/container/path/to/', 'mwstore://backend/container/path' ),
+                       array( 'mwstore://backend/container/path/', 'mwstore://backend/container' ),
+                       array( 'mwstore://backend/container/', null ),
+               );
+       }
+
+       /**
+        * @dataProvider provider_testExtensionFromPath
+        */
+       public function testExtensionFromPath( $path, $res ) {
+               $this->assertEquals( $res, FileBackend::extensionFromPath( $path ),
+                       "FileBackend::extensionFromPath on path '$path'" );
+       }
+
+       function provider_testExtensionFromPath() {
+               return array(
+                       array( 'mwstore://backend/container/path.txt', 'txt' ),
+                       array( 'mwstore://backend/container/path.svg.png', 'png' ),
+                       array( 'mwstore://backend/container/path', '' ),
+                       array( 'mwstore://backend/container/path.', '' ),
+               );
+       }
+
        /**
         * @dataProvider provider_testStore
         */
-       public function testStore( $op, $source, $dest ) {
-               $this->filesToPrune[] = $source;
-               $this->pathsToPrune[] = $dest;
+       public function testStore( $op ) {
+               $this->filesToPrune[] = $op['src'];
 
                $this->backend = $this->singleBackend;
-               $this->doTestStore( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestStore( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestStore( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestStore( $op );
+               $this->filesToPrune[] = $op['src']; # avoid file leaking
                $this->tearDownFiles();
        }
 
-       function doTestStore( $op, $source, $dest ) {
+       private function doTestStore( $op ) {
                $backendName = $this->backendClass();
 
+               $source = $op['src'];
+               $dest = $op['dst'];
+               $this->prepare( array( '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->assertEquals( array(), $status->errors,
                        "Store from $source to $dest succeeded without warnings ($backendName)." );
-               $this->assertEquals( true, $status->isOK(),
+               $this->assertEquals( array(), $status->errors,
                        "Store from $source to $dest succeeded ($backendName)." );
+               $this->assertEquals( array( 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( array( 'src' => $dest ) ),
@@ -95,7 +245,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $cases = array();
 
                $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
-               $toPath = $this->baseStorePath() . '/cont1/fun/obj1.txt';
+               $toPath = $this->baseStorePath() . '/unittest-cont1/fun/obj1.txt';
                $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
                $cases[] = array(
                        $op, // operation
@@ -103,9 +253,18 @@ class FileBackendTest extends MediaWikiTestCase {
                        $toPath, // dest
                );
 
-               $op['overwriteDest'] = true;
+               $op2 = $op;
+               $op2['overwrite'] = true;
                $cases[] = array(
-                       $op, // operation
+                       $op2, // operation
+                       $tmpName, // source
+                       $toPath, // dest
+               );
+
+               $op2 = $op;
+               $op2['overwriteSame'] = true;
+               $cases[] = array(
+                       $op2, // operation
                        $tmpName, // source
                        $toPath, // dest
                );
@@ -116,32 +275,43 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testCopy
         */
-       public function testCopy( $op, $source, $dest ) {
-               $this->pathsToPrune[] = $source;
-               $this->pathsToPrune[] = $dest;
-
+       public function testCopy( $op ) {
                $this->backend = $this->singleBackend;
-               $this->doTestCopy( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestCopy( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestCopy( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestCopy( $op );
                $this->tearDownFiles();
        }
 
-       function doTestCopy( $op, $source, $dest ) {
+       private function doTestCopy( $op ) {
                $backendName = $this->backendClass();
 
+               $source = $op['src'];
+               $dest = $op['dst'];
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+               $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-               $this->assertEquals( true, $status->isOK(),
+               $this->assertEquals( array(), $status->errors,
                        "Creation of file at $source succeeded ($backendName)." );
 
+               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+                       $this->backend->copy( $op );
+               }
+
                $status = $this->backend->doOperation( $op );
+
                $this->assertEquals( array(), $status->errors,
                        "Copy from $source to $dest succeeded without warnings ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Copy from $source to $dest succeeded ($backendName)." );
+               $this->assertEquals( array( 0 => true ), $status->success,
+                       "Copy from $source to $dest has proper 'success' field in Status ($backendName)." );
                $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
                        "Source file $source still exists ($backendName)." );
                $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
@@ -161,8 +331,8 @@ class FileBackendTest extends MediaWikiTestCase {
        public function provider_testCopy() {
                $cases = array();
 
-               $source = $this->baseStorePath() . '/cont1/file.txt';
-               $dest = $this->baseStorePath() . '/cont2/fileMoved.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/file.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt';
 
                $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
                $cases[] = array(
@@ -171,9 +341,18 @@ class FileBackendTest extends MediaWikiTestCase {
                        $dest, // dest
                );
 
-               $op['overwriteDest'] = true;
+               $op2 = $op;
+               $op2['overwrite'] = true;
                $cases[] = array(
-                       $op, // operation
+                       $op2, // operation
+                       $source, // source
+                       $dest, // dest
+               );
+
+               $op2 = $op;
+               $op2['overwriteSame'] = true;
+               $cases[] = array(
+                       $op2, // operation
                        $source, // source
                        $dest, // dest
                );
@@ -184,32 +363,42 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testMove
         */
-       public function testMove( $op, $source, $dest ) {
-               $this->pathsToPrune[] = $source;
-               $this->pathsToPrune[] = $dest;
-
+       public function testMove( $op ) {
                $this->backend = $this->singleBackend;
-               $this->doTestMove( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestMove( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestMove( $op, $source, $dest );
+               $this->tearDownFiles();
+               $this->doTestMove( $op );
                $this->tearDownFiles();
        }
 
-       public function doTestMove( $op, $source, $dest ) {
+       private function doTestMove( $op ) {
                $backendName = $this->backendClass();
 
+               $source = $op['src'];
+               $dest = $op['dst'];
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+               $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-               $this->assertEquals( true, $status->isOK(),
+               $this->assertEquals( array(), $status->errors,
                        "Creation of file at $source succeeded ($backendName)." );
 
+               if ( isset( $op['overwrite'] ) || isset( $op['overwriteSame'] ) ) {
+                       $this->backend->copy( $op );
+               }
+
                $status = $this->backend->doOperation( $op );
                $this->assertEquals( array(), $status->errors,
                        "Move from $source to $dest succeeded without warnings ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
                        "Move from $source to $dest succeeded ($backendName)." );
+               $this->assertEquals( array( 0 => true ), $status->success,
+                       "Move from $source to $dest has proper 'success' field in Status ($backendName)." );
                $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
                        "Source file $source does not still exists ($backendName)." );
                $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
@@ -231,8 +420,8 @@ class FileBackendTest extends MediaWikiTestCase {
        public function provider_testMove() {
                $cases = array();
 
-               $source = $this->baseStorePath() . '/cont1/file.txt';
-               $dest = $this->baseStorePath() . '/cont2/fileMoved.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/file.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/fileMoved.txt';
 
                $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
                $cases[] = array(
@@ -241,9 +430,18 @@ class FileBackendTest extends MediaWikiTestCase {
                        $dest, // dest
                );
 
-               $op['overwriteDest'] = true;
+               $op2 = $op;
+               $op2['overwrite'] = true;
                $cases[] = array(
-                       $op, // operation
+                       $op2, // operation
+                       $source, // source
+                       $dest, // dest
+               );
+
+               $op2 = $op;
+               $op2['overwriteSame'] = true;
+               $cases[] = array(
+                       $op2, // operation
                        $source, // source
                        $dest, // dest
                );
@@ -254,25 +452,28 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testDelete
         */
-       public function testDelete( $op, $source, $withSource, $okStatus ) {
-               $this->pathsToPrune[] = $source;
-
+       public function testDelete( $op, $withSource, $okStatus ) {
                $this->backend = $this->singleBackend;
-               $this->doTestDelete( $op, $source, $withSource, $okStatus );
+               $this->tearDownFiles();
+               $this->doTestDelete( $op, $withSource, $okStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestDelete( $op, $source, $withSource, $okStatus );
+               $this->tearDownFiles();
+               $this->doTestDelete( $op, $withSource, $okStatus );
                $this->tearDownFiles();
        }
 
-       public function doTestDelete( $op, $source, $withSource, $okStatus ) {
+       private function doTestDelete( $op, $withSource, $okStatus ) {
                $backendName = $this->backendClass();
 
+               $source = $op['src'];
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+
                if ( $withSource ) {
                        $status = $this->backend->doOperation(
                                array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
-                       $this->assertEquals( true, $status->isOK(),
+                       $this->assertEquals( array(), $status->errors,
                                "Creation of file at $source succeeded ($backendName)." );
                }
 
@@ -282,6 +483,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Deletion of file at $source succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Deletion of file at $source succeeded ($backendName)." );
+                       $this->assertEquals( array( 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)." );
@@ -302,19 +505,17 @@ class FileBackendTest extends MediaWikiTestCase {
        public function provider_testDelete() {
                $cases = array();
 
-               $source = $this->baseStorePath() . '/cont1/myfacefile.txt';
+               $source = $this->baseStorePath() . '/unittest-cont1/myfacefile.txt';
 
                $op = array( 'op' => 'delete', 'src' => $source );
                $cases[] = array(
                        $op, // operation
-                       $source, // source
                        true, // with source
                        true // succeeds
                );
 
                $cases[] = array(
                        $op, // operation
-                       $source, // source
                        false, // without source
                        false // fails
                );
@@ -322,7 +523,6 @@ class FileBackendTest extends MediaWikiTestCase {
                $op['ignoreMissingSource'] = true;
                $cases[] = array(
                        $op, // operation
-                       $source, // source
                        false, // without source
                        true // succeeds
                );
@@ -333,26 +533,29 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testCreate
         */
-       public function testCreate( $op, $dest, $alreadyExists, $okStatus, $newSize ) {
-               $this->pathsToPrune[] = $dest;
-
+       public function testCreate( $op, $alreadyExists, $okStatus, $newSize ) {
                $this->backend = $this->singleBackend;
-               $this->doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize );
+               $this->tearDownFiles();
+               $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize );
+               $this->tearDownFiles();
+               $this->doTestCreate( $op, $alreadyExists, $okStatus, $newSize );
                $this->tearDownFiles();
        }
 
-       public function doTestCreate( $op, $dest, $alreadyExists, $okStatus, $newSize ) {
+       private function doTestCreate( $op, $alreadyExists, $okStatus, $newSize ) {
                $backendName = $this->backendClass();
 
+               $dest = $op['dst'];
+               $this->prepare( array( 'dir' => dirname( $dest ) ) );
+
                $oldText = 'blah...blah...waahwaah';
                if ( $alreadyExists ) {
                        $status = $this->backend->doOperation(
                                array( 'op' => 'create', 'content' => $oldText, 'dst' => $dest ) );
-                       $this->assertEquals( true, $status->isOK(),
+                       $this->assertEquals( array(), $status->errors,
                                "Creation of file at $dest succeeded ($backendName)." );
                }
 
@@ -362,6 +565,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Creation of file at $dest succeeded without warnings ($backendName)." );
                        $this->assertEquals( true, $status->isOK(),
                                "Creation of file at $dest succeeded ($backendName)." );
+                       $this->assertEquals( array( 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)." );
@@ -394,33 +599,52 @@ class FileBackendTest extends MediaWikiTestCase {
        public function provider_testCreate() {
                $cases = array();
 
-               $source = $this->baseStorePath() . '/cont2/myspacefile.txt';
+               $dest = $this->baseStorePath() . '/unittest-cont2/myspacefile.txt';
 
-               $dummyText = 'hey hey';
-               $op = array( 'op' => 'create', 'content' => $dummyText, 'dst' => $source );
+               $op = array( 'op' => 'create', 'content' => 'test test testing', 'dst' => $dest );
                $cases[] = array(
                        $op, // operation
-                       $source, // source
                        false, // no dest already exists
                        true, // succeeds
-                       strlen( $dummyText )
+                       strlen( $op['content'] )
                );
 
+               $op2 = $op;
+               $op2['content'] = "\n";
                $cases[] = array(
-                       $op, // operation
-                       $source, // source
+                       $op2, // operation
+                       false, // no dest already exists
+                       true, // succeeds
+                       strlen( $op2['content'] )
+               );
+
+               $op2 = $op;
+               $op2['content'] = "fsf\n waf 3kt";
+               $cases[] = array(
+                       $op2, // operation
                        true, // dest already exists
                        false, // fails
-                       strlen( $dummyText )
+                       strlen( $op2['content'] )
                );
 
-               $op['overwriteDest'] = true;
+               $op2 = $op;
+               $op2['content'] = "egm'g gkpe gpqg eqwgwqg";
+               $op2['overwrite'] = true;
                $cases[] = array(
-                       $op, // operation
-                       $source, // source
+                       $op2, // operation
                        true, // dest already exists
                        true, // succeeds
-                       strlen( $dummyText )
+                       strlen( $op2['content'] )
+               );
+
+               $op2 = $op;
+               $op2['content'] = "39qjmg3-qg";
+               $op2['overwriteSame'] = true;
+               $cases[] = array(
+                       $op2, // operation
+                       true, // dest already exists
+                       false, // succeeds
+                       strlen( $op2['content'] )
                );
 
                return $cases;
@@ -430,26 +654,28 @@ class FileBackendTest extends MediaWikiTestCase {
         * @dataProvider provider_testConcatenate
         */
        public function testConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
-               $this->pathsToPrune = array_merge( $this->pathsToPrune, $srcs );
                $this->filesToPrune[] = $op['dst'];
 
                $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
                $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
                $this->tearDownFiles();
 
-               # FIXME
-               #$this->backend = $this->multiBackend;
-               #$this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
-               #$this->tearDownFiles();
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus );
+               $this->filesToPrune[] = $op['dst']; # avoid file leaking
+               $this->tearDownFiles();
        }
 
-       public function doTestConcatenate( $op, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
+       private function doTestConcatenate( $params, $srcs, $srcsContent, $alreadyExists, $okStatus ) {
                $backendName = $this->backendClass();
 
                $expContent = '';
                // Create sources
                $ops = array();
                foreach ( $srcs as $i => $source ) {
+                       $this->prepare( array( 'dir' => dirname( $source ) ) );
                        $ops[] = array(
                                'op'      => 'create', // operation
                                'dst'     => $source, // source
@@ -459,10 +685,10 @@ class FileBackendTest extends MediaWikiTestCase {
                }
                $status = $this->backend->doOperations( $ops );
 
-               $this->assertEquals( true, $status->isOK(),
+               $this->assertEquals( array(), $status->errors,
                        "Creation of source files succeeded ($backendName)." );
 
-               $dest = $op['dst'];
+               $dest = $params['dst'];
                if ( $alreadyExists ) {
                        $ok = file_put_contents( $dest, 'blah...blah...waahwaah' ) !== false;
                        $this->assertEquals( true, $ok,
@@ -473,8 +699,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Creation of 0-byte file at $dest succeeded ($backendName)." );
                }
 
-               // Combine them
-               $status = $this->backend->doOperation( $op );
+               // Combine the files into one
+               $status = $this->backend->concatenate( $params );
                if ( $okStatus ) {
                        $this->assertEquals( array(), $status->errors,
                                "Creation of concat file at $dest succeeded without warnings ($backendName)." );
@@ -511,16 +737,16 @@ class FileBackendTest extends MediaWikiTestCase {
                $rand = mt_rand( 0, 2000000000 ) . time();
                $dest = wfTempDir() . "/randomfile!$rand.txt";
                $srcs = array(
-                       $this->baseStorePath() . '/cont1/file1.txt',
-                       $this->baseStorePath() . '/cont1/file2.txt',
-                       $this->baseStorePath() . '/cont1/file3.txt',
-                       $this->baseStorePath() . '/cont1/file4.txt',
-                       $this->baseStorePath() . '/cont1/file5.txt',
-                       $this->baseStorePath() . '/cont1/file6.txt',
-                       $this->baseStorePath() . '/cont1/file7.txt',
-                       $this->baseStorePath() . '/cont1/file8.txt',
-                       $this->baseStorePath() . '/cont1/file9.txt',
-                       $this->baseStorePath() . '/cont1/file10.txt'
+                       $this->baseStorePath() . '/unittest-cont1/file1.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file2.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file3.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file4.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file5.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file6.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file7.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file8.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file9.txt',
+                       $this->baseStorePath() . '/unittest-cont1/file10.txt'
                );
                $content = array(
                        'egfage',
@@ -534,10 +760,10 @@ class FileBackendTest extends MediaWikiTestCase {
                        'lkaem;a',
                        'legma'
                );
-               $op = array( 'op' => 'concatenate', 'srcs' => $srcs, 'dst' => $dest );
+               $params = array( 'srcs' => $srcs, 'dst' => $dest );
 
                $cases[] = array(
-                       $op, // operation
+                       $params, // operation
                        $srcs, // sources
                        $content, // content for each source
                        false, // no dest already exists
@@ -545,7 +771,7 @@ class FileBackendTest extends MediaWikiTestCase {
                );
 
                $cases[] = array(
-                       $op, // operation
+                       $params, // operation
                        $srcs, // sources
                        $content, // content for each source
                        true, // dest already exists
@@ -556,45 +782,107 @@ class FileBackendTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider provider_testGetFileContents
+        * @dataProvider provider_testGetFileStat
         */
-       public function testGetFileContents( $src, $content ) {
-               $this->pathsToPrune[] = $src;
-
+       public function testGetFileStat( $path, $content, $alreadyExists ) {
                $this->backend = $this->singleBackend;
-               $this->doTestGetFileContents( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetFileStat( $path, $content, $alreadyExists );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestGetFileContents( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetFileStat( $path, $content, $alreadyExists );
                $this->tearDownFiles();
        }
 
+       private function doTestGetFileStat( $path, $content, $alreadyExists ) {
+               $backendName = $this->backendClass();
+
+               if ( $alreadyExists ) {
+                       $this->prepare( array( 'dir' => dirname( $path ) ) );
+                       $status = $this->backend->create( array( 'dst' => $path, 'content' => $content ) );
+                       $this->assertEquals( array(), $status->errors,
+                               "Creation of file at $path succeeded ($backendName)." );
+
+                       $size = $this->backend->getFileSize( array( 'src' => $path ) );
+                       $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+                       $stat = $this->backend->getFileStat( array( 'src' => $path ) );
+
+                       $this->assertEquals( strlen( $content ), $size,
+                               "Correct file size of '$path'" );
+                       $this->assertTrue( abs( time() - wfTimestamp( TS_UNIX, $time ) ) < 5,
+                               "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 ) ) < 5,
+                               "Correct file timestamp of '$path'" );
+               } else {
+                       $size = $this->backend->getFileSize( array( 'src' => $path ) );
+                       $time = $this->backend->getFileTimestamp( array( 'src' => $path ) );
+                       $stat = $this->backend->getFileStat( array( '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'" );
+               }
+       }
+
+       function provider_testGetFileStat() {
+               $cases = array();
+
+               $base = $this->baseStorePath();
+               $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents", true );
+               $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "", true );
+               $cases[] = array( "$base/unittest-cont1/b/some-diff_file.txt", null, false );
+
+               return $cases;
+       }
+
        /**
         * @dataProvider provider_testGetFileContents
         */
-       public function doTestGetFileContents( $src, $content ) {
+       public function testGetFileContents( $source, $content ) {
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestGetFileContents( $source, $content );
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestGetFileContents( $source, $content );
+               $this->tearDownFiles();
+       }
+
+       private function doTestGetFileContents( $source, $content ) {
                $backendName = $this->backendClass();
 
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+
                $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
+                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+               $this->assertEquals( array(), $status->errors,
+                       "Creation of file at $source succeeded ($backendName)." );
                $this->assertEquals( true, $status->isOK(),
-                       "Creation of file at $src succeeded ($backendName)." );
+                       "Creation of file at $source succeeded with OK status ($backendName)." );
 
-               $newContents = $this->backend->getFileContents( array( 'src' => $src ) );
+               $newContents = $this->backend->getFileContents( array( 'src' => $source, 'latest' => 1 ) );
                $this->assertNotEquals( false, $newContents,
-                       "Read of file at $src succeeded ($backendName)." );
+                       "Read of file at $source succeeded ($backendName)." );
 
                $this->assertEquals( $content, $newContents,
-                       "Contents read match data at $src ($backendName)." );
+                       "Contents read match data at $source ($backendName)." );
        }
 
        function provider_testGetFileContents() {
                $cases = array();
 
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/cont1/b/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/cont1/b/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/b/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/b/some-other_file.txt", "more file contents" );
 
                return $cases;
        }
@@ -602,40 +890,42 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testGetLocalCopy
         */
-       public function testGetLocalCopy( $src, $content ) {
-               $this->pathsToPrune[] = $src;
-
+       public function testGetLocalCopy( $source, $content ) {
                $this->backend = $this->singleBackend;
-               $this->doTestGetLocalCopy( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetLocalCopy( $source, $content );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestGetLocalCopy( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetLocalCopy( $source, $content );
                $this->tearDownFiles();
        }
 
-       public function doTestGetLocalCopy( $src, $content ) {
+       private function doTestGetLocalCopy( $source, $content ) {
                $backendName = $this->backendClass();
 
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+
                $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
-               $this->assertEquals( true, $status->isOK(),
-                       "Creation of file at $src succeeded ($backendName)." );
+                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+               $this->assertEquals( array(), $status->errors,
+                       "Creation of file at $source succeeded ($backendName)." );
 
-               $tmpFile = $this->backend->getLocalCopy( array( 'src' => $src ) );
+               $tmpFile = $this->backend->getLocalCopy( array( 'src' => $source ) );
                $this->assertNotNull( $tmpFile,
-                       "Creation of local copy of $src succeeded ($backendName)." );
+                       "Creation of local copy of $source succeeded ($backendName)." );
 
                $contents = file_get_contents( $tmpFile->getPath() );
-               $this->assertNotEquals( false, $contents, "Local copy of $src exists ($backendName)." );
+               $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
        }
 
        function provider_testGetLocalCopy() {
                $cases = array();
 
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/cont1/a/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/cont1/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" );
 
                return $cases;
        }
@@ -643,92 +933,352 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testGetLocalReference
         */
-       public function testGetLocalReference( $src, $content ) {
-               $this->pathsToPrune[] = $src;
-
+       public function testGetLocalReference( $source, $content ) {
                $this->backend = $this->singleBackend;
-               $this->doTestGetLocalReference( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetLocalReference( $source, $content );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
-               $this->doTestGetLocalReference( $src, $content );
+               $this->tearDownFiles();
+               $this->doTestGetLocalReference( $source, $content );
                $this->tearDownFiles();
        }
 
-       public function doTestGetLocalReference( $src, $content ) {
+       private function doTestGetLocalReference( $source, $content ) {
                $backendName = $this->backendClass();
 
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
+
                $status = $this->backend->doOperation(
-                       array( 'op' => 'create', 'content' => $content, 'dst' => $src ) );
-               $this->assertEquals( true, $status->isOK(),
-                       "Creation of file at $src succeeded ($backendName)." );
+                       array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
+               $this->assertEquals( array(), $status->errors,
+                       "Creation of file at $source succeeded ($backendName)." );
 
-               $tmpFile = $this->backend->getLocalReference( array( 'src' => $src ) );
+               $tmpFile = $this->backend->getLocalReference( array( 'src' => $source ) );
                $this->assertNotNull( $tmpFile,
-                       "Creation of local copy of $src succeeded ($backendName)." );
+                       "Creation of local copy of $source succeeded ($backendName)." );
 
                $contents = file_get_contents( $tmpFile->getPath() );
-               $this->assertNotEquals( false, $contents, "Local copy of $src exists ($backendName)." );
+               $this->assertNotEquals( false, $contents, "Local copy of $source exists ($backendName)." );
        }
 
        function provider_testGetLocalReference() {
                $cases = array();
 
                $base = $this->baseStorePath();
-               $cases[] = array( "$base/cont1/a/z/some_file.txt", "some file contents" );
-               $cases[] = array( "$base/cont1/a/some-other_file.txt", "more file contents" );
+               $cases[] = array( "$base/unittest-cont1/a/z/some_file.txt", "some file contents" );
+               $cases[] = array( "$base/unittest-cont1/a/some-other_file.txt", "more file contents" );
 
                return $cases;
        }
 
-       // @TODO: testPrepare
+       /**
+        * @dataProvider provider_testPrepareAndClean
+        */
+       public function testPrepareAndClean( $path, $isOK ) {
+               $this->backend = $this->singleBackend;
+               $this->doTestPrepareAndClean( $path, $isOK );
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->doTestPrepareAndClean( $path, $isOK );
+               $this->tearDownFiles();
+       }
+
+       function provider_testPrepareAndClean() {
+               $base = $this->baseStorePath();
+               return array(
+                       array( "$base/unittest-cont1/a/z/some_file1.txt", true ),
+                       array( "$base/unittest-cont2/a/z/some_file2.txt", true ),
+                       # Specific to FS backend with no basePath field set
+                       #array( "$base/unittest-cont3/a/z/some_file3.txt", false ),
+               );
+       }
+
+       private function doTestPrepareAndClean( $path, $isOK ) {
+               $backendName = $this->backendClass();
+
+               $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
+               if ( $isOK ) {
+                       $this->assertEquals( array(), $status->errors,
+                               "Preparing dir $path succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Preparing dir $path succeeded ($backendName)." );
+               } else {
+                       $this->assertEquals( false, $status->isOK(),
+                               "Preparing dir $path failed ($backendName)." );
+               }
+
+               $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
+               if ( $isOK ) {
+                       $this->assertEquals( array(), $status->errors,
+                               "Cleaning dir $path succeeded without warnings ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Cleaning dir $path succeeded ($backendName)." );
+               } else {
+                       $this->assertEquals( false, $status->isOK(),
+                               "Cleaning dir $path failed ($backendName)." );
+               }
+       }
+
+       public function testRecursiveClean() {
+               $this->backend = $this->singleBackend;
+               $this->doTestRecursiveClean();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->doTestRecursiveClean();
+               $this->tearDownFiles();
+       }
+
+       function doTestRecursiveClean() {
+               $backendName = $this->backendClass();
+
+               $base = $this->baseStorePath();
+               $dirs = array(
+                       "$base/unittest-cont1/a",
+                       "$base/unittest-cont1/a/b",
+                       "$base/unittest-cont1/a/b/c",
+                       "$base/unittest-cont1/a/b/c/d0",
+                       "$base/unittest-cont1/a/b/c/d1",
+                       "$base/unittest-cont1/a/b/c/d2",
+                       "$base/unittest-cont1/a/b/c/d0/1",
+                       "$base/unittest-cont1/a/b/c/d0/2",
+                       "$base/unittest-cont1/a/b/c/d1/3",
+                       "$base/unittest-cont1/a/b/c/d1/4",
+                       "$base/unittest-cont1/a/b/c/d2/5",
+                       "$base/unittest-cont1/a/b/c/d2/6"
+               );
+               foreach ( $dirs as $dir ) {
+                       $status = $this->prepare( array( 'dir' => $dir ) );
+                       $this->assertEquals( array(), $status->errors,
+                               "Preparing dir $dir succeeded without warnings ($backendName)." );
+               }
+
+               if ( $this->backend instanceof FSFileBackend ) {
+                       foreach ( $dirs as $dir ) {
+                               $this->assertEquals( true, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+                                       "Dir $dir exists ($backendName)." );
+                       }
+               }
+
+               $status = $this->backend->clean(
+                       array( 'dir' => "$base/unittest-cont1", 'recursive' => 1 ) );
+               $this->assertEquals( array(), $status->errors,
+                       "Recursive cleaning of dir $dir succeeded without warnings ($backendName)." );
+
+               foreach ( $dirs as $dir ) {
+                       $this->assertEquals( false, $this->backend->directoryExists( array( 'dir' => $dir ) ),
+                               "Dir $dir no longer exists ($backendName)." );
+               }
+       }
 
        // @TODO: testSecure
 
-       // @TODO: testClean
+       public function testDoOperations() {
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestDoOperations();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestDoOperations();
+               $this->tearDownFiles();
+
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestDoOperationsFailing();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestDoOperationsFailing();
+               $this->tearDownFiles();
+
+               // @TODO: test some cases where the ops should fail
+       }
+
+       private function doTestDoOperations() {
+               $base = $this->baseStorePath();
+
+               $fileA = "$base/unittest-cont1/a/b/fileA.txt";
+               $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+               $fileB = "$base/unittest-cont1/a/b/fileB.txt";
+               $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+               $fileC = "$base/unittest-cont1/a/b/fileC.txt";
+               $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+               $fileD = "$base/unittest-cont1/a/b/fileD.txt";
+
+               $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+               $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+               $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileD ) ) );
+
+               $status = $this->backend->doOperations( array(
+                       array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<empty>, D:<A>
+                       array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC ),
+                       // Now: A:<A>, B:<empty>, C:<B>, D:<A>
+                       array( 'op' => 'move', 'src' => $fileD, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileA, 'overwrite' => 1 ),
+                       // Now: A:<B>, B:<empty>, C:<empty>, D:<empty>
+                       array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC ),
+                       // Now: A:<B>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'move', 'src' => $fileA, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Now: A:<empty>, B:<empty>, C:<B>, D:<empty>
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Does nothing
+                       array( 'op' => 'null' ),
+                       // Does nothing
+               ) );
+
+               $this->assertEquals( array(), $status->errors, "Operation batch succeeded" );
+               $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+               $this->assertEquals( 13, count( $status->success ),
+                       "Operation batch has correct success array" );
+
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileA ) ),
+                       "File does not exist at $fileA" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+                       "File does not exist at $fileB" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+                       "File does not exist at $fileD" );
+
+               $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+                       "File exists at $fileC" );
+               $this->assertEquals( $fileBContents,
+                       $this->backend->getFileContents( array( 'src' => $fileC ) ),
+                       "Correct file contents of $fileC" );
+               $this->assertEquals( strlen( $fileBContents ),
+                       $this->backend->getFileSize( array( 'src' => $fileC ) ),
+                       "Correct file size of $fileC" );
+               $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+                       $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
+                       "Correct file SHA-1 of $fileC" );
+       }
 
-       // @TODO: testDoOperations
+       private function doTestDoOperationsFailing() {
+               $base = $this->baseStorePath();
+
+               $fileA = "$base/unittest-cont2/a/b/fileA.txt";
+               $fileAContents = '3tqtmoeatmn4wg4qe-mg3qt3 tq';
+               $fileB = "$base/unittest-cont2/a/b/fileB.txt";
+               $fileBContents = 'g-jmq3gpqgt3qtg q3GT ';
+               $fileC = "$base/unittest-cont2/a/b/fileC.txt";
+               $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
+               $fileD = "$base/unittest-cont2/a/b/fileD.txt";
+
+               $this->prepare( array( 'dir' => dirname( $fileA ) ) );
+               $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileB ) ) );
+               $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
+               $this->prepare( array( 'dir' => dirname( $fileC ) ) );
+               $this->backend->create( array( 'dst' => $fileC, 'content' => $fileCContents ) );
+
+               $status = $this->backend->doOperations( array(
+                       array( 'op' => 'copy', 'src' => $fileA, 'dst' => $fileC, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty> (file:<orginal contents>)
+                       array( 'op' => 'copy', 'src' => $fileC, 'dst' => $fileA, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty>
+                       array( 'op' => 'copy', 'src' => $fileB, 'dst' => $fileD, 'overwrite' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<B>
+                       array( 'op' => 'move', 'src' => $fileC, 'dst' => $fileD ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+                       array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileC, 'overwriteSame' => 1 ),
+                       // Now: A:<A>, B:<B>, C:<A>, D:<empty> (failed)
+                       array( 'op' => 'move', 'src' => $fileB, 'dst' => $fileA, 'overwrite' => 1 ),
+                       // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+                       array( 'op' => 'delete', 'src' => $fileD ),
+                       // Now: A:<B>, B:<empty>, C:<A>, D:<empty>
+                       array( 'op' => 'null' ),
+                       // Does nothing
+               ), array( 'force' => 1 ) );
+
+               $this->assertNotEquals( array(), $status->errors, "Operation had warnings" );
+               $this->assertEquals( true, $status->isOK(), "Operation batch succeeded" );
+               $this->assertEquals( 8, count( $status->success ),
+                       "Operation batch has correct success array" );
+
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileB ) ),
+                       "File does not exist at $fileB" );
+               $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $fileD ) ),
+                       "File does not exist at $fileD" );
+
+               $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileA ) ),
+                       "File does not exist at $fileA" );
+               $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $fileC ) ),
+                       "File exists at $fileC" );
+               $this->assertEquals( $fileBContents,
+                       $this->backend->getFileContents( array( 'src' => $fileA ) ),
+                       "Correct file contents of $fileA" );
+               $this->assertEquals( strlen( $fileBContents ),
+                       $this->backend->getFileSize( array( 'src' => $fileA ) ),
+                       "Correct file size of $fileA" );
+               $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
+                       $this->backend->getFileSha1Base36( array( 'src' => $fileA ) ),
+                       "Correct file SHA-1 of $fileA" );
+       }
 
        public function testGetFileList() {
                $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
                $this->doTestGetFileList();
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
                $this->doTestGetFileList();
                $this->tearDownFiles();
        }
 
-       public function doTestGetFileList() {
+       private function doTestGetFileList() {
                $backendName = $this->backendClass();
 
                $base = $this->baseStorePath();
                $files = array(
-                       "$base/cont1/test1.txt",
-                       "$base/cont1/test2.txt",
-                       "$base/cont1/test3.txt",
-                       "$base/cont1/subdir1/test1.txt",
-                       "$base/cont1/subdir1/test2.txt",
-                       "$base/cont1/subdir2/test3.txt",
-                       "$base/cont1/subdir2/test4.txt",
-                       "$base/cont1/subdir2/subdir/test1.txt",
-                       "$base/cont1/subdir2/subdir/test2.txt",
-                       "$base/cont1/subdir2/subdir/test3.txt",
-                       "$base/cont1/subdir2/subdir/test4.txt",
-                       "$base/cont1/subdir2/subdir/test5.txt",
-                       "$base/cont1/subdir2/subdir/sub/test0.txt",
-                       "$base/cont1/subdir2/subdir/sub/120-px-file.txt",
-               );
-               $this->pathsToPrune = array_merge( $this->pathsToPrune, $files );
+                       "$base/unittest-cont1/test1.txt",
+                       "$base/unittest-cont1/test2.txt",
+                       "$base/unittest-cont1/test3.txt",
+                       "$base/unittest-cont1/subdir1/test1.txt",
+                       "$base/unittest-cont1/subdir1/test2.txt",
+                       "$base/unittest-cont1/subdir2/test3.txt",
+                       "$base/unittest-cont1/subdir2/test4.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test1.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test2.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test3.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test4.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test5.txt",
+                       "$base/unittest-cont1/subdir2/subdir/sub/test0.txt",
+                       "$base/unittest-cont1/subdir2/subdir/sub/120-px-file.txt",
+               );
 
                // Add the files
                $ops = array();
                foreach ( $files as $file ) {
+                       $this->prepare( array( 'dir' => dirname( $file ) ) );
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
                $status = $this->backend->doOperations( $ops );
-               $this->assertEquals( true, $status->isOK(),
+               $this->assertEquals( array(), $status->errors,
                        "Creation of files succeeded ($backendName)." );
+               $this->assertEquals( true, $status->isOK(),
+                       "Creation of files succeeded with OK status ($backendName)." );
 
                // Expected listing
                $expected = array(
@@ -751,7 +1301,7 @@ class FileBackendTest extends MediaWikiTestCase {
 
                // Actual listing (no trailing slash)
                $list = array();
-               $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1" ) );
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
@@ -761,7 +1311,29 @@ class FileBackendTest extends MediaWikiTestCase {
 
                // Actual listing (with trailing slash)
                $list = array();
-               $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1/" ) );
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+               // Expected listing
+               $expected = array(
+                       "test1.txt",
+                       "test2.txt",
+                       "test3.txt",
+                       "test4.txt",
+                       "test5.txt",
+                       "sub/test0.txt",
+                       "sub/120-px-file.txt",
+               );
+               sort( $expected );
+
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
                foreach ( $iter as $file ) {
                        $list[] = $file;
                }
@@ -769,21 +1341,324 @@ class FileBackendTest extends MediaWikiTestCase {
 
                $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
 
+               // Actual listing (with trailing slash)
+               $list = array();
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
+
+               // Actual listing (using iterator second time)
+               $list = array();
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct file listing ($backendName), second iteration." );
+
+               // Expected listing (top files only)
+               $expected = array(
+                       "test1.txt",
+                       "test2.txt",
+                       "test3.txt",
+                       "test4.txt",
+                       "test5.txt"
+               );
+               sort( $expected );
+
+               // Actual listing (top files only)
+               $list = array();
+               $iter = $this->backend->getTopFileList( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top file listing ($backendName)." );
+
+               foreach ( $files as $file ) { // clean up
+                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+               }
+
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
+               foreach ( $iter as $iter ) {} // no errors
+       }
+
+       public function testGetDirectoryList() {
+               $this->backend = $this->singleBackend;
+               $this->tearDownFiles();
+               $this->doTestGetDirectoryList();
+               $this->tearDownFiles();
+
+               $this->backend = $this->multiBackend;
+               $this->tearDownFiles();
+               $this->doTestGetDirectoryList();
+               $this->tearDownFiles();
+       }
+
+       private function doTestGetDirectoryList() {
+               $backendName = $this->backendClass();
+
+               $base = $this->baseStorePath();
+               $files = array(
+                       "$base/unittest-cont1/test1.txt",
+                       "$base/unittest-cont1/test2.txt",
+                       "$base/unittest-cont1/test3.txt",
+                       "$base/unittest-cont1/subdir1/test1.txt",
+                       "$base/unittest-cont1/subdir1/test2.txt",
+                       "$base/unittest-cont1/subdir2/test3.txt",
+                       "$base/unittest-cont1/subdir2/test4.txt",
+                       "$base/unittest-cont1/subdir2/subdir/test1.txt",
+                       "$base/unittest-cont1/subdir3/subdir/test2.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test3.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test4.txt",
+                       "$base/unittest-cont1/subdir4/subdir/test5.txt",
+                       "$base/unittest-cont1/subdir4/subdir/sub/test0.txt",
+                       "$base/unittest-cont1/subdir4/subdir/sub/120-px-file.txt",
+               );
+
+               // Add the files
+               $ops = array();
                foreach ( $files as $file ) {
-                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => "$base/$file" ) );
+                       $this->prepare( array( 'dir' => dirname( $file ) ) );
+                       $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
                }
+               $status = $this->backend->doOperations( $ops );
+               $this->assertEquals( array(), $status->errors,
+                       "Creation of files succeeded ($backendName)." );
+               $this->assertEquals( true, $status->isOK(),
+                       "Creation of files succeeded with OK status ($backendName)." );
 
-               $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1/not/exists" ) );
+               // Expected listing
+               $expected = array(
+                       "subdir1",
+                       "subdir2",
+                       "subdir3",
+                       "subdir4",
+               );
+               sort( $expected );
+
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir1" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( true,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/subdir" ) ),
+                       "Directory exists in ($backendName)." );
+               $this->assertEquals( false,
+                       $this->backend->directoryExists( array( 'dir' => "$base/unittest-cont1/subdir2/test1.txt" ) ),
+                       "Directory does not exists in ($backendName)." );
+
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (with trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Expected listing
+               $expected = array(
+                       "subdir",
+               );
+               sort( $expected );
+
+               // Actual listing (no trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (with trailing slash)
+               $list = array();
+               $iter = $this->backend->getTopDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir2/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName)." );
+
+               // Actual listing (using iterator second time)
+               $list = array();
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct top dir listing ($backendName), second iteration." );
+
+               // Expected listing (recursive)
+               $expected = array(
+                       "subdir1",
+                       "subdir2",
+                       "subdir3",
+                       "subdir4",
+                       "subdir2/subdir",
+                       "subdir3/subdir",
+                       "subdir4/subdir",
+                       "subdir4/subdir/sub",
+               );
+               sort( $expected );
+
+               // Actual listing (recursive)
+               $list = array();
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               // Expected listing (recursive)
+               $expected = array(
+                       "subdir",
+                       "subdir/sub",
+               );
+               sort( $expected );
+
+               // Actual listing (recursive)
+               $list = array();
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/subdir4" ) );
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               // Actual listing (recursive, second time)
+               $list = array();
+               foreach ( $iter as $file ) {
+                       $list[] = $file;
+               }
+               sort( $list );
+
+               $this->assertEquals( $expected, $list, "Correct dir listing ($backendName)." );
+
+               foreach ( $files as $file ) { // clean up
+                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+               }
+
+               $iter = $this->backend->getDirectoryList( array( 'dir' => "$base/unittest-cont1/not/exists" ) );
                foreach ( $iter as $iter ) {} // no errors
        }
 
+       public function testLockCalls() {
+               $this->backend = $this->singleBackend;
+               $this->doTestLockCalls();
+       }
+
+       private function doTestLockCalls() {
+               $backendName = $this->backendClass();
+
+               for ( $i=0; $i<50; $i++ ) {
+                       $paths = array(
+                               "test1.txt",
+                               "test2.txt",
+                               "test3.txt",
+                               "subdir1",
+                               "subdir1", // duplicate
+                               "subdir1/test1.txt",
+                               "subdir1/test2.txt",
+                               "subdir2",
+                               "subdir2", // duplicate
+                               "subdir2/test3.txt",
+                               "subdir2/test4.txt",
+                               "subdir2/subdir",
+                               "subdir2/subdir/test1.txt",
+                               "subdir2/subdir/test2.txt",
+                               "subdir2/subdir/test3.txt",
+                               "subdir2/subdir/test4.txt",
+                               "subdir2/subdir/test5.txt",
+                               "subdir2/subdir/sub",
+                               "subdir2/subdir/sub/test0.txt",
+                               "subdir2/subdir/sub/120-px-file.txt",
+                       );
+
+                       $status = $this->backend->lockFiles( $paths, LockManager::LOCK_EX );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+
+                       $status = $this->backend->lockFiles( $paths, LockManager::LOCK_SH );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+
+                       $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_SH );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+
+                       $status = $this->backend->unlockFiles( $paths, LockManager::LOCK_EX );
+                       $this->assertEquals( array(), $status->errors,
+                               "Locking of files succeeded ($backendName)." );
+                       $this->assertEquals( true, $status->isOK(),
+                               "Locking of files succeeded with OK status ($backendName)." );
+               }
+       }
+
+       // test helper wrapper for backend prepare() function
+       private function prepare( array $params ) {
+               $this->dirsToPrune[] = $params['dir'];
+               return $this->backend->prepare( $params );
+       }
+
        function tearDownFiles() {
                foreach ( $this->filesToPrune as $file ) {
                        @unlink( $file );
                }
-               foreach ( $this->pathsToPrune as $file ) {
-                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
+               $containers = array( 'unittest-cont1', 'unittest-cont2', 'unittest-cont3' );
+               foreach ( $containers as $container ) {
+                       $this->deleteFiles( $container );
+               }
+               foreach ( $this->dirsToPrune as $dir ) {
+                       $this->recursiveClean( $dir );
                }
+               $this->filesToPrune = $this->dirsToPrune = array();
+       }
+
+       private function deleteFiles( $container ) {
+               $base = $this->baseStorePath();
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) );
+               if ( $iter ) {
+                       foreach ( $iter as $file ) {
+                               $this->backend->delete( array( 'src' => "$base/$container/$file" ),
+                                       array( 'force' => 1, 'nonLocking' => 1 ) );
+                       }
+               }
+       }
+
+       private function recursiveClean( $dir ) {
+               do {
+                       if ( !$this->backend->clean( array( 'dir' => $dir ) )->isOK() ) {
+                               break;
+                       }
+               } while ( $dir = FileBackend::parentStoragePath( $dir ) );
        }
 
        function tearDown() {