diff --git a/tests/unit/Vcs/Validator/BuildTriggerTest.php b/tests/unit/Vcs/Validator/BuildTriggerTest.php index b26033b20b..66aa94abed 100644 --- a/tests/unit/Vcs/Validator/BuildTriggerTest.php +++ b/tests/unit/Vcs/Validator/BuildTriggerTest.php @@ -258,4 +258,209 @@ class BuildTriggerTest extends TestCase $this->assertTrue($validator->isValid('staging')); // passes — no inclusions required $this->assertTrue($validator->isValid('feature/foo')); } + + // ------------------------------------------------------------------------- + // Standalone wildcards + // ------------------------------------------------------------------------- + + public function testStarAloneMatchesSingleSegmentOnly(): void + { + // * → ^[^/]*$ — matches any single segment; the [^/]* guard prevents crossing / + $validator = new BuildTrigger(['*']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('develop')); + $this->assertFalse($validator->isValid('feature/foo')); // * cannot cross / + $this->assertFalse($validator->isValid('a/b/c')); + } + + public function testDoubleStarAloneMatchesEverything(): void + { + // ** alone → ^.*$ — matches any string including paths with separators + $validator = new BuildTrigger(['**']); + $this->assertTrue($validator->isValid('main')); + $this->assertTrue($validator->isValid('feature/foo')); + $this->assertTrue($validator->isValid('src/a/b/c/d/file.php')); + } + + // ------------------------------------------------------------------------- + // Extension patterns — * scope vs. ** scope + // ------------------------------------------------------------------------- + + public function testStarDotExtMatchesRootLevelOnly(): void + { + // *.php → ^[^/]*\.php$ — root-level only; * cannot cross / + $validator = new BuildTrigger(['*.php']); + $this->assertTrue($validator->isValid('Foo.php')); + $this->assertTrue($validator->isValid('index.php')); + $this->assertFalse($validator->isValid('src/Foo.php')); // * does not cross / + $this->assertFalse($validator->isValid('a/b/Foo.php')); + $this->assertFalse($validator->isValid('Foo.js')); + } + + public function testDoubleStarSlashExtMatchesAnyDepth(): void + { + // **/*.php → ^(?:.+/)?[^/]*\.php$ — (?:.+/)? is optional so root also matches + $validator = new BuildTrigger(['**/*.php']); + $this->assertTrue($validator->isValid('Foo.php')); // root — zero leading dirs + $this->assertTrue($validator->isValid('src/Foo.php')); + $this->assertTrue($validator->isValid('src/components/Foo.php')); + $this->assertTrue($validator->isValid('a/b/c/d/Foo.php')); // four levels deep + $this->assertFalse($validator->isValid('Foo.js')); + $this->assertFalse($validator->isValid('src/Foo.js')); + } + + public function testDirPrefixDoubleStarExtPattern(): void + { + // src/**/*.php → ^src/(?:.+/)?[^/]*\.php$ — scoped to src/ at any depth + $validator = new BuildTrigger(['src/**/*.php']); + $this->assertTrue($validator->isValid('src/Foo.php')); + $this->assertTrue($validator->isValid('src/components/Foo.php')); + $this->assertTrue($validator->isValid('src/a/b/c/Foo.php')); + $this->assertFalse($validator->isValid('Foo.php')); // outside src/ + $this->assertFalse($validator->isValid('lib/Foo.php')); + $this->assertFalse($validator->isValid('src/Foo.js')); + } + + // ------------------------------------------------------------------------- + // Dots as literal characters + // ------------------------------------------------------------------------- + + public function testDotsInPatternAreLiteral(): void + { + // preg_quote escapes . to \. — dots are never regex wildcards + $validator = new BuildTrigger(['release-1.0.0']); + $this->assertTrue($validator->isValid('release-1.0.0')); + $this->assertFalse($validator->isValid('release-1X0Y0')); // X/Y must not satisfy a literal dot + $this->assertFalse($validator->isValid('release-1.0.0-hotfix')); // extra suffix rejected by $ anchor + } + + public function testVersionWildcardBranchPattern(): void + { + // v*.*.* → ^v[^/]*\.[^/]*\.[^/]*$ — dots literal; [^/]* is greedy and can consume dots, + // so v1.2.3.4 also matches (first [^/]* eats "1.2") — documents current behavior + $validator = new BuildTrigger(['v*.*.*']); + $this->assertTrue($validator->isValid('v1.2.3')); + $this->assertTrue($validator->isValid('v10.20.30')); + $this->assertTrue($validator->isValid('v1.2.3.4')); // greedy match — first [^/]* eats "1.2" + $this->assertFalse($validator->isValid('v1.2')); // only two dot-segments; third \.[^/]* fails + $this->assertFalse($validator->isValid('1.2.3')); // missing leading v + $this->assertFalse($validator->isValid('v1/2/3')); // [^/]* cannot cross / + } + + public function testDottedFilenamePattern(): void + { + // *.test.js → ^[^/]*\.test\.js$ — both dots literal; * stays in root segment + $validator = new BuildTrigger(['*.test.js']); + $this->assertTrue($validator->isValid('Button.test.js')); + $this->assertTrue($validator->isValid('App.test.js')); + $this->assertFalse($validator->isValid('ButtonXtestYjs')); // dots must be literal + $this->assertFalse($validator->isValid('src/Button.test.js')); // * does not cross / + $this->assertFalse($validator->isValid('Button.test.ts')); + } + + // ------------------------------------------------------------------------- + // Prefix wildcard + // ------------------------------------------------------------------------- + + public function testPrefixWildcardBranchPattern(): void + { + // main* → ^main[^/]*$ — suffix wildcard; [^/]* cannot cross / + $validator = new BuildTrigger(['main*']); + $this->assertTrue($validator->isValid('main')); // zero trailing chars + $this->assertTrue($validator->isValid('main-extra')); + $this->assertTrue($validator->isValid('mainline')); + $this->assertFalse($validator->isValid('main/branch')); // [^/]* cannot cross / + $this->assertFalse($validator->isValid('develop')); + } + + // ------------------------------------------------------------------------- + // Deep nesting + // ------------------------------------------------------------------------- + + public function testDoubleWildcardInMiddleDeepNesting(): void + { + // a/**/b → ^a/(?:.+/)?b$ — .+ inside (?:.+/)? matches any chars including /, + // so it handles arbitrarily many intermediate directories + $validator = new BuildTrigger(['a/**/b']); + $this->assertTrue($validator->isValid('a/x/y/z/b')); // three intermediate dirs + $this->assertTrue($validator->isValid('a/p/q/r/s/b')); // four intermediate dirs + $this->assertTrue($validator->isValid('a/1/2/3/4/5/b')); // five intermediate dirs + $this->assertFalse($validator->isValid('a/x/y/z/b/extra')); // trailing segment rejected + $this->assertFalse($validator->isValid('prefix/a/x/b')); // leading segment rejected + } + + public function testDoubleWildcardAtStartDeepNesting(): void + { + // **/README.md → ^(?:.+/)?README\.md$ — matches at any depth; $ anchor prevents suffixes + $validator = new BuildTrigger(['**/README.md']); + $this->assertTrue($validator->isValid('README.md')); // zero leading dirs + $this->assertTrue($validator->isValid('docs/README.md')); + $this->assertTrue($validator->isValid('a/b/c/d/README.md')); // four levels deep + $this->assertTrue($validator->isValid('x/y/z/w/v/README.md')); // five levels deep + $this->assertFalse($validator->isValid('a/b/c/README.md.bak')); // $ anchor — no suffix + $this->assertFalse($validator->isValid('a/b/c/README.md/extra')); // trailing segment rejected + } + + // ------------------------------------------------------------------------- + // Real-world path patterns + // ------------------------------------------------------------------------- + + public function testGeneratedFilesAnywhereExclusion(): void + { + // !**/generated/** → ^(?:.+/)?generated/.*$ — excludes generated/ at any depth + $validator = new BuildTrigger(['!**/generated/**']); + $this->assertFalse($validator->isValid('generated/Foo.php')); // root generated/ + $this->assertFalse($validator->isValid('src/generated/Foo.php')); + $this->assertFalse($validator->isValid('src/api/generated/Bar.php')); // deep generated/ + $this->assertFalse($validator->isValid('generated/sub/deep/File.php')); // deep inside root generated/ + $this->assertTrue($validator->isValid('src/components/Button.php')); // not under generated/ + $this->assertTrue($validator->isValid('main')); + } + + public function testMultipleExtensionInclusions(): void + { + // OR semantics: any PHP or JS file triggers; other extensions do not + $validator = new BuildTrigger(['**/*.php', '**/*.js']); + $this->assertTrue($validator->isValid('index.php')); + $this->assertTrue($validator->isValid('src/App.php')); + $this->assertTrue($validator->isValid('index.js')); + $this->assertTrue($validator->isValid('src/components/App.js')); + $this->assertFalse($validator->isValid('styles.css')); + $this->assertFalse($validator->isValid('src/styles.css')); + $this->assertFalse($validator->isValid('README.md')); + } + + // ------------------------------------------------------------------------- + // Named-prefix single-level branch + // ------------------------------------------------------------------------- + + public function testReleaseBranchPattern(): void + { + // release/* → ^release/[^/]*$ — one level only; * stops at the next / + $validator = new BuildTrigger(['release/*']); + $this->assertTrue($validator->isValid('release/1.0')); + $this->assertTrue($validator->isValid('release/hotfix')); + $this->assertTrue($validator->isValid('release/2024-01-15')); + $this->assertFalse($validator->isValid('release/1.0/patch')); // * stops at / + $this->assertFalse($validator->isValid('release')); // missing the /* segment + $this->assertFalse($validator->isValid('main')); + } + + // ------------------------------------------------------------------------- + // Case sensitivity + // ------------------------------------------------------------------------- + + public function testPatternMatchingIsCaseSensitive(): void + { + // preg_match is used without the i flag — matching is always case-sensitive + $branchValidator = new BuildTrigger(['main']); + $this->assertTrue($branchValidator->isValid('main')); + $this->assertFalse($branchValidator->isValid('Main')); + $this->assertFalse($branchValidator->isValid('MAIN')); + + $wildcardValidator = new BuildTrigger(['feature/*']); + $this->assertTrue($wildcardValidator->isValid('feature/foo')); + $this->assertFalse($wildcardValidator->isValid('Feature/foo')); + $this->assertFalse($wildcardValidator->isValid('FEATURE/foo')); + } }