1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6 "os"
7 "path/filepath"
8 "sync/atomic"
9 "testing"
10 "time"
11)
12
13func TestGraphStampTrusted(t *testing.T) {
14 const sec = int64(time.Second)
15 mt := 1_000 * sec
16 win := int64(racyWindow)
17 exists := func(m int64) graphFileStamp { return graphFileStamp{exists: true, mtime: m} }
18 for _, c := range []struct {
19 name string
20 stamp graphStamp
21 at int64
22 want bool
23 }{
24 {"no graph", graphStamp{}, mt, true},
25 {"stamped inside the window", graphStamp{single: exists(mt)}, mt + win, false},
26 {"stamped just after the window", graphStamp{single: exists(mt)}, mt + win + 1, true},
27 {"newest of two decides", graphStamp{single: exists(mt - 100*sec), chain: exists(mt)}, mt + sec, false},
28 {"both old", graphStamp{single: exists(mt - 100*sec), chain: exists(mt - 50*sec)}, mt, true},
29 {"mtime in the future", graphStamp{chain: exists(mt + 10*sec)}, mt, false},
30 } {
31 if got := c.stamp.trusted(c.at); got != c.want {
32 t.Errorf("%s: trusted = %v, want %v", c.name, got, c.want)
33 }
34 }
35}
36
37func graphFiles(r *gtRepo) []string {
38 info := filepath.Join(r.objects(), "info")
39 return []string{filepath.Join(info, "commit-graph"), filepath.Join(info, "commit-graphs", "commit-graph-chain")}
40}
41
42func ageGraph(t *testing.T, r *gtRepo, age time.Duration) {
43 t.Helper()
44 mt := time.Now().Add(-age)
45 n := 0
46 for _, p := range graphFiles(r) {
47 if _, err := os.Stat(p); err == nil {
48 n++
49 if err := os.Chtimes(p, mt, mt); err != nil {
50 t.Fatal(err)
51 }
52 }
53 }
54 if n == 0 {
55 t.Fatal("no graph file")
56 }
57}
58
59// A graph that is older than the racy window is trusted when its stat data
60// is unchanged, however recently it was loaded: Refresh at the pace of the
61// adapter (every second) must not reload it, and so not remap it.
62func TestGraphRefreshDoesNotReloadAnUnchangedGraph(t *testing.T) {
63 r := newGTRepo(t)
64 r.commit("a", "a\n")
65 r.commit("b", "b\n")
66 r.git("commit-graph", "write", "--reachable", "--split")
67 ageGraph(t, r, time.Hour)
68 repo, err := Open(r.dir)
69 if err != nil {
70 t.Fatal(err)
71 }
72 defer func() { _ = repo.Close() }()
73 first := repo.graph.Load()
74 if first == nil || first.g == nil {
75 t.Fatal("no graph loaded")
76 }
77 for i := range 20 {
78 if err := repo.Refresh(); err != nil {
79 t.Fatal(err)
80 }
81 if repo.graph.Load() != first {
82 t.Fatalf("Refresh %d reloaded an unchanged graph", i)
83 }
84 }
85}
86
87// Refresh once a second on a graph written just now: the graph is reloaded
88// while its modification time is within the racy window of the stamp, and
89// never again after that. A fake clock stands in for the waiting.
90func TestGraphRefreshRacyWindow(t *testing.T) {
91 r := newGTRepo(t)
92 r.commit("a", "a\n")
93 r.git("commit-graph", "write", "--reachable", "--split")
94 start := time.Now()
95 var elapsed atomic.Int64
96 repo, err := Open(r.dir)
97 if err != nil {
98 t.Fatal(err)
99 }
100 defer func() { _ = repo.Close() }()
101 repo.clock = func() time.Time { return start.Add(time.Duration(elapsed.Load())) }
102 if err := repo.Refresh(); err != nil { // stamps the graph at "start"
103 t.Fatal(err)
104 }
105 reloads := 0
106 last := repo.graph.Load()
107 for sec := 1; sec <= 12; sec++ {
108 elapsed.Store(int64(sec) * int64(time.Second))
109 if err := repo.Refresh(); err != nil {
110 t.Fatal(err)
111 }
112 cur := repo.graph.Load()
113 if cur != last {
114 reloads++
115 last = cur
116 if sec > 4 {
117 t.Errorf("Refresh %d s after the write still reloads the graph", sec)
118 }
119 }
120 }
121 if reloads == 0 {
122 t.Error("a graph inside the racy window was trusted")
123 }
124 t.Logf("reloads: %d in 12 refreshes, 1 s apart", reloads)
125}
126
127// A graph rewritten the way git does it (new file renamed over the old) is
128// always seen, trusted or not.
129func TestGraphRefreshSeesARewrite(t *testing.T) {
130 r := newGTRepo(t)
131 r.commit("a", "a\n")
132 r.git("commit-graph", "write", "--reachable")
133 ageGraph(t, r, time.Hour)
134 repo, err := Open(r.dir)
135 if err != nil {
136 t.Fatal(err)
137 }
138 defer func() { _ = repo.Close() }()
139 first := repo.graph.Load()
140 for range 3 {
141 if err := repo.Refresh(); err != nil {
142 t.Fatal(err)
143 }
144 }
145 if repo.graph.Load() != first {
146 t.Fatal("unchanged graph reloaded")
147 }
148 n := first.g.numCommits()
149 r.commit("b", "b\n")
150 r.git("commit-graph", "write", "--reachable")
151 if err := repo.Refresh(); err != nil {
152 t.Fatal(err)
153 }
154 next := repo.graph.Load()
155 if next == first || next.g == nil || next.g.numCommits() != n+1 {
156 t.Fatalf("rewrite not seen: reloaded %v", next != first)
157 }
158}