~bigbes/sr-ht-dolt

ref: cd0b0c0fb427bff6060c3f785d67784b999610f3 sr-ht-dolt/web/milestones_test.go -rw-r--r-- 9.8 KiB
cd0b0c0f — Eugene Blikh web: rename a database from its settings page 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
package web

import (
	"fmt"
	"net/http"
	"strings"
	"testing"

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

// The grouping itself is tested in the beads package; what is left here is the
// pairing with the Beads tab and that milestones.html renders the rollup.

// milestoneFixture: six issues across two milestones. m1 carries the full
// hierarchy — a milestone-typed head, an epic with one (closed) subtask, and a
// loose bug; m2 has a single loose feature; one issue has no milestone label.
func milestoneFixture() *fakeSession {
	issues := &browse.RowPage{
		Columns: []string{"id", "title", "status", "priority", "issue_type", "assignee"},
		Rows: [][]string{
			{"i-m", "Mike", "open", "0", "milestone", ""},
			{"i-e", "Echo", "open", "1", "epic", ""},
			{"i-a", "Alpha", "closed", "1", "task", "alice"}, // subtask of i-e
			{"i-b", "Bravo", "open", "0", "bug", "bob"},
			{"i-c", "Charlie", "open", "2", "feature", ""},
			{"i-d", "Delta", "open", "1", "task", ""}, // no milestone
		},
		Total: 6,
	}
	labels := &browse.RowPage{
		Columns: []string{"issue_id", "label"},
		Rows: [][]string{
			{"i-m", "milestone:m1"},
			{"i-e", "milestone:m1"},
			{"i-a", "milestone:m1"},
			{"i-b", "milestone:m1"},
			{"i-c", "milestone:m2"},
			{"i-b", "backend"}, // non-milestone label ignored by grouping
		},
		Total: 6,
	}
	deps := &browse.RowPage{
		Columns: []string{"issue_id", "depends_on_issue_id", "type"},
		Rows: [][]string{
			{"i-a", "i-e", "parent-child"},
			{"i-b", "i-a", "blocks"}, // non-hierarchy edge ignored by nesting
		},
		Total: 2,
	}
	statuses := &browse.RowPage{
		Columns: []string{"name", "category"},
		Rows:    [][]string{{"open", "open"}, {"closed", "closed"}},
		Total:   2,
	}
	return &fakeSession{
		branches: []browse.Branch{{Name: "main", Head: "abcdef1234567890"}},
		tables:   beadsTables(),
		rowsByTable: map[string]*browse.RowPage{
			"issues":          issues,
			"labels":          labels,
			"dependencies":    deps,
			"custom_statuses": statuses,
		},
	}
}

// milestoneClippedFixture is that same tracker grown past the cap: the six rows
// above, filler up to beads.Max, and five more — among them i-tail, which
// carries milestone:m1 and sits in the tail no read here reaches. The read hands
// over the first beads.Max rows and a Total saying 2005 exist.
//
// So m1 rolls up four of its five members, and every number on the page is
// arithmetic over a partial read while looking exactly like arithmetic over the
// tracker. The rows are built whole and then clipped, rather than a short page
// given a large Total by hand: the missing member has to be genuinely absent
// from what the projection reads, or the test asserts the notice while never
// producing the situation the notice is about.
func milestoneClippedFixture() *fakeSession {
	sess := milestoneFixture()
	issues := sess.rowsByTable["issues"]
	rows := append([][]string{}, issues.Rows...)
	for i := len(rows); i < beads.Max; i++ {
		rows = append(rows, []string{fmt.Sprintf("i-%04d", i), fmt.Sprintf("Filler %d", i), "open", "1", "task", ""})
	}
	rows = append(rows, []string{"i-tail", "Tail task", "open", "1", "task", "alice"})
	for i := len(rows); i < beads.Max+5; i++ {
		rows = append(rows, []string{fmt.Sprintf("i-%04d", i), fmt.Sprintf("Filler %d", i), "open", "1", "task", ""})
	}
	sess.rowsByTable["issues"] = clipPage(
		&browse.RowPage{Columns: issues.Columns, Rows: rows, Total: len(rows)}, beads.Max)

	labels := sess.rowsByTable["labels"]
	labels.Rows = append(labels.Rows, []string{"i-tail", "milestone:m1"})
	labels.Total = len(labels.Rows)
	return sess
}

// milestoneClippedLabelsFixture leaves every issue readable and clips the table
// that decides milestone membership instead: the six real label rows lead, so
// the rollup still renders, and beads.Max+3 rows exist of which the read returns
// beads.Max. Nothing about the issue count is wrong here, which is why the
// notice's second sentence has to be conditional.
func milestoneClippedLabelsFixture() *fakeSession {
	sess := milestoneFixture()
	labels := sess.rowsByTable["labels"]
	rows := append([][]string{}, labels.Rows...)
	for i := len(rows); i < beads.Max+3; i++ {
		rows = append(rows, []string{"i-d", fmt.Sprintf("label-%04d", i)})
	}
	sess.rowsByTable["labels"] = clipPage(
		&browse.RowPage{Columns: labels.Columns, Rows: rows, Total: len(rows)}, beads.Max)
	return sess
}

func TestMilestonesApplies(t *testing.T) {
	// The milestone tab appears exactly where the beads tab does.
	if (&milestonesView{}).Applies(beadsTables()) != (&beadsView{}).Applies(beadsTables()) {
		t.Fatalf("milestones Applies should mirror beads Applies")
	}
	if (&milestonesView{}).Applies([]browse.TableInfo{{Name: "widgets"}}) {
		t.Fatalf("milestones should not apply to an unrelated schema")
	}
}

func TestMilestonesRender(t *testing.T) {
	h := newHarness(t)
	h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
	h.browse.sess = milestoneFixture()
	setViews(t, h, &beadsView{}, &milestonesView{})

	rec := h.do("GET", "/~alice/db/view/milestones", nil, nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("milestones view: got %d; body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	for _, want := range []string{
		"&middot; milestones",         // page heading
		"ms-title\">m1<",              // milestone name
		"1</b>/4 done",                // rollup counts
		"ms-issue head",               // the milestone-typed issue leads the list
		"ms-issue epic",               // the epic row
		"ms-issue child done",         // its subtask, nested and closed
		`class="r-sub">1/1<`,          // the epic's subtask rollup
		"Charlie",                     // an issue under m2
		"carry no milestone",          // unlabeled footnote
		`view/milestones">Milestones`, // the tab link
		"nav-link active",             // the active tab marker
	} {
		if !strings.Contains(body, want) {
			t.Errorf("milestones render missing %q", want)
		}
	}
	// Nothing was clipped here, so the page makes no claim about a partial read.
	if strings.Contains(body, "This read was clipped") {
		t.Errorf("a complete read must not be dressed up as a clipped one; body=%s", body)
	}
}

// --- a read that was clipped -------------------------------------------------

// Every count on this page is a rollup: "1/4 done", "N of M carry no milestone
// label". Over a clipped read they are arithmetic over the rows that were read
// and they render identically to arithmetic over the tracker, so the page has to
// say which one it is — in the detail pane's words, for the same fact.
func TestMilestonesReportAClippedRead(t *testing.T) {
	h := newHarness(t)
	h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
	h.browse.sess = milestoneClippedFixture()
	setViews(t, h, &beadsView{}, &milestonesView{})

	rec := h.do("GET", "/~alice/db/view/milestones", nil, nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("milestones view: got %d; body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "This read was clipped") {
		t.Errorf("the rollup is over a partial read and the page says nothing; body=%s", body)
	}
	// The number is the tracker's true size, which is what makes the line
	// checkable rather than a bare warning.
	if !strings.Contains(body, "The tracker holds 2005 issues and only the first of them were read.") {
		t.Errorf("the page does not name the tracker's true size; body=%s", body)
	}
	// And the arithmetic the notice is about: m1 has five members and the page
	// counts four, because the fifth is past the cap.
	if !strings.Contains(body, "1</b>/4 done") {
		t.Errorf("m1's rollup is the members that were read; body=%s", body)
	}
	if strings.Contains(body, "Tail task") {
		t.Errorf("the tail member cannot be on the page; body=%s", body)
	}
}

// A clip in labels — the table that decides membership — with every issue read.
// The notice appears; its second sentence does not, because the issue count is
// not the thing that went short.
func TestMilestonesReportAClippedLabelsRead(t *testing.T) {
	h := newHarness(t)
	h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
	h.browse.sess = milestoneClippedLabelsFixture()
	setViews(t, h, &beadsView{}, &milestonesView{})

	rec := h.do("GET", "/~alice/db/view/milestones", nil, nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("milestones view: got %d; body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "This read was clipped") {
		t.Errorf("membership itself was read in part and the page says nothing; body=%s", body)
	}
	if strings.Contains(body, "The tracker holds") {
		t.Errorf("every issue was read, so the page must not claim otherwise; body=%s", body)
	}
}

// The page with no milestones at all still carries the notice: an empty rollup
// over a clipped read is the one most easily mistaken for a fact about the
// tracker.
func TestMilestonesReportAClippedReadWithNoMilestones(t *testing.T) {
	h := newHarness(t)
	h.store.add(&core.Repo{Name: "db", OwnerID: 1, OwnerName: "alice", Path: "/d", Visibility: core.VisibilityPublic})
	sess := milestoneClippedFixture()
	sess.rowsByTable["labels"] = &browse.RowPage{Columns: []string{"issue_id", "label"}}
	h.browse.sess = sess
	setViews(t, h, &beadsView{}, &milestonesView{})

	rec := h.do("GET", "/~alice/db/view/milestones", nil, nil)
	if rec.Code != http.StatusOK {
		t.Fatalf("milestones view: got %d; body=%s", rec.Code, rec.Body.String())
	}
	body := rec.Body.String()
	if !strings.Contains(body, "No milestones.") {
		t.Fatalf("the fixture is meant to produce an empty rollup; body=%s", body)
	}
	if !strings.Contains(body, "This read was clipped") {
		t.Errorf("\"no milestones\" over a clipped read is not an answer about the tracker; body=%s", body)
	}
}