v0.7.2 ab6686c

history_evict_inplace_test.go RawHistoryFolder

1package vt
2
3import (
4	"fmt"
5	"testing"
6
7	renderer "github.com/bnema/vev-vt/core"
8	"github.com/stretchr/testify/require"
9)
10
11// Eviction advances an uncaptured head wrapper in place. A view or snapshot
12// taken between evictions must keep its rows, accounting, and IDs.
13func TestHistoryInPlaceEvictionKeepsCapturedViewsStable(t *testing.T) {
14	tests := []struct {
15		name    string
16		capture func(*History) func() ([]string, int, uint64, RowID)
17	}{
18		{name: "view", capture: func(h *History) func() ([]string, int, uint64, RowID) {
19			v := h.View()
20			return func() ([]string, int, uint64, RowID) {
21				return rangeTextsNoT(v), v.Len(), v.LogicalBytes(), v.RowID(0)
22			}
23		}},
24		{name: "snapshot", capture: func(h *History) func() ([]string, int, uint64, RowID) {
25			s := h.SnapshotView()
26			return func() ([]string, int, uint64, RowID) {
27				var texts []string
28				for i := range s.ChunkCount() {
29					c := s.Chunk(i)
30					for r := range c.len() {
31						texts = append(texts, rowText(c.row(r)))
32					}
33				}
34				texts = append(texts, rangeTextsNoT(s.Tail())...)
35				var first RowID
36				if s.ChunkCount() > 0 {
37					first = s.Chunk(0).rowIDs[0]
38				}
39				return texts, s.Len(), s.LogicalBytes(), first
40			}
41		}},
42	}
43	for _, tt := range tests {
44		t.Run(tt.name, func(t *testing.T) {
45			h := NewHistory(HistoryConfig{MaxRows: 12, MaxBytes: 1 << 20, ChunkRows: 4})
46			add := func(i int) {
47				require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%3+1), LineBound{End: 4}))
48			}
49			for i := range 14 { // two evictions: the head wrapper is now private
50				add(i)
51			}
52			read := tt.capture(h)
53			texts, rows, bytes, first := read()
54			for i := 14; i < 30; i++ { // evicts through the captured head and beyond
55				add(i)
56			}
57			gotTexts, gotRows, gotBytes, gotFirst := read()
58			require.Equal(t, texts, gotTexts)
59			require.Equal(t, rows, gotRows)
60			require.Equal(t, bytes, gotBytes)
61			require.Equal(t, first, gotFirst)
62			require.Equal(t, 12, h.Len())
63			require.Equal(t, "r018", rangeTextsNoT(h.View())[0])
64		})
65	}
66}
67
68// In-place eviction keeps logical accounting identical to wrapper replacement.
69func TestHistoryInPlaceEvictionAccountingMatchesFreshBuild(t *testing.T) {
70	h := NewHistory(HistoryConfig{MaxRows: 10, MaxBytes: 1 << 20, ChunkRows: 4})
71	for i := range 37 {
72		require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%5+1), LineBound{End: 4}))
73	}
74	view := h.View()
75	for i := range view.ChunkCount() {
76		require.NoError(t, view.Chunk(i).CheckInvariants())
77	}
78	var sum uint64
79	for i := range view.ChunkCount() {
80		sum += historyChunkLogicalBytes(view.Chunk(i))
81	}
82	require.Equal(t, sum, h.LogicalBytes())
83}
84
85// An idle pass releases spare mutable-tail capacity without changing content,
86// and later appends regrow it.
87func TestHistoryCompressIdleTrimsTailCapacity(t *testing.T) {
88	for _, tailRows := range []int{0, 3} {
89		t.Run(fmt.Sprintf("tail-%d", tailRows), func(t *testing.T) {
90			h := NewHistory(HistoryConfig{MaxRows: 64, MaxBytes: 1 << 20, ChunkRows: 8})
91			for i := range 16 + tailRows {
92				require.NoError(t, h.Append(styledRow(fmt.Sprintf("r%03d", i), i%4+1), LineBound{End: 4}))
93			}
94			before := rangeTextsNoT(h.View())
95			bytes := h.LogicalBytes()
96			_, err := h.CompressIdle(1)
97			require.NoError(t, err)
98			require.Equal(t, len(h.tailCells), cap(h.tailCells))
99			require.Equal(t, before, rangeTextsNoT(h.View()))
100			require.Equal(t, bytes, h.LogicalBytes())
101			require.NoError(t, h.Append(styledRow("next", 1), LineBound{End: 4}))
102			got := rangeTextsNoT(h.View())
103			require.Equal(t, append(before, "next"), got)
104		})
105	}
106}
107
108// RangeText must agree with Range on every row, including wide pairs, zero
109// runes, evicted prefixes and cold pages.
110func TestHistoryRangeTextMatchesRange(t *testing.T) {
111	build := func(cold bool) HistoryView {
112		h := NewHistory(HistoryConfig{MaxRows: 20, MaxBytes: 1 << 20, ChunkRows: 4})
113		for i := range 27 {
114			row := historyRow(fmt.Sprintf("r%02d", i))
115			row = append(row,
116				renderer.Cell{Rune: '界'}, renderer.Cell{Continuation: true}, // wide pair
117				renderer.Cell{}, // zero rune reads as a space
118				renderer.Cell{Rune: 'x'})
119			require.NoError(t, h.Append(row, LineBound{End: len(row)}))
120		}
121		if cold {
122			for range 2 {
123				_, err := h.CompressIdle(100)
124				require.NoError(t, err)
125			}
126		}
127		return h.View()
128	}
129	for _, cold := range []bool{false, true} {
130		t.Run(fmt.Sprintf("cold-%v", cold), func(t *testing.T) {
131			view := build(cold)
132			type rowText struct {
133				runes   []rune
134				columns []int
135				width   int
136			}
137			var want []rowText
138			require.NoError(t, view.Range(func(row []renderer.Cell) bool {
139				w := rowText{width: len(row)}
140				for x, c := range row {
141					if c.Continuation {
142						continue
143					}
144					r := c.Rune
145					if r == 0 {
146						r = ' '
147					}
148					w.runes = append(w.runes, r)
149					w.columns = append(w.columns, x)
150				}
151				want = append(want, w)
152				return true
153			}))
154			var got []rowText
155			require.NoError(t, view.RangeText(func(i int, runes []rune, columns []int, width int) bool {
156				require.Equal(t, len(got), i)
157				got = append(got, rowText{append([]rune(nil), runes...), append([]int(nil), columns...), width})
158				return true
159			}))
160			require.Equal(t, want, got)
161			require.Len(t, got, 20)
162		})
163	}
164}
165
166func TestStyleSet(t *testing.T) {
167	style := func(i int) renderer.Style { return renderer.Style{Foreground: i % 256, Bold: i >= 256} }
168	tests := []struct {
169		name  string
170		input []int
171		want  int
172	}{
173		{name: "empty", want: 0},
174		{name: "duplicates", input: []int{1, 1, 2, 2, 1}, want: 2},
175		{name: "inline capacity", input: seqInts(styleSetInline), want: styleSetInline},
176		{name: "spills to map", input: append(seqInts(styleSetInline+5), 0, 1, 2), want: styleSetInline + 5},
177		{name: "large", input: seqInts(400), want: 400},
178	}
179	var s styleSet
180	for _, tt := range tests {
181		t.Run(tt.name, func(t *testing.T) {
182			s.reset()
183			want := map[renderer.Style]struct{}{}
184			for _, i := range tt.input {
185				s.add(style(i))
186				want[style(i)] = struct{}{}
187			}
188			require.Equal(t, tt.want, s.len())
189			got := map[renderer.Style]struct{}{}
190			for st := range s.all() {
191				_, dup := got[st]
192				require.False(t, dup, "style yielded twice")
193				got[st] = struct{}{}
194			}
195			require.Equal(t, want, got)
196		})
197	}
198}
199
200func seqInts(n int) []int {
201	out := make([]int, n)
202	for i := range out {
203		out[i] = i
204	}
205	return out
206}
207
208func rangeTextsNoT(v HistoryView) []string {
209	var out []string
210	_ = v.Range(func(row []renderer.Cell) bool {
211		out = append(out, rowText(row))
212		return true
213	})
214	return out
215}

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 2

Tags 13

↑↓ moveenter openesc close