Completed
Push — master ( 5163d9...817b84 )
by Mike
07:06 queued 11s
created

FinderTest::mockFileSystem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 1
nc 1
nop 2
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of phpDocumentor.
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 *
11
 * @link      http://phpdoc.org
12
 */
13
14
namespace Flyfinder;
15
16
use Flyfinder\Specification\Glob;
17
use Flyfinder\Specification\HasExtension;
18
use Flyfinder\Specification\InPath;
19
use Flyfinder\Specification\IsHidden;
20
use Generator;
21
use League\Flysystem\Filesystem;
22
use League\Flysystem\FilesystemInterface;
23
use Mockery as m;
24
use PHPUnit\Framework\TestCase;
25
use function array_map;
26
use function array_values;
27
use function iterator_to_array;
28
use function pathinfo;
29
use function sort;
30
use function substr;
31
use function trim;
32
33
/**
34
 * Test case for Finder
35
 *
36
 * @coversDefaultClass Flyfinder\Finder
37
 */
38
class FinderTest extends TestCase
39
{
40
    /** @var Finder */
41
    private $fixture;
42
43
    /**
44
     * Initializes the fixture for this test.
45
     */
46
    public function setUp() : void
47
    {
48
        $this->fixture = new Finder();
49
    }
50
51
    public function tearDown() : void
52
    {
53
        m::close();
54
    }
55
56
    /**
57
     * @covers ::getMethod
58
     */
59
    public function testGetMethod() : void
60
    {
61
        $this->assertSame('find', $this->fixture->getMethod());
62
    }
63
64
    public function testIfNotHiddenLetsSubpathsThrough() : void
65
    {
66
        $files = [ 'foo/bar/.hidden/baz/not-hidden.txt' ];
67
        $this->fixture->setFilesystem($this->mockFileSystem($files));
68
        $notHidden = (new IsHidden())->notSpecification();
69
        $this->assertEquals(
70
            $files,
71
            $this->generatorToFileList($this->fixture->handle($notHidden))
72
        );
73
    }
74
75
    public function testIfDoubleNotHiddenLetsSubpathsThrough() : void
76
    {
77
        $files = [ '.foo/.bar/not-hidden/.baz/.hidden.txt' ];
78
        $this->fixture->setFilesystem($this->mockFileSystem($files));
79
        $notHidden = (new IsHidden())->notSpecification()->notSpecification();
80
        $this->assertEquals(
81
            $files,
82
            $this->generatorToFileList($this->fixture->handle($notHidden))
83
        );
84
    }
85
86
    public function testIfNeitherHiddenNorExtLetsSubpathsThrough() : void
87
    {
88
        $files = [ 'foo/bar/.hidden/baz.ext/neither-hidden-nor.ext.zzz' ];
89
        $this->fixture->setFilesystem($this->mockFileSystem($files));
90
91
        $neitherHiddenNorExt =
92
            (new IsHidden())->notSpecification()
93
                ->andSpecification((new HasExtension(['ext']))->notSpecification());
94
        $this->assertEquals(
95
            $files,
96
            $this->generatorToFileList($this->fixture->handle($neitherHiddenNorExt))
97
        );
98
99
        $neitherHiddenNorExtDeMorgan = (new IsHidden())->orSpecification(new HasExtension(['ext']))->notSpecification();
100
        $this->assertEquals(
101
            $files,
102
            $this->generatorToFileList($this->fixture->handle($neitherHiddenNorExtDeMorgan))
103
        );
104
    }
105
106
    public function testIfNegatedOrCullsExactMatches() : void
107
    {
108
        $files = [
109
            'foo/bar/baz/whatever.txt',
110
            'foo/gen/pics/bottle.jpg',
111
            'foo/lou/time.txt',
112
        ];
113
        $this->fixture->setFilesystem($this->mockFileSystem($files, ['foo/bar', 'foo/gen']));
114
115
        $negatedOr =
116
            (new InPath(new Path('foo/gen')))
117
                ->orSpecification(new InPath(new Path('foo/bar')))
118
                ->notSpecification();
119
120
        $this->assertEquals(
121
            ['foo/lou/time.txt'],
122
            $this->generatorToFileList($this->fixture->handle($negatedOr))
123
        );
124
125
        $negatedOrDeMorgan =
126
            (new InPath(new Path('foo/gen')))->notSpecification()
127
            ->andSpecification((new InPath(new Path('foo/bar')))->notSpecification());
128
129
        $this->assertEquals(
130
            ['foo/lou/time.txt'],
131
            $this->generatorToFileList($this->fixture->handle($negatedOrDeMorgan))
132
        );
133
    }
134
135
    public function testIfNegatedAndCullsExactMatches() : void
136
    {
137
        $files    = [
138
            'foo/bar/baz/whatever.txt',
139
            'foo/gen/pics/bottle.jpg',
140
            'foo/lou/time.txt',
141
        ];
142
        $expected = [
143
            'foo/gen/pics/bottle.jpg',
144
            'foo/lou/time.txt',
145
        ];
146
        $this->fixture->setFilesystem($this->mockFileSystem($files, ['foo/bar']));
147
148
        $negatedAnd =
149
            (new InPath(new Path('foo/*')))
150
                ->andSpecification(new InPath(new Path('*/bar')))
151
                ->notSpecification();
152
153
        $this->assertEquals(
154
            $expected,
155
            $this->generatorToFileList($this->fixture->handle($negatedAnd))
156
        );
157
158
        $negatedAndDeMorgan =
159
            (new InPath(new Path('foo/*')))->notSpecification()
160
                ->orSpecification((new InPath(new Path('*/bar')))->notSpecification());
161
162
        $this->assertEquals(
163
            $expected,
164
            $this->generatorToFileList($this->fixture->handle($negatedAndDeMorgan))
165
        );
166
    }
167
168
169
    /**
170
     * @covers ::handle
171
     * @covers ::setFilesystem
172
     * @covers ::<private>
173
     */
174
    public function testIfCorrectFilesAreBeingYielded() : void
175
    {
176
        $isHidden   = m::mock(IsHidden::class);
177
        $filesystem = m::mock(Filesystem::class);
178
179
        $listContents1 = [
180
            0 => [
181
                'type' => 'dir',
182
                'path' => '.hiddendir',
183
                'dirname' => '',
184
                'basename' => '.hiddendir',
185
                'filename' => '.hiddendir',
186
            ],
187
            1 => [
188
                'type' => 'file',
189
                'path' => 'test.txt',
190
                'basename' => 'test.txt',
191
            ],
192
        ];
193
194
        $listContents2 = [
195
            0 => [
196
                'type' => 'file',
197
                'path' => '.hiddendir/.test.txt',
198
                'dirname' => '.hiddendir',
199
                'basename' => '.test.txt',
200
                'filename' => '.test',
201
                'extension' => 'txt',
202
            ],
203
        ];
204
205
        $filesystem->shouldReceive('listContents')
206
            ->with('')
207
            ->andReturn($listContents1);
208
209
        $filesystem->shouldReceive('listContents')
210
            ->with('.hiddendir')
211
            ->andReturn($listContents2);
212
213
        $isHidden->shouldReceive('isSatisfiedBy')
214
            ->with($listContents1[0])
215
            ->andReturn(true);
216
217
        $isHidden->shouldReceive('canBeSatisfiedBySomethingBelow')
218
            ->with($listContents1[0])
219
            ->andReturn(true);
220
221
        $isHidden->shouldReceive('isSatisfiedBy')
222
            ->with($listContents1[1])
223
            ->andReturn(false);
224
225
        $isHidden->shouldReceive('isSatisfiedBy')
226
            ->with($listContents2[0])
227
            ->andReturn(true);
228
229
        $this->fixture->setFilesystem($filesystem);
0 ignored issues
show
Documentation introduced by
$filesystem is of type object<Mockery\LegacyMockInterface>, but the function expects a object<League\Flysystem\FilesystemInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
230
        $generator = $this->fixture->handle($isHidden);
0 ignored issues
show
Documentation introduced by
$isHidden is of type object<Mockery\LegacyMockInterface>, but the function expects a object<Flyfinder\Specifi...SpecificationInterface>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
231
232
        $result = [];
233
234
        foreach ($generator as $value) {
235
            $result[] = $value;
236
        }
237
238
        $expected = [
239
            0 => [
240
                'type' => 'file',
241
                'path' => '.hiddendir/.test.txt',
242
                'dirname' => '.hiddendir',
243
                'basename' => '.test.txt',
244
                'filename' => '.test',
245
                'extension' => 'txt',
246
            ],
247
        ];
248
249
        $this->assertSame($expected, $result);
250
    }
251
252
    public function testSubtreePruningOptimization() : void
253
    {
254
        $filesystem = $this->mockFileSystem(
255
            [
256
                'foo/bar/baz/file.txt',
257
                'foo/bar/baz/file2.txt',
258
                'foo/bar/baz/excluded/excluded.txt',
259
                'foo/bar/baz/excluded/culled/culled.txt',
260
                'foo/bar/baz/excluded/important/reincluded.txt',
261
                'foo/bar/file3.txt',
262
                'foo/lou/someSubdir/file4.txt',
263
                'foo/irrelevant1/',
264
                'irrelevant2/irrelevant3/irrelevantFile.txt',
265
            ],
266
            [
267
                'foo/irrelevant1',
268
                'irrelevant2',
269
                'foo/bar/baz/excluded/culled',
270
            ]
271
        );
272
273
        $inFooBar = new InPath(new Path('foo/bar'));
274
        $inFooLou = new InPath(new Path('foo/lou'));
275
        $inExcl   = new InPath(new Path('foo/bar/baz/excl*'));
276
        $inReincl = new InPath(new Path('foo/bar/baz/*/important'));
277
        $spec     =
278
            $inFooBar
279
                ->orSpecification($inFooLou)
280
                ->andSpecification($inExcl->notSpecification())
281
                ->orSpecification($inReincl);
282
283
        $finder = $this->fixture;
284
        $finder->setFilesystem($filesystem);
285
        $generator = $finder->handle($spec);
286
287
        $expected = [
288
            'foo/bar/baz/file.txt',
289
            'foo/bar/baz/file2.txt',
290
            'foo/bar/file3.txt',
291
            'foo/bar/baz/excluded/important/reincluded.txt',
292
            'foo/lou/someSubdir/file4.txt',
293
        ];
294
        sort($expected);
295
296
        $this->assertEquals($expected, $this->generatorToFileList($generator));
297
    }
298
299
    public function testGlobSubtreePruning() : void
300
    {
301
        $filesystem  = $this->mockFileSystem(
302
            [
303
                'foo/bar/baz/file.txt',
304
                'foo/bar/baz/file2.txt',
305
                'foo/bar/baz/excluded/excluded.txt',
306
                'foo/bar/baz/excluded/culled/culled.txt',
307
                'foo/bar/baz/excluded/important/reincluded.txt',
308
                'foo/bar/file3.txt',
309
                'foo/lou/someSubdir/file4.txt',
310
                'foo/irrelevant1/',
311
                'irrelevant2/irrelevant3/irrelevantFile.txt',
312
            ],
313
            [
314
                'foo/irrelevant1',
315
                'irrelevant2',
316
                'foo/bar/baz/excluded/culled',
317
            ]
318
        );
319
        $txtInFooBar = new Glob('/foo/bar/**/*.txt');
320
        $inFooLou    = new Glob('/foo/lou/**/*');
321
        $inExcl      = new Glob('/foo/bar/baz/excl*/**/*');
322
        $inReincl    = new Glob('/foo/bar/baz/*/important/**/*');
323
        $spec        = $txtInFooBar
324
            ->orSpecification($inFooLou)
325
            ->andSpecification($inExcl->notSpecification())
326
            ->orSpecification($inReincl);
327
328
        $finder = $this->fixture;
329
        $finder->setFilesystem($filesystem);
330
        $generator = $finder->handle($spec);
331
        $expected  = [
332
            'foo/bar/baz/file.txt',
333
            'foo/bar/baz/file2.txt',
334
            'foo/bar/file3.txt',
335
            'foo/bar/baz/excluded/important/reincluded.txt',
336
            'foo/lou/someSubdir/file4.txt',
337
        ];
338
        sort($expected);
339
340
        $this->assertEquals($expected, $this->generatorToFileList($generator));
341
    }
342
343
    /**
344
     * @return string[]
345
     */
346
    protected function generatorToFileList(Generator $generator) : array
347
    {
348
        $actual = array_values(array_map(static function ($v) {
349
            return $v['path'];
350
        }, iterator_to_array($generator)));
351
        sort($actual);
352
        return $actual;
353
    }
354
355
    /**
356
     * @param string[] $pathList
357
     *
358
     * @return mixed[]
359
     */
360
    protected function mockFileTree(array $pathList) : array
361
    {
362
        $result = [
363
            '.' => [
364
                'type' => 'dir',
365
                'path' => '',
366
                'dirname' => '.',
367
                'basename' => '.',
368
                'filename' => '.',
369
                'contents' => [],
370
            ],
371
        ];
372
        foreach ($pathList as $path) {
373
            $isFile = substr($path, -1) !== '/';
374
            $child  = null;
375
            while (true) {
376
                $info = pathinfo($path);
377
                if ($isFile) {
378
                    $isFile        = false;
379
                    $result[$path] = [
380
                        'type' => 'file',
381
                        'path' => $path,
382
                        'dirname' => $info['dirname'],
383
                        'basename' => $info['basename'],
384
                        'filename' => $info['filename'],
385
                        'extension' => $info['extension'],
386
                    ];
387
                } else {
388
                    $existed = true;
389
                    if (!isset($result[$path])) {
390
                        $existed       = false;
391
                        $result[$path] = [
392
                            'type' => 'dir',
393
                            'path' => $path,
394
                            'basename' => $info['basename'],
395
                            'filename' => $info['filename'],
396
                            'contents' => [],
397
                        ];
398
                    }
399
                    if ($child!==null) {
400
                        $result[$path]['contents'][] = $child;
401
                    }
402
                    if ($existed) {
403
                        break;
404
                    }
405
                }
406
                $child = $info['basename'];
407
                $path  = $info['dirname'];
408
            }
409
        }
410
        return $result;
411
    }
412
413
    /**
414
     * @param mixed[] $fileTreeMock
415
     *
416
     * @return mixed[]
417
     */
418
    protected function mockListContents(array $fileTreeMock, string $path) : array
419
    {
420
        $path = trim($path, '/');
421
        if (substr($path . '  ', 0, 2)==='./') {
422
            $path = substr($path, 2);
423
        }
424
        if ($path==='') {
425
            $path = '.';
426
        }
427
428
        if (!isset($fileTreeMock[$path]) || $fileTreeMock[$path]['type'] === 'file') {
429
            return [];
430
        }
431
        $result = [];
432
        foreach ($fileTreeMock[$path]['contents'] as $basename) {
433
            $childPath = ($path==='.' ? '' : $path . '/') . $basename;
434
            if (!isset($fileTreeMock[$childPath])) {
435
                continue;
436
            }
437
438
            $result[$basename] = $fileTreeMock[$childPath];
439
        }
440
        return $result;
441
    }
442
443
    /**
444
     * @param string[] $paths
445
     * @param string[] $pathsThatShouldNotBeListed
446
     */
447
    protected function mockFileSystem(array $paths, array $pathsThatShouldNotBeListed = []) : FilesystemInterface
448
    {
449
        $fsData     = $this->mockFileTree($paths);
450
        $filesystem = m::mock(Filesystem::class);
451
        $filesystem->shouldReceive('listContents')
452
            ->zeroOrMoreTimes()
453
            ->andReturnUsing(function (string $path) use ($fsData, $pathsThatShouldNotBeListed) : array {
454
                $this->assertNotContains($path, $pathsThatShouldNotBeListed);
455
                return array_values($this->mockListContents($fsData, $path));
456
            });
457
        return $filesystem;
458
    }
459
}
460