133728e barerepo 1mo
1
package gitx
133728e barerepo 1mo
3
import (
133728e barerepo 1mo
4
"context"
133728e barerepo 1mo
5
"os"
133728e barerepo 1mo
6
"os/exec"
133728e barerepo 1mo
7
"path/filepath"
133728e barerepo 1mo
8
"strconv"
133728e barerepo 1mo
9
"strings"
133728e barerepo 1mo
10
"testing"
133728e barerepo 1mo
11
"time"
133728e barerepo 1mo
14
// Keeping cat-file open rests on git rereading the pack directory whenever it answers "missing".
133728e barerepo 1mo
15
func TestAKeptReaderSeesAnObjectThatArrivedInAPack(t *testing.T) {
133728e barerepo 1mo
16
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
17
t.Skip("git is not installed")
133728e barerepo 1mo
19
ctx := context.Background()
133728e barerepo 1mo
20
dir := t.TempDir()
133728e barerepo 1mo
21
mustGit(t, dir, "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
23
// A referenced object, then gc, is how an object comes to exist only inside a pack.
133728e barerepo 1mo
24
sha := writeObject(t, dir, "only in a pack\n")
133728e barerepo 1mo
25
mustGit(t, dir, "prune-packed")
133728e barerepo 1mo
26
if err := os.Remove(filepath.Join(dir, "objects", sha[:2], sha[2:])); err != nil {
133728e barerepo 1mo
27
t.Fatal(err)
133728e barerepo 1mo
29
// Asked once while it is absent, so the reader has already answered "missing" for this id.
133728e barerepo 1mo
30
if got, err := Batch(ctx, dir, []string{sha}); err != nil || got[sha] != nil {
133728e barerepo 1mo
31
t.Fatalf("the object was readable before it was written: %v %v", got[sha], err)
133728e barerepo 1mo
33
sha = writeObject(t, dir, "only in a pack\n")
133728e barerepo 1mo
34
tree := mktree(t, dir, "100644 blob "+sha+"\tfile\n")
133728e barerepo 1mo
35
commit := mustGit(t, dir, "commit-tree", tree, "-m", "one")
133728e barerepo 1mo
36
mustGit(t, dir, "update-ref", "refs/heads/master", commit)
133728e barerepo 1mo
37
mustGit(t, dir, "gc", "--prune=now", "-q")
133728e barerepo 1mo
38
if _, err := os.Stat(filepath.Join(dir, "objects", sha[:2], sha[2:])); err == nil {
133728e barerepo 1mo
39
t.Skip("git kept the loose copy, so this repository proves nothing")
133728e barerepo 1mo
42
got, err := Batch(ctx, dir, []string{sha})
133728e barerepo 1mo
43
if err != nil {
133728e barerepo 1mo
44
t.Fatal(err)
133728e barerepo 1mo
46
if got[sha] == nil {
133728e barerepo 1mo
47
t.Fatal("a kept reader could not see an object that git packed after it started")
133728e barerepo 1mo
49
if got[sha].Body != "only in a pack\n" {
133728e barerepo 1mo
50
t.Errorf("the object read as %q", got[sha].Body)
133728e barerepo 1mo
54
// An object that is genuinely absent must stay absent, and must not cost a process every time.
133728e barerepo 1mo
55
func TestAMissingObjectIsMissing(t *testing.T) {
133728e barerepo 1mo
56
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
57
t.Skip("git is not installed")
133728e barerepo 1mo
59
ctx := context.Background()
133728e barerepo 1mo
60
dir := t.TempDir()
133728e barerepo 1mo
61
mustGit(t, dir, "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
63
absent := strings.Repeat("0", 40)
133728e barerepo 1mo
64
for range 3 {
133728e barerepo 1mo
65
got, err := Batch(ctx, dir, []string{absent})
133728e barerepo 1mo
66
if err != nil {
133728e barerepo 1mo
67
t.Fatal(err)
133728e barerepo 1mo
69
if got[absent] != nil {
133728e barerepo 1mo
70
t.Fatal("an object that was never written came back")
133728e barerepo 1mo
75
// The pool answers in the right order, because one stream out of step corrupts every later read.
133728e barerepo 1mo
76
func TestManyObjectsComeBackInOrder(t *testing.T) {
133728e barerepo 1mo
77
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
78
t.Skip("git is not installed")
133728e barerepo 1mo
80
ctx := context.Background()
133728e barerepo 1mo
81
dir := t.TempDir()
133728e barerepo 1mo
82
mustGit(t, dir, "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
84
bodies := map[string]string{}
133728e barerepo 1mo
85
var specs []string
133728e barerepo 1mo
86
for i := range 200 {
133728e barerepo 1mo
87
body := strings.Repeat("line\n", i+1)
133728e barerepo 1mo
88
sha := writeObject(t, dir, body)
133728e barerepo 1mo
89
bodies[sha] = body
133728e barerepo 1mo
90
specs = append(specs, sha)
133728e barerepo 1mo
92
// A missing spec in the middle must not shift the answers after it.
133728e barerepo 1mo
93
specs = append(specs[:100], append([]string{strings.Repeat("0", 40)}, specs[100:]...)...)
133728e barerepo 1mo
95
got, err := Batch(ctx, dir, specs)
133728e barerepo 1mo
96
if err != nil {
133728e barerepo 1mo
97
t.Fatal(err)
133728e barerepo 1mo
99
for sha, want := range bodies {
133728e barerepo 1mo
100
if got[sha] == nil {
133728e barerepo 1mo
101
t.Fatalf("%s did not come back", sha)
133728e barerepo 1mo
102
}
133728e barerepo 1mo
103
if got[sha].Body != want {
133728e barerepo 1mo
104
t.Fatalf("%s came back as %d bytes, wanted %d", sha, len(got[sha].Body), len(want))
133728e barerepo 1mo
105
}
133728e barerepo 1mo
106
}
133728e barerepo 1mo
109
// Idle readers are closed, or a barerepo with many repositories holds a process for each one.
133728e barerepo 1mo
110
func TestIdleReadersAreClosed(t *testing.T) {
133728e barerepo 1mo
111
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
112
t.Skip("git is not installed")
133728e barerepo 1mo
113
}
133728e barerepo 1mo
114
// The pool is shared with every other test in this package, so start from none.
133728e barerepo 1mo
115
CloseIdleReaders(0)
133728e barerepo 1mo
116
ctx := context.Background()
133728e barerepo 1mo
117
dir := t.TempDir()
133728e barerepo 1mo
118
mustGit(t, dir, "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
119
if _, err := Batch(ctx, dir, []string{"HEAD"}); err != nil {
133728e barerepo 1mo
120
t.Fatal(err)
133728e barerepo 1mo
121
}
133728e barerepo 1mo
122
if n := CloseIdleReaders(time.Hour); n != 0 {
133728e barerepo 1mo
123
t.Errorf("a reader idle for no time was closed")
133728e barerepo 1mo
124
}
133728e barerepo 1mo
125
if n := CloseIdleReaders(0); n != 1 {
133728e barerepo 1mo
126
t.Errorf("closed %d readers, wanted the one in the pool", n)
133728e barerepo 1mo
127
}
133728e barerepo 1mo
130
func mustGit(t *testing.T, dir string, args ...string) string {
133728e barerepo 1mo
131
t.Helper()
133728e barerepo 1mo
132
cmd := exec.Command("git", args...)
133728e barerepo 1mo
133
cmd.Dir = dir
133728e barerepo 1mo
134
out, err := cmd.CombinedOutput()
133728e barerepo 1mo
135
if err != nil {
133728e barerepo 1mo
136
t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out)
133728e barerepo 1mo
137
}
133728e barerepo 1mo
138
return strings.TrimSpace(string(out))
133728e barerepo 1mo
141
func writeObject(t *testing.T, dir, body string) string {
133728e barerepo 1mo
142
t.Helper()
133728e barerepo 1mo
143
cmd := exec.Command("git", "hash-object", "-w", "--stdin")
133728e barerepo 1mo
144
cmd.Dir = dir
133728e barerepo 1mo
145
cmd.Stdin = strings.NewReader(body)
133728e barerepo 1mo
146
out, err := cmd.Output()
133728e barerepo 1mo
147
if err != nil {
133728e barerepo 1mo
148
t.Fatal(err)
133728e barerepo 1mo
149
}
133728e barerepo 1mo
150
return strings.TrimSpace(string(out))
133728e barerepo 1mo
153
func mktree(t *testing.T, dir, entries string) string {
133728e barerepo 1mo
154
t.Helper()
133728e barerepo 1mo
155
cmd := exec.Command("git", "mktree")
133728e barerepo 1mo
156
cmd.Dir = dir
133728e barerepo 1mo
157
cmd.Stdin = strings.NewReader(entries)
133728e barerepo 1mo
158
out, err := cmd.Output()
133728e barerepo 1mo
159
if err != nil {
133728e barerepo 1mo
160
t.Fatal(err)
133728e barerepo 1mo
161
}
133728e barerepo 1mo
162
return strings.TrimSpace(string(out))
133728e barerepo 1mo
165
// A batch larger than the pipe deadlocks if the specs are written before the answers are read.
133728e barerepo 1mo
166
func TestABatchLargerThanThePipe(t *testing.T) {
133728e barerepo 1mo
167
if _, err := exec.LookPath("git"); err != nil {
133728e barerepo 1mo
168
t.Skip("git is not installed")
133728e barerepo 1mo
169
}
133728e barerepo 1mo
170
ctx := context.Background()
133728e barerepo 1mo
171
dir := t.TempDir()
133728e barerepo 1mo
172
mustGit(t, dir, "init", "-q", "--bare", "-b", "master")
133728e barerepo 1mo
174
// Four thousand ids is 164kb of input and megabytes of answer, both well past a 64kb pipe.
133728e barerepo 1mo
175
bodies := make([]string, 4000)
133728e barerepo 1mo
176
for i := range bodies {
133728e barerepo 1mo
177
bodies[i] = strings.Repeat("x", 2048) + strconv.Itoa(i) + "\n"
133728e barerepo 1mo
178
}
133728e barerepo 1mo
179
specs := writeObjects(t, dir, bodies)
133728e barerepo 1mo
181
done := make(chan map[string]*Object, 1)
133728e barerepo 1mo
182
go func() {
133728e barerepo 1mo
183
got, err := Batch(ctx, dir, specs)
133728e barerepo 1mo
184
if err != nil {
133728e barerepo 1mo
185
t.Error(err)
133728e barerepo 1mo
186
}
133728e barerepo 1mo
187
done <- got
133728e barerepo 1mo
188
}()
133728e barerepo 1mo
189
select {
133728e barerepo 1mo
190
case got := <-done:
133728e barerepo 1mo
191
for i, sha := range specs {
133728e barerepo 1mo
192
if got[sha] == nil || got[sha].Body != bodies[i] {
133728e barerepo 1mo
193
t.Fatalf("%s came back wrong", sha)
133728e barerepo 1mo
194
}
133728e barerepo 1mo
195
}
133728e barerepo 1mo
196
case <-time.After(30 * time.Second):
133728e barerepo 1mo
197
t.Fatal("a batch of 4000 objects never finished, which is the write and read deadlock")
133728e barerepo 1mo
198
}
133728e barerepo 1mo
201
// writeObjects writes many blobs in one git process, so the setup does not dominate the test.
133728e barerepo 1mo
202
func writeObjects(t *testing.T, dir string, bodies []string) []string {
133728e barerepo 1mo
203
t.Helper()
133728e barerepo 1mo
204
files := t.TempDir()
133728e barerepo 1mo
205
var paths strings.Builder
133728e barerepo 1mo
206
for i, body := range bodies {
133728e barerepo 1mo
207
p := filepath.Join(files, strconv.Itoa(i))
133728e barerepo 1mo
208
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
133728e barerepo 1mo
209
t.Fatal(err)
133728e barerepo 1mo
210
}
133728e barerepo 1mo
211
paths.WriteString(p + "\n")
133728e barerepo 1mo
212
}
133728e barerepo 1mo
213
cmd := exec.Command("git", "hash-object", "-w", "--stdin-paths")
133728e barerepo 1mo
214
cmd.Dir = dir
133728e barerepo 1mo
215
cmd.Stdin = strings.NewReader(paths.String())
133728e barerepo 1mo
216
out, err := cmd.Output()
133728e barerepo 1mo
217
if err != nil {
133728e barerepo 1mo
218
t.Fatal(err)
133728e barerepo 1mo
219
}