fork of https://github.com/sourcegraph/zoekt
1package gitindex
2
3import (
4 "bytes"
5 "io"
6 "os"
7 "os/exec"
8 "path/filepath"
9 "strings"
10 "testing"
11
12 "github.com/go-git/go-git/v5/plumbing"
13)
14
15// createTestRepo creates a git repo with various test files and returns
16// the repo path and a map of filename -> blob SHA.
17func createTestRepo(t *testing.T) (string, map[string]plumbing.Hash) {
18 t.Helper()
19 dir := t.TempDir()
20 repoDir := filepath.Join(dir, "repo")
21
22 runGit(t, dir, "init", "-b", "main", "repo")
23
24 files := []struct {
25 name string
26 content []byte
27 }{
28 {name: "hello.txt", content: []byte("hello world\n")},
29 {name: "empty.txt"},
30 {name: "binary.bin", content: []byte{0x00, 0x01, 0x02, '\n', 'h', 'e', 'l', 'l', 'o', '\n', 'w', 'o', 'r', 'l', 'd', '\n', 0x03, 0x04}},
31 {name: "large.bin", content: bytes.Repeat([]byte("0123456789abcdef"), 4096)},
32 }
33
34 for _, file := range files {
35 if err := os.WriteFile(filepath.Join(repoDir, file.name), file.content, 0o644); err != nil {
36 t.Fatalf("WriteFile(%q): %v", file.name, err)
37 }
38 }
39
40 runGit(t, repoDir, "add", ".")
41 runGit(t, repoDir, "commit", "-m", "initial")
42
43 // Get blob SHAs for each file.
44 blobs := map[string]plumbing.Hash{}
45 for _, file := range files {
46 name := file.name
47 out, err := exec.Command("git", "-C", repoDir, "rev-parse", "HEAD:"+name).Output()
48 if err != nil {
49 t.Fatalf("rev-parse %s: %v", name, err)
50 }
51 sha := strings.TrimSpace(string(out))
52 blobs[name] = plumbing.NewHash(sha)
53 }
54
55 return repoDir, blobs
56}
57
58func TestCatfileReader(t *testing.T) {
59 t.Parallel()
60
61 repoDir, blobs := createTestRepo(t)
62
63 ids := []plumbing.Hash{
64 blobs["hello.txt"],
65 blobs["empty.txt"],
66 blobs["binary.bin"],
67 blobs["large.bin"],
68 }
69
70 cr, err := newCatfileReader(repoDir, ids, catfileReaderOptions{})
71 if err != nil {
72 t.Fatalf("newCatfileReader: %v", err)
73 }
74 defer cr.Close()
75
76 // hello.txt
77 size, missing, excluded, err := cr.Next()
78 if err != nil {
79 t.Fatalf("Next hello.txt: %v", err)
80 }
81 if missing || excluded {
82 t.Fatal("hello.txt unexpectedly missing")
83 }
84 if size != 12 {
85 t.Errorf("hello.txt size = %d, want 12", size)
86 }
87 content := make([]byte, size)
88 if _, err := io.ReadFull(cr, content); err != nil {
89 t.Fatalf("ReadFull hello.txt: %v", err)
90 }
91 if string(content) != "hello world\n" {
92 t.Errorf("hello.txt content = %q", content)
93 }
94
95 // empty.txt
96 size, missing, excluded, err = cr.Next()
97 if err != nil {
98 t.Fatalf("Next empty.txt: %v", err)
99 }
100 if missing || excluded {
101 t.Fatal("empty.txt unexpectedly missing")
102 }
103 if size != 0 {
104 t.Errorf("empty.txt size = %d, want 0", size)
105 }
106
107 // binary.bin — read content and verify binary data survives.
108 size, missing, excluded, err = cr.Next()
109 if err != nil {
110 t.Fatalf("Next binary.bin: %v", err)
111 }
112 if missing || excluded {
113 t.Fatal("binary.bin unexpectedly missing")
114 }
115 binContent := make([]byte, size)
116 if _, err := io.ReadFull(cr, binContent); err != nil {
117 t.Fatalf("ReadFull binary.bin: %v", err)
118 }
119 if binContent[0] != 0x00 || binContent[3] != '\n' {
120 t.Errorf("binary.bin unexpected leading bytes: %x", binContent[:5])
121 }
122
123 // large.bin
124 size, missing, excluded, err = cr.Next()
125 if err != nil {
126 t.Fatalf("Next large.bin: %v", err)
127 }
128 if missing || excluded {
129 t.Fatal("large.bin unexpectedly missing")
130 }
131 if size != 64*1024 {
132 t.Errorf("large.bin size = %d, want %d", size, 64*1024)
133 }
134 largeContent := make([]byte, size)
135 if _, err := io.ReadFull(cr, largeContent); err != nil {
136 t.Fatalf("ReadFull large.bin: %v", err)
137 }
138
139 // EOF after all entries.
140 _, _, _, err = cr.Next()
141 if err != io.EOF {
142 t.Errorf("expected io.EOF after last entry, got %v", err)
143 }
144}
145
146func TestCatfileReader_Skip(t *testing.T) {
147 t.Parallel()
148
149 repoDir, blobs := createTestRepo(t)
150
151 ids := []plumbing.Hash{
152 blobs["hello.txt"],
153 blobs["large.bin"],
154 blobs["binary.bin"],
155 }
156
157 cr, err := newCatfileReader(repoDir, ids, catfileReaderOptions{})
158 if err != nil {
159 t.Fatalf("newCatfileReader: %v", err)
160 }
161 defer cr.Close()
162
163 // Skip hello.txt by calling Next again without reading.
164 _, _, _, err = cr.Next()
165 if err != nil {
166 t.Fatalf("Next hello.txt: %v", err)
167 }
168
169 // Skip large.bin too.
170 size, _, _, err := cr.Next()
171 if err != nil {
172 t.Fatalf("Next large.bin: %v", err)
173 }
174 if size != 64*1024 {
175 t.Errorf("large.bin size = %d, want %d", size, 64*1024)
176 }
177
178 // Read binary.bin after skipping two entries.
179 size, _, _, err = cr.Next()
180 if err != nil {
181 t.Fatalf("Next binary.bin: %v", err)
182 }
183 content := make([]byte, size)
184 if _, err := io.ReadFull(cr, content); err != nil {
185 t.Fatalf("ReadFull binary.bin: %v", err)
186 }
187 if content[0] != 0x00 {
188 t.Errorf("binary.bin first byte = %x, want 0x00", content[0])
189 }
190}
191
192func TestCatfileReader_Missing(t *testing.T) {
193 t.Parallel()
194
195 repoDir, blobs := createTestRepo(t)
196
197 fakeHash := plumbing.NewHash("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef")
198 ids := []plumbing.Hash{
199 blobs["hello.txt"],
200 fakeHash,
201 blobs["empty.txt"],
202 }
203
204 cr, err := newCatfileReader(repoDir, ids, catfileReaderOptions{})
205 if err != nil {
206 t.Fatalf("newCatfileReader: %v", err)
207 }
208 defer cr.Close()
209
210 // hello.txt — read normally.
211 size, missing, excluded, err := cr.Next()
212 if err != nil || missing || excluded {
213 t.Fatalf("Next hello.txt: err=%v missing=%v excluded=%v", err, missing, excluded)
214 }
215 content := make([]byte, size)
216 if _, err := io.ReadFull(cr, content); err != nil {
217 t.Fatalf("ReadFull hello.txt: %v", err)
218 }
219 if string(content) != "hello world\n" {
220 t.Errorf("hello.txt = %q", content)
221 }
222
223 // fakeHash — missing.
224 _, missing, excluded, err = cr.Next()
225 if err != nil {
226 t.Fatalf("Next fakeHash: %v", err)
227 }
228 if excluded {
229 t.Error("expected fakeHash to be missing, not excluded")
230 }
231 if !missing {
232 t.Error("expected fakeHash to be missing")
233 }
234
235 // empty.txt — still works after missing entry.
236 size, missing, excluded, err = cr.Next()
237 if err != nil || missing || excluded {
238 t.Fatalf("Next empty.txt: err=%v missing=%v excluded=%v", err, missing, excluded)
239 }
240 if size != 0 {
241 t.Errorf("empty.txt size = %d, want 0", size)
242 }
243}
244
245func TestCatfileReader_Excluded(t *testing.T) {
246 t.Parallel()
247
248 repoDir, blobs := createTestRepo(t)
249
250 ids := []plumbing.Hash{
251 blobs["large.bin"],
252 blobs["hello.txt"],
253 }
254
255 cr, err := newCatfileReader(repoDir, ids, catfileReaderOptions{filterSpec: "blob:limit=1k"})
256 if err != nil {
257 t.Fatalf("newCatfileReader: %v", err)
258 }
259 defer cr.Close()
260
261 _, missing, excluded, err := cr.Next()
262 if err != nil {
263 t.Fatalf("Next large.bin: %v", err)
264 }
265 if missing {
266 t.Fatal("large.bin unexpectedly missing")
267 }
268 if !excluded {
269 t.Fatal("large.bin unexpectedly included")
270 }
271
272 size, missing, excluded, err := cr.Next()
273 if err != nil {
274 t.Fatalf("Next hello.txt: %v", err)
275 }
276 if missing || excluded {
277 t.Fatalf("hello.txt unexpectedly skipped: missing=%v excluded=%v", missing, excluded)
278 }
279 content := make([]byte, size)
280 if _, err := io.ReadFull(cr, content); err != nil {
281 t.Fatalf("ReadFull hello.txt: %v", err)
282 }
283 if string(content) != "hello world\n" {
284 t.Errorf("hello.txt = %q", content)
285 }
286}