1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "bytes"
7 "context"
8 "fmt"
9 "os/exec"
10 "slices"
11 "strings"
12 "sync"
13 "sync/atomic"
14 "testing"
15 "time"
16)
17
18// gitQuiet runs git in the fixture without touching the testing.T, so it can
19// be used from goroutines.
20func (w *wfix) gitQuiet(stdin string, args ...string) error {
21 cmd := exec.Command("git", args...)
22 cmd.Dir = w.dir
23 cmd.Env = append(gitEnv(), "GIT_DIR="+w.dir)
24 cmd.Stdin = strings.NewReader(stdin)
25 var errb bytes.Buffer
26 cmd.Stderr = &errb
27 if err := cmd.Run(); err != nil {
28 return fmt.Errorf("git %s: %v: %s", strings.Join(args, " "), err, errb.String())
29 }
30 return nil
31}
32
33// TestWalkConcurrentRefresh runs walks and LastCommits while the repository
34// changes under them: Refresh, new commits arriving by fast-import (a push:
35// new pack, new graph layer), rewrites of the commit-graph (with and without
36// Bloom filters, split and single, or removed) and repacks. Every walk starts
37// from a fixed tip and must give the same answer as before the changes.
38func TestWalkConcurrentRefresh(t *testing.T) {
39 requireGit(t)
40 cs := randomHistory(21, 120)
41 w := buildWFix(t, cs)
42 w.setGraph(wgraphs[4])
43 tipIdx := len(cs) - 1
44 tip := mustID(t, w.ids[tipIdx])
45 paths := []string{"", "a", "d", "d/e/z", "q/r"}
46 want := map[string][]string{}
47 for _, p := range paths {
48 want[p] = w.gitLog(tipIdx, 0, 0, p)
49 }
50 names := []string{"a", "b", "d", "m", "q", "nope"}
51 wantLast := w.lastOracle(tipIdx, "", names)
52
53 r, err := Open(w.dir)
54 if err != nil {
55 t.Fatal(err)
56 }
57 defer func() { _ = r.Close() }()
58
59 dur := 3 * time.Second
60 if testing.Short() {
61 dur = time.Second
62 }
63 deadline := time.Now().Add(dur)
64 var stop atomic.Bool
65 var wg sync.WaitGroup
66 errc := make(chan error, 64)
67 fail := func(err error) {
68 select {
69 case errc <- err:
70 default:
71 }
72 stop.Store(true)
73 }
74
75 for g := 0; g < 6; g++ {
76 wg.Add(1)
77 go func(g int) {
78 defer wg.Done()
79 for i := 0; !stop.Load(); i++ {
80 p := paths[(g+i)%len(paths)]
81 var got []string
82 for c, err := range r.Walk(context.Background(), tip, WalkOptions{Path: p, IDOnly: i%2 == 0}) {
83 if err != nil {
84 fail(fmt.Errorf("walk %q: %w", p, err))
85 return
86 }
87 got = append(got, c.ID.String())
88 }
89 if !slices.Equal(got, want[p]) {
90 fail(fmt.Errorf("walk %q: wrong history (%d commits, want %d)", p, len(got), len(want[p])))
91 return
92 }
93 if i%3 == 0 {
94 res, err := r.LastCommits(context.Background(), tip, "", names)
95 if err != nil {
96 fail(fmt.Errorf("LastCommits: %w", err))
97 return
98 }
99 if !slices.Equal(res, wantLast) {
100 fail(fmt.Errorf("LastCommits: wrong answer"))
101 return
102 }
103 }
104 }
105 }(g)
106 }
107
108 wg.Add(1)
109 go func() { // Refresh as fast as possible
110 defer wg.Done()
111 for !stop.Load() {
112 if err := r.Refresh(); err != nil {
113 fail(fmt.Errorf("Refresh: %w", err))
114 return
115 }
116 time.Sleep(time.Millisecond)
117 }
118 }()
119
120 wg.Add(1)
121 go func() { // the writer
122 defer wg.Done()
123 defer stop.Store(true)
124 var in bytes.Buffer
125 for _, id := range w.ids {
126 fmt.Fprintln(&in, id)
127 }
128 for i := 0; time.Now().Before(deadline) && !stop.Load(); i++ {
129 var err error
130 switch i % 6 {
131 case 0: // a push: a new commit on top of the tip, a new pack
132 err = w.gitQuiet(fmt.Sprintf("commit refs/heads/push%d\ncommitter T <t@example.com> %d +0000\ndata 1\nx\nfrom %s\n\n", i, 1_700_000_000+i, w.ids[tipIdx]), "fast-import", "--quiet")
133 case 1:
134 err = w.gitQuiet(in.String(), "commit-graph", "write", "--stdin-commits", "--split", "--changed-paths")
135 case 2:
136 err = w.gitQuiet("", "repack", "-adq")
137 case 3:
138 err = w.gitQuiet(in.String(), "commit-graph", "write", "--stdin-commits", "--no-changed-paths")
139 case 4:
140 err = w.gitQuiet(in.String(), "commit-graph", "write", "--stdin-commits", "--split=replace", "--changed-paths")
141 case 5:
142 err = w.gitQuiet("", "commit-graph", "write", "--reachable", "--split=no-merge", "--changed-paths")
143 }
144 if err != nil {
145 fail(err)
146 return
147 }
148 }
149 }()
150 wg.Wait()
151 close(errc)
152 for err := range errc {
153 t.Error(err)
154 }
155}