Passed
Pull Request — master (#5)
by Wilmer
02:20
created

FileHelperTest::testCopyDirToAnotherWithSameName()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 13
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 13
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
    /**
20
     * Setup.
21
     *
22
     * @return void
23
     */
24
    public function setUp(): void
25
    {
26
        $this->testFilePath = sys_get_temp_dir() . '/' . get_class($this);
27
        
28
        FileHelper::createDirectory($this->testFilePath, 0777);
29
30
        if (!file_exists($this->testFilePath)) {
31
            $this->markTestIncomplete('Unit tests runtime directory should have writable permissions!');
32
        }
33
    }
34
35
    /**
36
     * Check if chmod works as expected.
37
     *
38
     * On remote file systems and vagrant mounts chmod returns true but file permissions are not set properly.
39
     */
40
    private function isChmodReliable(): bool
41
    {
42
        $directory = $this->testFilePath . '/test_chmod';
43
        mkdir($directory);
44
        chmod($directory, 0700);
45
        $mode = $this->getMode($directory);
46
        rmdir($directory);
47
48
        return $mode === '0700';
49
    }
50
51
    /**
52
     * TearDown.
53
     *
54
     * @return void
55
     */
56
    public function tearDown(): void
57
    {
58
        FileHelper::removeDirectory($this->testFilePath);
59
    }
60
61
    /**
62
     * Get file permission mode.
63
     *
64
     * @param string $file file name.
65
     *
66
     * @return string permission mode.
67
     */
68
    private function getMode(string $file): string
69
    {
70
        return substr(sprintf('%04o', fileperms($file)), -4);
71
    }
72
73
    /**
74
     * Asserts that file has specific permission mode.
75
     *
76
     * @param int $expectedMode expected file permission mode.
77
     * @param string $fileName file name.
78
     * @param string $message error message
79
     *
80
     * @return void
81
     */
82
    private function assertFileMode(int $expectedMode, string $fileName, string $message = ''): void
83
    {
84
        $expectedMode = sprintf('%04o', $expectedMode);
85
        $this->assertEquals($expectedMode, $this->getMode($fileName), $message);
86
    }
87
88
    /**
89
     * Create directory.
90
     *
91
     * @return void
92
     */
93
    public function testCreateDirectory(): void
94
    {
95
        $basePath = $this->testFilePath;
96
        $directory = $basePath . '/test_dir_level_1/test_dir_level_2';
97
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true if directory was created!');
98
        $this->assertFileExists($directory, 'Unable to create directory recursively!');
99
        $this->assertTrue(FileHelper::createDirectory($directory), 'FileHelper::createDirectory should return true for already existing directories!');
100
    }
101
102
    /**
103
     * Create directory permissions.
104
     *
105
     * @return void
106
     */
107
    public function testCreateDirectoryPermissions(): void
108
    {
109
        if (!$this->isChmodReliable()) {
110
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
111
        }
112
113
        $basePath = $this->testFilePath;
114
        $dirName = $basePath . '/test_dir_perms';
115
116
        $this->assertTrue(FileHelper::createDirectory($dirName, 0700));
117
        $this->assertFileMode(0700, $dirName);
118
    }
119
120
    /**
121
     * Remove directory.
122
     *
123
     * @return void
124
     */
125
    public function testRemoveDirectory(): void
126
    {
127
        $dirName = 'test_dir_for_remove';
128
129
        $this->createFileStructure([
130
            $dirName => [
131
                'file1.txt' => 'file 1 content',
132
                'file2.txt' => 'file 2 content',
133
                'test_sub_dir' => [
134
                    'sub_dir_file_1.txt' => 'sub dir file 1 content',
135
                    'sub_dir_file_2.txt' => 'sub dir file 2 content',
136
                ],
137
            ],
138
        ]);
139
140
        $basePath = $this->testFilePath;
141
        $dirName = $basePath . '/' . $dirName;
142
143
        FileHelper::removeDirectory($dirName);
144
145
        $this->assertFileNotExists($dirName, 'Unable to remove directory!');
146
147
        // should be silent about non-existing directories
148
        FileHelper::removeDirectory($basePath . '/nonExisting');
149
    }
150
151
    /**
152
     * Remove directory symlinks1.
153
     *
154
     * @return void
155
     */
156
    public function testRemoveDirectorySymlinks1(): void
157
    {
158
        $dirName = 'remove-directory-symlinks-1';
159
160
        $this->createFileStructure([
161
            $dirName => [
162
                'file' => 'Symlinked file.',
163
                'directory' => [
164
                    'standard-file-1' => 'Standard file 1.',
165
                ],
166
                'symlinks' => [
167
                    'standard-file-2' => 'Standard file 2.',
168
                    'symlinked-file' => ['symlink', '../file'],
169
                    'symlinked-directory' => ['symlink', '../directory'],
170
                ],
171
            ],
172
        ]);
173
174
        $basePath = $this->testFilePath . '/' . $dirName . '/';
175
176
        $this->assertFileExists($basePath . 'file');
177
        $this->assertDirectoryExists($basePath . 'directory');
178
        $this->assertFileExists($basePath . 'directory/standard-file-1');
179
        $this->assertDirectoryExists($basePath . 'symlinks');
180
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
181
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
182
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
183
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
184
185
        FileHelper::removeDirectory($basePath . 'symlinks');
186
187
        $this->assertFileExists($basePath . 'file');
188
        $this->assertDirectoryExists($basePath . 'directory');
189
        $this->assertFileExists($basePath . 'directory/standard-file-1'); // symlinked directory still have it's file
190
        $this->assertDirectoryNotExists($basePath . 'symlinks');
191
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
192
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
193
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
194
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
195
    }
196
197
    /**
198
     * Remove directory symlinks2.
199
     *
200
     * @return void
201
     */
202
    public function testRemoveDirectorySymlinks2(): void
203
    {
204
        $dirName = 'remove-directory-symlinks-2';
205
206
        $this->createFileStructure([
207
            $dirName => [
208
                'file' => 'Symlinked file.',
209
                'directory' => [
210
                    'standard-file-1' => 'Standard file 1.',
211
                ],
212
                'symlinks' => [
213
                    'standard-file-2' => 'Standard file 2.',
214
                    'symlinked-file' => ['symlink', '../file'],
215
                    'symlinked-directory' => ['symlink', '../directory'],
216
                ],
217
            ],
218
        ]);
219
220
        $basePath = $this->testFilePath . '/' . $dirName . '/';
221
222
        $this->assertFileExists($basePath . 'file');
223
        $this->assertDirectoryExists($basePath . 'directory');
224
        $this->assertFileExists($basePath . 'directory/standard-file-1');
225
        $this->assertDirectoryExists($basePath . 'symlinks');
226
        $this->assertFileExists($basePath . 'symlinks/standard-file-2');
227
        $this->assertFileExists($basePath . 'symlinks/symlinked-file');
228
        $this->assertDirectoryExists($basePath . 'symlinks/symlinked-directory');
229
        $this->assertFileExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
230
231
        FileHelper::removeDirectory($basePath . 'symlinks', ['traverseSymlinks' => true]);
232
233
        $this->assertFileExists($basePath . 'file');
234
        $this->assertDirectoryExists($basePath . 'directory');
235
        $this->assertFileNotExists($basePath . 'directory/standard-file-1'); // symlinked directory doesn't have it's file now
236
        $this->assertDirectoryNotExists($basePath . 'symlinks');
237
        $this->assertFileNotExists($basePath . 'symlinks/standard-file-2');
238
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-file');
239
        $this->assertDirectoryNotExists($basePath . 'symlinks/symlinked-directory');
240
        $this->assertFileNotExists($basePath . 'symlinks/symlinked-directory/standard-file-1');
241
    }
242
243
    /**
244
     * Normalize path.
245
     *
246
     * @return void
247
     */
248
    public function testNormalizePath(): void
249
    {
250
        $this->assertEquals('/a/b', FileHelper::normalizePath('//a\\b/'));
251
        $this->assertEquals('/b/c', FileHelper::normalizePath('/a/../b/c'));
252
        $this->assertEquals('/c', FileHelper::normalizePath('/a\\b/../..///c'));
253
        $this->assertEquals('/c', FileHelper::normalizePath('/a/.\\b//../../c'));
254
        $this->assertEquals('c', FileHelper::normalizePath('/a/.\\b/../..//../c'));
255
        $this->assertEquals('../c', FileHelper::normalizePath('//a/.\\b//..//..//../../c'));
256
257
        // relative paths
258
        $this->assertEquals('.', FileHelper::normalizePath('.'));
259
        $this->assertEquals('.', FileHelper::normalizePath('./'));
260
        $this->assertEquals('a', FileHelper::normalizePath('.\\a'));
261
        $this->assertEquals('a/b', FileHelper::normalizePath('./a\\b'));
262
        $this->assertEquals('.', FileHelper::normalizePath('./a\\../'));
263
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a'));
264
        $this->assertEquals('../../a', FileHelper::normalizePath('../..\\a/../a'));
265
        $this->assertEquals('../../b', FileHelper::normalizePath('../..\\a/../b'));
266
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a'));
267
        $this->assertEquals('../a', FileHelper::normalizePath('././..\\a'));
268
        $this->assertEquals('../a', FileHelper::normalizePath('./..\\a/../a'));
269
        $this->assertEquals('../b', FileHelper::normalizePath('./..\\a/../b'));
270
271
        // Windows file system may have paths for network shares that start with two backslashes. These two backslashes
272
        // should not be touched.
273
        // https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx
274
        // https://github.com/yiisoft/yii2/issues/13034
275
        $this->assertEquals('\\\\server/share/path/file', FileHelper::normalizePath('\\\\server\share\path\file'));
276
    }
277
278
    /**
279
     * Creates test files structure.
280
     *
281
     * @param array $items file system objects to be created in format: objectName => objectContent
282
     *                         Arrays specifies directories, other values - files.
283
     * @param string $basePath structure base file path.
284
     *
285
     * @return void
286
     */
287
    private function createFileStructure(array $items, ?string $basePath = null): void
288
    {
289
        $basePath = $basePath ?? $this->testFilePath;
290
291
        if (empty($basePath)) {
292
            $basePath = $this->testFilePath;
293
        }
294
        foreach ($items as $name => $content) {
295
            $itemName = $basePath . DIRECTORY_SEPARATOR . $name;
296
            if (is_array($content)) {
297
                if (isset($content[0], $content[1]) && $content[0] === 'symlink') {
298
                    symlink($basePath . '/' . $content[1], $itemName);
299
                } else {
300
                    mkdir($itemName, 0777, true);
301
                    $this->createFileStructure($content, $itemName);
302
                }
303
            } else {
304
                file_put_contents($itemName, $content);
305
            }
306
        }
307
    }
308
309
    /**
310
     * Copy directory.
311
     *
312
     * @depends testCreateDirectory
313
     *
314
     * @return void
315
     */
316
    public function testCopyDirectory(): void
317
    {
318
        $source = 'test_src_dir';
319
        $files = [
320
            'file1.txt' => 'file 1 content',
321
            'file2.txt' => 'file 2 content',
322
        ];
323
324
        $this->createFileStructure([
325
            $source => $files,
326
        ]);
327
328
        $basePath = $this->testFilePath;
329
        $source = $basePath . '/' . $source;
330
        $destination = $basePath . '/test_dst_dir';
331
332
        FileHelper::copyDirectory($source, $destination);
333
334
        $this->assertFileExists($destination, 'Destination directory does not exist!');
335
336
        foreach ($files as $name => $content) {
337
            $fileName = $destination . '/' . $name;
338
            $this->assertFileExists($fileName);
339
            $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
340
        }
341
    }
342
343
    /**
344
     * Copy directory recursive.
345
     *
346
     * @return void
347
     */
348
    public function testCopyDirectoryRecursive(): void
349
    {
350
        $source = 'test_src_dir_rec';
351
        $structure = [
352
            'directory1' => [
353
                'file1.txt' => 'file 1 content',
354
                'file2.txt' => 'file 2 content',
355
            ],
356
            'directory2' => [
357
                'file3.txt' => 'file 3 content',
358
                'file4.txt' => 'file 4 content',
359
            ],
360
            'file5.txt' => 'file 5 content',
361
        ];
362
363
        $this->createFileStructure([
364
            $source => $structure,
365
        ]);
366
367
        $basePath = $this->testFilePath;
368
        $source = $basePath . '/' . $source;
369
        $destination = $basePath . '/test_dst_dir';
370
371
        FileHelper::copyDirectory($source, $destination);
372
373
        $this->assertFileExists($destination, 'Destination directory does not exist!');
374
375
        $checker = function ($structure, $dstDirName) use (&$checker) {
376
            foreach ($structure as $name => $content) {
377
                if (is_array($content)) {
378
                    $checker($content, $dstDirName . '/' . $name);
379
                } else {
380
                    $fileName = $dstDirName . '/' . $name;
381
                    $this->assertFileExists($fileName);
382
                    $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
383
                }
384
            }
385
        };
386
        $checker($structure, $destination);
387
    }
388
389
    /**
390
     * Copy directory not recursive.
391
     *
392
     * @return void
393
     */
394
    public function testCopyDirectoryNotRecursive(): void
395
    {
396
        $source = 'test_src_dir_not_rec';
397
        $structure = [
398
            'directory1' => [
399
                'file1.txt' => 'file 1 content',
400
                'file2.txt' => 'file 2 content',
401
            ],
402
            'directory2' => [
403
                'file3.txt' => 'file 3 content',
404
                'file4.txt' => 'file 4 content',
405
            ],
406
            'file5.txt' => 'file 5 content',
407
        ];
408
409
        $this->createFileStructure([
410
            $source => $structure,
411
        ]);
412
413
        $basePath = $this->testFilePath;
414
        $source = $basePath . '/' . $source;
415
        $destination = $basePath . '/' . 'test_dst_dir';
416
417
        FileHelper::copyDirectory($source, $destination, ['recursive' => false]);
418
419
        $this->assertFileExists($destination, 'Destination directory does not exist!');
420
421
        foreach ($structure as $name => $content) {
422
            $fileName = $destination . '/' . $name;
423
            if (is_array($content)) {
424
                $this->assertFileNotExists($fileName);
425
            } else {
426
                $this->assertFileExists($fileName);
427
                $this->assertStringEqualsFile($fileName, $content, 'Incorrect file content!');
428
            }
429
        }
430
    }
431
432
    /**
433
     * Copy directory permissions.
434
     *
435
     * @depends testCopyDirectory
436
     *
437
     * @return void
438
     */
439
    public function testCopyDirectoryPermissions(): void
440
    {
441
        if (!$this->isChmodReliable()) {
442
            $this->markTestSkipped('Skipping test since chmod is not reliable in this environment.');
443
        }
444
445
        $isWindows = DIRECTORY_SEPARATOR === '\\';
446
447
        if ($isWindows) {
448
            $this->markTestSkipped('Skipping tests on Windows because fileperms() always return 0777.');
449
        }
450
451
        $source = 'test_src_dir';
452
        $subDirectory = 'test_sub_dir';
453
        $fileName = 'test_file.txt';
454
455
        $this->createFileStructure([
456
            $source => [
457
                $subDirectory => [],
458
                $fileName => 'test file content',
459
            ],
460
        ]);
461
462
        $basePath = $this->testFilePath;
463
        $source = $basePath . '/' . $source;
464
        $destination = $basePath . '/test_dst_dir';
465
        $directoryMode = 0755;
466
        $fileMode = 0755;
467
        $options = [
468
            'dirMode' => $directoryMode,
469
            'fileMode' => $fileMode,
470
        ];
471
472
        FileHelper::copyDirectory($source, $destination, $options);
473
474
        $this->assertFileMode($directoryMode, $destination, 'Destination directory has wrong mode!');
475
        $this->assertFileMode($directoryMode, $destination . '/' . $subDirectory, 'Copied sub directory has wrong mode!');
476
        $this->assertFileMode($fileMode, $destination . '/' . $fileName, 'Copied file has wrong mode!');
477
    }
478
479
    /**
480
     * Copy directory to it self.
481
     *
482
     * @see https://github.com/yiisoft/yii2/issues/10710
483
     *
484
     * @return void
485
     */
486
    public function testCopyDirectoryToItself(): void
487
    {
488
        $directoryName = 'test_dir';
489
490
        $this->createFileStructure([
491
            $directoryName => [],
492
        ]);
493
        $this->expectException(\InvalidArgumentException::class);
494
495
        $directoryName = $this->testFilePath . '/test_dir';
496
497
        FileHelper::copyDirectory($directoryName, $directoryName);
498
    }
499
500
    /**
501
     * Copy directory to sudirectory of it self.
502
     *
503
     * @see https://github.com/yiisoft/yii2/issues/10710
504
     *
505
     * @return void
506
     */
507
    public function testCopyDirToSubdirOfItself(): void
508
    {
509
        $this->createFileStructure([
510
            'data' => [],
511
            'backup' => ['data' => []],
512
        ]);
513
        $this->expectException(\InvalidArgumentException::class);
514
515
        FileHelper::copyDirectory(
516
            $this->testFilePath . '/backup',
517
            $this->testFilePath . '/backup/data'
518
        );
519
    }
520
521
    /**
522
     * Copy directory to another with same name.
523
     *
524
     * @see https://github.com/yiisoft/yii2/issues/10710
525
     *
526
     * @return void
527
     */
528
    public function testCopyDirToAnotherWithSameName(): void
529
    {
530
        $this->createFileStructure([
531
            'data' => [],
532
            'backup' => ['data' => []],
533
        ]);
534
535
        FileHelper::copyDirectory(
536
            $this->testFilePath . '/data',
537
            $this->testFilePath . '/backup/data'
538
        );
539
540
        $this->assertFileExists($this->testFilePath . '/backup/data');
541
    }
542
543
    /**
544
     * Copy directory with same name.
545
     *
546
     * @see https://github.com/yiisoft/yii2/issues/10710
547
     *
548
     * @return void
549
     */
550
    public function testCopyDirWithSameName(): void
551
    {
552
        $this->createFileStructure([
553
            'data' => [],
554
            'data-backup' => [],
555
        ]);
556
557
        FileHelper::copyDirectory(
558
            $this->testFilePath . '/data',
559
            $this->testFilePath . '/data-backup'
560
        );
561
562
        $this->assertTrue(true, 'no error');
563
    }
564
}
565