Localisation updates from https://translatewiki.net.
[mediawiki.git] / tests / phpunit / mocks / filerepo / TestRepoTrait.php
blob4e68f5f75f1dc26f627e6be01bb9e6eb4a53490c
1 <?php
3 namespace MediaWiki\Tests\FileRepo;
5 use InvalidArgumentException;
6 use LocalRepo;
7 use LogicException;
8 use MediaWiki\FileBackend\FileBackendGroup;
9 use MediaWiki\MediaWikiServices;
10 use MediaWiki\Title\Title;
11 use PHPUnit\Framework\Assert;
12 use RepoGroup;
13 use Wikimedia\FileBackend\FileBackend;
14 use Wikimedia\FileBackend\FSFileBackend;
16 trait TestRepoTrait {
18 private static ?string $mockRepoTraitDir = null;
20 /**
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 ) ) {
37 unlink( $tmp );
39 mkdir( $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();
60 /**
61 * Destroys a mock repo.
62 * Should be called in tearDownAfterClass()
64 private static function destroyTestRepo() {
65 if ( !self::$mockRepoTraitDir ) {
66 return;
69 $dir = self::$mockRepoTraitDir;
71 if ( !is_dir( $dir ) ) {
72 return;
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(
103 $localFileRepo,
105 $services->getMainWANObjectCache(),
106 $mimeAnalyzer
108 return $repoGroup;
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 ) ) {
122 return $backend;
125 return null;
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'];
145 $info = $options + [
146 "class" => LocalRepo::class,
147 "name" => "test",
148 "domainId" => "mywiki",
149 "directory" => $dir,
150 "scriptDirUrl" => $scriptPath,
151 "favicon" => "/favicon.ico",
152 "url" => "$scriptPath/images",
153 "hashLevels" => 2,
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 );
168 return $info;
171 private function createFileBackend( array $info = [] ) {
172 $dir = $info['directory'] ?? self::$mockRepoTraitDir;
173 $name = $info['name'] ?? 'test';
175 $info += [
176 "domainId" => "mywiki",
177 'name' => $info['backend'] ?? 'local-backend',
178 'basePath' => $dir,
179 'obResetFunc' => static function () {
180 ob_end_flush();
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'] );
197 if ( !$overrides ) {
198 return new FSFileBackend( $info );
201 $backend = $this->getMockBuilder( FSFileBackend::class )
202 ->setConstructorArgs( [ $info ] )
203 ->onlyMethods( array_keys( $overrides ) )
204 ->getMock();
206 foreach ( $overrides as $name => $will ) {
207 if ( is_callable( $will ) ) {
208 $backend->method( $name )->willReturnCallback( $will );
209 } else {
210 $backend->method( $name )->willReturn( $will );
214 return $backend;
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() );
241 return $file;
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 );
257 if ( $zonePath ) {
258 $dst = "$zonePath/$dst";
262 $dir = dirname( $dst );
264 if ( $dir !== '' ) {
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 ) {
284 // no-op