~bigbes/sr-ht-dolt

ref: bd2d8a3680e62716440ea0165d2271d79f9d526e sr-ht-dolt/beads/prefixes_test.go -rw-r--r-- 11.0 KiB
bd2d8a36 — Eugene Blikh doltsrht: serve /query and the api-meta.json beside it 3 days ago
                                                                                
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package beads

import (
	"errors"
	"fmt"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	"sourcecraft.dev/bigbes/sr-ht-dolt/browse"
)

// --- fixtures ----------------------------------------------------------------

// prefixTracker is a beads database whose config names an issue prefix, with the
// settings a real tracker carries beside it — the projection has to pick one row
// out of the table rather than read the first one.
func prefixTracker(head, prefix string) *fakeReadyDB {
	return &fakeReadyDB{
		branches: []browse.Branch{{Name: "main", Head: head}},
		tables:   beadsTables(),
		rows: map[string]*browse.RowPage{
			"config": {
				Columns: []string{"key", "value"},
				Rows: [][]string{
					{"compact_tier2_days", "30"},
					{"kv.memory.handoff", "not a prefix"},
					{"issue_prefix", prefix},
				},
				Total: 3,
			},
		},
	}
}

// prefixFixture is two trackers, "global" and "artifacts", as the instance has
// them: one cross-project tracker and one per-project.
func prefixFixture() (*readyInstance, []ReadyDatabase) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: prefixTracker("h-global", "global"),
		2: prefixTracker("h-artifacts", "artifacts"),
	}}
	return in, []ReadyDatabase{
		{ID: 1, OwnerName: "bigbes", Name: "beads-global"},
		{ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"},
	}
}

// --- tests -------------------------------------------------------------------

// The index: one prefix per database, read out of config, and nothing claimed
// for a prefix no database named.
func TestPrefixesAcrossIndexesTheDatabasesItIsHanded(t *testing.T) {
	in, dbs := prefixFixture()

	index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)

	assert.Equal(t, []string{"artifacts", "global"}, index.Prefixes())
	d, ok := index.Lookup("global")
	require.True(t, ok)
	assert.Equal(t, "bigbes/beads-global", d.Slug())
	d, ok = index.Lookup("artifacts")
	require.True(t, ok)
	assert.Equal(t, "bigbes/sourcehut-artifacts", d.Slug())
	_, ok = index.Lookup("nosuch")
	assert.False(t, ok, "a prefix no database named is not in the index")
	assert.Empty(t, index.Failed)

	// Every session opened is closed again: this is the read pattern the
	// per-request browse discipline is bounded against.
	for id, f := range in.dbs {
		assert.Equal(t, f.opens, f.closes, "database %d: %d opens, %d closes", id, f.opens, f.closes)
	}
}

// The head-hash gate: a second build with unmoved heads reads no rows. Asserted
// by counting reads on the fake, never by timing.
func TestPrefixesAcrossHeadHashGateSkipsTheRead(t *testing.T) {
	in, dbs := prefixFixture()
	cache := &PrefixCache{}

	first := PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow)
	require.Len(t, first.Prefixes(), 2)
	global := in.dbs[1]
	require.Greater(t, global.rowReads, 0, "the first build must read config")
	reads := global.rowReads

	second := PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(30*time.Second))

	assert.Equal(t, reads, global.rowReads, "a second build with an unmoved head must read no rows")
	assert.Equal(t, first.Prefixes(), second.Prefixes(), "and it is the same answer")
	// The session is still opened and its branches listed — that is what the gate
	// is gated on, and it is the cheap half.
	assert.Equal(t, 2, global.opens)
	assert.Equal(t, 2, global.closes)
}

// A head that moved is a prefix that may have been renamed: read again, even
// well inside the TTL.
func TestPrefixesAcrossMovedHeadForcesAReread(t *testing.T) {
	in, dbs := prefixFixture()
	cache := &PrefixCache{}
	global := in.dbs[1]

	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow)
	reads := global.rowReads

	global.branches = []browse.Branch{{Name: "main", Head: "h-global-2"}}
	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(time.Second))

	assert.Greater(t, global.rowReads, reads, "a moved head must be re-read")
	assert.Equal(t, 1, in.dbs[2].rowReads, "the sibling's head did not move")
}

// The TTL is the same one /ready is bounded by, because it is the same cache.
func TestPrefixesAcrossTTLExpiryForcesAReread(t *testing.T) {
	in, dbs := prefixFixture()
	cache := &PrefixCache{}
	global := in.dbs[1]

	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow)
	reads := global.rowReads

	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(ReadyCacheTTL-time.Nanosecond))
	assert.Equal(t, reads, global.rowReads, "inside the TTL the projection stands")

	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow.Add(ReadyCacheTTL))
	assert.Greater(t, global.rowReads, reads, "at the TTL the projection is re-read")
}

// The ceiling: at most ReadyMaxDatabases stores opened per build, and the first
// database listed — the caller puts the page's own database there — is inside
// it.
func TestPrefixesAcrossCeiling(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{}}
	var dbs []ReadyDatabase
	for i := 1; i <= ReadyMaxDatabases+3; i++ {
		in.dbs[i] = prefixTracker(fmt.Sprintf("h%d", i), fmt.Sprintf("p%02d", i))
		dbs = append(dbs, ReadyDatabase{ID: i, OwnerName: "alice", Name: fmt.Sprintf("db%02d", i)})
	}

	index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)

	assert.Len(t, index.Prefixes(), ReadyMaxDatabases)
	_, ok := index.Lookup("p01")
	assert.True(t, ok, "the first database listed is never dropped by the ceiling")
	for i := ReadyMaxDatabases + 1; i <= ReadyMaxDatabases+3; i++ {
		assert.Zero(t, in.dbs[i].opens, "database %d is past the ceiling", i)
	}
}

// Two databases claiming one prefix: neither is linked. Linking to one of two
// candidates would be a guess rendered as a fact, and the id reads as text
// either way.
func TestPrefixesAcrossDropsAnAmbiguousPrefix(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: prefixTracker("h1", "shared"),
		2: prefixTracker("h2", "shared"),
		3: prefixTracker("h3", "mine"),
	}}
	dbs := []ReadyDatabase{
		{ID: 1, OwnerName: "alice", Name: "one"},
		{ID: 2, OwnerName: "bob", Name: "two"},
		{ID: 3, OwnerName: "carol", Name: "three"},
	}

	index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)

	_, ok := index.Lookup("shared")
	assert.False(t, ok, "a prefix two databases claim belongs to neither")
	assert.Equal(t, []string{"mine"}, index.Prefixes(), "the unambiguous ones are unaffected")
}

// A database that names no prefix — no config table at all, or a config without
// the row — is simply not in the index.
func TestPrefixesAcrossSkipsADatabaseWithNoPrefix(t *testing.T) {
	noConfig := &fakeReadyDB{branches: []browse.Branch{{Name: "main", Head: "h1"}}, tables: beadsTables()}
	noRow := &fakeReadyDB{
		branches: []browse.Branch{{Name: "main", Head: "h2"}},
		tables:   beadsTables(),
		rows: map[string]*browse.RowPage{
			"config": {Columns: []string{"key", "value"}, Rows: [][]string{{"compact_tier2_days", "30"}}, Total: 1},
		},
	}
	empty := &fakeReadyDB{} // a store with no branches: never pushed to
	in := &readyInstance{dbs: map[int]*fakeReadyDB{1: noConfig, 2: noRow, 3: empty}}
	dbs := []ReadyDatabase{
		{ID: 1, OwnerName: "alice", Name: "plain"},
		{ID: 2, OwnerName: "alice", Name: "settings-only"},
		{ID: 3, OwnerName: "alice", Name: "fresh"},
	}

	index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)

	assert.Empty(t, index.Prefixes())
	assert.Empty(t, index.Failed, "naming no prefix is not a failure")
	assert.Zero(t, empty.rowReads, "a store with no branches is not read")
}

// A database that cannot be opened, or whose config cannot be read, costs the
// ids it owns their link and nothing else.
func TestPrefixesAcrossFailingDatabaseCostsItselfOnly(t *testing.T) {
	in, dbs := prefixFixture()
	in.dbs[1].openErr = errors.New("browse: open store: no such file or directory")

	index := PrefixesAcross(t.Context(), dbs, in.open, &PrefixCache{}, readyNow)

	require.Len(t, index.Failed, 1)
	assert.Equal(t, "bigbes/beads-global", index.Failed[0].Database.Slug())
	assert.ErrorContains(t, index.Failed[0].Err, "no such file")
	assert.Equal(t, []string{"artifacts"}, index.Prefixes())

	// A store that opens but cannot list its branches fails the same way, and
	// still closes its session.
	in2, dbs2 := prefixFixture()
	in2.dbs[1].branchesErr = errors.New("browse: list branches: corrupt chunk")
	index2 := PrefixesAcross(t.Context(), dbs2, in2.open, &PrefixCache{}, readyNow)
	require.Len(t, index2.Failed, 1)
	assert.Equal(t, []string{"artifacts"}, index2.Prefixes())
	assert.Equal(t, 1, in2.dbs[1].closes)
}

// The pattern: what counts as an id and what does not. The suffix carries no
// hyphen, which is what makes the split unambiguous for a hyphenated prefix.
func TestPrefixIndexScan(t *testing.T) {
	index := &PrefixIndex{byPrefix: map[string]ReadyDatabase{
		"global":     {ID: 1, OwnerName: "bigbes", Name: "beads-global"},
		"artifacts":  {ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"},
		"sr-ht-dolt": {ID: 3, OwnerName: "bigbes", Name: "sourcehut-dolt"},
	}}

	for _, tc := range []struct {
		name string
		text string
		want []string
	}{
		{"a plain id", "blocked by artifacts-nex", []string{"artifacts-nex"}},
		{"the subtask form", "see artifacts-46c.2 for the bucket", []string{"artifacts-46c.2"}},
		{"a hyphenated prefix", "sr-ht-dolt-44n.6 lands first", []string{"sr-ht-dolt-44n.6"}},
		{"several in one line", "global-b08 needs artifacts-46c.2", []string{"global-b08", "artifacts-46c.2"}},
		{"an unknown prefix", "nosuch-46c and other-1 are not ids", nil},
		{"a hyphenated word", "the read-only surface", nil},
		{"an id at the end of a sentence", "closed by artifacts-46c.", []string{"artifacts-46c"}},
		{"an id in brackets", "(artifacts-46c) and [global-b08]", []string{"artifacts-46c", "global-b08"}},
		{"an id inside a longer word", "xxartifacts-46c and artifactsx-1", nil},
		{"uppercase is not an id", "ARTIFACTS-46C", nil},
		{"a prefix on its own", "the artifacts tracker", nil},
		{"a shorter known prefix inside a longer token", "some-global-b08", nil},
		{"empty text", "", nil},
	} {
		t.Run(tc.name, func(t *testing.T) {
			var got []string
			for _, ref := range index.Scan(tc.text) {
				got = append(got, ref.ID)
				assert.Equal(t, ref.ID, tc.text[ref.Start:ref.End],
					"the offsets must name the id they carry")
			}
			assert.Equal(t, tc.want, got)
		})
	}

	// Each reference names the database that owns it.
	refs := index.Scan("global-b08 blocks artifacts-46c.2")
	require.Len(t, refs, 2)
	assert.Equal(t, "bigbes/beads-global", refs[0].Database.Slug())
	assert.Equal(t, "bigbes/sourcehut-artifacts", refs[1].Database.Slug())
}

// An index that was never built knows no prefix — the same answer an unknown one
// gets, which is what keeps "the caller may not see that database" and "there is
// no such database" indistinguishable.
func TestNilPrefixIndexKnowsNothing(t *testing.T) {
	var index *PrefixIndex

	assert.Nil(t, index.Scan("artifacts-46c.2"))
	assert.Nil(t, index.Prefixes())
	_, ok := index.Lookup("artifacts")
	assert.False(t, ok)

	// So does an empty one.
	empty := PrefixesAcross(t.Context(), nil, nil, &PrefixCache{}, readyNow)
	assert.Nil(t, empty.Scan("artifacts-46c.2"))
}