Passed
Pull Request — master (#5)
by Wilmer
01:28
created

FileHelperTest::testCopyDirToSubdirOfItself()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 7
nc 1
nop 0
dl 0
loc 11
rs 10
c 1
b 0
f 1
1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Files\Tests;
5
6
use PHPUnit\Framework\TestCase;
7
use Yiisoft\Files\FileHelper;
8
9
/**
10
 * File helper tests class.
11
 */
12
final class FileHelperTest extends TestCase
13
{
14
    /**
15
     * @var string test files path.
16
     */
17
    private $testFilePath = '';
18
19
    public function setUp(): void
20
    {
21
        $this->testFilePath = FileHelper::normalizePath(sys_get_temp_dir() . '/' . get_class($this));
22
        
23
        FileHelper::createDirectory($this->testFilePath, 0777);
24
25
        if (!file_exists($this->testFilePath)) {
26
            $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!');
27
        }
28
    }
29
30
    public function tearDown(): void
31
    {
32
        FileHelper::removeDirectory($this->testFilePath);
33
    }
34
35
    /**
36
     * Create directory.
37
     *
38
     * @return void
39
     */
40
    public function testCreateDirectory(): void
41
    {
42
        $basePath = $this->testFilePath;
43
        $directory = $basePath . '/test_dir_level_1/test_dir_level_2';
44
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true if directory was created!');
45
        $this->assertFileExists($directory, 'Unable to create directory recursively!');
46
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true for already existing directories!');
47
    }
48
49
    /**
50
     * Create directory permissions.
51
     *
52
     * @return void
53
     */
54
    public function testCreateDirectoryPermissions(): void
55
    {
56
        if (!$this->isChmodReliable()) {
57
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
58
        }
59
60
        $basePath = $this->testFilePath;
61
        $dirName = $basePath . '/test_dir_perms';
62
63
        $this->assertTrue(FileHelper::createDirectory($dirName, 0700));
64
        $this->assertFileMode(0700, $dirName);
65
    }
66
67
    /**
68
     * Remove directory.
69
     *
70
     * @return void
71
     */
72
    public function testRemoveDirectory(): void
73
    {
74
        $dirName = 'test_dir_for_remove';
75
76
        $this->createFileStructure([
77
            $dirName => [
78
                'file1.txt' => 'file 1 content',
79
                'file2.txt' => 'file 2 content',
80
                'test_sub_dir' => [
81
                    'sub_dir_file_1.txt' => 'sub dir file 1 content',
82
                    'sub_dir_file_2.txt' => 'sub dir file 2 content',
83
                ],
84
            ],
85
        ]);
86
87
        $basePath = $this->testFilePath;
88
        $dirName = $basePath . '/' . $dirName;
89
90
        FileHelper::removeDirectory($dirName);
91
92
        $this->assertFileNotExists($dirName, 'Unable to remove directory!');
93
94
        // should be silent about non-existing directories
95
        FileHelper::removeDirectory($basePath . '/nonExisting');
96
    }
97
98
    /**
99
     * Remove directory symlinks1.
100
     *
101
     * @return void
102
     */
103
    public function testRemoveDirectorySymlinks1(): void
104
    {
105
        $dirName = 'remove-directory-symlinks-1';
106
107
        $this->createFileStructure([
108
            $dirName => [
109
                'file' => 'Symlinked file.',
110
                'directory' => [
111
                    'standard-file-1' => 'Standard file 1.',
112
                ],
113
                'symlinks' => [
114
                    'standard-file-2' => 'Standard file 2.',
115
                    'symlinked-file' => ['symlink', '../file'],
116
                    'symlinked-directory' => ['symlink', '../directory'],
117
                ],
118
            ],
119
        ]);
120
121
        $basePath = $this->testFilePath . '/' . $dirName . '/';
122
123
        $this->assertFileExists($basePath . 'file');
124
        $this->assertDirectoryExists($basePath . 'directory');
125
        $this->assertFileExists($basePath . 'directory/standard-file-1');
126
        $this->assertDirectoryExists($basePath . 'symlinks');
127
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
128
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
129
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
130
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
131
132
        FileHelper::removeDirectory($basePath . 'symlinks');
133
134
        $this->assertFileExists($basePath . 'file');
135
        $this->assertDirectoryExists($basePath . 'directory');
136
        $this->assertFileExists($basePath . 'directory/standard-file-1'); // symlinked directory still have it's file
137
        $this->assertDirectoryNotExists($basePath . 'symlinks');
138
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
139
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
140
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
141
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
142
    }
143
144
    /**
145
     * Remove directory symlinks2.
146
     *
147
     * @return void
148
     */
149
    public function testRemoveDirectorySymlinks2(): void
150
    {
151
        $dirName = 'remove-directory-symlinks-2';
152
153
        $this->createFileStructure([
154
            $dirName => [
155
                'file' => 'Symlinked file.',
156
                'directory' => [
157
                    'standard-file-1' => 'Standard file 1.',
158
                ],
159
                'symlinks' => [
160
                    'standard-file-2' => 'Standard file 2.',
161
                    'symlinked-file' => ['symlink', '../file'],
162
                    'symlinked-directory' => ['symlink', '../directory'],
163
                ],
164
            ],
165
        ]);
166
167
        $basePath = $this->testFilePath . '/' . $dirName . '/';
168
169
        $this->assertFileExists($basePath . 'file');
170
        $this->assertDirectoryExists($basePath . 'directory');
171
        $this->assertFileExists($basePath . 'directory/standard-file-1');
172
        $this->assertDirectoryExists($basePath . 'symlinks');
173
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
174
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
175
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
176
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
177
178
        FileHelper::removeDirectory($basePath . 'symlinks', ['traverseSymlinks' => true]);
179
180
        $this->assertFileExists($basePath . 'file');
181
        $this->assertDirectoryExists($basePath . 'directory');
182
        $this->assertFileNotExists($basePath . 'directory/standard-file-1'); // symlinked directory doesn't have it's file now
183
        $this->assertDirectoryNotExists($basePath . 'symlinks');
184
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
185
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
186
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
187
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
188
    }
189
190
    /**
191
     * Normalize path.
192
     *
193
     * @return void
194
     */
195
    public function testNormalizePath(): void
196
    {
197
        $this->assertEquals('/a/b', FileHelper::normalizePath('//a\\b/'));
198
        $this->assertEquals('/b/c', FileHelper::normalizePath('/a/../b/c'));
199
        $this->assertEquals('/c', FileHelper::normalizePath('/a\\b/../..///c'));
200
        $this->assertEquals('/c', FileHelper::normalizePath('/a/.\\b//../../c'));
201
        $this->assertEquals('c', FileHelper::normalizePath('/a/.\\b/../..//../c'));
202
        $this->assertEquals('../c', FileHelper::normalizePath('//a/.\\b//..//..//../../c'));
203
204
        // relative paths
205
        $this->assertEquals('.', FileHelper::normalizePath('.'));
206
        $this->assertEquals('.', FileHelper::normalizePath('./'));
207
        $this->assertEquals('a', FileHelper::normalizePath('.\\a'));
208
        $this->assertEquals('a/b', FileHelper::normalizePath('./a\\b'));
209
        $this->assertEquals('.', FileHelper::normalizePath('./a\\../'));
210
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a'));
211
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a/../a'));
212
        $this->assertEquals('../../b', FileHelper::normalizePath('../..\\a/../b'));
213
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a'));
214
        $this->assertEquals('../a', FileHelper::normalizePath('././..\\a'));
215
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a/../a'));
216
        $this->assertEquals('../b', FileHelper::normalizePath('./..\\a/../b'));
217
218
        // Windows file system may have paths for network shares that start with two backslashes. These two backslashes
219
        // should not be touched.
220
        // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
221
        // https://github.com/yiisoft/yii2/issues/13034
222
        $this->assertEquals('\\\\server/share/path/file', FileHelper::normalizePath('\\\\server\share\path\file'));
223
        $this->assertEquals('\\\\server/share/path/file', FileHelper::normalizePath('\\\\server\share\path//file'));
224
    }
225
226
    /**
227
     * Copy directory.
228
     *
229
     * @depends testCreateDirectory
230
     *
231
     * @return void
232
     */
233
    public function testCopyDirectory(): void
234
    {
235
        $source = 'test_src_dir';
236
        $files = [
237
            'file1.txt' => 'file 1 content',
238
            'file2.txt' => 'file 2 content',
239
        ];
240
241
        $this->createFileStructure([
242
            $source => $files,
243
        ]);
244
245
        $basePath = $this->testFilePath;
246
        $source = $basePath . '/' . $source;
247
        $destination = $basePath . '/test_dst_dir';
248
249
        FileHelper::copyDirectory($source, $destination);
250
251
        $this->assertFileExists($destination, 'Destination directory does not exist!');
252
253
        foreach ($files as $name => $content) {
254
            $fileName = $destination . '/' . $name;
255
            $this->assertFileExists($fileName);
256
            $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
257
        }
258
    }
259
260
    /**
261
     * Copy directory recursive.
262
     *
263
     * @return void
264
     */
265
    public function testCopyDirectoryRecursive(): void
266
    {
267
        $source = 'test_src_dir_rec';
268
        $structure = [
269
            'directory1' => [
270
                'file1.txt' => 'file 1 content',
271
                'file2.txt' => 'file 2 content',
272
            ],
273
            'directory2' => [
274
                'file3.txt' => 'file 3 content',
275
                'file4.txt' => 'file 4 content',
276
            ],
277
            'file5.txt' => 'file 5 content',
278
        ];
279
280
        $this->createFileStructure([
281
            $source => $structure,
282
        ]);
283
284
        $basePath = $this->testFilePath;
285
        $source = $basePath . '/' . $source;
286
        $destination = $basePath . '/test_dst_dir';
287
288
        FileHelper::copyDirectory($source, $destination);
289
290
        $this->assertFileExists($destination, 'Destination directory does not exist!');
291
292
        $checker = function ($structure, $dstDirName) use (&$checker) {
293
            foreach ($structure as $name => $content) {
294
                if (is_array($content)) {
295
                    $checker($content, $dstDirName . '/' . $name);
296
                } else {
297
                    $fileName = $dstDirName . '/' . $name;
298
                    $this->assertFileExists($fileName);
299
                    $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
300
                }
301
            }
302
        };
303
        $checker($structure, $destination);
304
    }
305
306
    /**
307
     * Copy directory not recursive.
308
     *
309
     * @return void
310
     */
311
    public function testCopyDirectoryNotRecursive(): void
312
    {
313
        $source = 'test_src_dir_not_rec';
314
        $structure = [
315
            'directory1' => [
316
                'file1.txt' => 'file 1 content',
317
                'file2.txt' => 'file 2 content',
318
            ],
319
            'directory2' => [
320
                'file3.txt' => 'file 3 content',
321
                'file4.txt' => 'file 4 content',
322
            ],
323
            'file5.txt' => 'file 5 content',
324
        ];
325
326
        $this->createFileStructure([
327
            $source => $structure,
328
        ]);
329
330
        $basePath = $this->testFilePath;
331
        $source = $basePath . '/' . $source;
332
        $destination = $basePath . '/' . 'test_dst_dir';
333
334
        FileHelper::copyDirectory($source, $destination, ['recursive' => false]);
335
336
        $this->assertFileExists($destination, 'Destination directory does not exist!');
337
338
        foreach ($structure as $name => $content) {
339
            $fileName = $destination . '/' . $name;
340
            if (is_array($content)) {
341
                $this->assertFileNotExists($fileName);
342
            } else {
343
                $this->assertFileExists($fileName);
344
                $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
345
            }
346
        }
347
    }
348
349
    /**
350
     * Copy directory permissions.
351
     *
352
     * @depends testCopyDirectory
353
     *
354
     * @return void
355
     */
356
    public function testCopyDirectoryPermissions(): void
357
    {
358
        if (!$this->isChmodReliable()) {
359
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
360
        }
361
362
        $isWindows = DIRECTORY_SEPARATOR === '\\';
363
364
        if ($isWindows) {
365
            $this->markTestSkipped('Skipping tests on Windows because fileperms() always return 0777.');
366
        }
367
368
        $source = 'test_src_dir';
369
        $subDirectory = 'test_sub_dir';
370
        $fileName = 'test_file.txt';
371
372
        $this->createFileStructure([
373
            $source => [
374
                $subDirectory => [],
375
                $fileName => 'test file content',
376
            ],
377
        ]);
378
379
        $basePath = $this->testFilePath;
380
        $source = $basePath . '/' . $source;
381
        $destination = $basePath . '/test_dst_dir';
382
        $directoryMode = 0755;
383
        $fileMode = 0755;
384
        $options = [
385
            'dirMode' => $directoryMode,
386
            'fileMode' => $fileMode,
387
        ];
388
389
        FileHelper::copyDirectory($source, $destination, $options);
390
391
        $this->assertFileMode($directoryMode, $destination, 'Destination directory has wrong mode!');
392
        $this->assertFileMode($directoryMode, $destination . '/' . $subDirectory, 'Copied sub directory has wrong mode!');
393
        $this->assertFileMode($fileMode, $destination . '/' . $fileName, 'Copied file has wrong mode!');
394
    }
395
396
    /**
397
     * Copy directory to it self.
398
     *
399
     * @see https://github.com/yiisoft/yii2/issues/10710
400
     *
401
     * @return void
402
     */
403
    public function testCopyDirectoryToItself(): void
404
    {
405
        $directoryName = 'test_dir';
406
407
        $this->createFileStructure([
408
            $directoryName => [],
409
        ]);
410
        $this->expectException(\InvalidArgumentException::class);
411
412
        $directoryName = $this->testFilePath . '/test_dir';
413
414
        FileHelper::copyDirectory($directoryName, $directoryName);
415
    }
416
417
    /**
418
     * Copy directory to sudirectory of it self.
419
     *
420
     * @see https://github.com/yiisoft/yii2/issues/10710
421
     *
422
     * @return void
423
     */
424
    public function testCopyDirToSubdirOfItself(): void
425
    {
426
        $this->createFileStructure([
427
            'data' => [],
428
            'backup' => ['data' => []],
429
        ]);
430
        $this->expectException(\InvalidArgumentException::class);
431
432
        FileHelper::copyDirectory(
433
            $this->testFilePath . '/backup',
434
            $this->testFilePath . '/backup/data'
435
        );
436
    }
437
438
    /**
439
     * Copy directory to another with same name.
440
     *
441
     * @see https://github.com/yiisoft/yii2/issues/10710
442
     *
443
     * @return void
444
     */
445
    public function testCopyDirToAnotherWithSameName(): void
446
    {
447
        $this->createFileStructure([
448
            'data' => [],
449
            'backup' => ['data' => []],
450
        ]);
451
452
        FileHelper::copyDirectory(
453
            $this->testFilePath . '/data',
454
            $this->testFilePath . '/backup/data'
455
        );
456
457
        $this->assertFileExists($this->testFilePath . '/backup/data');
458
    }
459
460
    /**
461
     * Copy directory with same name.
462
     *
463
     * @see https://github.com/yiisoft/yii2/issues/10710
464
     *
465
     * @return void
466
     */
467
    public function testCopyDirWithSameName(): void
468
    {
469
        $this->createFileStructure([
470
            'data' => [],
471
            'data-backup' => [],
472
        ]);
473
474
        FileHelper::copyDirectory(
475
            $this->testFilePath . '/data',
476
            $this->testFilePath . '/data-backup'
477
        );
478
479
        $this->assertTrue(true, 'no error');
480
    }
481
482
    public function testsCopyDirectoryFilterPath()
483
    {
484
        $source = 'boostrap4';
485
486
        $structure = [
487
            'css' => [
488
                'bootstrap.css'           => 'file 1 content',
489
                'bootstrap.css.map'       => 'file 2 content',
490
                'bootstrap.min.css'       => 'file 3 content',
491
                'bootstrap.min.css.map'   => 'file 4 content'
492
            ],
493
            'js' => [
494
                'bootstrap.js'            => 'file 5 content',
495
                'bootstrap.bundle.js'     => 'file 6 content',
496
                'bootstrap.bundle.js.map' => 'file 7 content',
497
                'bootstrap.min.js'        => 'file 8 content'
498
            ]
499
        ];
500
501
        $this->createFileStructure([
502
            $source => $structure,
503
        ]);
504
505
        $basePath = $this->testFilePath;
506
        $source = $basePath . '/' . $source;
507
        $destination = $basePath . '/assets';
508
509
        // without filter options return all directory.
510
        $options = [];
511
512
        FileHelper::copyDirectory($source, $destination, $options);
513
514
        $this->assertFileExists($destination, 'Destination directory does not exist!');
515
        $this->checkExist($structure, $destination);
516
    }
517
518
    public function testsCopyDirectoryFilterPathOnly()
519
    {
520
        $source = 'boostrap4';
521
522
        $structure = [
523
            'css' => [
524
                'bootstrap.css'           => 'file 1 content',
525
                'bootstrap.css.map'       => 'file 2 content',
526
                'bootstrap.min.css'       => 'file 3 content',
527
                'bootstrap.min.css.map'   => 'file 4 content'
528
            ],
529
            'js' => [
530
                'bootstrap.js'            => 'file 5 content',
531
                'bootstrap.bundle.js'     => 'file 6 content',
532
                'bootstrap.bundle.js.map' => 'file 7 content',
533
                'bootstrap.min.js'        => 'file 8 content'
534
            ]
535
        ];
536
537
        $exist = [
538
            'css' => [
539
                'bootstrap.css'           => 'file 1 content',
540
                'bootstrap.min.css'       => 'file 3 content',
541
            ]
542
        ];
543
544
        $noexist = [
545
            'css' => [
546
                'bootstrap.css.map'       => 'file 2 content',
547
                'bootstrap.min.css.map'   => 'file 4 content'
548
            ],
549
            'js' => [
550
                'bootstrap.js'            => 'file 5 content',
551
                'bootstrap.bundle.js'     => 'file 6 content',
552
                'bootstrap.bundle.js.map' => 'file 7 content',
553
                'bootstrap.min.js'        => 'file 8 content'
554
            ]
555
        ];
556
557
        $this->createFileStructure([
558
            $source => $structure,
559
        ]);
560
561
        $basePath = $this->testFilePath;
562
        $source = $basePath . '/' . $source;
563
        $destination = $basePath . '/assets';
564
565
        // without filter options return all directory.
566
        $options = [
567
            // options default false AssetManager
568
            'copyEmptyDirectories' => false,
569
            'only' => [
570
                'css/*.css',
571
            ]
572
        ];
573
574
        FileHelper::copyDirectory($source, $destination, $options);
575
576
        $this->assertFileExists($destination, 'Destination directory does not exist!');
577
        $this->checkExist($exist, $destination);
578
        $this->checkNoexist($noexist, $destination);
579
    }
580
581
    public function testsCopyDirectoryFilterPathExcept()
582
    {
583
        $source = 'boostrap4';
584
585
        $structure = [
586
            'css' => [
587
                'bootstrap.css'           => 'file 1 content',
588
                'bootstrap.css.map'       => 'file 2 content',
589
                'bootstrap.min.css'       => 'file 3 content',
590
                'bootstrap.min.css.map'   => 'file 4 content'
591
            ],
592
            'js' => [
593
                'bootstrap.js'            => 'file 5 content',
594
                'bootstrap.bundle.js'     => 'file 6 content',
595
                'bootstrap.bundle.js.map' => 'file 7 content',
596
                'bootstrap.min.js'        => 'file 8 content'
597
            ]
598
        ];
599
600
        $exist = [
601
            'css' => [
602
                'bootstrap.css'           => 'file 1 content',
603
            ]
604
        ];
605
606
        $noexist = [
607
            'css' => [
608
                'bootstrap.css.map'       => 'file 2 content',
609
                'bootstrap.min.css'       => 'file 3 content',
610
                'bootstrap.min.css.map'   => 'file 4 content'
611
            ],
612
            'js' => [
613
                'bootstrap.js'            => 'file 5 content',
614
                'bootstrap.bundle.js'     => 'file 6 content',
615
                'bootstrap.bundle.js.map' => 'file 7 content',
616
                'bootstrap.min.js'        => 'file 8 content'
617
            ]
618
        ];
619
620
        $this->createFileStructure([
621
            $source => $structure,
622
        ]);
623
624
        $basePath = $this->testFilePath;
625
        $source = $basePath . '/' . $source;
626
        $destination = $basePath . '/assets';
627
628
        // without filter options return all directory.
629
        $options = [
630
            // options default false AssetManager
631
            'copyEmptyDirectories' => false,
632
            'only' => [
633
                'css/*.css',
634
            ],
635
            'except' => [
636
                'css/bootstrap.min.css'
637
            ]
638
        ];
639
640
        FileHelper::copyDirectory($source, $destination, $options);
641
642
        $this->assertFileExists($destination, 'Destination directory does not exist!');
643
        $this->checkExist($exist, $destination);
644
        $this->checkNoexist($noexist, $destination);
645
    }
646
647
    /**
648
     * Check if exist filename.
649
     *
650
     * @param array $exist
651
     * @param string $dstDirName
652
     *
653
     * @return void
654
     */
655
    private function checkExist(array $exist, string $dstDirName): void
656
    {
657
        foreach ($exist as $name => $content) {
658
            if (is_array($content)) {
659
                $this->checkExist($content, $dstDirName . '/' . $name);
660
            } else {
661
                $fileName = $dstDirName . '/' . $name;
662
                $this->assertFileExists($fileName);
663
                $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
664
            }
665
        }
666
    }
667
668
    /**
669
     * Check if no exist filename.
670
     *
671
     * @param array $noexist
672
     * @param string $dstDirName
673
     *
674
     * @return void
675
     */
676
    private function checkNoexist(array $noexist, string $dstDirName): void
677
    {
678
        foreach ($noexist as $name => $content) {
679
            if (is_array($content)) {
680
                $this->checkNoexist($content, $dstDirName . '/' . $name);
681
            } else {
682
                $fileName = $dstDirName . '/' . $name;
683
                $this->assertFileNotExists($fileName);
684
            }
685
        }
686
    }
687
688
    /**
689
     * Asserts that file has specific permission mode.
690
     *
691
     * @param int $expectedMode expected file permission mode.
692
     * @param string $fileName file name.
693
     * @param string $message error message
694
     *
695
     * @return void
696
     */
697
    private function assertFileMode(int $expectedMode, string $fileName, string $message = ''): void
698
    {
699
        $expectedMode = sprintf('%04o', $expectedMode);
700
        $this->assertEquals($expectedMode, $this->getMode($fileName), $message);
701
    }
702
703
    /**
704
     * Creates test files structure.
705
     *
706
     * @param array $items file system objects to be created in format: objectName => objectContent
707
     *                         Arrays specifies directories, other values - files.
708
     * @param string $basePath structure base file path.
709
     *
710
     * @return void
711
     */
712
    private function createFileStructure(array $items, ?string $basePath = null): void
713
    {
714
        $basePath = $basePath ?? $this->testFilePath;
715
716
        if (empty($basePath)) {
717
            $basePath = $this->testFilePath;
718
        }
719
        foreach ($items as $name => $content) {
720
            $itemName = $basePath . DIRECTORY_SEPARATOR . $name;
721
            if (is_array($content)) {
722
                if (isset($content[0], $content[1]) && $content[0] === 'symlink') {
723
                    symlink($basePath . '/' . $content[1], $itemName);
724
                } else {
725
                    mkdir($itemName, 0777, true);
726
                    $this->createFileStructure($content, $itemName);
727
                }
728
            } else {
729
                file_put_contents($itemName, $content);
730
            }
731
        }
732
    }
733
734
    /**
735
     * Get file permission mode.
736
     *
737
     * @param string $file file name.
738
     *
739
     * @return string permission mode.
740
     */
741
    private function getMode(string $file): string
742
    {
743
        return substr(sprintf('%04o', fileperms($file)), -4);
744
    }
745
746
    /**
747
     * Check if chmod works as expected.
748
     *
749
     * On remote file systems and vagrant mounts chmod returns true but file permissions are not set properly.
750
     */
751
    private function isChmodReliable(): bool
752
    {
753
        $directory = $this->testFilePath . '/test_chmod';
754
        mkdir($directory);
755
        chmod($directory, 0700);
756
        $mode = $this->getMode($directory);
757
        rmdir($directory);
758
759
        return $mode === '0700';
760
    }
761
}
762