223 lines
9.9 KiB
Go
223 lines
9.9 KiB
Go
package builder
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"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)
|
|
}
|
|
}
|
|
|
|
// **Issue 306**: the throwaway store is raised without durability — it lives for one check — so a suite
|
|
// that makes a database per test does not wait on the disk of a busy machine for every flush.
|
|
|
|
// aRepository is a git repository holding these files, committed, and its 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"
|
|
|
|
// 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.
|
|
|
|
// 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
func main() {
|
|
switch {
|
|
case len(os.Args) == 2 && os.Args[1] == "merge-gate":
|
|
fmt.Println("usage: merge-gate --facts <file> --store <postgres>")
|
|
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":
|
|
for i, a := range os.Args {
|
|
if a == "--group" && i+1 < len(os.Args) {
|
|
body, _ := os.ReadFile(os.Args[i+1])
|
|
var heads []struct {
|
|
Repository string ` + "`json:\"repository\"`" + `
|
|
Tree string ` + "`json:\"tree\"`" + `
|
|
}
|
|
_ = json.Unmarshal(body, &heads)
|
|
var said []string
|
|
for _, h := range heads {
|
|
if _, err := os.Stat(h.Tree + "/module.json"); err == nil {
|
|
said = append(said, h.Repository)
|
|
}
|
|
}
|
|
fmt.Printf("{\"verdict\":\"pass\",\"summary\":\"composed with %s\"}\n", strings.Join(said, ","))
|
|
return
|
|
}
|
|
}
|
|
fmt.Println(` + "`" + `{"verdict":"warning","summary":"a merge rebuilds 14 module(s)"}` + "`" + `)
|
|
}
|
|
}
|
|
`,
|
|
}
|
|
|
|
// **Only a commit on the trunk is published** (novox/hq ADR 0238): the build seat reads, from the clone it
|
|
// just made, the branch the forge names its default and whether the commit built is on it.
|
|
|
|
// **Issue 286**: a failed merge-check.sh was said by its last line — a bare "FAIL", or the name of a file
|
|
// gofmt listed — which named nothing a reader could act on. The status says what failed.
|
|
func TestAFailedScriptIsSaidByWhatFailed(t *testing.T) {
|
|
for out, want := range map[string]string{
|
|
"not gofmt'd:\ninternal/bootstrap/publish_test.go\n": "not gofmt'd by the toolchain's gofmt: internal/bootstrap/publish_test.go",
|
|
"ok \tx/a\t1s\n--- FAIL: TestTheInstallersFirstUserList (0.59s)\n t.go:37: refused\nFAIL\nFAIL\tx/b\t1s\nFAIL\n": "--- FAIL: TestTheInstallersFirstUserList (0.59s)",
|
|
"ok \tx/a\t1s\nFAIL\tx/b [build failed]\nFAIL\n": "FAIL\tx/b [build failed]",
|
|
"npm ERR! missing script: typecheck\n": "npm ERR! missing script: typecheck",
|
|
} {
|
|
if got := whatFailed(out); got != want {
|
|
t.Errorf("%q is said as %q, not %q", out, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **Issue 285**: a judge from before the rule passed "every machine composes with the change as it did
|
|
// without (0 of 4 compose)". The seat reads the machines itself: a machine the mesh composes that did not
|
|
// compose in the gate's store makes the gate an error, whatever judged it.
|
|
func TestTheSeatCallsAGateThatRaisedNoMachineAnError(t *testing.T) {
|
|
old := "bus users without a credential: controller\n" + `{"verdict":"pass","summary":"every machine composes with the change as it did without (0 of 2 compose)",
|
|
"machines":[{"described":"the hub","live-composes":true,"base":{"composes":false}},
|
|
{"described":"a machine","live-composes":true,"base":{"composes":false}}]}`
|
|
v, ok := readGateVerdict([]byte(old))
|
|
if !ok {
|
|
t.Fatal("a verdict after a line of composition's was not read")
|
|
}
|
|
verdict, summary, raised := gateRaisedTheMesh(v)
|
|
if raised || verdict != "error" || !strings.Contains(summary, "2 of 2 machines") || !strings.Contains(summary, "the hub") {
|
|
t.Errorf("0 of 2 composing is %q %q", verdict, summary)
|
|
}
|
|
good := `{"verdict":"pass","summary":"(2 of 2 compose)","machines":[{"described":"the hub","live-composes":true,"base":{"composes":true}},
|
|
{"described":"a laptop","live-composes":false,"base":{"composes":false}}]}`
|
|
v, _ = readGateVerdict([]byte(good))
|
|
if _, _, raised := gateRaisedTheMesh(v); !raised {
|
|
t.Error("a machine that does not compose on the mesh either was read as the gate's failure")
|
|
}
|
|
failed := `{"verdict":"fail","summary":"x","machines":[{"described":"the hub","live-composes":true,"base":{"composes":false}}]}`
|
|
v, _ = readGateVerdict([]byte(failed))
|
|
if _, _, raised := gateRaisedTheMesh(v); !raised {
|
|
t.Error("a failing verdict is the change's, and stands")
|
|
}
|
|
}
|
|
|
|
// **Issue 285**: an ask redelivered after its holder stopped mid-check found its own throwaway store still
|
|
// there under its name, and the check said it could not run. What an earlier delivery left goes first.
|
|
|
|
// **A repository in two languages checks both, each in its own toolchain** (novox/hq issue 302): the lab's
|
|
// merge-check.sh runs in the TypeScript toolchain, and its Go replays were said "NOT CHECKED HERE" on every
|
|
// pull request, so a register that did not compile would have merged green. Its script declares the Go
|
|
// part; the check runs it in the Go toolchain's container after the script, and the layer passes only
|
|
// when both do.
|
|
|
|
// A branch tried before its pull request may narrow what the repository's script runs (novox/hq issue 686); a
|
|
// pull request's check is handed nothing, and runs whole.
|
|
func TestOnlyATrysSelectionReachesTheScript(t *testing.T) {
|
|
if env := selection(CheckSpec{Number: 7}); len(env) != 0 {
|
|
t.Fatalf("a pull request's check was handed a selection: %v", env)
|
|
}
|
|
env := selection(CheckSpec{Tests: "^TestX$", StoreOnly: true})
|
|
if len(env) != 2 || env[0] != "MESH_CHECK_TESTS=^TestX$" || env[1] != "MESH_CHECK_STORE_ONLY=1" {
|
|
t.Fatalf("a try's selection: %v", env)
|
|
}
|
|
}
|