packages/deriver/tests/Unit/Finder/PathPatternTest.php

1<?php
2
3declare(strict_types=1);
4
5namespace Tests\Unit\Finder;
6
7use Deriver\Exception\InvalidInputException;
8use Deriver\Finder\PathPattern;
9use PHPUnit\Framework\Attributes\CoversClass;
10use PHPUnit\Framework\Attributes\DataProvider;
11use PHPUnit\Framework\Attributes\Small;
12use PHPUnit\Framework\Attributes\UsesClass;
13use PHPUnit\Framework\TestCase;
14
15#[CoversClass(PathPattern::class)]
16#[UsesClass(InvalidInputException::class)]
17#[Small]
18final class PathPatternTest extends TestCase
19{
20    public function testMatchesAnyAcceptsOneMatchAndRejectsEmptyAlternatives(): void
21    {
22        $patterns = [new PathPattern('src/*.php'), new PathPattern('tests/**/*.php')];
23        self::assertTrue(PathPattern::matchesAny($patterns, 'tests/deep/Test.php'));
24        self::assertFalse(PathPattern::matchesAny($patterns, 'vendor/File.php'));
25        self::assertFalse(PathPattern::matchesAny([], 'src/File.php'));
26    }
27
28    #[DataProvider('providerPatterns')]
29    public function testMatchesCompleteRelativePaths(string $pattern, string $path, bool $expected): void
30    {
31        self::assertSame($expected, (new PathPattern($pattern))->matches($path));
32    }
33
34    /**
35     * @return iterable<string, array{string, string, bool}>
36     */
37    public static function providerPatterns(): iterable
38    {
39        yield 'zero directories' => ['**/*.php', 'a.php', true];
40        yield 'multiple directories' => ['**/*.php', 'a/b/c.php', true];
41        yield 'one component' => ['*.php', 'a/b.php', false];
42        yield 'any extension' => ['src/**', 'src/deep/a.inc', true];
43        yield 'prunable directory' => ['**/vendor/**', 'src/vendor/', true];
44        yield 'root anchored' => ['vendor/**', 'src/vendor/a.php', false];
45        yield 'suffix required' => ['**/*.php', 'a.php.txt', false];
46        yield 'case sensitive' => ['**/*.php', 'A.PHP', false];
47        yield 'literal punctuation' => ['a[1]+(x).php', 'a[1]+(x).php', true];
48        yield 'no regex interpretation' => ['a+.php', 'aa.php', false];
49        yield 'dot prefix and separators' => ['././src\\*.php', 'src/a.php', true];
50        yield 'question does not cross directories' => ['a?b', 'a/b', false];
51    }
52
53    #[DataProvider('providerInvalidPatterns')]
54    public function testRejectsPatternsOutsideTheRoot(string $pattern): void
55    {
56        $this->expectException(InvalidInputException::class);
57        new PathPattern($pattern);
58    }
59
60    /**
61     * @return iterable<array{string}>
62     */
63    public static function providerInvalidPatterns(): iterable
64    {
65        foreach (['', './', '/src/*.php', '../*.php', 'src/../../*', 'php://*', 'C:\\src\\*'] as $pattern) {
66            yield [$pattern];
67        }
68    }
69}
70