v0.1.0 5228465

stress_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package git
4
5import (
6	"bytes"
7	"errors"
8	"fmt"
9	"io"
10	"sync"
11	"sync/atomic"
12	"testing"
13	"time"
14)
15
16// TestRefreshDuringRepack runs readers while git repacks (deleting the packs
17// they read from), prunes and packs refs, and the Repo is refreshed. No read
18// may fail, and every read must return the right content.
19func TestRefreshDuringRepack(t *testing.T) {
20	f := newFx(t)
21	var ids []ObjectID
22	var blobs [][]byte
23	for i := 0; i < 40; i++ {
24		data := append(textLines(500, i%3), []byte(fmt.Sprintf("rev %d\n", i))...)
25		f.write("f.txt", data, 0o644)
26		f.write(fmt.Sprintf("d/%d.txt", i), []byte(fmt.Sprint(i)), 0o644)
27		f.commit(fmt.Sprintf("c%d", i))
28		ids = append(ids, mustID(t, f.git("rev-parse", "HEAD:f.txt")))
29		blobs = append(blobs, data)
30	}
31	f.git("repack", "-adf", "--depth=50", "--window=50")
32	r := f.open(WithDeltaCacheSize(1 << 20))
33
34	stop := make(chan struct{})
35	var wg sync.WaitGroup
36	var reads atomic.Int64
37	var failures atomic.Int64
38	fail := func(format string, a ...any) {
39		failures.Add(1)
40		t.Errorf(format, a...)
41	}
42	for w := 0; w < 6; w++ {
43		wg.Add(1)
44		go func(w int) {
45			defer wg.Done()
46			var buf []byte
47			for i := w; ; i++ {
48				select {
49				case <-stop:
50					return
51				default:
52				}
53				k := i % len(ids)
54				switch i % 4 {
55				case 0:
56					typ, got, err := r.ReadObject(ids[k], buf)
57					if err != nil || typ != TypeBlob || !bytes.Equal(got, blobs[k]) {
58						fail("ReadObject: %v", err)
59						return
60					}
61					buf = got
62				case 1:
63					size, rc, err := r.OpenBlob(ids[k])
64					if err != nil {
65						fail("OpenBlob: %v", err)
66						return
67					}
68					// Read slowly so the repack happens meanwhile.
69					got, err := io.ReadAll(io.LimitReader(rc, 1<<20))
70					_ = rc.Close()
71					if err != nil || size != int64(len(blobs[k])) || !bytes.Equal(got, blobs[k]) {
72						fail("blob read: %v", err)
73						return
74					}
75				case 2:
76					if _, n, err := r.ObjectHeader(ids[k]); err != nil || n != int64(len(blobs[k])) {
77						fail("ObjectHeader: %v", err)
78						return
79					}
80				case 3:
81					if _, err := r.ResolvePrefix(ids[k].String()[:12]); err != nil && !errors.Is(err, ErrAmbiguous) {
82						fail("ResolvePrefix: %v", err)
83						return
84					}
85					for ref, err := range r.Refs("refs/") {
86						if err != nil {
87							fail("Refs: %v", err)
88							return
89						}
90						if ref.Name == "refs/heads/main" && ref.ID.IsZero() {
91							fail("main is zero")
92							return
93						}
94					}
95				}
96				reads.Add(1)
97			}
98		}(w)
99	}
100	// Refresher.
101	wg.Add(1)
102	go func() {
103		defer wg.Done()
104		for {
105			select {
106			case <-stop:
107				return
108			default:
109			}
110			if err := r.Refresh(); err != nil {
111				fail("Refresh: %v", err)
112				return
113			}
114			time.Sleep(time.Millisecond)
115		}
116	}()
117
118	// The writer: repack -ad (deletes the packs), new commits, pack-refs, prune.
119	for round := 0; round < 12 && failures.Load() == 0; round++ {
120		f.write("w.txt", []byte(fmt.Sprint(round)), 0o644)
121		f.commit(fmt.Sprintf("w%d", round))
122		f.git("tag", fmt.Sprintf("t%d", round))
123		switch round % 3 {
124		case 0:
125			f.git("repack", "-ad")
126		case 1:
127			f.git("repack", "-adf", "--depth=20", "--window=20")
128			f.git("pack-refs", "--all")
129		case 2:
130			f.git("gc", "--prune=now", "-q")
131		}
132	}
133	close(stop)
134	wg.Wait()
135	if failures.Load() != 0 {
136		t.FailNow()
137	}
138	if err := r.Refresh(); err != nil {
139		t.Fatal(err)
140	}
141	t.Logf("%d reads during 12 repack rounds", reads.Load())
142	compareRefs(t, r, f.root())
143	compareObjects(t, r, f.root())
144	// Packs that were replaced are unmapped once no reader needs them.
145	if n := len(r.snap.Load().packs); n != 1 {
146		t.Errorf("%d packs left in the snapshot", n)
147	}
148}
149
150// A reader blocked on an old pack keeps it mapped until it is closed.
151func TestOldPackStaysMappedWhileInUse(t *testing.T) {
152	f := newFx(t)
153	big := textLines(100000, 1)
154	f.write("big", big, 0o644)
155	f.commit("one")
156	f.git("repack", "-adf")
157	r := f.open()
158	id := mustID(t, f.git("rev-parse", "HEAD:big"))
159	_, rc, err := r.OpenBlob(id)
160	if err != nil {
161		t.Fatal(err)
162	}
163	old := r.snap.Load().packs[0]
164	buf := make([]byte, 1000)
165	if _, err := io.ReadFull(rc, buf); err != nil {
166		t.Fatal(err)
167	}
168	f.write("more", []byte("more"), 0o644)
169	f.commit("two")
170	f.git("repack", "-adf") // deletes the pack that rc reads from
171	if err := r.Refresh(); err != nil {
172		t.Fatal(err)
173	}
174	if r.snap.Load().packs[0] == old {
175		t.Fatal("the pack list did not change")
176	}
177	if old.data == nil || old.refs.Load() != 1 {
178		t.Fatalf("old pack released while in use (refs=%d)", old.refs.Load())
179	}
180	rest, err := io.ReadAll(rc)
181	if err != nil || !bytes.Equal(append(buf, rest...), big) {
182		t.Fatalf("read after repack: %v", err)
183	}
184	_ = rc.Close()
185	if old.data != nil {
186		t.Fatal("old pack still mapped after the last reader closed")
187	}
188}
189
190// A ref-delta cycle must not loop forever.
191func TestRefDeltaCycle(t *testing.T) {
192	// Two ref-deltas that are each other's base.
193	idA, idB := ObjectID{0x10}, ObjectID{0x20}
194	body := append(varint(4), varint(4)...)
195	body = append(body, 4, 'a', 'b', 'c', 'd')
196	ea := entryBytes(objRefDelta, len(body), idB[:], body)
197	eb := entryBytes(objRefDelta, len(body), idA[:], body)
198	var offs []uint64
199	pk, idx := buildPack([][]byte{ea, eb}, []ObjectID{idA, idB}, &offs)
200	g := t.TempDir()
201	writeFile(t, g+"/HEAD", "ref: refs/heads/main\n")
202	writeFile(t, g+"/refs/.keep", "")
203	name := fmt.Sprintf("pack-%x", pk[len(pk)-20:])
204	writeFile(t, g+"/objects/pack/"+name+".pack", string(pk))
205	writeFile(t, g+"/objects/pack/"+name+".idx", string(idx))
206	r, err := Open(g)
207	if err != nil {
208		t.Fatal(err)
209	}
210	defer func() { _ = r.Close() }()
211	if _, _, err := r.ReadObject(idA, nil); !errors.Is(err, ErrCorrupt) {
212		t.Fatalf("ReadObject = %v, want ErrCorrupt", err)
213	}
214	if _, _, err := r.ObjectHeader(idA); !errors.Is(err, ErrCorrupt) {
215		t.Fatalf("ObjectHeader = %v, want ErrCorrupt", err)
216	}
217}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 2

↑↓ moveenter openesc close