3 namespace MediaWiki\Tests\FileRepo
;
5 use InvalidArgumentException
;
8 use MediaWiki\FileBackend\FileBackendGroup
;
9 use MediaWiki\MediaWikiServices
;
10 use MediaWiki\Title\Title
;
11 use PHPUnit\Framework\Assert
;
13 use Wikimedia\FileBackend\FileBackend
;
14 use Wikimedia\FileBackend\FSFileBackend
;
18 private static ?
string $mockRepoTraitDir = null;
21 * Initializes a mock repository in a temporary directory.
22 * Must only be called in addDbDataOnce().
23 * Must be paired with a call to destroyTestRepo() in tearDownAfterClass().
25 private function initTestRepoGroup(): RepoGroup
{
26 if ( self
::$mockRepoTraitDir ) {
27 throw new LogicException( 'Mock repo already initialized. ' .
28 'initTestRepogroup() must only be called from addDBDataOnce() ' .
29 'and must be paired with a call to destroyTestRepo() in ' .
30 'tearDownAfterClass().' );
33 $tmp = tempnam( wfTempDir(), 'mw-mock-repo-' );
35 // tmpnam creates a file, we need a directory
36 if ( file_exists( $tmp ) ) {
41 self
::$mockRepoTraitDir = $tmp;
42 $this->installTestRepoGroup();
43 return $this->getTestRepoGroup();
46 private function getTestRepoGroup(): RepoGroup
{
47 if ( self
::$mockRepoTraitDir === null ) {
48 throw new LogicException( 'Mock repo not initialized. ' .
49 'Call initTestRepo() from addDBDataOnce() and a call ' .
50 'to destroyTestRepo() in tearDownAfterClass().' );
53 return $this->getServiceContainer()->getRepoGroup();
56 private function getTestRepo(): LocalRepo
{
57 return $this->getTestRepoGroup()->getLocalRepo();
61 * Destroys a mock repo.
62 * Should be called in tearDownAfterClass()
64 private static function destroyTestRepo() {
65 if ( !self
::$mockRepoTraitDir ) {
69 $dir = self
::$mockRepoTraitDir;
71 if ( !is_dir( $dir ) ) {
75 if ( !str_starts_with( $dir, wfTempDir() ) ) {
76 throw new InvalidArgumentException( "Not in temp dir: $dir" );
79 $name = basename( $dir );
80 if ( !str_starts_with( $name, 'mw-mock-repo-' ) ) {
81 throw new InvalidArgumentException( "Not a mock repo dir: $dir" );
84 // TODO: Recursively delete the directory. Scary!
86 self
::$mockRepoTraitDir = null;
89 private function installTestRepoGroup( array $options = [] ) {
90 $repoGroup = $this->createTestRepoGroup( $options );
91 $this->setService( 'RepoGroup', $repoGroup );
93 $this->installTestBackendGroup( $repoGroup->getLocalRepo()->getBackend() );
96 private function createTestRepoGroup( $options = [], ?MediaWikiServices
$services = null ) {
97 $services ??
= $this->getServiceContainer();
98 $localFileRepo = $this->getLocalFileRepoConfig( $options );
100 $mimeAnalyzer = $services->getMimeAnalyzer();
102 $repoGroup = new RepoGroup(
105 $services->getMainWANObjectCache(),
111 private function installTestBackendGroup( FileBackend
$backend ) {
112 $this->setService( 'FileBackendGroup', $this->createTestBackendGroup( $backend ) );
115 private function createTestBackendGroup( FileBackend
$backend ) {
116 $expected = "mwstore://{$backend->getName()}/";
118 $backendGroup = $this->createNoOpMock( FileBackendGroup
::class, [ 'backendFromPath' ] );
119 $backendGroup->method( 'backendFromPath' )->willReturnCallback(
120 static function ( $path ) use ( $expected, $backend ) {
121 if ( str_starts_with( $path, $expected ) ) {
129 return $backendGroup;
132 private function getLocalFileRepoConfig( $options = [] ): array {
133 if ( self
::$mockRepoTraitDir === null ) {
134 throw new LogicException( 'Mock repo not initialized. ' .
135 'Call initTestRepo() from addDBDataOnce() and a call ' .
136 'to destroyTestRepo() in tearDownAfterClass().' );
139 $options['directory'] ??
= self
::$mockRepoTraitDir;
140 $options['scriptDirUrl'] ??
= '/w';
142 $scriptPath = $options['scriptDirUrl'];
143 $dir = $options['directory'];
146 "class" => LocalRepo
::class,
148 "domainId" => "mywiki",
150 "scriptDirUrl" => $scriptPath,
151 "favicon" => "/favicon.ico",
152 "url" => "$scriptPath/images",
154 "abbrvThreshold" => 16,
155 "thumbScriptUrl" => "$scriptPath/thumb.php",
156 "transformVia404" => false,
157 "deletedDir" => "$dir/deleted",
158 "deletedHashLevels" => 0,
159 "updateCompatibleMetadata" => false,
160 "reserializeMetadata" => false,
161 "backend" => 'local-backend',
164 if ( !$info['backend'] instanceof FileBackend
) {
165 $info['backend'] = $this->createFileBackend( $info );
171 private function createFileBackend( array $info = [] ) {
172 $dir = $info['directory'] ?? self
::$mockRepoTraitDir;
173 $name = $info['name'] ??
'test';
176 "domainId" => "mywiki",
177 'name' => $info['backend'] ??
'local-backend',
179 'obResetFunc' => static function () {
182 'headerFunc' => function ( string $header ) {
183 $this->recordHeader( $header );
185 'containerPaths' => [
186 "$name-public" => "$dir",
187 "$name-thumb" => "$dir/thumb",
188 "$name-transcoded" => "$dir/transcoded",
189 "$name-deleted" => "$dir/deleted",
190 "$name-temp" => "$dir/temp",
194 $overrides = $info['overrides'] ??
[];
195 unset( $info['overrides'] );
198 return new FSFileBackend( $info );
201 $backend = $this->getMockBuilder( FSFileBackend
::class )
202 ->setConstructorArgs( [ $info ] )
203 ->onlyMethods( array_keys( $overrides ) )
206 foreach ( $overrides as $name => $will ) {
207 if ( is_callable( $will ) ) {
208 $backend->method( $name )->willReturnCallback( $will );
210 $backend->method( $name )->willReturn( $will );
217 private function importDirToTestRepo( string $dir ) {
218 foreach ( new \
DirectoryIterator( $dir ) as $name ) {
219 $path = "$dir/$name";
220 if ( is_file( $path ) ) {
221 $this->importFileToTestRepo( $path );
226 private function importFileToTestRepo( string $path, ?
string $destName = null ) {
227 $repo = self
::getTestRepo();
229 $destName ??
= pathinfo( $path, PATHINFO_BASENAME
);
231 $title = Title
::makeTitleSafe( NS_FILE
, $destName );
232 $name = $title->getDBkey();
234 $file = $repo->newFile( $name );
235 $status = $file->upload( $path, 'test import', 'test image' );
237 if ( !$status->isOK() ) {
238 Assert
::fail( "Error recording file $name: " . $status->getWikiText() );
244 private function copyFileToTestBackend( string $src, string $dst ) {
245 $repo = self
::getTestRepo();
246 $backend = $repo->getBackend();
248 $zone = strstr( ltrim( $dst, '/' ), '/', true );
249 $name = basename( $dst );
251 $dstFile = $repo->newFile( $name );
252 $dst = $dstFile->getRel();
254 if ( $zone !== null ) {
255 $zonePath = $repo->getZonePath( $zone );
258 $dst = "$zonePath/$dst";
262 $dir = dirname( $dst );
265 $status = $backend->prepare(
266 [ 'op' => 'prepare', 'dir' => $dir ]
269 if ( !$status->isOK() ) {
270 Assert
::fail( "Error copying file $src to $dst: " . $status );
274 $status = $backend->store(
275 [ 'op' => 'store', 'src' => $src, 'dst' => $dst, ],
278 if ( !$status->isOK() ) {
279 Assert
::fail( "Error copying file $src to $dst: " . $status );
283 private function recordHeader( string $header ) {