e2e/stack_test.go

v1.19.0
gitbay/e2e/stack_test.go history · blame · raw

157 lines · 5886 bytes

  1package e2e
  2
  3import (
  4	"encoding/json"
  5	"os"
  6	"path/filepath"
  7	"strings"
  8	"testing"
  9)
 10
 11// A three-deep stack, merged bottom-up. Each merge moves the stack above
 12// it onto the merged target with its reviews intact; a squash under a
 13// stack is refused.
 14func TestStackedMergeRequests(t *testing.T) {
 15	inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
 16	aliceKey := inst.newKey(t, "alice")
 17	bobKey := inst.newKey(t, "bob")
 18	inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub", "--email", "alice@example.test", "--verified")
 19	inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub", "--email", "bob@example.test", "--verified")
 20	if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/lib"); code != 0 {
 21		t.Fatalf("repo create: %s", errOut)
 22	}
 23	if _, _, code := inst.ssh(t, aliceKey, "", "repo", "access", "grant", "alice/lib", "bob", "write"); code != 0 {
 24		t.Fatal("grant failed")
 25	}
 26	work := t.TempDir()
 27	env := inst.gitEnv(aliceKey)
 28	mustGit(t, work, env, "clone", inst.sshURL("alice/lib"), "w")
 29	dir := filepath.Join(work, "w")
 30	commit := func(branch, from, file string) {
 31		t.Helper()
 32		if from == "" {
 33			mustGit(t, dir, env, "checkout", "-q", "-b", branch)
 34		} else {
 35			mustGit(t, dir, env, "checkout", "-q", "-b", branch, from)
 36		}
 37		os.WriteFile(filepath.Join(dir, file), []byte(file+"\n"), 0o644)
 38		mustGit(t, dir, env, "add", ".")
 39		mustGit(t, dir, env, "commit", "-q", "-m", file)
 40		mustGit(t, dir, env, "push", "-q", "origin", branch)
 41	}
 42	commit("main", "", "base.txt")
 43	commit("feat-a", "main", "a.txt")
 44	commit("feat-b", "feat-a", "b.txt")
 45	commit("feat-c", "feat-b", "c.txt")
 46
 47	create := func(src, dst, title string) string {
 48		t.Helper()
 49		out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "create", "alice/lib", "--source", src, "--target", dst, "--title", title, "--json")
 50		if code != 0 {
 51			t.Fatalf("mr create %s: %s", src, errOut)
 52		}
 53		return out
 54	}
 55	if out := create("feat-a", "main", "A"); strings.Contains(out, "stacked_on") {
 56		t.Fatalf("A is not stacked:\n%s", out)
 57	}
 58	if out := create("feat-b", "feat-a", "B"); !strings.Contains(out, `"stacked_on":{"number":1`) {
 59		t.Fatalf("B create does not report its stack:\n%s", out)
 60	}
 61	create("feat-c", "feat-b", "C")
 62
 63	type stackShow struct {
 64		mrShow
 65		StackedOn *struct {
 66			Number int64 `json:"number"`
 67		} `json:"stacked_on"`
 68		Stacked []struct {
 69			Number int64 `json:"number"`
 70		} `json:"stacked"`
 71		Comments []struct {
 72			Body string `json:"body"`
 73		} `json:"comments"`
 74	}
 75	show := func(n string) stackShow {
 76		t.Helper()
 77		out, errOut, code := inst.ssh(t, aliceKey, "", "mr", "show", "alice/lib", n, "--json")
 78		if code != 0 {
 79			t.Fatalf("mr show %s: %s", n, errOut)
 80		}
 81		var env struct {
 82			Data stackShow `json:"data"`
 83		}
 84		if err := json.Unmarshal([]byte(out), &env); err != nil {
 85			t.Fatalf("show: %v\n%s", err, out)
 86		}
 87		return env.Data
 88	}
 89	a, b, c := show("1"), show("2"), show("3")
 90	if a.StackedOn != nil || len(a.Stacked) != 1 || a.Stacked[0].Number != 2 {
 91		t.Fatalf("A stack: %+v", a)
 92	}
 93	if b.StackedOn == nil || b.StackedOn.Number != 1 || len(b.Stacked) != 1 || b.Stacked[0].Number != 3 {
 94		t.Fatalf("B stack: %+v", b)
 95	}
 96	if c.StackedOn == nil || c.StackedOn.Number != 2 || len(c.Stacked) != 0 {
 97		t.Fatalf("C stack: %+v", c)
 98	}
 99	if out, _, _ := inst.ssh(t, aliceKey, "", "mr", "list", "alice/lib"); !strings.Contains(out, "stacked on !1") || !strings.Contains(out, "stacked on !2") {
100		t.Fatalf("mr list lacks the stack:\n%s", out)
101	}
102
103	// Bob approves B; the approval must survive the retarget.
104	if _, errOut, code := inst.ssh(t, bobKey, "", "mr", "review", "alice/lib", "2", "--approve"); code != 0 {
105		t.Fatalf("review: %s", errOut)
106	}
107
108	// A squash under the stack is refused, naming the stack.
109	if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1", "--strategy", "squash"); code != 2 || !strings.Contains(errOut, "!2") {
110		t.Fatalf("squash under a stack: exit %d %s", code, errOut)
111	}
112	// Merge A: B moves onto main, C stays on feat-b.
113	if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "1"); code != 0 {
114		t.Fatalf("merge A: %s", errOut)
115	}
116	b = show("2")
117	if b.TargetRef != "main" || b.StackedOn != nil || len(b.Reviews) != 1 || b.Reviews[0].Stale {
118		t.Fatalf("B after A merged: %+v", b)
119	}
120	found := false
121	for _, cm := range b.Comments {
122		if strings.Contains(cm.Body, "retargeted from feat-a to main: !1 merged") {
123			found = true
124		}
125	}
126	if !found {
127		t.Fatalf("B carries no retarget comment: %+v", b.Comments)
128	}
129	if c = show("3"); c.TargetRef != "feat-b" || c.StackedOn == nil || c.StackedOn.Number != 2 {
130		t.Fatalf("C after A merged: %+v", c)
131	}
132	// The web page says both directions.
133	root := inst.login(t, aliceKey)
134	if _, body := browserGet(t, root, inst.base()+"/alice/lib/mrs/2"); !strings.Contains(body, "Builds on this") || !strings.Contains(body, "/alice/lib/mrs/3") {
135		t.Fatalf("B page lacks its stack:\n%s", body)
136	}
137	if _, body := browserGet(t, root, inst.base()+"/alice/lib/mrs/3"); !strings.Contains(body, "Stacked on") || !strings.Contains(body, "/alice/lib/mrs/2") {
138		t.Fatalf("C page lacks its parent:\n%s", body)
139	}
140	// Merge B, then C: each lands its own commit only.
141	if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "2"); code != 0 {
142		t.Fatalf("merge B: %s", errOut)
143	}
144	if c = show("3"); c.TargetRef != "main" || c.StackedOn != nil {
145		t.Fatalf("C after B merged: %+v", c)
146	}
147	if _, errOut, code := inst.ssh(t, aliceKey, "", "mr", "merge", "alice/lib", "3"); code != 0 {
148		t.Fatalf("merge C: %s", errOut)
149	}
150	mustGit(t, dir, env, "checkout", "-q", "main")
151	mustGit(t, dir, env, "pull", "-q", "origin", "main")
152	for _, f := range []string{"a.txt", "b.txt", "c.txt"} {
153		if _, err := os.Stat(filepath.Join(dir, f)); err != nil {
154			t.Fatalf("%s missing from main after the stack merged", f)
155		}
156	}
157}