package builder import ( "context" "encoding/json" "fmt" "os" "os/exec" "path/filepath" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/artifacts" "github.com/novox/mesh-controller/internal/facts" ) // A merge check on the build seat (novox/hq to-be 45 §9), against a real container runtime and a real // registry: the repository's own merge-check.sh runs with the facts the controller keeps, beside a // throwaway store and bus of **the versions the mesh runs**, and everything raised is removed. // // MESH_TEST_DOCKER=1 MESH_TEST_REGISTRY=127.0.0.1:15000 go test ./internal/builder -run Check func TestTheStoreAndBusAChecksStandsOnAreTheOnesTheMeshRuns(t *testing.T) { if got := StoreImage("17.11"); got != "postgres:17-alpine" { t.Errorf("a store at 17.11 is checked against %s", got) } if got := StoreImage("16.4 (Debian 16.4-1.pgdg120+1)"); got != "postgres:16-alpine" { t.Errorf("a store at 16.4 is checked against %s", got) } if got := BusImage("2.11.17"); got != "nats:2.11.17-alpine" { t.Errorf("a bus at 2.11.17 is checked against %s", got) } } // aRepository is a git repository holding these files, committed, and its head. func aCheckedRepository(t *testing.T, files map[string]string) (string, string) { t.Helper() dir := t.TempDir() git := func(args ...string) string { 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)) } git("init", "--quiet", "-b", "main") 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("commit", "--quiet", "-m", "x") return dir, git("rev-parse", "HEAD") } func checkEnvironment(t *testing.T) string { t.Helper() if os.Getenv("MESH_TEST_DOCKER") != "1" || os.Getenv("MESH_TEST_REGISTRY") == "" { t.Skip("MESH_TEST_DOCKER=1 and MESH_TEST_REGISTRY: a check raises containers and reads the registry") } registry := os.Getenv("MESH_TEST_REGISTRY") body, err := json.Marshal(facts.Facts{Format: facts.Format, Taken: time.Now().UTC(), Versions: facts.Versions{Bus: "2.11.17", Store: "17.11"}, Machines: []facts.Machine{{Name: "abcdef", Length: 6}}}) if err != nil { t.Fatal(err) } if _, err := (artifacts.Store{Address: registry}).PutTagged(t.Context(), facts.Repository, facts.Tag, facts.MediaType, body); err != nil { t.Fatal(err) } return registry } // goToolchain is the Go image a check's script runs in here: the base the controller's own image is built on. const goToolchain = "golang@sha256:8ac98ca534ac3f51e1f420a1dd2c15e74c75cfa0f23f3ad27eb5d7236c349a0c" func labelled(id string) []string { out, _ := exec.Command("docker", "ps", "-aq", "--filter", "label="+BuildLabel+"="+id).Output() return strings.Fields(string(out)) } func TestACheckRunsTheRepositorysOwnScriptBesideTheMeshsVersionsAndLeavesNothing(t *testing.T) { registry := checkEnvironment(t) // The script proves what it was given: the facts, a store that answers, a bus that answers — and no // container runtime socket. script := `set -e test -s "$MESH_FACTS" grep -q '"bus": "2.11.17"' "$MESH_FACTS" || grep -q '"bus":"2.11.17"' "$MESH_FACTS" case "$MESH_TEST_POSTGRES" in postgres://*127.0.0.1:*) ;; *) echo "no store: $MESH_TEST_POSTGRES"; exit 1;; esac case "$MESH_TEST_NATS" in nats://127.0.0.1:*) ;; *) echo "no bus: $MESH_TEST_NATS"; exit 1;; esac test "$MESH_CHECK_REPOSITORY" = "novox/hq" test "$MESH_CHECK_CHANGED" = "a.go,b.go" test ! -S /var/run/docker.sock || { echo "the check holds the container runtime's socket"; exit 1; } echo checked ` repo, head := aCheckedRepository(t, map[string]string{CheckScript: script}) id := fmt.Sprintf("check-test-%d", time.Now().UnixNano()) v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Number: 7, Paths: []string{"a.go", "b.go"}, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } // Nothing of the graph touched: the gate a pass that says so, the repository's own check run. if v.Gate == nil || v.Gate.Verdict != "pass" || v.Gate.Summary != noModule { t.Errorf("the gate answered %+v", v.Gate) } if v.Repo == nil || v.Repo.Verdict != "pass" || !strings.Contains(v.Report, "checked") { t.Fatalf("the repository's check answered %+v\n%s", v.Repo, v.Report) } if left := labelled(id); len(left) > 0 { t.Errorf("the check left %d container(s) behind", len(left)) } } func TestAFailingCheckFailsAndOneThatCannotRunIsNeverAPass(t *testing.T) { registry := checkEnvironment(t) repo, head := aCheckedRepository(t, map[string]string{CheckScript: "echo 'resource \"x.service\": refused'; exit 3\n"}) v, err := Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-fail-%d", time.Now().UnixNano()), Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Repo == nil || v.Repo.Verdict != "fail" || !strings.Contains(v.Repo.Summary, "refused") { t.Fatalf("a failing script answered %+v", v.Repo) } // A script in a toolchain the mesh does not hold: an error, never a pass. repo, head = aCheckedRepository(t, map[string]string{CheckScript: "# mesh-check-toolchain: cobol\nexit 0\n"}) v, err = Check(t.Context(), Command, CheckSpec{ID: fmt.Sprintf("check-cobol-%d", time.Now().UnixNano()), Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil || v.Repo == nil || v.Repo.Verdict != "error" { t.Fatalf("a script in a toolchain nobody holds answered %+v, %v", v.Repo, err) } // Past its bound: an error, not a pass. was := CheckTimeout CheckTimeout = 25 * time.Second t.Cleanup(func() { CheckTimeout = was }) repo, head = aCheckedRepository(t, map[string]string{CheckScript: "sleep 120\n"}) v, err = Check(context.Background(), Command, CheckSpec{ID: fmt.Sprintf("check-slow-%d", time.Now().UnixNano()), Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err == nil && (v.Repo == nil || v.Repo.Verdict != "error") { t.Fatalf("a check past its bound answered %+v", v.Repo) } // No facts: it cannot run, and says so. repo, head = aCheckedRepository(t, map[string]string{CheckScript: "exit 0\n"}) _, err = Check(t.Context(), Command, CheckSpec{ID: "check-nofacts", Repository: repo, Ref: head, Owner: "novox", Repo: "hq", Toolchain: goToolchain}, t.TempDir(), "127.0.0.1:1", GitCredential{}, nil) if err == nil || !strings.Contains(err.Error(), "facts snapshot") { t.Fatalf("a check with no facts said %v", err) } } // A repository that touches nothing of the graph and has no script of its own runs nothing, and says // both: the gate a pass that names why, the repository a warning — never silent. func TestACheckWithNothingToRunSaysSo(t *testing.T) { repo, head := aCheckedRepository(t, map[string]string{"README.md": "x"}) v, err := Check(t.Context(), Command, CheckSpec{ID: "check-nothing", Repository: repo, Ref: head, Owner: "novox", Repo: "hq"}, t.TempDir(), "", GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Gate == nil || v.Gate.Verdict != "pass" || v.Repo == nil || v.Repo.Verdict != "warning" || !strings.Contains(v.Repo.Summary, CheckScript) { t.Fatalf("a check with nothing to run said %+v / %+v", v.Gate, v.Repo) } // A gated change never takes that path: with no store to read the facts from, it cannot run. _, err = Check(t.Context(), Command, CheckSpec{ID: "check-gated", Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-catalog", Modules: []string{"gitea"}}, t.TempDir(), "", GitCredential{}, nil) if err == nil { t.Fatal("a change touching a module ran nothing and was not refused") } } // A script declares its toolchain among its first lines; go when it declares none. func TestAScriptDeclaresItsToolchain(t *testing.T) { for script, want := range map[string]string{ "#!/bin/sh\nset -eu\n": "go", "#!/bin/sh\n# mesh-check-toolchain: typescript\nnpm test\n": "typescript", "#!/bin/sh\n#mesh-check-toolchain:go\n": "go", "#!/bin/sh\necho '# mesh-check-toolchain: python'\n": "go", } { if got := ScriptToolchain([]byte(script)); got != want { t.Errorf("%q declares %q, read as %q", script, want, got) } } held := map[string]string{"mesh-tools/build": "reg/mesh-tools-build@sha256:a", "mesh-tools-go/build": "reg/go@sha256:b"} chains := ToolchainsOf(held) if chains["typescript"] != "reg/mesh-tools-build@sha256:a" || chains["go"] != "reg/go@sha256:b" || ToolchainOf(held) != chains["go"] { t.Fatalf("the toolchains held read as %v", chains) } if _, held := chains["python"]; held { t.Error("a toolchain the mesh does not hold read as held") } } // A node-engine change's validator is put in place of the one the judge vendors: its Go files, never its tests. func TestTheChangesValidatorReplacesTheVendoredOne(t *testing.T) { from, into := t.TempDir(), t.TempDir() for name, body := range map[string]string{"v.go": "package validate // new", "v_test.go": "package validate"} { if err := os.WriteFile(filepath.Join(from, name), []byte(body), 0o644); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(into, "old.go"), []byte("package validate // old"), 0o644); err != nil { t.Fatal(err) } if err := replaceGoFiles(from, into); err != nil { t.Fatal(err) } got, _ := filepath.Glob(filepath.Join(into, "*.go")) if len(got) != 1 || filepath.Base(got[0]) != "v.go" { t.Fatalf("the vendored validator holds %v", got) } } // aJudge is a controller that judges as told: `merge-gate` answers a warning, `module check` refuses a // manifest that says it is broken. What the gate layer is tested against without building the real one. var aJudge = map[string]string{ "go.mod": "module example.org/judge\n\ngo 1.22\n", "cmd/mesh-controller/main.go": `package main import ( "fmt" "os" "strings" ) func main() { switch { case len(os.Args) == 2 && os.Args[1] == "merge-gate": fmt.Println("usage: merge-gate --facts --store ") os.Exit(2) case len(os.Args) > 2 && os.Args[1] == "module": for _, m := range os.Args[3:] { body, _ := os.ReadFile(m) if strings.Contains(string(body), "broken") { fmt.Println(m + ": refused, it says it is broken") os.Exit(1) } fmt.Println(m + ": ok") } case os.Args[1] == "merge-gate": fmt.Println(` + "`" + `{"verdict":"warning","summary":"a merge rebuilds 14 module(s)"}` + "`" + `) } } `, } func TestTheGateRunsWhenTheGraphIsTouchedBesideTheRepositorysOwnCheck(t *testing.T) { registry := checkEnvironment(t) judgeRepo, judgeHead := aCheckedRepository(t, aJudge) beside := map[string]Beside{"mesh-controller": {Repository: judgeRepo, Ref: judgeHead}} check := func(files map[string]string, judge string) CheckVerdict { t.Helper() repo, head := aCheckedRepository(t, files) id := fmt.Sprintf("check-gate-%d", time.Now().UnixNano()) v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: head, Owner: "novox", Repo: "mesh-catalog", Number: 9, Paths: []string{"modules/gitea/index.ts"}, Beside: beside, Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Judge: judge, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if left := labelled(id); len(left) > 0 { t.Errorf("the check left %d container(s) behind", len(left)) } return v } v := check(map[string]string{"modules/gitea/module.json": `{"module":"gitea"}`, CheckScript: "echo own; exit 0\n"}, "") if v.Gate == nil || v.Gate.Verdict != "warning" || v.Gate.Summary != "a merge rebuilds 14 module(s)" || strings.Join(v.Gate.Modules, ",") != "gitea" || v.Verdict != "warning" { t.Fatalf("the gate answered %+v\n%s", v.Gate, v.Report) } if v.Repo == nil || v.Repo.Verdict != "pass" { t.Fatalf("its own check answered %+v", v.Repo) } v = check(map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`}, "") if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "module check") || v.Repo.Verdict != "warning" { t.Fatalf("a manifest the judge refuses answered %+v / %+v\n%s", v.Gate, v.Repo, v.Report) } // A fault the base branch already had is said and fails nothing; one the change brings fails. repo, _ := aCheckedRepository(t, map[string]string{"modules/gitea/module.json": `{"module":"gitea","broken":true}`}) git := func(args ...string) string { cmd := exec.Command("git", args...) cmd.Dir = repo 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)) } git("checkout", "--quiet", "-b", "change") if err := os.WriteFile(filepath.Join(repo, "modules/gitea/index.ts"), []byte("x"), 0o644); err != nil { t.Fatal(err) } git("add", "-A") git("commit", "--quiet", "-m", "y") id := fmt.Sprintf("check-base-%d", time.Now().UnixNano()) v, err := Check(t.Context(), Command, CheckSpec{ID: id, Repository: repo, Ref: git("rev-parse", "HEAD"), Owner: "novox", Repo: "mesh-catalog", Base: "main", Paths: []string{"modules/gitea/index.ts"}, Beside: beside, Modules: []string{"gitea"}, Manifests: []string{"modules/gitea/module.json"}, Toolchain: goToolchain}, t.TempDir(), registry, GitCredential{}, nil) if err != nil { t.Fatal(err) } if v.Gate.Verdict != "warning" || !strings.Contains(v.Report, "already") { t.Fatalf("a fault the base already had failed the change: %+v\n%s", v.Gate, v.Report) } // A change to the controller judges itself: one that does not build fails its own gate. v = check(map[string]string{"go.mod": "module x\n\ngo 1.22\n", "cmd/mesh-controller/main.go": "package main\nfunc main() { nope }\n", "modules/gitea/module.json": "{}"}, "self") if v.Gate.Verdict != "fail" || !strings.Contains(v.Gate.Summary, "does not build") { t.Fatalf("a controller that does not build judged itself %+v", v.Gate) } }