Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 1h ago 4b8c9e1d
history blame raw 4.1 KiB
1package server_test
2
3// A landing whose base moved must test the tree it writes, and a merger must not
4// have to click twice while that test runs.
5
6import (
7 "strings"
8 "testing"
9 "time"
10
11 "rodent.software/vole/ci/runs"
12 "rodent.software/vole/forge/config"
13 "rodent.software/vole/metadata"
14)
15
16func TestNonFastForwardLandingWaitsForTheCombinedResult(t *testing.T) {
17 t.Parallel()
18 f := newFixtureWith(t, func(c *config.Config) { c.LeaseSeconds = 2 })
19 writerKey := f.addWriter(t)
20 f.publish(f.repo)
21
22 workflow := "version: 1\njobs:\n test:\n image: alpine:3.22\n steps:\n - run: echo ok\n"
23 if _, errs, err := f.git(workRepo(t, workflowFile(workflow)),
24 "push", f.sshURL(f.repo), "main:refs/heads/main"); err != nil {
25 t.Fatalf("initial main: %s", errs)
26 }
27 m := mustRepo(t, f)
28 a, _ := m.ACL()
29 a.Protected = []string{"refs/heads/main"}
30 if err := m.SetACL(a, "test"); err != nil {
31 t.Fatal(err)
32 }
33
34 // The reviewed head, green on its own branch.
35 feature, head := f.cloneWithChange(t, map[string]string{"a.md": "feature\n"}, "feature")
36 if _, errs, err := f.gitWithKey(writerKey, feature, "push", f.sshURL(f.repo), "HEAD:refs/heads/feature"); err != nil {
37 t.Fatalf("feature: %s", errs)
38 }
39 f.waitForRun(t, 10*time.Second, func(r *runs.Run) bool { return r.SHA == head })
40 runUntil(t, f, func() bool { return runSucceeded(f, head) })
41
42 // Somebody else lands first, so the approved head is no longer a fast-forward.
43 moveDir, moved := f.cloneWithChange(t, map[string]string{"b.md": "base\n"}, "base move")
44 if _, errs, err := f.git(moveDir, "push", f.sshURL(f.repo), "HEAD:refs/heads/main"); err != nil {
45 t.Fatalf("base move: %s", errs)
46 }
47 f.openAndApprove(t, head, "wilbur")
48
49 // The merger asks for the landing before the combined result exists. This is
50 // what the Merge button does when the click arrives early.
51 if err := f.srv.Runs.ArmAutoMerge(f.repo, reqID, f.ownerID, time.Now().UTC()); err != nil {
52 t.Fatal(err)
53 }
54 candidate := f.waitForRun(t, 25*time.Second, func(r *runs.Run) bool {
55 return strings.HasPrefix(r.Ref, "refs/vole/candidates/") && r.Branch == "main"
56 })
57 if candidate.SHA == head || candidate.SHA == moved {
58 t.Fatalf("combined result %s is not a merge of the two tips", candidate.SHA)
59 }
60 parents, err := m.Git().CommitParents(candidate.SHA)
61 if err != nil {
62 t.Fatal(err)
63 }
64 if len(parents) != 2 || parents[0] != moved || parents[1] != head {
65 t.Fatalf("combined parents = %v, want [%s %s]", parents, moved, head)
66 }
67
68 // Run whichever jobs are queued (the base move's, then the combined result's)
69 // until the combined result is green.
70 runUntil(t, f, func() bool { return runSucceeded(f, candidate.SHA) })
71
72 deadline := time.Now().Add(25 * time.Second)
73 for {
74 tip, err := m.Git().RevParse("refs/heads/main")
75 if err != nil {
76 t.Fatal(err)
77 }
78 if tip == candidate.SHA {
79 break
80 }
81 if time.Now().After(deadline) {
82 t.Fatalf("main = %s after the combined result passed; want %s", tip, candidate.SHA)
83 }
84 time.Sleep(100 * time.Millisecond)
85 }
86 // The landing is the merge, so both sides are present in the tree that landed.
87 for _, name := range []string{"a.md", "b.md"} {
88 if _, err := m.Git().BlobAt(candidate.SHA, name); err != nil {
89 t.Fatalf("landed tree is missing %s: %v", name, err)
90 }
91 }
92 req, err := metadata.GetRequest(m.Git(), reqID)
93 if err != nil {
94 t.Fatal(err)
95 }
96 if req.State != "merged" || req.MergedHead != candidate.SHA {
97 t.Fatalf("request = %s at %s; want merged at %s", req.State, req.MergedHead, candidate.SHA)
98 }
99}
100
101// runUntil runs one queued job at a time until done reports true. The runner is
102// --once, so a test that has more than one queued job has to ask more than once.
103func runUntil(t *testing.T, f *fixture, done func() bool) {
104 t.Helper()
105 deadline := time.Now().Add(30 * time.Second)
106 for !done() {
107 if time.Now().After(deadline) {
108 t.Fatal("the queued job did not finish")
109 }
110 f.runOnce(t)
111 time.Sleep(50 * time.Millisecond)
112 }
113}
114
115func runSucceeded(f *fixture, sha string) bool {
116 list, err := f.srv.Runs.Recent(f.repo, 100)
117 if err != nil {
118 return false
119 }
120 for _, r := range list {
121 if r.SHA == sha && r.Status == runs.StatusSuccess {
122 return true
123 }
124 }
125 return false
126}
127