package builder import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // **A merge check judges the merge, not the head** (novox/hq ADR 0323, issue 341): the head merged into the trunk // commit in the check's own clone, never pushed; a head that does not merge cleanly fails, naming the files; the // verdict names the head, the trunk commit and the commits of the repositories read beside it. // aTrunkAndABranch is a repository whose main moved after the branch was cut: main holds trunkFiles over the // common files, the branch headFiles. It answers the repository, the branch's head and main's head. func aTrunkAndABranch(t *testing.T, common, trunkFiles, headFiles map[string]string) (string, string, string) { t.Helper() dir := t.TempDir() git := func(args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = dir cmd.Env = append(os.Environ(), "GIT_AUTHOR_NAME=t", "GIT_AUTHOR_EMAIL=t@example.org", "GIT_COMMITTER_NAME=t", "GIT_COMMITTER_EMAIL=t@example.org") out, err := cmd.CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } write := func(files map[string]string) { for name, body := range files { if err := os.MkdirAll(filepath.Dir(filepath.Join(dir, name)), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, name), []byte(body), 0o755); err != nil { t.Fatal(err) } } git("add", "-A") } git("init", "--quiet", "-b", "main") write(common) git("commit", "--quiet", "-m", "common") git("checkout", "--quiet", "-b", "change") write(headFiles) git("commit", "--quiet", "-m", "the change") head := git("rev-parse", "HEAD") git("checkout", "--quiet", "main") write(trunkFiles) git("commit", "--quiet", "-m", "the trunk moved") return dir, head, git("rev-parse", "HEAD") } func TestACheckJudgesTheHeadMergedIntoTheTrunk(t *testing.T) { // main's script passes only in a tree holding both the trunk's file and the head's: the merge. repo, head, trunk := aTrunkAndABranch(t, map[string]string{CheckScript: "test -f trunk-only && test -f head-only || { echo 'FAIL: not the merge'; exit 1; }\n"}, map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"}) beside, besideHead := aCheckedRepository(t, map[string]string{"README": "the catalogue"}) registry := aFactsRegistry(t) aRuntimeOnThisMachine(t) v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-lab", Number: 1, Base: "main", Toolchain: "golang", Beside: map[string]Beside{"mesh-catalog": {Repository: beside, Ref: besideHead}}}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Repo == nil || v.Repo.Verdict != "pass" { t.Fatalf("the check did not judge the head merged into the trunk: %+v\n%s", v.Repo, v.Report) } if v.Head != head[:12] || v.Trunk != trunk[:12] { t.Fatalf("the verdict names head %q and trunk %q, not %s and %s", v.Head, v.Trunk, head[:12], trunk[:12]) } if v.Beside["mesh-catalog"] != besideHead[:12] { t.Fatalf("the verdict names the catalogue read beside it as %q, not %s: %v", v.Beside["mesh-catalog"], besideHead[:12], v.Beside) } if v.Merge == "" || v.Merge == head || v.Merge == trunk { t.Fatalf("no merge of the head into the trunk was made for the check: %q", v.Merge) } } func TestAHeadThatDoesNotMergeCleanlyFailsNamingTheFiles(t *testing.T) { repo, head, trunk := aTrunkAndABranch(t, map[string]string{"store.go": "package store\n\nconst Declared = 1\n", "other.go": "package store\n"}, map[string]string{"store.go": "package store\n\nconst Declared = 2\n"}, map[string]string{"store.go": "package store\n\nconst Declared = 3\n"}) // No artifact store and no container runtime: a head that does not merge is judged before either is needed. v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-conflict", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-lab", Number: 2, Base: "main", Toolchain: "golang"}, t.TempDir(), "", GitCredential{}, nil) if err != nil { t.Fatalf("a head that does not merge cleanly was not judged: %v", err) } want := "does not merge cleanly into trunk " + trunk[:12] for name, l := range map[string]*Layer{"gate": v.Gate, "repository": v.Repo} { if l == nil || l.Verdict != "fail" || !strings.Contains(l.Summary, want) || !strings.Contains(l.Summary, "store.go") { t.Fatalf("its %s layer said %+v, not a failure saying %q and naming store.go", name, l, want) } } if strings.Contains(v.Summary, "other.go") { t.Fatalf("a file not in conflict is named: %s", v.Summary) } if v.Head != head[:12] || v.Trunk != trunk[:12] { t.Fatalf("the failure names head %q and trunk %q", v.Head, v.Trunk) } } // **The merge made for a check is only checked** (ADR 0323 §2): the forge never receives it. func TestTheMergeMadeForACheckIsNeverPushed(t *testing.T) { repo, head, trunk := aTrunkAndABranch(t, map[string]string{CheckScript: "true\n"}, map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"}) registry := aFactsRegistry(t) aRuntimeOnThisMachine(t) refsBefore := gitOut(t, repo, "for-each-ref") v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-push", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-lab", Number: 3, Base: "main", Toolchain: "golang"}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Merge == "" { t.Fatal("the check made no merge of the head into the trunk") } if err := exec.Command("git", "-C", repo, "cat-file", "-e", v.Merge+"^{commit}").Run(); err == nil { t.Fatalf("the merge %s made for the check reached the forge's repository", v.Merge) } if after := gitOut(t, repo, "for-each-ref"); after != refsBefore { t.Fatalf("the check moved a ref of the forge's repository:\nbefore %s\nafter %s", refsBefore, after) } if got := gitOut(t, repo, "rev-parse", "main"); got != trunk { t.Fatalf("the trunk moved to %s", got) } } // A delivery group's composed check merges each member's head into its own trunk's head (ADR 0323 §6), and names // both for each; a member that does not merge cleanly fails the composed check, naming it. func TestAGroupsComposedCheckMergesEachMemberIntoItsTrunk(t *testing.T) { repo, head, trunk := aTrunkAndABranch(t, map[string]string{CheckScript: "true\n"}, map[string]string{"trunk-only": "t"}, map[string]string{"head-only": "h"}) member, memberHead, memberTrunk := aTrunkAndABranch(t, map[string]string{"a": "1"}, map[string]string{"b": "trunk"}, map[string]string{"c": "head"}) registry := aFactsRegistry(t) aRuntimeOnThisMachine(t) v, err := Check(t.Context(), Command, CheckSpec{ID: "check-341-group", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-controller", Number: 4, Base: "main", Toolchain: "golang", Group: []GroupHead{{Owner: "novox", Repo: "mesh-host", Repository: member, Ref: memberHead, Base: "main"}}}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Head != head[:12] || v.Trunk != trunk[:12] { t.Fatalf("the composed check names its first head %q into %q", v.Head, v.Trunk) } if len(v.Members) != 1 || v.Members[0].Head != memberHead[:12] || v.Members[0].Trunk != memberTrunk[:12] { t.Fatalf("the composed check names its members %+v, not mesh-host %s into %s", v.Members, memberHead[:12], memberTrunk[:12]) } conflicting, conflictHead, _ := aTrunkAndABranch(t, map[string]string{"a": "1"}, map[string]string{"a": "2"}, map[string]string{"a": "3"}) v, err = Check(t.Context(), Command, CheckSpec{ID: "check-341-group-conflict", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-controller", Number: 4, Base: "main", Toolchain: "golang", Group: []GroupHead{{ Owner: "novox", Repo: "mesh-host", Repository: conflicting, Ref: conflictHead, Base: "main"}}}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Verdict != "fail" || !strings.Contains(v.Summary, "novox/mesh-host") || !strings.Contains(v.Summary, "does not merge cleanly") { t.Fatalf("a member that does not merge cleanly was answered %s: %s", v.Verdict, v.Summary) } } func gitOut(t *testing.T, dir string, args ...string) string { t.Helper() out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).CombinedOutput() if err != nil { t.Fatalf("git %v: %v\n%s", args, err, out) } return strings.TrimSpace(string(out)) } // No hook runs when a check makes its merge: not one in the clone, whatever the build machine's git configuration says. func TestACheckMergeRunsNoHook(t *testing.T) { repo, head, _ := aTrunkAndABranch(t, map[string]string{"a": "1"}, map[string]string{"b": "trunk"}, map[string]string{"c": "head"}) clone := filepath.Join(t.TempDir(), "clone") if out, err := exec.Command("git", "clone", "--quiet", repo, clone).CombinedOutput(); err != nil { t.Fatalf("%v\n%s", err, out) } ran := filepath.Join(t.TempDir(), "ran") for _, hook := range []string{"pre-merge-commit", "commit-msg", "prepare-commit-msg", "post-merge"} { if err := os.WriteFile(filepath.Join(clone, ".git", "hooks", hook), []byte("#!/bin/sh\necho "+hook+" >> "+ran+"\n"), 0o755); err != nil { t.Fatal(err) } } m, err := mergeIntoTrunk(t.Context(), Command, clone, "main", head) if err != nil || m.Merge == "" { t.Fatalf("the merge was not made: %+v, %v", m, err) } if said, err := os.ReadFile(ran); err == nil { t.Fatalf("a hook ran when the check made its merge: %s", said) } }