3 use MediaWiki\MainConfigNames
;
4 use MediaWiki\Page\PageIdentityValue
;
5 use MediaWiki\Title\TitleValue
;
6 use MediaWiki\WikiMap\WikiMap
;
7 use Wikimedia\FileBackend\FSFile\FSFile
;
8 use Wikimedia\FileBackend\FSFile\TempFSFile
;
9 use Wikimedia\FileBackend\FSFileBackend
;
11 class FileTest
extends MediaWikiMediaTestCase
{
14 * @param string $filename
15 * @param bool $expected
16 * @dataProvider providerCanAnimate
17 * @covers \File::canAnimateThumbIfAppropriate
19 public function testCanAnimateThumbIfAppropriate( $filename, $expected ) {
20 $this->overrideConfigValue( MainConfigNames
::MaxAnimatedGifArea
, 9000 );
21 $file = $this->dataFile( $filename );
22 $this->assertEquals( $expected, $file->canAnimateThumbIfAppropriate() );
25 public static function providerCanAnimate() {
27 [ 'nonanimated.gif', true ],
28 [ 'jpeg-comment-utf.jpg', true ],
29 [ 'test.tiff', true ],
30 [ 'Animated_PNG_example_bouncing_beach_ball.png', false ],
31 [ 'greyscale-png.png', true ],
32 [ 'Toll_Texas_1.svg', true ],
33 [ 'LoremIpsum.djvu', true ],
34 [ '80x60-2layers.xcf', true ],
35 [ 'Soccer_ball_animated.svg', false ],
36 [ 'Bishzilla_blink.gif', false ],
37 [ 'animated.gif', true ],
42 * @dataProvider getThumbnailBucketProvider
43 * @covers \File::getThumbnailBucket
45 public function testGetThumbnailBucket( $data ) {
46 $this->overrideConfigValues( [
47 MainConfigNames
::ThumbnailBuckets
=> $data['buckets'],
48 MainConfigNames
::ThumbnailMinimumBucketDistance
=> $data['minimumBucketDistance'],
51 $fileMock = $this->getMockBuilder( File
::class )
52 ->setConstructorArgs( [ 'fileMock', false ] )
53 ->onlyMethods( [ 'getWidth' ] )
54 ->getMockForAbstractClass();
56 $fileMock->method( 'getWidth' )
57 ->willReturn( $data['width'] );
60 $data['expectedBucket'],
61 $fileMock->getThumbnailBucket( $data['requestedWidth'] ),
65 public static function getThumbnailBucketProvider() {
66 $defaultBuckets = [ 256, 512, 1024, 2048, 4096 ];
70 'buckets' => $defaultBuckets,
71 'minimumBucketDistance' => 0,
73 'requestedWidth' => 120,
74 'expectedBucket' => 256,
75 'message' => 'Picking bucket bigger than requested size'
78 'buckets' => $defaultBuckets,
79 'minimumBucketDistance' => 0,
81 'requestedWidth' => 300,
82 'expectedBucket' => 512,
83 'message' => 'Picking bucket bigger than requested size'
86 'buckets' => $defaultBuckets,
87 'minimumBucketDistance' => 0,
89 'requestedWidth' => 1024,
90 'expectedBucket' => 2048,
91 'message' => 'Picking bucket bigger than requested size'
94 'buckets' => $defaultBuckets,
95 'minimumBucketDistance' => 0,
97 'requestedWidth' => 2048,
98 'expectedBucket' => false,
99 'message' => 'Picking no bucket because none is bigger than the requested size'
102 'buckets' => $defaultBuckets,
103 'minimumBucketDistance' => 0,
105 'requestedWidth' => 3500,
106 'expectedBucket' => false,
107 'message' => 'Picking no bucket because requested size is bigger than original'
110 'buckets' => [ 1024 ],
111 'minimumBucketDistance' => 0,
113 'requestedWidth' => 1024,
114 'expectedBucket' => false,
115 'message' => 'Picking no bucket because requested size equals biggest bucket'
119 'minimumBucketDistance' => 0,
121 'requestedWidth' => 1024,
122 'expectedBucket' => false,
123 'message' => 'Picking no bucket because no buckets have been specified'
126 'buckets' => [ 256, 512 ],
127 'minimumBucketDistance' => 10,
129 'requestedWidth' => 245,
130 'expectedBucket' => 256,
131 'message' => 'Requested width is distant enough from next bucket for it to be picked'
134 'buckets' => [ 256, 512 ],
135 'minimumBucketDistance' => 10,
137 'requestedWidth' => 246,
138 'expectedBucket' => 512,
139 'message' => 'Requested width is too close to next bucket, picking next one'
145 * @dataProvider getThumbnailSourceProvider
146 * @covers \File::getThumbnailSource
148 public function testGetThumbnailSource( $data ) {
149 $backendMock = $this->getMockBuilder( FSFileBackend
::class )
150 ->setConstructorArgs( [ [ 'name' => 'backendMock', 'wikiId' => WikiMap
::getCurrentWikiId() ] ] )
153 $repoMock = $this->getMockBuilder( FileRepo
::class )
154 ->setConstructorArgs( [ [ 'name' => 'repoMock', 'backend' => $backendMock ] ] )
155 ->onlyMethods( [ 'fileExists', 'getLocalReference' ] )
158 $tempDir = wfTempDir();
159 $fsFile = new FSFile( 'fsFilePath' );
161 $repoMock->method( 'fileExists' )
162 ->willReturn( true );
164 $repoMock->method( 'getLocalReference' )
165 ->willReturn( $fsFile );
167 $handlerMock = $this->getMockBuilder( BitmapHandler
::class )
168 ->onlyMethods( [ 'supportsBucketing' ] )->getMock();
169 $handlerMock->method( 'supportsBucketing' )
170 ->willReturn( $data['supportsBucketing'] );
172 $fileMock = $this->getMockBuilder( File
::class )
173 ->setConstructorArgs( [ 'fileMock', $repoMock ] )
174 ->onlyMethods( [ 'getThumbnailBucket', 'getLocalRefPath', 'getHandler' ] )
175 ->getMockForAbstractClass();
177 $fileMock->method( 'getThumbnailBucket' )
178 ->willReturn( $data['thumbnailBucket'] );
180 $fileMock->method( 'getLocalRefPath' )
181 ->willReturn( 'localRefPath' );
183 $fileMock->method( 'getHandler' )
184 ->willReturn( $handlerMock );
186 $reflection = new ReflectionClass( $fileMock );
187 $reflection_property = $reflection->getProperty( 'handler' );
188 $reflection_property->setAccessible( true );
189 $reflection_property->setValue( $fileMock, $handlerMock );
191 if ( $data['tmpBucketedThumbCache'] !== null ) {
192 foreach ( $data['tmpBucketedThumbCache'] as &$tmpBucketed ) {
193 $tmpBucketed = str_replace( '/tmp', $tempDir, $tmpBucketed );
195 $reflection_property = $reflection->getProperty( 'tmpBucketedThumbCache' );
196 $reflection_property->setAccessible( true );
197 $reflection_property->setValue( $fileMock, $data['tmpBucketedThumbCache'] );
200 $result = $fileMock->getThumbnailSource(
201 [ 'physicalWidth' => $data['physicalWidth'] ] );
204 str_replace( '/tmp', $tempDir, $data['expectedPath'] ),
210 public static function getThumbnailSourceProvider() {
213 'supportsBucketing' => true,
214 'tmpBucketedThumbCache' => null,
215 'thumbnailBucket' => 1024,
216 'physicalWidth' => 2048,
217 'expectedPath' => 'fsFilePath',
218 'message' => 'Path downloaded from storage'
221 'supportsBucketing' => true,
222 'tmpBucketedThumbCache' => [ 1024 => '/tmp/shouldnotexist' . rand() ],
223 'thumbnailBucket' => 1024,
224 'physicalWidth' => 2048,
225 'expectedPath' => 'fsFilePath',
226 'message' => 'Path downloaded from storage because temp file is missing'
229 'supportsBucketing' => true,
230 'tmpBucketedThumbCache' => [ 1024 => '/tmp' ],
231 'thumbnailBucket' => 1024,
232 'physicalWidth' => 2048,
233 'expectedPath' => '/tmp',
234 'message' => 'Temporary path because temp file was found'
237 'supportsBucketing' => false,
238 'tmpBucketedThumbCache' => null,
239 'thumbnailBucket' => 1024,
240 'physicalWidth' => 2048,
241 'expectedPath' => 'localRefPath',
242 'message' => 'Original file path because bucketing is unsupported by handler'
245 'supportsBucketing' => true,
246 'tmpBucketedThumbCache' => null,
247 'thumbnailBucket' => false,
248 'physicalWidth' => 2048,
249 'expectedPath' => 'localRefPath',
250 'message' => 'Original file path because no width provided'
256 * @dataProvider generateBucketsIfNeededProvider
257 * @covers \File::generateBucketsIfNeeded
259 public function testGenerateBucketsIfNeeded( $data ) {
260 $this->overrideConfigValue( MainConfigNames
::ThumbnailBuckets
, $data['buckets'] );
262 $backendMock = $this->getMockBuilder( FSFileBackend
::class )
263 ->setConstructorArgs( [ [ 'name' => 'backendMock', 'wikiId' => WikiMap
::getCurrentWikiId() ] ] )
266 $repoMock = $this->getMockBuilder( FileRepo
::class )
267 ->setConstructorArgs( [ [ 'name' => 'repoMock', 'backend' => $backendMock ] ] )
268 ->onlyMethods( [ 'fileExists', 'getLocalReference' ] )
271 $fileMock = $this->getMockBuilder( File
::class )
272 ->setConstructorArgs( [ 'fileMock', $repoMock ] )
273 ->onlyMethods( [ 'getWidth', 'getBucketThumbPath', 'makeTransformTmpFile',
274 'generateAndSaveThumb', 'getHandler' ] )
275 ->getMockForAbstractClass();
277 $handlerMock = $this->getMockBuilder( JpegHandler
::class )
278 ->onlyMethods( [ 'supportsBucketing' ] )->getMock();
279 $handlerMock->method( 'supportsBucketing' )
280 ->willReturn( true );
282 $fileMock->method( 'getHandler' )
283 ->willReturn( $handlerMock );
285 $reflectionMethod = new ReflectionMethod( File
::class, 'generateBucketsIfNeeded' );
286 $reflectionMethod->setAccessible( true );
288 $fileMock->method( 'getWidth' )
289 ->willReturn( $data['width'] );
291 $fileMock->expects( $data['expectedGetBucketThumbPathCalls'] )
292 ->method( 'getBucketThumbPath' );
294 $repoMock->expects( $data['expectedFileExistsCalls'] )
295 ->method( 'fileExists' )
296 ->willReturn( $data['fileExistsReturn'] );
298 $fileMock->expects( $data['expectedMakeTransformTmpFile'] )
299 ->method( 'makeTransformTmpFile' )
300 ->willReturn( $data['makeTransformTmpFileReturn'] );
302 $fileMock->expects( $data['expectedGenerateAndSaveThumb'] )
303 ->method( 'generateAndSaveThumb' )
304 ->willReturn( $data['generateAndSaveThumbReturn'] );
306 $this->assertEquals( $data['expectedResult'],
307 $reflectionMethod->invoke(
310 'physicalWidth' => $data['physicalWidth'],
311 'physicalHeight' => $data['physicalHeight'] ]
316 public function generateBucketsIfNeededProvider() {
317 $defaultBuckets = [ 256, 512, 1024, 2048, 4096 ];
321 'buckets' => $defaultBuckets,
323 'physicalWidth' => 256,
324 'physicalHeight' => 100,
325 'expectedGetBucketThumbPathCalls' => $this->never(),
326 'expectedFileExistsCalls' => $this->never(),
327 'fileExistsReturn' => null,
328 'expectedMakeTransformTmpFile' => $this->never(),
329 'makeTransformTmpFileReturn' => false,
330 'expectedGenerateAndSaveThumb' => $this->never(),
331 'generateAndSaveThumbReturn' => false,
332 'expectedResult' => false,
333 'message' => 'No bucket found, nothing to generate'
336 'buckets' => $defaultBuckets,
338 'physicalWidth' => 300,
339 'physicalHeight' => 200,
340 'expectedGetBucketThumbPathCalls' => $this->once(),
341 'expectedFileExistsCalls' => $this->once(),
342 'fileExistsReturn' => true,
343 'expectedMakeTransformTmpFile' => $this->never(),
344 'makeTransformTmpFileReturn' => false,
345 'expectedGenerateAndSaveThumb' => $this->never(),
346 'generateAndSaveThumbReturn' => false,
347 'expectedResult' => false,
348 'message' => 'File already exists, no reason to generate buckets'
351 'buckets' => $defaultBuckets,
353 'physicalWidth' => 300,
354 'physicalHeight' => 200,
355 'expectedGetBucketThumbPathCalls' => $this->once(),
356 'expectedFileExistsCalls' => $this->once(),
357 'fileExistsReturn' => false,
358 'expectedMakeTransformTmpFile' => $this->once(),
359 'makeTransformTmpFileReturn' => false,
360 'expectedGenerateAndSaveThumb' => $this->never(),
361 'generateAndSaveThumbReturn' => false,
362 'expectedResult' => false,
363 'message' => 'Cannot generate temp file for bucket'
366 'buckets' => $defaultBuckets,
368 'physicalWidth' => 300,
369 'physicalHeight' => 200,
370 'expectedGetBucketThumbPathCalls' => $this->once(),
371 'expectedFileExistsCalls' => $this->once(),
372 'fileExistsReturn' => false,
373 'expectedMakeTransformTmpFile' => $this->once(),
374 'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
375 'expectedGenerateAndSaveThumb' => $this->once(),
376 'generateAndSaveThumbReturn' => false,
377 'expectedResult' => false,
378 'message' => 'Bucket image could not be generated'
381 'buckets' => $defaultBuckets,
383 'physicalWidth' => 300,
384 'physicalHeight' => 200,
385 'expectedGetBucketThumbPathCalls' => $this->once(),
386 'expectedFileExistsCalls' => $this->once(),
387 'fileExistsReturn' => false,
388 'expectedMakeTransformTmpFile' => $this->once(),
389 'makeTransformTmpFileReturn' => new TempFSFile( '/tmp/foo' ),
390 'expectedGenerateAndSaveThumb' => $this->once(),
391 'generateAndSaveThumbReturn' => new ThumbnailImage( false, 'bar', false, false ),
392 'expectedResult' => true,
393 'message' => 'Bucket image could not be generated'
399 * @covers \File::getDisplayWidthHeight
400 * @dataProvider providerGetDisplayWidthHeight
401 * @param array $dim Array [maxWidth, maxHeight, width, height]
402 * @param array $expected Array [width, height] The width and height we expect to display at
404 public function testGetDisplayWidthHeight( $dim, $expected ) {
405 $fileMock = $this->getMockBuilder( File
::class )
406 ->setConstructorArgs( [ 'fileMock', false ] )
407 ->onlyMethods( [ 'getWidth', 'getHeight' ] )
408 ->getMockForAbstractClass();
410 $fileMock->method( 'getWidth' )->willReturn( $dim[2] );
411 $fileMock->method( 'getHeight' )->willReturn( $dim[3] );
413 $actual = $fileMock->getDisplayWidthHeight( $dim[0], $dim[1] );
414 $this->assertEquals( $expected, $actual );
417 public static function providerGetDisplayWidthHeight() {
420 [ 1024.0, 768.0, 600.0, 600.0 ],
424 [ 1024.0, 768.0, 1600.0, 600.0 ],
428 [ 1024.0, 768.0, 1024.0, 768.0 ],
432 [ 1024.0, 768.0, 800.0, 1000.0 ],
436 [ 1024.0, 768.0, 0, 1000 ],
440 [ 1024.0, 768.0, 2000, 0 ],
446 public static function provideNormalizeTitle() {
447 yield
[ 'some name.jpg', 'Some_name.jpg' ];
448 yield
[ new TitleValue( NS_FILE
, 'Some_name.jpg' ), 'Some_name.jpg' ];
449 yield
[ new TitleValue( NS_MEDIA
, 'Some_name.jpg' ), 'Some_name.jpg' ];
450 yield
[ new PageIdentityValue( 0, NS_FILE
, 'Some_name.jpg', false ), 'Some_name.jpg' ];
454 * @covers \File::normalizeTitle
455 * @dataProvider provideNormalizeTitle
457 public function testNormalizeTitle( $title, $expected ) {
458 $actual = File
::normalizeTitle( $title );
460 $this->assertSame( NS_FILE
, $actual->getNamespace() );
461 $this->assertSame( $expected, $actual->getDBkey() );
464 public static function provideNormalizeTitleFails() {
467 yield
[ new TitleValue( NS_USER
, 'Some_name.jpg' ) ];
468 yield
[ new PageIdentityValue( 0, NS_USER
, 'Some_name.jpg', false ) ];
472 * @covers \File::normalizeTitle
473 * @dataProvider provideNormalizeTitleFails
475 public function testNormalizeTitleFails( $title ) {
476 $actual = File
::normalizeTitle( $title );
477 $this->assertNull( $actual );
479 $this->expectException( RuntimeException
::class );
480 File
::normalizeTitle( $title, 'exception' );
484 * @covers \File::setHandlerState
485 * @covers \File::getHandlerState
487 public function testSetHandlerState() {
489 $file = new class extends File
{
490 public function __construct() {
493 $this->assertNull( $file->getHandlerState( 'test' ) );
494 $file->setHandlerState( 'test', $obj );
495 $this->assertSame( $obj, $file->getHandlerState( 'test' ) );