e2e/stack_test.go

dd06e80a071d451ebb7b083363e0580550481114
gitbay/e2e/stack_test.go history · blame · raw

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