package gitx import ( "context" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" ) // Keeping cat-file open rests on git rereading the pack directory whenever it answers "missing". func TestAKeptReaderSeesAnObjectThatArrivedInAPack(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := t.TempDir() mustGit(t, dir, "init", "-q", "--bare", "-b", "master") // A referenced object, then gc, is how an object comes to exist only inside a pack. sha := writeObject(t, dir, "only in a pack\n") mustGit(t, dir, "prune-packed") if err := os.Remove(filepath.Join(dir, "objects", sha[:2], sha[2:])); err != nil { t.Fatal(err) } // Asked once while it is absent, so the reader has already answered "missing" for this id. if got, err := Batch(ctx, dir, []string{sha}); err != nil || got[sha] != nil { t.Fatalf("the object was readable before it was written: %v %v", got[sha], err) } sha = writeObject(t, dir, "only in a pack\n") tree := mktree(t, dir, "100644 blob "+sha+"\tfile\n") commit := mustGit(t, dir, "commit-tree", tree, "-m", "one") mustGit(t, dir, "update-ref", "refs/heads/master", commit) mustGit(t, dir, "gc", "--prune=now", "-q") if _, err := os.Stat(filepath.Join(dir, "objects", sha[:2], sha[2:])); err == nil { t.Skip("git kept the loose copy, so this repository proves nothing") } got, err := Batch(ctx, dir, []string{sha}) if err != nil { t.Fatal(err) } if got[sha] == nil { t.Fatal("a kept reader could not see an object that git packed after it started") } if got[sha].Body != "only in a pack\n" { t.Errorf("the object read as %q", got[sha].Body) } } // An object that is genuinely absent must stay absent, and must not cost a process every time. func TestAMissingObjectIsMissing(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := t.TempDir() mustGit(t, dir, "init", "-q", "--bare", "-b", "master") absent := strings.Repeat("0", 40) for range 3 { got, err := Batch(ctx, dir, []string{absent}) if err != nil { t.Fatal(err) } if got[absent] != nil { t.Fatal("an object that was never written came back") } } } // The pool answers in the right order, because one stream out of step corrupts every later read. func TestManyObjectsComeBackInOrder(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := t.TempDir() mustGit(t, dir, "init", "-q", "--bare", "-b", "master") bodies := map[string]string{} var specs []string for i := range 200 { body := strings.Repeat("line\n", i+1) sha := writeObject(t, dir, body) bodies[sha] = body specs = append(specs, sha) } // A missing spec in the middle must not shift the answers after it. specs = append(specs[:100], append([]string{strings.Repeat("0", 40)}, specs[100:]...)...) got, err := Batch(ctx, dir, specs) if err != nil { t.Fatal(err) } for sha, want := range bodies { if got[sha] == nil { t.Fatalf("%s did not come back", sha) } if got[sha].Body != want { t.Fatalf("%s came back as %d bytes, wanted %d", sha, len(got[sha].Body), len(want)) } } } // Idle readers are closed, or a barerepo with many repositories holds a process for each one. func TestIdleReadersAreClosed(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } // The pool is shared with every other test in this package, so start from none. CloseIdleReaders(0) ctx := context.Background() dir := t.TempDir() mustGit(t, dir, "init", "-q", "--bare", "-b", "master") if _, err := Batch(ctx, dir, []string{"HEAD"}); err != nil { t.Fatal(err) } if n := CloseIdleReaders(time.Hour); n != 0 { t.Errorf("a reader idle for no time was closed") } if n := CloseIdleReaders(0); n != 1 { t.Errorf("closed %d readers, wanted the one in the pool", n) } } func mustGit(t *testing.T, dir string, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %s: %v\n%s", strings.Join(args, " "), err, out) } return strings.TrimSpace(string(out)) } func writeObject(t *testing.T, dir, body string) string { t.Helper() cmd := exec.Command("git", "hash-object", "-w", "--stdin") cmd.Dir = dir cmd.Stdin = strings.NewReader(body) out, err := cmd.Output() if err != nil { t.Fatal(err) } return strings.TrimSpace(string(out)) } func mktree(t *testing.T, dir, entries string) string { t.Helper() cmd := exec.Command("git", "mktree") cmd.Dir = dir cmd.Stdin = strings.NewReader(entries) out, err := cmd.Output() if err != nil { t.Fatal(err) } return strings.TrimSpace(string(out)) } // A batch larger than the pipe deadlocks if the specs are written before the answers are read. func TestABatchLargerThanThePipe(t *testing.T) { if _, err := exec.LookPath("git"); err != nil { t.Skip("git is not installed") } ctx := context.Background() dir := t.TempDir() mustGit(t, dir, "init", "-q", "--bare", "-b", "master") // Four thousand ids is 164kb of input and megabytes of answer, both well past a 64kb pipe. bodies := make([]string, 4000) for i := range bodies { bodies[i] = strings.Repeat("x", 2048) + strconv.Itoa(i) + "\n" } specs := writeObjects(t, dir, bodies) done := make(chan map[string]*Object, 1) go func() { got, err := Batch(ctx, dir, specs) if err != nil { t.Error(err) } done <- got }() select { case got := <-done: for i, sha := range specs { if got[sha] == nil || got[sha].Body != bodies[i] { t.Fatalf("%s came back wrong", sha) } } case <-time.After(30 * time.Second): t.Fatal("a batch of 4000 objects never finished, which is the write and read deadlock") } } // writeObjects writes many blobs in one git process, so the setup does not dominate the test. func writeObjects(t *testing.T, dir string, bodies []string) []string { t.Helper() files := t.TempDir() var paths strings.Builder for i, body := range bodies { p := filepath.Join(files, strconv.Itoa(i)) if err := os.WriteFile(p, []byte(body), 0o600); err != nil { t.Fatal(err) } paths.WriteString(p + "\n") } cmd := exec.Command("git", "hash-object", "-w", "--stdin-paths") cmd.Dir = dir cmd.Stdin = strings.NewReader(paths.String()) out, err := cmd.Output() if err != nil { t.Fatal(err) } return strings.Fields(string(out)) }