[FileBackend] File locking fixes.
[lhc/web/wiklou.git] / tests / phpunit / includes / filerepo / FileBackendTest.php
index b6ff05c..9c36a57 100644 (file)
@@ -2,16 +2,18 @@
 
 /**
  * @group FileRepo
+ * @group FileBackend
  */
 class FileBackendTest extends MediaWikiTestCase {
        private $backend, $multiBackend;
-       private $filesToPrune;
+       private $filesToPrune = array();
+       private $dirsToPrune = array();
        private static $backendToUse;
 
        function setUp() {
                global $wgFileBackends;
                parent::setUp();
-               $tmpDir = wfTempDir() . '/file-backend-test-' . time() . '-' . mt_rand();
+               $tmpPrefix = wfTempDir() . '/filebackend-unittest-' . time() . '-' . mt_rand();
                if ( $this->getCliArg( 'use-filebackend=' ) ) {
                        if ( self::$backendToUse ) {
                                $this->singleBackend = self::$backendToUse;
@@ -21,10 +23,12 @@ class FileBackendTest extends MediaWikiTestCase {
                                foreach ( $wgFileBackends as $conf ) {
                                        if ( $conf['name'] == $name ) {
                                                $useConfig = $conf;
+                                               break;
                                        }
                                }
                                $useConfig['name'] = 'localtesting'; // swap name
-                               self::$backendToUse = new $conf['class']( $useConfig );
+                               $class = $useConfig['class'];
+                               self::$backendToUse = new $class( $useConfig );
                                $this->singleBackend = self::$backendToUse;
                        }
                } else {
@@ -32,8 +36,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                'name'        => 'localtesting',
                                'lockManager' => 'fsLockManager',
                                'containerPaths' => array(
-                                       'unittest-cont1' => "$tmpDir/localtesting/unittest-cont1",
-                                       'unittest-cont2' => "$tmpDir/localtesting/unittest-cont2" )
+                                       'unittest-cont1' => "{$tmpPrefix}-localtesting-cont1",
+                                       'unittest-cont2' => "{$tmpPrefix}-localtesting-cont2" )
                        ) );
                }
                $this->multiBackend = new FileBackendMultiWrite( array(
@@ -45,8 +49,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
-                                               'unittest-cont1' => "$tmpDir/localtestingmulti1/cont1",
-                                               'unittest-cont2' => "$tmpDir/localtestingmulti1/unittest-cont2" ),
+                                               'unittest-cont1' => "{$tmpPrefix}-localtestingmulti1-cont1",
+                                               'unittest-cont2' => "{$tmpPrefix}-localtestingmulti1-cont2" ),
                                        'isMultiMaster' => false
                                ),
                                array(
@@ -54,8 +58,8 @@ class FileBackendTest extends MediaWikiTestCase {
                                        'class'         => 'FSFileBackend',
                                        'lockManager'   => 'nullLockManager',
                                        'containerPaths' => array(
-                                               'unittest-cont1' => "$tmpDir/localtestingmulti2/cont1",
-                                               'unittest-cont2' => "$tmpDir/localtestingmulti2/unittest-cont2" ),
+                                               'unittest-cont1' => "{$tmpPrefix}-localtestingmulti2-cont1",
+                                               'unittest-cont2' => "{$tmpPrefix}-localtestingmulti2-cont2" ),
                                        'isMultiMaster' => true
                                )
                        )
@@ -71,27 +75,142 @@ 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;
+       public function testStore( $op ) {
+               $this->filesToPrune[] = $op['src'];
 
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestStore( $op, $source, $dest );
+               $this->doTestStore( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestStore( $op, $source, $dest );
+               $this->doTestStore( $op );
+               $this->filesToPrune[] = $op['src']; # avoid file leaking
                $this->tearDownFiles();
        }
 
-       function doTestStore( $op, $source, $dest ) {
+       private function doTestStore( $op ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $dest ) ) );
+               $source = $op['src'];
+               $dest = $op['dst'];
+               $this->prepare( array( 'dir' => dirname( $dest ) ) );
 
                file_put_contents( $source, "Unit test file" );
 
@@ -156,23 +275,25 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testCopy
         */
-       public function testCopy( $op, $source, $dest ) {
+       public function testCopy( $op ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op, $source, $dest );
+               $this->doTestCopy( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestCopy( $op, $source, $dest );
+               $this->doTestCopy( $op );
                $this->tearDownFiles();
        }
 
-       function doTestCopy( $op, $source, $dest ) {
+       private function doTestCopy( $op ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $source ) ) );
-               $this->backend->prepare( array( 'dir' => dirname( $dest ) ) );
+               $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 ) );
@@ -242,23 +363,25 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testMove
         */
-       public function testMove( $op, $source, $dest ) {
+       public function testMove( $op ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op, $source, $dest );
+               $this->doTestMove( $op );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestMove( $op, $source, $dest );
+               $this->doTestMove( $op );
                $this->tearDownFiles();
        }
 
-       private function doTestMove( $op, $source, $dest ) {
+       private function doTestMove( $op ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $source ) ) );
-               $this->backend->prepare( array( 'dir' => dirname( $dest ) ) );
+               $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 ) );
@@ -329,22 +452,23 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testDelete
         */
-       public function testDelete( $op, $source, $withSource, $okStatus ) {
+       public function testDelete( $op, $withSource, $okStatus ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $source, $withSource, $okStatus );
+               $this->doTestDelete( $op, $withSource, $okStatus );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestDelete( $op, $source, $withSource, $okStatus );
+               $this->doTestDelete( $op, $withSource, $okStatus );
                $this->tearDownFiles();
        }
 
-       private function doTestDelete( $op, $source, $withSource, $okStatus ) {
+       private function doTestDelete( $op, $withSource, $okStatus ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $source ) ) );
+               $source = $op['src'];
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
 
                if ( $withSource ) {
                        $status = $this->backend->doOperation(
@@ -386,14 +510,12 @@ class FileBackendTest extends MediaWikiTestCase {
                $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
                );
@@ -401,7 +523,6 @@ class FileBackendTest extends MediaWikiTestCase {
                $op['ignoreMissingSource'] = true;
                $cases[] = array(
                        $op, // operation
-                       $source, // source
                        false, // without source
                        true // succeeds
                );
@@ -428,7 +549,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $backendName = $this->backendClass();
 
                $dest = $op['dst'];
-               $this->backend->prepare( array( 'dir' => dirname( $dest ) ) );
+               $this->prepare( array( 'dir' => dirname( $dest ) ) );
 
                $oldText = 'blah...blah...waahwaah';
                if ( $alreadyExists ) {
@@ -543,17 +664,18 @@ class FileBackendTest extends MediaWikiTestCase {
                $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( $params, $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->backend->prepare( array( 'dir' => dirname( $source ) ) );
+                       $this->prepare( array( 'dir' => dirname( $source ) ) );
                        $ops[] = array(
                                'op'      => 'create', // operation
                                'dst'     => $source, // source
@@ -660,27 +782,85 @@ class FileBackendTest extends MediaWikiTestCase {
        }
 
        /**
-        * @dataProvider provider_testGetFileContents
+        * @dataProvider provider_testGetFileStat
         */
-       public function testGetFileContents( $src, $content ) {
+       public function testGetFileStat( $path, $content, $alreadyExists ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestGetFileContents( $src, $content );
+               $this->doTestGetFileStat( $path, $content, $alreadyExists );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestGetFileContents( $src, $content );
+               $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( $source, $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->backend->prepare( array( 'dir' => dirname( $source ) ) );
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
 
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
@@ -689,7 +869,7 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->assertEquals( true, $status->isOK(),
                        "Creation of file at $source succeeded with OK status ($backendName)." );
 
-               $newContents = $this->backend->getFileContents( array( 'src' => $source ) );
+               $newContents = $this->backend->getFileContents( array( 'src' => $source, 'latest' => 1 ) );
                $this->assertNotEquals( false, $newContents,
                        "Read of file at $source succeeded ($backendName)." );
 
@@ -710,22 +890,22 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testGetLocalCopy
         */
-       public function testGetLocalCopy( $src, $content ) {
+       public function testGetLocalCopy( $source, $content ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestGetLocalCopy( $src, $content );
+               $this->doTestGetLocalCopy( $source, $content );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestGetLocalCopy( $src, $content );
+               $this->doTestGetLocalCopy( $source, $content );
                $this->tearDownFiles();
        }
 
-       public function doTestGetLocalCopy( $source, $content ) {
+       private function doTestGetLocalCopy( $source, $content ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $source ) ) );
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
 
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
@@ -753,22 +933,22 @@ class FileBackendTest extends MediaWikiTestCase {
        /**
         * @dataProvider provider_testGetLocalReference
         */
-       public function testGetLocalReference( $src, $content ) {
+       public function testGetLocalReference( $source, $content ) {
                $this->backend = $this->singleBackend;
                $this->tearDownFiles();
-               $this->doTestGetLocalReference( $src, $content );
+               $this->doTestGetLocalReference( $source, $content );
                $this->tearDownFiles();
 
                $this->backend = $this->multiBackend;
                $this->tearDownFiles();
-               $this->doTestGetLocalReference( $src, $content );
+               $this->doTestGetLocalReference( $source, $content );
                $this->tearDownFiles();
        }
 
        private function doTestGetLocalReference( $source, $content ) {
                $backendName = $this->backendClass();
 
-               $this->backend->prepare( array( 'dir' => dirname( $source ) ) );
+               $this->prepare( array( 'dir' => dirname( $source ) ) );
 
                $status = $this->backend->doOperation(
                        array( 'op' => 'create', 'content' => $content, 'dst' => $source ) );
@@ -799,9 +979,11 @@ class FileBackendTest extends MediaWikiTestCase {
        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() {
@@ -814,10 +996,10 @@ class FileBackendTest extends MediaWikiTestCase {
                );
        }
 
-       function doTestPrepareAndClean( $path, $isOK ) {
+       private function doTestPrepareAndClean( $path, $isOK ) {
                $backendName = $this->backendClass();
 
-               $status = $this->backend->prepare( array( 'dir' => $path ) );
+               $status = $this->prepare( array( 'dir' => dirname( $path ) ) );
                if ( $isOK ) {
                        $this->assertEquals( array(), $status->errors,
                                "Preparing dir $path succeeded without warnings ($backendName)." );
@@ -828,7 +1010,7 @@ class FileBackendTest extends MediaWikiTestCase {
                                "Preparing dir $path failed ($backendName)." );
                }
 
-               $status = $this->backend->clean( array( 'dir' => $path ) );
+               $status = $this->backend->clean( array( 'dir' => dirname( $path ) ) );
                if ( $isOK ) {
                        $this->assertEquals( array(), $status->errors,
                                "Cleaning dir $path succeeded without warnings ($backendName)." );
@@ -840,17 +1022,85 @@ class FileBackendTest extends MediaWikiTestCase {
                }
        }
 
+       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
 
        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
        }
 
-       function doTestDoOperations() {
+       private function doTestDoOperations() {
                $base = $this->baseStorePath();
 
                $fileA = "$base/unittest-cont1/a/b/fileA.txt";
@@ -861,18 +1111,19 @@ class FileBackendTest extends MediaWikiTestCase {
                $fileCContents = 'eigna[ogmewt 3qt g3qg flew[ag';
                $fileD = "$base/unittest-cont1/a/b/fileD.txt";
 
-               $this->backend->prepare( array( 'dir' => dirname( $fileA ) ) );
+               $this->prepare( array( 'dir' => dirname( $fileA ) ) );
                $this->backend->create( array( 'dst' => $fileA, 'content' => $fileAContents ) );
-               $this->backend->prepare( array( 'dir' => dirname( $fileB ) ) );
+               $this->prepare( array( 'dir' => dirname( $fileB ) ) );
                $this->backend->create( array( 'dst' => $fileB, 'content' => $fileBContents ) );
-               $this->backend->prepare( array( 'dir' => dirname( $fileC ) ) );
+               $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:<D> (file:<orginal contents>)
+                       // 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:<D>
+                       // 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 ),
@@ -920,8 +1171,68 @@ class FileBackendTest extends MediaWikiTestCase {
                $this->assertEquals( wfBaseConvert( sha1( $fileBContents ), 16, 36, 31 ),
                        $this->backend->getFileSha1Base36( array( 'src' => $fileC ) ),
                        "Correct file SHA-1 of $fileC" );
+       }
 
-               // @TODO: test some cases where the ops should fail
+       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() {
@@ -960,8 +1271,8 @@ class FileBackendTest extends MediaWikiTestCase {
                // Add the files
                $ops = array();
                foreach ( $files as $file ) {
+                       $this->prepare( array( 'dir' => dirname( $file ) ) );
                        $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
-                       $this->backend->prepare( array( 'dir' => dirname( $file ) ) );
                }
                $status = $this->backend->doOperations( $ops );
                $this->assertEquals( array(), $status->errors,
@@ -1040,38 +1351,316 @@ class FileBackendTest extends MediaWikiTestCase {
 
                $this->assertEquals( $expected, $list, "Correct file listing ($backendName)." );
 
-               foreach ( $files as $file ) {
-                       $this->backend->doOperation( array( 'op' => 'delete', 'src' => "$base/$file" ) );
+               // 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->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)." );
+
+               // 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 );
                }
                $containers = array( 'unittest-cont1', 'unittest-cont2', 'unittest-cont3' );
                foreach ( $containers as $container ) {
-                       $this->deleteFiles( $this->backend, $container );
+                       $this->deleteFiles( $container );
+               }
+               foreach ( $this->dirsToPrune as $dir ) {
+                       $this->recursiveClean( $dir );
                }
+               $this->filesToPrune = $this->dirsToPrune = array();
        }
 
-       private function deleteFiles( $backend, $container ) {
+       private function deleteFiles( $container ) {
                $base = $this->baseStorePath();
-               $iter = $backend->getFileList( array( 'dir' => "$base/$container" ) );
+               $iter = $this->backend->getFileList( array( 'dir' => "$base/$container" ) );
                if ( $iter ) {
                        foreach ( $iter as $file ) {
-                               $backend->delete( array( 'src' => "$base/$container/$file", 'ignoreMissingSource' => 1 ) );
-                               $tmp = $file;
-                               while ( $tmp = FileBackend::parentStoragePath( $tmp ) ) {
-                                       $backend->clean( array( 'dir' => $tmp ) );
-                               }
+                               $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() {
                parent::tearDown();
        }