~bigbes/sr-ht-dolt

ref: ede3b0bb2671ffde35a66527b920c40eaccc096f sr-ht-dolt/beads/prefixes_test.go -rw-r--r-- 15.7 KiB
ede3b0bb — Eugene Blikh go.mod: take the shared libraries' current heads 2 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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
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"))
}

// --- the memory half of the same read ------------------------------------------

// memoryTracker is a tracker whose config holds memories beside its settings.
// The prefix is a parameter because a tracker that names none still holds
// memories, and the two halves of the projection are independent.
func memoryTracker(head, prefix string, slugs ...string) *fakeReadyDB {
	page := &browse.RowPage{
		Columns: []string{"key", "value"},
		Rows:    [][]string{{"compact_tier2_days", "30"}},
	}
	if prefix != "" {
		page.Rows = append(page.Rows, []string{"issue_prefix", prefix})
	}
	for _, s := range slugs {
		page.Rows = append(page.Rows, []string{memoryPrefix + s, "the body of " + s})
	}
	page.Total = len(page.Rows)
	return &fakeReadyDB{
		branches: []browse.Branch{{Name: "main", Head: head}},
		tables:   beadsTables(),
		rows:     map[string]*browse.RowPage{"config": page},
	}
}

// The slugs come out of the same read the prefix does, and a slug no database
// holds is unknown — which is also the answer for one held by a database this
// caller may not browse, since such a database is never handed to the index.
func TestPrefixesAcrossIndexesMemorySlugs(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: memoryTracker("h-global", "global", "go-vcs-stamp-dirty", "srht-service-release"),
		2: memoryTracker("h-artifacts", "artifacts", "apk-index-cadence"),
	}}
	dbs := []ReadyDatabase{
		{ID: 1, OwnerName: "bigbes", Name: "beads-global"},
		{ID: 2, OwnerName: "bigbes", Name: "sourcehut-artifacts"},
	}

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

	assert.Equal(t,
		[]string{"apk-index-cadence", "go-vcs-stamp-dirty", "srht-service-release"},
		index.MemorySlugs())
	d, ok := index.LookupMemory("go-vcs-stamp-dirty")
	require.True(t, ok)
	assert.Equal(t, "bigbes/beads-global", d.Slug())
	d, ok = index.LookupMemory("apk-index-cadence")
	require.True(t, ok)
	assert.Equal(t, "bigbes/sourcehut-artifacts", d.Slug())
	_, ok = index.LookupMemory("never-written")
	assert.False(t, ok)

	// One read per database answered both questions.
	assert.Equal(t, 1, in.dbs[1].rowReads)
	assert.Equal(t, []string{"artifacts", "global"}, index.Prefixes())
}

// A slug two trackers hold resolves to the first one listed, unlike a prefix two
// trackers claim, which resolves to neither. The difference is what the
// duplicate means: two trackers claiming one prefix own two different id spaces,
// while the same memory slug in two trackers is normally one memory that was
// re-filed and left a copy behind. The caller lists the page's own database
// first, so this is "the tracker you are reading wins".
func TestPrefixesAcrossResolvesADuplicateSlugToTheFirstDatabase(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: memoryTracker("h1", "here", "shared-note"),
		2: memoryTracker("h2", "there", "shared-note"),
	}}
	dbs := []ReadyDatabase{
		{ID: 1, OwnerName: "alice", Name: "own"},
		{ID: 2, OwnerName: "bob", Name: "other"},
	}

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

	d, ok := index.LookupMemory("shared-note")
	require.True(t, ok, "a slug two trackers hold is still a link")
	assert.Equal(t, "alice/own", d.Slug())
}

// A tracker that names no issue prefix still contributes its memories: the two
// halves of the projection do not gate each other.
func TestPrefixesAcrossIndexesMemoriesOfAPrefixlessDatabase(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: memoryTracker("h1", "", "only-a-memory"),
	}}
	dbs := []ReadyDatabase{{ID: 1, OwnerName: "alice", Name: "no-prefix"}}

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

	assert.Empty(t, index.Prefixes())
	d, ok := index.LookupMemory("only-a-memory")
	require.True(t, ok)
	assert.Equal(t, "alice/no-prefix", d.Slug())
}

// The cached projection carries the slugs, so a second build behind the
// head-hash gate answers the memory question without reading a row either.
func TestPrefixesAcrossCachesTheSlugs(t *testing.T) {
	in := &readyInstance{dbs: map[int]*fakeReadyDB{
		1: memoryTracker("h1", "here", "cached-slug"),
	}}
	dbs := []ReadyDatabase{{ID: 1, OwnerName: "alice", Name: "own"}}
	cache := &PrefixCache{}

	PrefixesAcross(t.Context(), dbs, in.open, cache, readyNow)
	reads := in.dbs[1].rowReads
	require.Greater(t, reads, 0)

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

	assert.Equal(t, reads, in.dbs[1].rowReads, "the gate holds for the slugs too")
	_, ok := second.LookupMemory("cached-slug")
	assert.True(t, ok)
}

// An index that was never built holds no memory either.
func TestNilPrefixIndexKnowsNoMemory(t *testing.T) {
	var index *PrefixIndex

	assert.Nil(t, index.MemorySlugs())
	_, ok := index.LookupMemory("anything")
	assert.False(t, ok)
}