r105413 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r105412‎ | r105413 | r105414 >
Date:03:28, 7 December 2011
Author:aaron
Status:deferred
Tags:
Comment:
* Merge backend statuses in FileBackendMultiWrite::streamFile()
* Redid FSFileBackend::getFileList() using RecursiveDirectoryIterator from SPL
* Added more unit tests (including one for getFileList)
* Use null lock manger for parser tests to keep things simple
Modified paths:
  • /branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php (modified) (history)
  • /branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php (modified) (history)
  • /branches/FileBackend/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php (modified) (history)
  • /branches/FileBackend/phase3/tests/phpunit/includes/parser/NewParserTest.php (modified) (history)

Diff [purge]

Index: branches/FileBackend/phase3/tests/phpunit/includes/filerepo/FileBackendTest.php
@@ -1,7 +1,8 @@
22 <?php
33
44 class FileBackendTest extends MediaWikiTestCase {
5 - private $backend, $filesToPrune, $pathsToPrune;
 5+ private $backend, $multiBackend;
 6+ private $filesToPrune, $pathsToPrune;
67
78 function setUp() {
89 parent::setUp();
@@ -12,11 +13,34 @@
1314 'cont1' => wfTempDir() . '/localtesting/cont1',
1415 'cont2' => wfTempDir() . '/localtesting/cont2' )
1516 ) );
16 - $this->filesToPrune = array();
17 - $this->pathsToPrune = array();
 17+ $this->multiBackend = new FileBackendMultiWrite( array(
 18+ 'name' => 'localtestingmulti',
 19+ 'lockManager' => 'fsLockManager',
 20+ 'backends' => array(
 21+ array(
 22+ 'name' => 'localmutlitesting1',
 23+ 'class' => 'FSFileBackend',
 24+ 'lockManager' => 'nullLockManager',
 25+ 'containerPaths' => array(
 26+ 'cont1' => wfTempDir() . '/localtestingmulti1/cont1',
 27+ 'cont2' => wfTempDir() . '/localtestingmulti1/cont2' ),
 28+ 'isMultiMaster' => false
 29+ ),
 30+ array(
 31+ 'name' => 'localmutlitesting2',
 32+ 'class' => 'FSFileBackend',
 33+ 'lockManager' => 'nullLockManager',
 34+ 'containerPaths' => array(
 35+ 'cont1' => wfTempDir() . '/localtestingmulti2/cont1',
 36+ 'cont2' => wfTempDir() . '/localtestingmulti2/cont2' ),
 37+ 'isMultiMaster' => true
 38+ )
 39+ )
 40+ ) );
 41+ $this->filesToPrune = $this->pathsToPrune = array();
1842 }
1943
20 - private function basePath() {
 44+ private function singleBasePath() {
2145 return 'mwstore://localtesting';
2246 }
2347
@@ -49,7 +73,7 @@
5074 $cases = array();
5175
5276 $tmpName = TempFSFile::factory( "unittests_", 'txt' )->getPath();
53 - $toPath = $this->basePath() . '/cont1/fun/obj1.txt';
 77+ $toPath = $this->singleBasePath() . '/cont1/fun/obj1.txt';
5478 $op = array( 'op' => 'store', 'src' => $tmpName, 'dst' => $toPath );
5579 $cases[] = array(
5680 $op, // operation
@@ -70,36 +94,241 @@
7195 /**
7296 * @dataProvider provider_testCopy
7397 */
74 - public function testCopy() {
75 -
 98+ public function testCopy( $op, $source, $dest ) {
 99+ $this->pathsToPrune[] = $source;
 100+ $this->pathsToPrune[] = $dest;
 101+
 102+ $status = $this->backend->doOperation(
 103+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
 104+ $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 105+
 106+ $status = $this->backend->doOperation( $op );
 107+ $this->assertEquals( true, $status->isOK(),
 108+ "Copy from $source to $dest succeeded." );
 109+ $this->assertEquals( true, $status->isGood(),
 110+ "Copy from $source to $dest succeeded without warnings." );
 111+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
 112+ "Source file $source still exists." );
 113+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
 114+ "Destination file $dest exists after copy." );
 115+
 116+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
 117+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
 118+ $this->assertEquals( $props1, $props2,
 119+ "Source and destination have the same props." );
76120 }
77121
 122+ public function provider_testCopy() {
 123+ $cases = array();
 124+
 125+ $source = $this->singleBasePath() . '/cont1/file.txt';
 126+ $dest = $this->singleBasePath() . '/cont2/fileMoved.txt';
 127+
 128+ $op = array( 'op' => 'copy', 'src' => $source, 'dst' => $dest );
 129+ $cases[] = array(
 130+ $op, // operation
 131+ $source, // source
 132+ $dest, // dest
 133+ );
 134+
 135+ $op['overwriteDest'] = true;
 136+ $cases[] = array(
 137+ $op, // operation
 138+ $source, // source
 139+ $dest, // dest
 140+ );
 141+
 142+ return $cases;
 143+ }
 144+
78145 /**
79146 * @dataProvider provider_testMove
80147 */
81 - public function testMove() {
82 -
 148+ public function testMove( $op, $source, $dest ) {
 149+ $this->pathsToPrune[] = $source;
 150+ $this->pathsToPrune[] = $dest;
 151+
 152+ $status = $this->backend->doOperation(
 153+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
 154+ $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 155+
 156+ $status = $this->backend->doOperation( $op );
 157+ $this->assertEquals( true, $status->isOK(),
 158+ "Move from $source to $dest succeeded." );
 159+ $this->assertEquals( true, $status->isGood(),
 160+ "Move from $source to $dest succeeded without warnings." );
 161+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
 162+ "Source file $source does not still exists." );
 163+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $dest ) ),
 164+ "Destination file $dest exists after move." );
 165+
 166+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
 167+ $props2 = $this->backend->getFileProps( array( 'src' => $dest ) );
 168+ $this->assertEquals( false, $props1['fileExists'],
 169+ "Source file does not exist accourding to props." );
 170+ $this->assertEquals( true, $props2['fileExists'],
 171+ "Destination file exists accourding to props." );
83172 }
84173
 174+ public function provider_testMove() {
 175+ $cases = array();
 176+
 177+ $source = $this->singleBasePath() . '/cont1/file.txt';
 178+ $dest = $this->singleBasePath() . '/cont2/fileMoved.txt';
 179+
 180+ $op = array( 'op' => 'move', 'src' => $source, 'dst' => $dest );
 181+ $cases[] = array(
 182+ $op, // operation
 183+ $source, // source
 184+ $dest, // dest
 185+ );
 186+
 187+ $op['overwriteDest'] = true;
 188+ $cases[] = array(
 189+ $op, // operation
 190+ $source, // source
 191+ $dest, // dest
 192+ );
 193+
 194+ return $cases;
 195+ }
 196+
85197 /**
86198 * @dataProvider provider_testDelete
87199 */
88 - public function testDelete() {
89 -
 200+ public function testDelete( $op, $source, $withSource, $okStatus ) {
 201+ $this->pathsToPrune[] = $source;
 202+
 203+ if ( $withSource ) {
 204+ $status = $this->backend->doOperation(
 205+ array( 'op' => 'create', 'content' => 'blahblah', 'dst' => $source ) );
 206+ $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 207+ }
 208+
 209+ $status = $this->backend->doOperation( $op );
 210+ if ( $okStatus ) {
 211+ $this->assertEquals( true, $status->isOK(), "Deletion of file at $source succeeded." );
 212+ } else {
 213+ $this->assertEquals( false, $status->isOK(), "Deletion of file at $source failed." );
 214+ }
 215+
 216+ $this->assertEquals( false, $this->backend->fileExists( array( 'src' => $source ) ),
 217+ "Source file $source does not exist after move." );
 218+
 219+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
 220+ $this->assertEquals( false, $props1['fileExists'],
 221+ "Source file $source does not exist according to props." );
90222 }
91223
 224+ public function provider_testDelete() {
 225+ $cases = array();
 226+
 227+ $source = $this->singleBasePath() . '/cont1/myfacefile.txt';
 228+
 229+ $op = array( 'op' => 'delete', 'src' => $source );
 230+ $cases[] = array(
 231+ $op, // operation
 232+ $source, // source
 233+ true, // with source
 234+ true // succeeds
 235+ );
 236+
 237+ $cases[] = array(
 238+ $op, // operation
 239+ $source, // source
 240+ false, // without source
 241+ false // fails
 242+ );
 243+
 244+ $op['ignoreMissingSource'] = true;
 245+ $cases[] = array(
 246+ $op, // operation
 247+ $source, // source
 248+ false, // without source
 249+ true // succeeds
 250+ );
 251+
 252+ return $cases;
 253+ }
 254+
92255 /**
93256 * @dataProvider provider_testCreate
94257 */
95 - public function testCreate() {
96 -
 258+ public function testCreate( $op, $source, $alreadyExists, $okStatus, $newSize ) {
 259+ $this->pathsToPrune[] = $source;
 260+
 261+ $oldText = 'blah...blah...waahwaah';
 262+ if ( $alreadyExists ) {
 263+ $status = $this->backend->doOperation(
 264+ array( 'op' => 'create', 'content' => $oldText, 'dst' => $source ) );
 265+ $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 266+ }
 267+
 268+ $status = $this->backend->doOperation( $op );
 269+ if ( $okStatus ) {
 270+ $this->assertEquals( true, $status->isOK(), "Creation of file at $source succeeded." );
 271+ } else {
 272+ $this->assertEquals( false, $status->isOK(), "Creation of file at $source failed." );
 273+ }
 274+
 275+ $this->assertEquals( true, $this->backend->fileExists( array( 'src' => $source ) ),
 276+ "Source file $source exists after creation." );
 277+
 278+ $props1 = $this->backend->getFileProps( array( 'src' => $source ) );
 279+ $this->assertEquals( true, $props1['fileExists'],
 280+ "Source file $source exists according to props." );
 281+ if ( $okStatus ) { // file content is what we saved
 282+ $this->assertEquals( $newSize, $props1['size'],
 283+ "Source file $source has expected size according to props." );
 284+ } else { // file content is some other previous text
 285+ $this->assertEquals( strlen( $oldText ), $props1['size'],
 286+ "Source file $source has different size that given text according to props." );
 287+ }
97288 }
98289
99290 /**
100291 * @dataProvider provider_testConcatenate
101292 */
 293+ public function provider_testCreate() {
 294+ $cases = array();
 295+
 296+ $source = $this->singleBasePath() . '/cont2/myspacefile.txt';
 297+
 298+ $dummyText = 'hey hey';
 299+ $op = array( 'op' => 'create', 'content' => $dummyText, 'dst' => $source );
 300+ $cases[] = array(
 301+ $op, // operation
 302+ $source, // source
 303+ false, // no dest already exists
 304+ true, // succeeds
 305+ strlen( $dummyText )
 306+ );
 307+
 308+ $cases[] = array(
 309+ $op, // operation
 310+ $source, // source
 311+ true, // dest already exists
 312+ false, // fails
 313+ strlen( $dummyText )
 314+ );
 315+
 316+ $op['overwriteDest'] = true;
 317+ $cases[] = array(
 318+ $op, // operation
 319+ $source, // source
 320+ true, // dest already exists
 321+ true, // succeeds
 322+ strlen( $dummyText )
 323+ );
 324+
 325+ return $cases;
 326+ }
 327+
 328+ /**
 329+ * @dataProvider provider_testConcatenate
 330+ */
102331 public function testConcatenate() {
103 -
 332+
104333 }
105334
106335 /**
@@ -130,16 +359,87 @@
131360
132361 }
133362
 363+ /**
 364+ * @dataProvider provider_testDoOperations
 365+ */
 366+ public function testDoOperations() {
 367+
 368+ }
 369+
 370+ public function testGetFileList() {
 371+ $base = $this->singleBasePath();
 372+ $files = array(
 373+ "$base/cont1/test1.txt",
 374+ "$base/cont1/test2.txt",
 375+ "$base/cont1/test3.txt",
 376+ "$base/cont1/subdir1/test1.txt",
 377+ "$base/cont1/subdir1/test2.txt",
 378+ "$base/cont1/subdir2/test3.txt",
 379+ "$base/cont1/subdir2/test4.txt",
 380+ "$base/cont1/subdir2/subdir/test1.txt",
 381+ "$base/cont1/subdir2/subdir/test2.txt",
 382+ "$base/cont1/subdir2/subdir/test3.txt",
 383+ "$base/cont1/subdir2/subdir/test4.txt",
 384+ "$base/cont1/subdir2/subdir/test5.txt",
 385+ "$base/cont1/subdir2/subdir/sub/test0.txt",
 386+ );
 387+ $this->pathsToPrune = array_merge( $this->pathsToPrune, $files );
 388+
 389+ // Add the files
 390+ $ops = array();
 391+ foreach ( $files as $file ) {
 392+ $ops[] = array( 'op' => 'create', 'content' => 'xxy', 'dst' => $file );
 393+ }
 394+ $status = $this->backend->doOperations( $ops );
 395+ $this->assertEquals( true, $status->isOK(), "Creation of files succeeded." );
 396+
 397+ // Expected listing
 398+ $expected = array(
 399+ "test1.txt",
 400+ "test2.txt",
 401+ "test3.txt",
 402+ "subdir1/test1.txt",
 403+ "subdir1/test2.txt",
 404+ "subdir2/test3.txt",
 405+ "subdir2/test1.txt",
 406+ "subdir2/subdir/test1.txt",
 407+ "subdir2/subdir/test2.txt",
 408+ "subdir2/subdir/test3.txt",
 409+ "subdir2/subdir/test4.txt",
 410+ "subdir2/subdir/test5.txt",
 411+ "subdir2/subdir/sub/test0.txt",
 412+ );
 413+ $expected = sort( $expected );
 414+
 415+ // Actual listing (no trailing slash)
 416+ $list = array();
 417+ $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1" ) );
 418+ foreach ( $iter as $file ) {
 419+ $list[] = $file;
 420+ }
 421+
 422+ $this->assertEquals( $expected, sort( $list ), "Correct file listing." );
 423+
 424+ // Actual listing (with trailing slash)
 425+ $list = array();
 426+ $iter = $this->backend->getFileList( array( 'dir' => "$base/cont1/" ) );
 427+ foreach ( $iter as $file ) {
 428+ $list[] = $file;
 429+ }
 430+
 431+ $this->assertEquals( $expected, sort( $list ), "Correct file listing." );
 432+ }
 433+
134434 function tearDown() {
135435 parent::tearDown();
136436 foreach ( $this->filesToPrune as $file ) {
137437 @unlink( $file );
138438 }
139439 foreach ( $this->pathsToPrune as $file ) {
140 - $this->backend->delete( array( 'src' => $file ) );
 440+ $this->backend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
 441+ $this->multiBackend->doOperation( array( 'op' => 'delete', 'src' => $file ) );
141442 }
142 - $this->backend = null;
143 - $this->filesToPrune = array();
144 - $this->pathsToPrune = array();
 443+ $this->backend = $this->multiBackend = null;
 444+ $this->filesToPrune = $this->pathsToPrune = array();
145445 }
146446 }
Index: branches/FileBackend/phase3/tests/phpunit/includes/parser/NewParserTest.php
@@ -317,7 +317,7 @@
318318 $backend = array(
319319 'name' => 'local-backend',
320320 'class' => 'FSFileBackend',
321 - 'lockManager' => 'fsLockManager',
 321+ 'lockManager' => 'nullLockManager',
322322 'containerPaths' => array(
323323 'images-public' => $this->uploadDir,
324324 'images-thumb' => $this->uploadDir . '/thumb' )
Index: branches/FileBackend/phase3/includes/filerepo/backend/FileBackendMultiWrite.php
@@ -188,7 +188,7 @@
189189 }
190190
191191 function fileExists( array $params ) {
192 - # Hit all backends in case an operation failed to copy/move/delete a file
 192+ # Hit all backends in case of failed operations (out of sync)
193193 foreach ( $this->backends as $backend ) {
194194 if ( $backend->fileExists( $params ) ) {
195195 return true;
@@ -213,6 +213,7 @@
214214 }
215215
216216 function getFileProps( array $params ) {
 217+ # Hit all backends in case of failed operations (out of sync)
217218 foreach ( $this->backends as $backend ) {
218219 $props = $backend->getFileProps( $params );
219220 if ( $props !== null ) {
@@ -223,18 +224,23 @@
224225 }
225226
226227 function streamFile( array $params ) {
 228+ $status = Status::newGood();
227229 foreach ( $this->backends as $backend ) {
228 - $status = $backend->streamFile( $params );
229 - if ( $status->isOK() ) {
 230+ $subStatus = $backend->streamFile( $params );
 231+ $status->merge( $subStatus );
 232+ if ( $subStatus->isOK() ) {
 233+ // Pass isOK() despite fatals from other backends
 234+ $status->setResult( true );
230235 return $status;
231236 } elseif ( headers_sent() ) {
232237 return $status; // died mid-stream...so this is already fubar
233238 }
234239 }
235 - return Status::newFatal( 'backend-fail-stream', $params['src'] );
 240+ return $status;
236241 }
237242
238243 function getLocalReference( array $params ) {
 244+ # Hit all backends in case of failed operations (out of sync)
239245 foreach ( $this->backends as $backend ) {
240246 $fsFile = $backend->getLocalReference( $params );
241247 if ( $fsFile ) {
@@ -245,6 +251,7 @@
246252 }
247253
248254 function getLocalCopy( array $params ) {
 255+ # Hit all backends in case of failed operations (out of sync)
249256 foreach ( $this->backends as $backend ) {
250257 $tmpFile = $backend->getLocalCopy( $params );
251258 if ( $tmpFile ) {
@@ -256,6 +263,7 @@
257264
258265 function getFileList( array $params ) {
259266 foreach ( $this->backends as $index => $backend ) {
 267+ # Get results from the first backend
260268 return $backend->getFileList( $params );
261269 }
262270 return array(); // sanity
Index: branches/FileBackend/phase3/includes/filerepo/backend/FSFileBackend.php
@@ -453,7 +453,7 @@
454454 if ( $dir === null ) { // invalid storage path
455455 return array(); // empty result
456456 }
457 - return new FSFileIterator( $dir );
 457+ return new RecursiveIteratorIterator( new RecursiveDirectoryIterator( $dir ) );
458458 }
459459
460460 function getLocalReference( array $params ) {
@@ -507,151 +507,3 @@
508508 return $ok;
509509 }
510510 }
511 -
512 -/**
513 - * Semi-DFS based file browsing iterator. The highest number of file handles
514 - * open at any given time is proportional to the height of the directory tree.
515 - */
516 -class FSFileIterator implements Iterator {
517 - private $directory; // starting directory
518 - private $itemStart = 0;
519 -
520 - private $position = 0;
521 - private $currentFile = false;
522 - private $dirStack = array(); // array of (dir name, dir handle) tuples
523 -
524 - private $loaded = false;
525 -
526 - /**
527 - * Get an iterator to list the files under $directory and its subdirectories
528 - * @param $directory string
529 - */
530 - public function __construct( $directory ) {
531 - // Removing any trailing slash
532 - if ( substr( $this->directory, -1 ) === '/' ) {
533 - $this->directory = substr( $this->directory, 0, -1 );
534 - }
535 - $this->directory = realpath( $directory );
536 - // Remove internal base directory and trailing slash from results
537 - $this->itemStart = strlen( $this->directory ) + 1;
538 - }
539 -
540 - private function load() {
541 - if ( !$this->loaded ) {
542 - $this->loaded = true;
543 - // If we get an invalid directory then the result is an empty list
544 - if ( is_dir( $this->directory ) ) {
545 - $handle = opendir( $this->directory );
546 - if ( $handle ) {
547 - $this->pushDirectory( $this->directory, $handle );
548 - $this->currentFile = $this->nextFile();
549 - }
550 - }
551 - }
552 - }
553 -
554 - function rewind() {
555 - $this->closeDirectories();
556 - $this->position = 0;
557 - $this->currentFile = false;
558 - $this->loaded = false;
559 - }
560 -
561 - function current() {
562 - $this->load();
563 - // Remove internal base directory and trailing slash from results
564 - return substr( $this->currentFile, $this->itemStart );
565 - }
566 -
567 - function key() {
568 - $this->load();
569 - return $this->position;
570 - }
571 -
572 - function next() {
573 - $this->load();
574 - ++$this->position;
575 - $this->currentFile = $this->nextFile();
576 - }
577 -
578 - function valid() {
579 - $this->load();
580 - return ( $this->currentFile !== false );
581 - }
582 -
583 - function nextFile() {
584 - if ( !$this->currentDirectory() ) {
585 - return false; // nothing else to scan
586 - }
587 - # Next file under the current directory (and subdirectories).
588 - # This may advance the current directory down to a descendent.
589 - # The current directory is set to the parent if nothing is found.
590 - $nextFile = $this->nextFileBelowCurrent();
591 - if ( $nextFile !== false ) {
592 - return $nextFile;
593 - } else {
594 - # Scan the higher directories
595 - return $this->nextFile();
596 - }
597 - }
598 -
599 - function nextFileBelowCurrent() {
600 - list( $dir, $handle ) = $this->currentDirectory();
601 - while ( false !== ( $file = readdir( $handle ) ) ) {
602 - // Exclude '.' and '..' as well .svn or .lock type files
603 - if ( $file[0] !== '.' ) {
604 - $path = "{$dir}/{$file}";
605 - // If the first thing we find is a file, then return it.
606 - // If the first thing we find is a directory, then return
607 - // the first file that it contains (via recursion).
608 - // We exclude symlink dirs in order to avoid cycles.
609 - if ( is_dir( $path ) && !is_link( $path ) ) {
610 - $subHandle = opendir( $path );
611 - if ( $subHandle ) {
612 - $this->pushDirectory( $path, $subHandle );
613 - $nextFile = $this->nextFileBelowCurrent();
614 - if ( $nextFile !== false ) {
615 - return $nextFile; // found the next one!
616 - }
617 - }
618 - } elseif ( is_file( $path ) ) {
619 - return $path; // found the next one!
620 - }
621 - }
622 - }
623 - # If we didn't find anything else in this directory,
624 - # then back out so we scan the other higher directories
625 - $this->popDirectory();
626 - return false;
627 - }
628 -
629 - private function currentDirectory() {
630 - if ( !count( $this->dirStack ) ) {
631 - return false;
632 - }
633 - return $this->dirStack[count( $this->dirStack ) - 1];
634 - }
635 -
636 - private function popDirectory() {
637 - if ( count( $this->dirStack ) ) {
638 - list( $dir, $handle ) = array_pop( $this->dirStack );
639 - closedir( $handle );
640 - }
641 - }
642 -
643 - private function pushDirectory( $dir, $handle ) {
644 - $this->dirStack[] = array( $dir, $handle );
645 - }
646 -
647 - private function closeDirectories() {
648 - foreach ( $this->dirStack as $set ) {
649 - list( $dir, $handle ) = $set;
650 - closedir( $handle );
651 - }
652 - $this->dirStack = array();
653 - }
654 -
655 - function __destruct() {
656 - $this->closeDirectories();
657 - }
658 -}

Status & tagging log