288 lines
7.3 KiB
PHP
288 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace MediaWiki\Tests\FileRepo;
|
|
|
|
use InvalidArgumentException;
|
|
use LocalRepo;
|
|
use LogicException;
|
|
use MediaWiki\FileBackend\FileBackendGroup;
|
|
use MediaWiki\MediaWikiServices;
|
|
use MediaWiki\Title\Title;
|
|
use PHPUnit\Framework\Assert;
|
|
use RepoGroup;
|
|
use Wikimedia\FileBackend\FileBackend;
|
|
use Wikimedia\FileBackend\FSFileBackend;
|
|
|
|
trait TestRepoTrait {
|
|
|
|
private static ?string $mockRepoTraitDir = null;
|
|
|
|
/**
|
|
* Initializes a mock repository in a temporary directory.
|
|
* Must only be called in addDbDataOnce().
|
|
* Must be paired with a call to destroyTestRepo() in tearDownAfterClass().
|
|
*/
|
|
private function initTestRepoGroup(): RepoGroup {
|
|
if ( self::$mockRepoTraitDir ) {
|
|
throw new LogicException( 'Mock repo already initialized. ' .
|
|
'initTestRepogroup() must only be called from addDBDataOnce() ' .
|
|
'and must be paired with a call to destroyTestRepo() in ' .
|
|
'tearDownAfterClass().' );
|
|
}
|
|
|
|
$tmp = tempnam( wfTempDir(), 'mw-mock-repo-' );
|
|
|
|
// tmpnam creates a file, we need a directory
|
|
if ( file_exists( $tmp ) ) {
|
|
unlink( $tmp );
|
|
}
|
|
mkdir( $tmp );
|
|
|
|
self::$mockRepoTraitDir = $tmp;
|
|
$this->installTestRepoGroup();
|
|
return $this->getTestRepoGroup();
|
|
}
|
|
|
|
private function getTestRepoGroup(): RepoGroup {
|
|
if ( self::$mockRepoTraitDir === null ) {
|
|
throw new LogicException( 'Mock repo not initialized. ' .
|
|
'Call initTestRepo() from addDBDataOnce() and a call ' .
|
|
'to destroyTestRepo() in tearDownAfterClass().' );
|
|
}
|
|
|
|
return $this->getServiceContainer()->getRepoGroup();
|
|
}
|
|
|
|
private function getTestRepo(): LocalRepo {
|
|
return $this->getTestRepoGroup()->getLocalRepo();
|
|
}
|
|
|
|
/**
|
|
* Destroys a mock repo.
|
|
* Should be called in tearDownAfterClass()
|
|
*/
|
|
private static function destroyTestRepo() {
|
|
if ( !self::$mockRepoTraitDir ) {
|
|
return;
|
|
}
|
|
|
|
$dir = self::$mockRepoTraitDir;
|
|
|
|
if ( !is_dir( $dir ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( !str_starts_with( $dir, wfTempDir() ) ) {
|
|
throw new InvalidArgumentException( "Not in temp dir: $dir" );
|
|
}
|
|
|
|
$name = basename( $dir );
|
|
if ( !str_starts_with( $name, 'mw-mock-repo-' ) ) {
|
|
throw new InvalidArgumentException( "Not a mock repo dir: $dir" );
|
|
}
|
|
|
|
// TODO: Recursively delete the directory. Scary!
|
|
|
|
self::$mockRepoTraitDir = null;
|
|
}
|
|
|
|
private function installTestRepoGroup( array $options = [] ) {
|
|
$repoGroup = $this->createTestRepoGroup( $options );
|
|
$this->setService( 'RepoGroup', $repoGroup );
|
|
|
|
$this->installTestBackendGroup( $repoGroup->getLocalRepo()->getBackend() );
|
|
}
|
|
|
|
private function createTestRepoGroup( $options = [], ?MediaWikiServices $services = null ) {
|
|
$services ??= $this->getServiceContainer();
|
|
$localFileRepo = $this->getLocalFileRepoConfig( $options );
|
|
|
|
$mimeAnalyzer = $services->getMimeAnalyzer();
|
|
|
|
$repoGroup = new RepoGroup(
|
|
$localFileRepo,
|
|
[],
|
|
$services->getMainWANObjectCache(),
|
|
$mimeAnalyzer
|
|
);
|
|
return $repoGroup;
|
|
}
|
|
|
|
private function installTestBackendGroup( FileBackend $backend ) {
|
|
$this->setService( 'FileBackendGroup', $this->createTestBackendGroup( $backend ) );
|
|
}
|
|
|
|
private function createTestBackendGroup( FileBackend $backend ) {
|
|
$expected = "mwstore://{$backend->getName()}/";
|
|
|
|
$backendGroup = $this->createNoOpMock( FileBackendGroup::class, [ 'backendFromPath' ] );
|
|
$backendGroup->method( 'backendFromPath' )->willReturnCallback(
|
|
static function ( $path ) use ( $expected, $backend ) {
|
|
if ( str_starts_with( $path, $expected ) ) {
|
|
return $backend;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
);
|
|
|
|
return $backendGroup;
|
|
}
|
|
|
|
private function getLocalFileRepoConfig( $options = [] ): array {
|
|
if ( self::$mockRepoTraitDir === null ) {
|
|
throw new LogicException( 'Mock repo not initialized. ' .
|
|
'Call initTestRepo() from addDBDataOnce() and a call ' .
|
|
'to destroyTestRepo() in tearDownAfterClass().' );
|
|
}
|
|
|
|
$options['directory'] ??= self::$mockRepoTraitDir;
|
|
$options['scriptDirUrl'] ??= '/w';
|
|
|
|
$scriptPath = $options['scriptDirUrl'];
|
|
$dir = $options['directory'];
|
|
|
|
$info = $options + [
|
|
"class" => LocalRepo::class,
|
|
"name" => "test",
|
|
"domainId" => "mywiki",
|
|
"directory" => $dir,
|
|
"scriptDirUrl" => $scriptPath,
|
|
"favicon" => "/favicon.ico",
|
|
"url" => "$scriptPath/images",
|
|
"hashLevels" => 2,
|
|
"abbrvThreshold" => 16,
|
|
"thumbScriptUrl" => "$scriptPath/thumb.php",
|
|
"transformVia404" => false,
|
|
"deletedDir" => "$dir/deleted",
|
|
"deletedHashLevels" => 0,
|
|
"updateCompatibleMetadata" => false,
|
|
"reserializeMetadata" => false,
|
|
"backend" => 'local-backend',
|
|
];
|
|
|
|
if ( !$info['backend'] instanceof FileBackend ) {
|
|
$info['backend'] = $this->createFileBackend( $info );
|
|
}
|
|
|
|
return $info;
|
|
}
|
|
|
|
private function createFileBackend( array $info = [] ) {
|
|
$dir = $info['directory'] ?? self::$mockRepoTraitDir;
|
|
$name = $info['name'] ?? 'test';
|
|
|
|
$info += [
|
|
"domainId" => "mywiki",
|
|
'name' => $info['backend'] ?? 'local-backend',
|
|
'basePath' => $dir,
|
|
'obResetFunc' => static function () {
|
|
ob_end_flush();
|
|
},
|
|
'headerFunc' => function ( string $header ) {
|
|
$this->recordHeader( $header );
|
|
},
|
|
'containerPaths' => [
|
|
"$name-public" => "$dir",
|
|
"$name-thumb" => "$dir/thumb",
|
|
"$name-transcoded" => "$dir/transcoded",
|
|
"$name-deleted" => "$dir/deleted",
|
|
"$name-temp" => "$dir/temp",
|
|
]
|
|
];
|
|
|
|
$overrides = $info['overrides'] ?? [];
|
|
unset( $info['overrides'] );
|
|
|
|
if ( !$overrides ) {
|
|
return new FSFileBackend( $info );
|
|
}
|
|
|
|
$backend = $this->getMockBuilder( FSFileBackend::class )
|
|
->setConstructorArgs( [ $info ] )
|
|
->onlyMethods( array_keys( $overrides ) )
|
|
->getMock();
|
|
|
|
foreach ( $overrides as $name => $will ) {
|
|
if ( is_callable( $will ) ) {
|
|
$backend->method( $name )->willReturnCallback( $will );
|
|
} else {
|
|
$backend->method( $name )->willReturn( $will );
|
|
}
|
|
}
|
|
|
|
return $backend;
|
|
}
|
|
|
|
private function importDirToTestRepo( string $dir ) {
|
|
foreach ( new \DirectoryIterator( $dir ) as $name ) {
|
|
$path = "$dir/$name";
|
|
if ( is_file( $path ) ) {
|
|
$this->importFileToTestRepo( $path );
|
|
}
|
|
}
|
|
}
|
|
|
|
private function importFileToTestRepo( string $path, ?string $destName = null ) {
|
|
$repo = self::getTestRepo();
|
|
|
|
$destName ??= pathinfo( $path, PATHINFO_BASENAME );
|
|
|
|
$title = Title::makeTitleSafe( NS_FILE, $destName );
|
|
$name = $title->getDBkey();
|
|
|
|
$file = $repo->newFile( $name );
|
|
$status = $file->upload( $path, 'test import', 'test image' );
|
|
|
|
if ( !$status->isOK() ) {
|
|
Assert::fail( "Error recording file $name: " . $status->getWikiText() );
|
|
}
|
|
|
|
return $file;
|
|
}
|
|
|
|
private function copyFileToTestBackend( string $src, string $dst ) {
|
|
$repo = self::getTestRepo();
|
|
$backend = $repo->getBackend();
|
|
|
|
$zone = strstr( ltrim( $dst, '/' ), '/', true );
|
|
$name = basename( $dst );
|
|
|
|
$dstFile = $repo->newFile( $name );
|
|
$dst = $dstFile->getRel();
|
|
|
|
if ( $zone !== null ) {
|
|
$zonePath = $repo->getZonePath( $zone );
|
|
|
|
if ( $zonePath ) {
|
|
$dst = "$zonePath/$dst";
|
|
}
|
|
}
|
|
|
|
$dir = dirname( $dst );
|
|
|
|
if ( $dir !== '' ) {
|
|
$status = $backend->prepare(
|
|
[ 'op' => 'prepare', 'dir' => $dir ]
|
|
);
|
|
|
|
if ( !$status->isOK() ) {
|
|
Assert::fail( "Error copying file $src to $dst: " . $status );
|
|
}
|
|
}
|
|
|
|
$status = $backend->store(
|
|
[ 'op' => 'store', 'src' => $src, 'dst' => $dst, ],
|
|
);
|
|
|
|
if ( !$status->isOK() ) {
|
|
Assert::fail( "Error copying file $src to $dst: " . $status );
|
|
}
|
|
}
|
|
|
|
private function recordHeader( string $header ) {
|
|
// no-op
|
|
}
|
|
|
|
}
|