summaryrefslogtreecommitdiff
path: root/internal/ignore/checker_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/ignore/checker_test.go')
-rw-r--r--internal/ignore/checker_test.go282
1 files changed, 282 insertions, 0 deletions
diff --git a/internal/ignore/checker_test.go b/internal/ignore/checker_test.go
new file mode 100644
index 0000000..3e3384c
--- /dev/null
+++ b/internal/ignore/checker_test.go
@@ -0,0 +1,282 @@
+package ignore
+
+import (
+ "os"
+ "path/filepath"
+ "sync"
+ "testing"
+)
+
+// writeGitignore creates a .gitignore in dir with the given lines.
+func writeGitignore(t *testing.T, dir string, lines ...string) {
+ t.Helper()
+ content := ""
+ for _, l := range lines {
+ content += l + "\n"
+ }
+ if err := os.WriteFile(filepath.Join(dir, ".gitignore"), []byte(content), 0o644); err != nil {
+ t.Fatalf("write .gitignore: %v", err)
+ }
+}
+
+func TestSimpleWildcard(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "app.log")); !ign {
+ t.Error("expected app.log to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "debug.log")); !ign {
+ t.Error("expected debug.log to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "app.go")); ign {
+ t.Error("app.go should not be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "log.txt")); ign {
+ t.Error("log.txt should not be ignored")
+ }
+}
+
+func TestDirectoryPattern(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "build/")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "build", "output.js")); !ign {
+ t.Error("expected build/output.js to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "rebuild", "x")); ign {
+ t.Error("rebuild/x should not be ignored")
+ }
+}
+
+func TestDoubleStarPattern(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "**/temp")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "a", "b", "temp")); !ign {
+ t.Error("expected a/b/temp to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "temp")); !ign {
+ t.Error("expected temp to be ignored")
+ }
+}
+
+func TestNegation(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log", "!important.log")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "debug.log")); !ign {
+ t.Error("expected debug.log to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "important.log")); ign {
+ t.Error("important.log should not be ignored (negated)")
+ }
+}
+
+func TestComments(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "# comment", "*.tmp")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "x.tmp")); !ign {
+ t.Error("expected x.tmp to be ignored")
+ }
+ // A file literally named "# comment" should not be ignored
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "# comment")); ign {
+ t.Error("file named '# comment' should not be ignored")
+ }
+}
+
+func TestExtensionGroups(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.out", "*.html")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "coverage.out")); !ign {
+ t.Error("expected coverage.out to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "main.go")); ign {
+ t.Error("main.go should not be ignored")
+ }
+}
+
+func TestNestedDirs(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "vendor/**")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "vendor", "lib", "x.go")); !ign {
+ t.Error("expected vendor/lib/x.go to be ignored")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "myvendor", "x")); ign {
+ t.Error("myvendor/x should not be ignored")
+ }
+}
+
+func TestExtraPatternsOnly(t *testing.T) {
+ // No gitignore, only extra patterns
+ c := New("", false, []string{"*.min.js", "dist/**"})
+
+ if ign, reason := c.IsIgnored("/project/app.min.js"); !ign {
+ t.Error("expected app.min.js to be ignored")
+ } else if reason != "matched extra ignore pattern" {
+ t.Errorf("unexpected reason: %s", reason)
+ }
+ if ign, _ := c.IsIgnored("/project/dist/bundle.js"); !ign {
+ t.Error("expected dist/bundle.js to be ignored")
+ }
+ if ign, _ := c.IsIgnored("/project/app.js"); ign {
+ t.Error("app.js should not be ignored")
+ }
+}
+
+func TestCombinedGitignoreAndExtra(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log")
+ c := New(dir, true, []string{"*.min.js"})
+
+ // gitignore match
+ if ign, reason := c.IsIgnored(filepath.Join(dir, "app.log")); !ign {
+ t.Error("expected app.log to be ignored")
+ } else if reason != "matched .gitignore pattern" {
+ t.Errorf("unexpected reason: %s", reason)
+ }
+ // extra pattern match
+ if ign, reason := c.IsIgnored(filepath.Join(dir, "app.min.js")); !ign {
+ t.Error("expected app.min.js to be ignored")
+ } else if reason != "matched extra ignore pattern" {
+ t.Errorf("unexpected reason: %s", reason)
+ }
+ // neither match
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "main.go")); ign {
+ t.Error("main.go should not be ignored")
+ }
+}
+
+func TestNilChecker(t *testing.T) {
+ var c *Checker
+ if ign, _ := c.IsIgnored("/some/file.go"); ign {
+ t.Error("nil checker should never ignore")
+ }
+}
+
+func TestEmptyChecker(t *testing.T) {
+ c := New("", false, nil)
+ if ign, _ := c.IsIgnored("/some/file.go"); ign {
+ t.Error("empty checker should never ignore")
+ }
+}
+
+func TestUpdatePatterns(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log")
+ c := New(dir, true, nil)
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "app.log")); !ign {
+ t.Error("expected app.log ignored initially")
+ }
+
+ // Update: disable gitignore, add extra pattern
+ c.Update(false, []string{"*.tmp"})
+
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "app.log")); ign {
+ t.Error("app.log should not be ignored after disabling gitignore")
+ }
+ if ign, _ := c.IsIgnored(filepath.Join(dir, "x.tmp")); !ign {
+ t.Error("expected x.tmp ignored after update")
+ }
+}
+
+func TestThreadSafety(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log")
+ c := New(dir, true, nil)
+
+ var wg sync.WaitGroup
+ // Concurrent reads
+ for i := 0; i < 50; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ c.IsIgnored(filepath.Join(dir, "app.log"))
+ c.IsIgnored(filepath.Join(dir, "main.go"))
+ }()
+ }
+ // Concurrent updates
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ c.Update(true, []string{"*.tmp"})
+ }()
+ }
+ wg.Wait()
+}
+
+func TestNoGitRoot(t *testing.T) {
+ // gitRoot empty but gitignore enabled — should not crash, gitignore has no effect
+ c := New("", true, []string{"*.bak"})
+
+ if ign, _ := c.IsIgnored("/any/file.go"); ign {
+ t.Error("should not ignore .go files")
+ }
+ if ign, _ := c.IsIgnored("/any/file.bak"); !ign {
+ t.Error("extra patterns should still work without git root")
+ }
+}
+
+func TestPathOutsideGitRoot(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir, "*.log")
+ c := New(dir, true, nil)
+
+ // Path outside the git root — relPath returns absolute, gitignore won't match
+ if ign, _ := c.IsIgnored("/completely/elsewhere/app.log"); ign {
+ t.Error("files outside git root should not be matched by gitignore")
+ }
+}
+
+func TestMixedRealGitignore(t *testing.T) {
+ dir := t.TempDir()
+ writeGitignore(t, dir,
+ "# Build outputs",
+ "bin/",
+ "*.exe",
+ "*.dll",
+ "",
+ "# Dependencies",
+ "vendor/**",
+ "",
+ "# IDE",
+ ".idea/",
+ ".vscode/",
+ )
+ c := New(dir, true, nil)
+
+ ignored := []string{
+ filepath.Join(dir, "bin", "app"),
+ filepath.Join(dir, "main.exe"),
+ filepath.Join(dir, "vendor", "lib", "x.go"),
+ filepath.Join(dir, ".idea", "workspace.xml"),
+ }
+ for _, p := range ignored {
+ if ign, _ := c.IsIgnored(p); !ign {
+ t.Errorf("expected %s to be ignored", p)
+ }
+ }
+
+ allowed := []string{
+ filepath.Join(dir, "main.go"),
+ filepath.Join(dir, "internal", "app.go"),
+ filepath.Join(dir, "README.md"),
+ }
+ for _, p := range allowed {
+ if ign, _ := c.IsIgnored(p); ign {
+ t.Errorf("%s should not be ignored", p)
+ }
+ }
+}