assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('feature/anything')); $this->assertTrue($validator->isValid('src/deep/nested/file.php')); } // ------------------------------------------------------------------------- // Pure inclusion — OR semantics (any one match is enough) // ------------------------------------------------------------------------- public function testSingleExactInclusion(): void { $validator = new BuildTrigger(['main']); $this->assertTrue($validator->isValid('main')); $this->assertFalse($validator->isValid('develop')); $this->assertFalse($validator->isValid('main-extra')); } public function testMultipleExactInclusionsOr(): void { $validator = new BuildTrigger(['main', 'develop', 'staging']); $this->assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('develop')); $this->assertTrue($validator->isValid('staging')); $this->assertFalse($validator->isValid('feature/foo')); $this->assertFalse($validator->isValid('production')); } public function testSingleWildcardInclusion(): void { $validator = new BuildTrigger(['feature/*']); $this->assertTrue($validator->isValid('feature/foo')); $this->assertTrue($validator->isValid('feature/bar')); $this->assertFalse($validator->isValid('feature/foo/bar')); // * does not cross / $this->assertFalse($validator->isValid('main')); } public function testWildcardWithDash(): void { $validator = new BuildTrigger(['feature/test-*']); $this->assertTrue($validator->isValid('feature/test-1')); $this->assertTrue($validator->isValid('feature/test-abc')); $this->assertFalse($validator->isValid('feature/other')); $this->assertFalse($validator->isValid('feature/test')); } public function testQuestionMarkWildcard(): void { $validator = new BuildTrigger(['v?.?']); $this->assertTrue($validator->isValid('v1.0')); $this->assertTrue($validator->isValid('v2.5')); $this->assertFalse($validator->isValid('v10.0')); // ? matches exactly one char, not two $this->assertFalse($validator->isValid('v1/0')); // ? does not cross / } public function testQuestionMarkDoesNotCrossSlash(): void { $validator = new BuildTrigger(['feature/?']); $this->assertTrue($validator->isValid('feature/a')); $this->assertTrue($validator->isValid('feature/z')); $this->assertFalse($validator->isValid('feature/ab')); // ? matches only one char $this->assertFalse($validator->isValid('feature/a/b')); // ? does not cross / $this->assertFalse($validator->isValid('feature/')); } public function testQuestionMarkMixedWithStar(): void { $validator = new BuildTrigger(['fix-?.*']); $this->assertTrue($validator->isValid('fix-1.php')); $this->assertTrue($validator->isValid('fix-a.js')); $this->assertFalse($validator->isValid('fix-12.php')); // ? matches only one char $this->assertFalse($validator->isValid('fix-.php')); // ? requires exactly one char } public function testDoubleWildcardAtEnd(): void { $validator = new BuildTrigger(['src/**']); $this->assertTrue($validator->isValid('src/foo.js')); $this->assertTrue($validator->isValid('src/a/b/c.js')); $this->assertTrue($validator->isValid('src/deep/nested/file.php')); $this->assertFalse($validator->isValid('lib/foo.js')); } public function testDoubleWildcardInMiddle(): void { $validator = new BuildTrigger(['a/**/b']); $this->assertTrue($validator->isValid('a/b')); // zero intermediate dirs $this->assertTrue($validator->isValid('a/x/b')); // one $this->assertTrue($validator->isValid('a/x/y/b')); // two $this->assertFalse($validator->isValid('a/b/c')); $this->assertFalse($validator->isValid('x/a/b')); } public function testDoubleWildcardAtStart(): void { $validator = new BuildTrigger(['**/foo']); $this->assertTrue($validator->isValid('foo')); // zero leading dirs $this->assertTrue($validator->isValid('a/foo')); // one $this->assertTrue($validator->isValid('a/b/foo')); // two $this->assertFalse($validator->isValid('foobar')); $this->assertFalse($validator->isValid('a/foobar')); } public function testMixedExactAndWildcardInclusions(): void { $validator = new BuildTrigger(['main', 'feature/*']); $this->assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('feature/foo')); $this->assertFalse($validator->isValid('develop')); $this->assertFalse($validator->isValid('feature/foo/bar')); } // ------------------------------------------------------------------------- // Pure exclusion — AND semantics (must not match any exclusion) // ------------------------------------------------------------------------- public function testSingleExactExclusion(): void { $validator = new BuildTrigger(['!main']); $this->assertFalse($validator->isValid('main')); $this->assertTrue($validator->isValid('develop')); $this->assertTrue($validator->isValid('feature/foo')); } public function testMultipleExactExclusionsAnd(): void { $validator = new BuildTrigger(['!main', '!develop']); $this->assertFalse($validator->isValid('main')); $this->assertFalse($validator->isValid('develop')); $this->assertTrue($validator->isValid('staging')); // neither excluded $this->assertTrue($validator->isValid('feature/foo')); } public function testWildcardExclusion(): void { $validator = new BuildTrigger(['!feature/*']); $this->assertFalse($validator->isValid('feature/foo')); $this->assertFalse($validator->isValid('feature/bar')); $this->assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('hotfix/urgent')); // not matched by feature/* } public function testDoubleWildcardExclusion(): void { $validator = new BuildTrigger(['!src/**']); $this->assertFalse($validator->isValid('src/foo.js')); $this->assertFalse($validator->isValid('src/a/b/c.js')); $this->assertTrue($validator->isValid('lib/foo.js')); $this->assertTrue($validator->isValid('main')); } // ------------------------------------------------------------------------- // Mixed inclusion + exclusion // ------------------------------------------------------------------------- public function testInclusionTakesPrecedenceWhenBothMatch(): void { // feature/abc matches both '!feature/*' (exclusion) and 'feature/abc' (inclusion) // Inclusion is checked first, so it wins $validator = new BuildTrigger(['!feature/*', 'feature/abc']); $this->assertTrue($validator->isValid('feature/abc')); // inclusion wins $this->assertFalse($validator->isValid('feature/xyz')); // only exclusion matches $this->assertFalse($validator->isValid('main')); // no inclusion matches } public function testInclusionWithNoMatchFails(): void { // Inclusions exist but none match — exclusion is irrelevant $validator = new BuildTrigger(['main', '!develop']); $this->assertTrue($validator->isValid('main')); $this->assertFalse($validator->isValid('develop')); // excluded even if inclusion didn't match $this->assertFalse($validator->isValid('staging')); // no inclusion match } public function testExclusionBlocksWhenInclusionDoesNotMatch(): void { $validator = new BuildTrigger(['feature/*', '!hotfix/*']); $this->assertTrue($validator->isValid('feature/foo')); // matches inclusion $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match, also excluded $this->assertFalse($validator->isValid('main')); // no inclusion match } public function testMultipleInclusionsWithSingleExclusion(): void { // feature/wip matches wildcard inclusion feature/* but specific exclusion !feature/wip overrides it $validator = new BuildTrigger(['main', 'develop', 'feature/*', '!feature/wip']); $this->assertTrue($validator->isValid('main')); $this->assertTrue($validator->isValid('develop')); $this->assertTrue($validator->isValid('feature/foo')); $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion overrides wildcard inclusion $this->assertFalse($validator->isValid('hotfix/urgent')); // no inclusion match } public function testSingleInclusionWithMultipleExclusions(): void { // specific exclusions !feature/wip and !feature/experimental override wildcard inclusion feature/** $validator = new BuildTrigger(['feature/**', '!feature/wip', '!feature/experimental']); $this->assertTrue($validator->isValid('feature/foo')); $this->assertTrue($validator->isValid('feature/a/b')); $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins $this->assertFalse($validator->isValid('main')); // no inclusion match } public function testMultipleInclusionsWithMultipleExclusions(): void { // specific exclusions override the wildcard inclusion; specific inclusion 'main' is unaffected $validator = new BuildTrigger(['main', 'feature/**', '!feature/wip', '!feature/experimental']); $this->assertTrue($validator->isValid('main')); // specific inclusion wins regardless $this->assertTrue($validator->isValid('feature/foo')); $this->assertTrue($validator->isValid('feature/a/b')); $this->assertFalse($validator->isValid('feature/wip')); // specific exclusion wins $this->assertFalse($validator->isValid('feature/experimental')); // specific exclusion wins $this->assertFalse($validator->isValid('develop')); // no inclusion match } public function testSpecificInclusionOverridesWildcardExclusion(): void { // Narrow allowlist with a carve-out: exclude all of feature/* except feature/hotfix/critical $validator = new BuildTrigger(['feature/hotfix/critical', '!feature/**']); $this->assertTrue($validator->isValid('feature/hotfix/critical')); // inclusion wins $this->assertFalse($validator->isValid('feature/foo')); // excluded $this->assertFalse($validator->isValid('feature/hotfix/other')); // excluded $this->assertFalse($validator->isValid('main')); // no inclusion match } public function testOnlyExclusionsDefaultToTrueUnlessExcluded(): void { $validator = new BuildTrigger(['!main', '!develop']); $this->assertFalse($validator->isValid('main')); $this->assertFalse($validator->isValid('develop')); $this->assertTrue($validator->isValid('staging')); // passes — no inclusions required $this->assertTrue($validator->isValid('feature/foo')); } }