e2e/stack_test.go
158 lines · 5900 bytes
1 symbol in this file
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}