~bigbes/sr-ht-dolt

ref: 93e699101e297244802590365e96da638404b579 sr-ht-dolt/beads/raw_test.go -rw-r--r-- 10.8 KiB
93e69910 — Eugene Blikh storage: create databases empty so the first push needs no --force 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
package beads

import (
	"context"
	"fmt"
	"net/url"
	"testing"

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

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

// The raw section is the pane's own check on itself: the rows the detail was
// built from, as read. What it has to get right is what every other projection
// here is allowed to get wrong — the column order it was read in, and the
// difference between a cell that holds no value and one that holds a string
// which happens to read like one.

// rawTableByName finds one table's entry in a built detail's raw section.
func rawTableByName(d *Data, name string) *RawTable {
	for i := range d.Raw {
		if d.Raw[i].Table == name {
			return &d.Raw[i]
		}
	}
	return nil
}

// rawCellByName finds one cell of a raw row by its column name.
func rawCellByName(r RawRow, column string) *RawCell {
	for i := range r.Cells {
		if r.Cells[i].Column == column {
			return &r.Cells[i]
		}
	}
	return nil
}

// rawColumns lists a raw row's columns in the order they are carried.
func rawColumns(r RawRow) []string {
	out := make([]string, 0, len(r.Cells))
	for _, c := range r.Cells {
		out = append(out, c.Column)
	}
	return out
}

// The issues row arrives in the table's own column order. beadsFixture orders
// its columns so nothing sits at a natural index, so an implementation that
// sorted or re-derived the order could not match this by accident.
func TestRawIssuesRowKeepsTheColumnOrderItWasReadIn(t *testing.T) {
	d, err := Build(context.Background(), beadsFixture(), "main", url.Values{"issue": {"i-done"}})
	require.NoError(t, err)

	tbl := rawTableByName(d, "issues")
	require.NotNil(t, tbl, "the issue's own row is the whole point of the section")
	require.Len(t, tbl.Rows, 1)
	assert.Equal(t, 1, tbl.Matched)
	assert.False(t, tbl.Clipped())

	assert.Equal(t,
		[]string{"id", "title", "status", "priority", "issue_type", "assignee",
			"created_at", "closed_at", "close_reason", "is_blocked"},
		rawColumns(tbl.Rows[0]),
		"column order is data: it is the order the table was read in")

	// Every column carries its own row value, addressed by name.
	require.NotNil(t, rawCellByName(tbl.Rows[0], "close_reason"))
	assert.Equal(t, "Fixed in commit abc123", rawCellByName(tbl.Rows[0], "close_reason").Value)
}

// The three states the section exists for. A cell browse reported as NULL, a
// cell storing the four characters "NULL", and a cell storing the empty string
// must be three distinguishable things in the model — the first two render
// identically as strings, and the last two are identical as strings once a null
// has been flattened.
func TestRawCellsSeparateAbsentFromStoredNullFromEmpty(t *testing.T) {
	issues := &browse.RowPage{
		Columns: []string{"id", "title", "assignee", "notes", "status"},
		Rows: [][]string{
			//     id     title   assignee  notes  status
			{"i-1", "NULL", "NULL", "", "open"},
		},
		Nulls: [][]bool{
			// title holds no value at all; assignee stores those four characters;
			// notes stores the empty string.
			{false, true, false, false, false},
		},
		Total: 1,
	}

	sess := &fakeSession{rowsByTable: map[string]*browse.RowPage{
		"issues": issues,
		"dependencies": {
			Columns: []string{"id", "issue_id", "depends_on_issue_id", "type"},
			Rows:    [][]string{},
			Nulls:   [][]bool{},
		},
	}}

	d, err := Build(context.Background(), sess, "main", url.Values{"issue": {"i-1"}})
	require.NoError(t, err)
	tbl := rawTableByName(d, "issues")
	require.NotNil(t, tbl)
	require.Len(t, tbl.Rows, 1)
	row := tbl.Rows[0]

	absent := rawCellByName(row, "title")
	require.NotNil(t, absent)
	assert.True(t, absent.Null, "a cell that holds no value says so")
	assert.Equal(t, "", absent.Value,
		"an absent cell carries no text: browse's \"NULL\" is a rendering of absence, not stored data")

	stored := rawCellByName(row, "assignee")
	require.NotNil(t, stored)
	assert.False(t, stored.Null, "those four characters are stored, so the cell holds a value")
	assert.Equal(t, "NULL", stored.Value)

	empty := rawCellByName(row, "notes")
	require.NotNil(t, empty)
	assert.False(t, empty.Null, "an empty string is a value")
	assert.Equal(t, "", empty.Value)

	// Pairwise distinguishable, which is the property the pane depends on: no two
	// of the three agree on both (Value, Null).
	assert.NotEqual(t, absent.Null, stored.Null, "absent vs. the stored text \"NULL\"")
	assert.NotEqual(t, absent.Null, empty.Null, "absent vs. the empty string")
	assert.NotEqual(t, stored.Value, empty.Value, "the stored text \"NULL\" vs. the empty string")
}

// A page that carries no mask at all — a hand-built one; browse fills a mask for
// every page it returns — is read the way this package read every page before
// the mask existed, and the raw section reads it the same way the fields above
// it do. One reading, one decision site.
func TestRawFollowsTheSameNullReadingAsTheFields(t *testing.T) {
	// beadsFixture's pages carry no Nulls, so "NULL" is how absence arrives.
	d, err := Build(context.Background(), beadsFixture(), "main", url.Values{"issue": {"i-open"}})
	require.NoError(t, err)
	require.NotNil(t, d.Issue)
	assert.Equal(t, "", d.Issue.ClosedAt, "the field reads the unmasked \"NULL\" as absent")

	tbl := rawTableByName(d, "issues")
	require.NotNil(t, tbl)
	closed := rawCellByName(tbl.Rows[0], "closed_at")
	require.NotNil(t, closed)
	assert.True(t, closed.Null, "and so does the raw row: they cannot disagree")
	assert.Equal(t, "", closed.Value)
}

// Every table the pane draws rows of this issue from is carried, in read order,
// and only the rows that belong to the issue: another issue's comment is another
// issue's business.
func TestRawCarriesEveryPerIssueTableInReadOrder(t *testing.T) {
	d, err := Build(context.Background(), beadsFixture(), "main", url.Values{"issue": {"i-open"}})
	require.NoError(t, err)

	var names []string
	for _, tbl := range d.Raw {
		names = append(names, tbl.Table)
	}
	assert.Equal(t, []string{"issues", "labels", "dependencies", "comments"}, names,
		"read order, and only the tables that had rows for this issue")

	labels := rawTableByName(d, "labels")
	require.NotNil(t, labels)
	assert.Equal(t, 2, labels.Matched, "both of i-open's label rows")

	// i-open is the target of i-blocked's edge: an edge is this issue's in either
	// direction, because the pane draws both lists from these rows.
	deps := rawTableByName(d, "dependencies")
	require.NotNil(t, deps)
	require.Len(t, deps.Rows, 1)
	assert.Equal(t, "i-blocked", rawCellByName(deps.Rows[0], "issue_id").Value)

	comments := rawTableByName(d, "comments")
	require.NotNil(t, comments)
	require.Len(t, comments.Rows, 1)
	assert.Equal(t, "first!", rawCellByName(comments.Rows[0], "text").Value,
		"i-prog's comment belongs to i-prog")

	// i-open has no audit events in this fixture, and a table with nothing to say
	// is not listed at all.
	assert.Nil(t, rawTableByName(d, "events"))

	// custom_statuses is read (the lane comes from it) and is deliberately absent:
	// its rows describe the tracker's statuses, not this issue.
	assert.Nil(t, rawTableByName(d, "custom_statuses"))
}

// The events table is where stored and rendered are furthest apart —
// humanizeEvent turns two JSON blobs into one sentence — so the rows behind the
// History tab are carried with the strings intact.
func TestRawCarriesTheStoredEventStrings(t *testing.T) {
	d, err := Build(context.Background(), beadsEpicFixture(), "main", url.Values{"issue": {"i-epic"}})
	require.NoError(t, err)

	events := rawTableByName(d, "events")
	require.NotNil(t, events)
	assert.Equal(t, 4, events.Matched, "i-c1's created event is not this issue's")
	require.Len(t, events.Rows, 4)
	assert.Equal(t, `{"status":"open"}`, rawCellByName(events.Rows[1], "old_value").Value,
		"the exact stored string behind the humanized line")
}

// The epic mode is the detail mode with a rollup on top, and it carries the raw
// rows for the same reason.
func TestRawIsCarriedInEpicMode(t *testing.T) {
	d, err := Build(context.Background(), beadsEpicFixture(), "main", url.Values{"issue": {"i-epic"}})
	require.NoError(t, err)
	require.Equal(t, "epic", d.Mode)

	tbl := rawTableByName(d, "issues")
	require.NotNil(t, tbl)
	require.Len(t, tbl.Rows, 1)
	assert.Equal(t, "i-epic", rawCellByName(tbl.Rows[0], "id").Value)
	assert.Equal(t, "epic", rawCellByName(tbl.Rows[0], "issue_type").Value)

	// The three parent-child edges are the issue's, in both directions.
	deps := rawTableByName(d, "dependencies")
	require.NotNil(t, deps)
	assert.Equal(t, 3, deps.Matched)
}

// An issue that is not there has no stored row, and the section has nothing to
// show rather than something empty to show.
func TestRawIsEmptyForAMissingIssue(t *testing.T) {
	d, err := Build(context.Background(), beadsFixture(), "main", url.Values{"issue": {"nope"}})
	require.NoError(t, err)
	require.True(t, d.Missing())
	assert.Empty(t, d.Raw, "nothing was found, so there is nothing stored to show")
}

// The board is not a detail pane and carries none of this: it renders no stored
// row, and a board that carried every row of every card would be the table
// browser with lanes drawn on it.
func TestRawIsNotCarriedOnTheBoard(t *testing.T) {
	d, err := Build(context.Background(), beadsFixture(), "main", url.Values{})
	require.NoError(t, err)
	require.Equal(t, "board", d.Mode)
	assert.Empty(t, d.Raw)
}

// A table with more of this issue's rows than RawMax is cut, and says how many
// there were: a section that dumps three hundred audit rows is a second page
// nobody reads, and one that silently shows fifty of three hundred is a lie.
func TestRawCapsARelatedTableAndCountsItWhole(t *testing.T) {
	sess := beadsFixture()
	rows := make([][]string, 0, RawMax+7)
	for i := range RawMax + 7 {
		rows = append(rows, []string{"i-open", "alice", fmt.Sprintf("comment %d", i), "2024-01-05"})
	}
	sess.rowsByTable["comments"] = &browse.RowPage{
		Columns: []string{"issue_id", "author", "text", "created_at"},
		Rows:    rows,
		Total:   len(rows),
	}

	d, err := Build(context.Background(), sess, "main", url.Values{"issue": {"i-open"}})
	require.NoError(t, err)

	tbl := rawTableByName(d, "comments")
	require.NotNil(t, tbl)
	assert.Len(t, tbl.Rows, RawMax)
	assert.Equal(t, RawMax+7, tbl.Matched)
	assert.True(t, tbl.Clipped())
	assert.Equal(t, "comment 0", rawCellByName(tbl.Rows[0], "text").Value, "the first of them, in table order")
}

// A row shorter than its own header is a malformed page. There is no cell to
// report for the missing columns, so they are left out rather than invented as
// nulls — and the cells that do exist still line up with their column names.
func TestRawSkipsColumnsAShortRowDoesNotHave(t *testing.T) {
	cols := []string{"id", "title", "status"}
	row := rowCells{values: []string{"i-1", "Ahoy"}}

	got := rawRow(cols, row)
	require.Len(t, got.Cells, 2)
	assert.Equal(t, RawCell{Column: "id", Value: "i-1"}, got.Cells[0])
	assert.Equal(t, RawCell{Column: "title", Value: "Ahoy"}, got.Cells[1])
}