@@ 1,10 1,12 @@
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"
)
@@ 65,6 67,54 @@ func milestoneFixture() *fakeSession {
}
}
+// 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()) {
@@ 103,4 153,89 @@ func TestMilestonesRender(t *testing.T) {
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)
+ }
}