summaryrefslogtreecommitdiff
path: root/internal/ignore/checker_test.go
blob: 3e3384ca4356df41c0ad6bf201294ae8f4abb50c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
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)
		}
	}
}