package web
import (
"bytes"
"encoding/json"
"strings"
"testing"
)
// A stack offers to land only what will land. The walk accepts each
// rung in turn and an accept refuses a rung whose required checks are
// not green, so a Land stack button over a waiting base landed nothing
// and said the rung would not merge. While a rung waits, the stack says
// which and for what, its green verdict is not shown, and the control
// lands the rungs below it or is not drawn.
func TestAStackOffersToLandOnlyWhatWillLand(t *testing.T) {
ids := []string{strings.Repeat("a1", 32), strings.Repeat("b2", 32), strings.Repeat("c3", 32)}
page := func(rungs, here int, stack string) string {
t.Helper()
var sv stackView
if err := json.Unmarshal([]byte(stack), &sv); err != nil {
t.Fatal(err)
}
sv.Segments = sv.Segments[:rungs]
for i := range sv.Segments {
sv.Segments[i].ID, sv.Segments[i].Position, sv.Segments[i].State = ids[i], i+1, "open"
sv.Segments[i].Branch = "b" + string(rune('1'+i))
}
sv.Top, sv.Base, sv.CI = ids[rungs-1], ids[0], "ci-pass"
d := landingDetail{
landingRow: landingRow{ID: ids[here-1], Branch: sv.Segments[here-1].Branch, Target: "trunk", State: "open"},
CanAccept: true, Mergeable: "clean", Stack: sv, Position: here,
}
if here > 1 {
d.Mergeable = "stacked"
}
var buf bytes.Buffer
if err := noHooks.template(landingPage).Execute(&buf, pageData{Title: "Landing", Repo: "quarry",
Section: "landings", Landing: &d}); err != nil {
t.Fatal(err)
}
return buf.String()
}
segs := `"segments":[{"preflight":"clean"},{"preflight":"clean"},{"preflight":"clean"}]`
// The base waits for its checks: nothing lands from the top.
top := page(2, 2, `{"preflight":"lands","landTo":"","landCount":0,"waitsAt":1,`+
`"segments":[{"preflight":"clean","waiting":["quarry","race"]},{"preflight":"clean"}]}`)
if strings.Contains(top, `/land"`) {
t.Error("FAIL: a stack whose base waits still offers to land")
}
if !strings.Contains(top, `data-hook="stack-blocked"`) || strings.Count(top, "waits for checks") != 1 {
t.Error("FAIL: the stack does not say once which rung waits")
}
if !strings.Contains(top, `data-hook="rung-waiting"`) || !strings.Contains(top, "<code>race</code>") {
t.Error("FAIL: the waiting rung does not name its checks")
}
if m := top[strings.Index(top, `data-hook="strata-map"`):]; strings.Contains(m[:strings.Index(m, "</p>")], "ci-pass") {
t.Error("FAIL: a waiting stack still shows a green verdict")
}
// Every rung green: the top lands up to itself, the whole stack.
green := page(2, 2, `{"preflight":"lands","landTo":"`+ids[1]+`","landCount":2,`+segs+`}`)
if !strings.Contains(green, `data-hook="land-stack" data-count="2" data-accept="stacked" formaction="/r/quarry/landing/`+ids[1][:12]+`/land"`) {
t.Error("FAIL: a green stack does not offer to land from its top")
}
// The third rung waits: the base lands the two below it.
part := page(3, 1, `{"preflight":"lands","landTo":"`+ids[1]+`","landCount":2,"waitsAt":3,`+segs+`}`)
if !strings.Contains(part, `data-hook="land-up-to" data-count="2" formaction="/r/quarry/landing/`+ids[1][:12]+`/land"`) {
t.Error("FAIL: the base does not offer to land the rungs below the waiting one")
}
if !strings.Contains(part, `data-hook="strata-waits" data-at="3"`) {
t.Error("FAIL: a stack landing in part does not say which rung waits")
}
// From the second rung of the same stack: up to itself, two rungs.
mid := page(3, 2, `{"preflight":"lands","landTo":"`+ids[2]+`","landCount":3,`+segs+`}`)
if !strings.Contains(mid, `data-count="2" data-accept="stacked" formaction="/r/quarry/landing/`+ids[1][:12]+`/land"`) {
t.Error("FAIL: a middle rung lands past itself")
}
}
// A lower rung waiting on its checks offers to land the whole stack
// when green, by arming the top rung, beside arming this rung alone;
// and the sentence counts what each lands rather than always saying
// "the whole stack".
func TestALowerRungWaitingOffersToLandTheStack(t *testing.T) {
ids := []string{strings.Repeat("a1", 32), strings.Repeat("b2", 32)}
var sv stackView
if err := json.Unmarshal([]byte(`{"preflight":"lands","landTo":"","landCount":0,"waitsAt":1,`+
`"segments":[{"preflight":"clean","waiting":["quarry"]},{"preflight":"clean"}]}`), &sv); err != nil {
t.Fatal(err)
}
for i := range sv.Segments {
sv.Segments[i].ID, sv.Segments[i].Position, sv.Segments[i].State = ids[i], i+1, "open"
sv.Segments[i].Branch = "b" + string(rune('1'+i))
}
sv.Top, sv.Base = ids[1], ids[0]
d := landingDetail{
landingRow: landingRow{ID: ids[0], Branch: "b1", Target: "trunk", State: "open"},
CanAccept: true, Mergeable: "clean", Stack: sv, Position: 1, Unmet: []string{"quarry"},
}
var buf bytes.Buffer
if err := noHooks.template(landingPage).Execute(&buf, pageData{Title: "Landing", Repo: "quarry",
Section: "landings", Landing: &d}); err != nil {
t.Fatal(err)
}
page := buf.String()
if !strings.Contains(page, `data-hook="stack-auto"`) || !strings.Contains(page, `action="/r/quarry/landing/`+ids[1][:12]+`/auto"`) {
t.Errorf("FAIL: the waiting base does not offer to arm the stack's top")
}
if !strings.Contains(page, `formaction="/r/quarry/landing/`+ids[0][:12]+`/auto"`) {
t.Errorf("FAIL: arming this rung alone is gone")
}
t.Log("ok: auto the stack")
}
// jva, ^2648: a lower rung shows the stack above it is armed, and the
// stack-wide arm is not offered while the base is behind its target.
func TestALowerRungShowsTheArmedStackAndNoArmWhileBehind(t *testing.T) {
ids := []string{strings.Repeat("a1", 32), strings.Repeat("b2", 32)}
render := func(behind int, armed string) string {
var sv stackView
if err := json.Unmarshal([]byte(`{"preflight":"lands","landTo":"","landCount":0,"waitsAt":1,`+
`"segments":[{"preflight":"clean","waiting":["quarry"]},{"preflight":"clean"}]}`), &sv); err != nil {
t.Fatal(err)
}
for i := range sv.Segments {
sv.Segments[i].ID, sv.Segments[i].Position, sv.Segments[i].State = ids[i], i+1, "open"
sv.Segments[i].Branch = "b" + string(rune('1'+i))
}
sv.Segments[0].Behind = behind
sv.Top, sv.Base = ids[1], ids[0]
d := landingDetail{
landingRow: landingRow{ID: ids[0], Branch: "b1", Target: "trunk", State: "open"},
CanAccept: true, Mergeable: "clean", Stack: sv, Position: 1, Unmet: []string{"quarry"},
StackArmedBy: armed, StackArmedAt: 2,
}
var buf bytes.Buffer
if err := noHooks.template(landingPage).Execute(&buf, pageData{Title: "Landing", Repo: "quarry",
Section: "landings", Landing: &d}); err != nil {
t.Fatal(err)
}
return buf.String()
}
if p := render(0, "dmestas"); !strings.Contains(p, `data-hook="stack-armed"`) {
t.Errorf("FAIL: a lower rung under an armed top does not say so")
}
if p := render(2, ""); strings.Contains(p, `data-hook="stack-auto"`) {
t.Errorf("FAIL: the stack-wide arm is offered while the base is behind")
}
t.Log("ok: armed shown, no arm while behind")
}
// jva, ^2659/^2660: an armed landing whose strip draws no Cancel (a
// stack's top rung) shows the arm with a Cancel, and the reason it waits.
func TestAnArmedTopRungCanBeCancelledAndSaysWhy(t *testing.T) {
ids := []string{strings.Repeat("a1", 32), strings.Repeat("b2", 32)}
var sv stackView
if err := json.Unmarshal([]byte(`{"preflight":"lands","landTo":"","landCount":0,"waitsAt":1,`+
`"segments":[{"preflight":"clean","waiting":["quarry"]},{"preflight":"clean"}]}`), &sv); err != nil {
t.Fatal(err)
}
for i := range sv.Segments {
sv.Segments[i].ID, sv.Segments[i].Position, sv.Segments[i].State = ids[i], i+1, "open"
sv.Segments[i].Branch = "b" + string(rune('1'+i))
}
sv.Top, sv.Base = ids[1], ids[0]
d := landingDetail{
landingRow: landingRow{ID: ids[1], Branch: "b2", Target: "b1", State: "open"},
CanAccept: true, Mergeable: "stacked", Stack: sv, Position: 2,
AutoBy: "dmestas", AutoWaiting: "failed: quarry. Re-run it from the landing",
}
var buf bytes.Buffer
if err := noHooks.template(landingPage).Execute(&buf, pageData{Title: "Landing", Repo: "quarry",
Section: "landings", Landing: &d}); err != nil {
t.Fatal(err)
}
p := buf.String()
if !strings.Contains(p, `data-hook="auto-cancel-row"`) || !strings.Contains(p, "failed: quarry") {
t.Errorf("FAIL: an armed top rung offers no Cancel or does not say it waits on a failed check")
}
t.Log("ok: cancel and reason on the armed top rung")
}
// An armed stack does not offer the press again: it would arm what is
// already armed, and seemed to do nothing.
func TestAnArmedStackDoesNotOfferToLandAgain(t *testing.T) {
ids := []string{strings.Repeat("a1", 32), strings.Repeat("b2", 32)}
var sv stackView
if err := json.Unmarshal([]byte(`{"preflight":"lands","landTo":"`+ids[1]+`","landCount":2,"waitsAt":1,`+
`"segments":[{"preflight":"clean","waiting":["quarry"]},{"preflight":"clean"}]}`), &sv); err != nil {
t.Fatal(err)
}
sv.Segments[0].ID, sv.Segments[0].Position, sv.Segments[0].State, sv.Segments[0].Branch = ids[0], 1, "open", "a"
sv.Segments[1].ID, sv.Segments[1].Position, sv.Segments[1].State, sv.Segments[1].Branch = ids[1], 2, "open", "b"
page := func(here int, armedTop, armedBelow bool) string {
d := landingDetail{
landingRow: landingRow{ID: ids[here-1], Branch: sv.Segments[here-1].Branch, Target: "trunk", State: "open"},
CanAccept: true, Mergeable: "clean", Stack: sv, Position: here,
}
if here > 1 {
d.Mergeable = "stacked"
}
if armedTop {
d.AutoBy = "dmestas"
}
if armedBelow {
d.StackArmedBy, d.StackArmedAt = "dmestas", 2
}
var buf bytes.Buffer
if err := noHooks.template(landingPage).Execute(&buf, pageData{Title: "Landing", Repo: "quarry",
Section: "landings", Landing: &d}); err != nil {
t.Fatal(err)
}
return buf.String()
}
if !strings.Contains(page(2, false, false), `data-hook="land-stack"`) {
t.Error("FAIL: an unarmed waiting stack no longer offers to arm by landing")
}
if strings.Contains(page(2, true, false), `data-hook="land-stack"`) {
t.Error("FAIL: the armed top still offers Land stack")
}
if strings.Contains(page(1, false, true), `data-hook="land-stack"`) {
t.Error("FAIL: a rung armed with the stack still offers Land stack")
}
t.Log("ok")
}