Files
mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go/preview_test.go
T

318 lines
12 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/forgekey"
)
const (
baseSHA = "abcdef0123456789abcdef0123456789abcdef01"
mergeSHA = "fedcba9876543210fedcba9876543210fedcba98"
treeSHA = "1111111111111111111111111111111111111111"
)
// previewRules answers the merge's commands as git does: the merge commit, the changed paths, the manifests, the trees.
func previewRules(conflict bool) func(string) (forgekey.Ran, bool) {
merged := false
return func(line string) (forgekey.Ran, bool) {
switch {
case strings.Contains(line, "--is-shallow-repository"):
return forgekey.Ran{Stdout: "true\n"}, true
case strings.Contains(line, " merge --no-ff"):
if conflict {
return forgekey.Ran{Status: 1, Stderr: "CONFLICT (content): Merge conflict in a.go\n"}, true
}
merged = true
return forgekey.Ran{}, true
case strings.Contains(line, "diff --name-only --diff-filter=U"):
return forgekey.Ran{Stdout: "a.go\nsub/b.go\n"}, true
case strings.Contains(line, "diff --name-only --no-renames"):
return forgekey.Ran{Stdout: "a.go\nmodules/m/main.go\n"}, true
case strings.Contains(line, " rev-parse HEAD") && merged:
return forgekey.Ran{Stdout: mergeSHA + "\n"}, true
case strings.Contains(line, " ls-files ") && strings.Contains(line, "module.json"):
return forgekey.Ran{Stdout: "modules/m/module.json\n"}, true
case strings.Contains(line, " rev-parse "+mergeSHA+":modules/m"):
return forgekey.Ran{Stdout: treeSHA + "\n"}, true
}
return forgekey.Ran{}, false
}
}
// 1675/AC1: with a base the fetch merges the head onto it with the SDK's merge (the identity and flags of the check
// merge, hooks off), and answers base, merge commit, changed paths and, per module directory, tree, manifest hash and lock hashes.
func TestFetchWithABaseMergesAndAnswersThePreview(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n", rule: previewRules(false)}
m, states := fetchModule(t, f)
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
// the workspace's module.json and go.sum are on disk once the fake checks the head out
for i, a := range args {
if a == "-C" && len(args) > i+1 && strings.Contains(strings.Join(args, " "), " checkout ") {
os.MkdirAll(filepath.Join(args[i+1], "modules", "m"), 0o755)
os.WriteFile(filepath.Join(args[i+1], "modules", "m", "module.json"), []byte("{}\n"), 0o644)
os.WriteFile(filepath.Join(args[i+1], "modules", "m", "go.sum"), []byte("sum\n"), 0o644)
}
}
return f.run(ctx, env, name, args...)
}
args := map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": baseSHA}
if _, err := fetchTool(t, m)(args); err != nil {
t.Fatal(err)
}
st := states.waitEnd(t)
if st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
var res FetchResult
if err := json.Unmarshal(st.Result, &res); err != nil {
t.Fatal(err)
}
if res.Base != baseSHA || res.Merge != mergeSHA || res.Commit != commitSHA || len(res.Conflicts) != 0 ||
strings.Join(res.Changed, ",") != "a.go,modules/m/main.go" || res.Programs == nil || len(*res.Programs) != 0 { // 1577: a merge says it found no program
t.Errorf("result = %s", st.Result)
}
mt, ok := res.Trees["modules/m"]
if !ok || mt.Tree != treeSHA || mt.Manifest != sha256Hex([]byte("{}\n")) || mt.Locks["go.sum"] != sha256Hex([]byte("sum\n")) {
t.Errorf("trees = %+v", res.Trees)
}
lines := strings.Join(f.lines(), "\n")
for _, want := range []string{"fetch --no-tags --unshallow origin " + baseSHA, "checkout -q --detach " + baseSHA,
"core.hooksPath=/dev/null", "merge --no-ff --no-edit --no-verify"} {
if !strings.Contains(lines, want) {
t.Errorf("git was not asked %q:\n%s", want, lines)
}
}
}
// 1675/AC2: a conflict is an answer: the step ends passed, the files are named, no merge commit, nothing downloaded.
func TestFetchWithAConflictEndsPassedAndNamesTheFiles(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n", rule: previewRules(true)}
m, states := fetchModule(t, f)
args := map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": baseSHA}
if _, err := fetchTool(t, m)(args); err != nil {
t.Fatal(err)
}
st := states.waitEnd(t)
if st.State != delivery.StepPassed {
t.Fatalf("a conflict ended the step %s: %s", st.State, st.Reason)
}
var res FetchResult
json.Unmarshal(st.Result, &res)
if res.Merge != "" || res.Base != baseSHA || strings.Join(res.Conflicts, ",") != "a.go,sub/b.go" || len(res.Changed) != 0 {
t.Errorf("result = %s", st.Result)
}
lines := strings.Join(f.lines(), "\n")
if !strings.Contains(lines, "merge --abort") || strings.Contains(lines, "mod download") {
t.Errorf("the merge was not undone, or modules were downloaded:\n%s", lines)
}
}
// 1675/AC3: no base is today's fetch: no merge, no extra command, and the answer carries none of the preview.
func TestFetchWithoutABaseIsTodaysFetch(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n"}
m, states := fetchModule(t, f)
fetchTool(t, m)(fetchArgs)
st := states.waitEnd(t)
lines := strings.Join(f.lines(), "\n")
if strings.Contains(lines, "merge") || strings.Contains(lines, "unshallow") {
t.Errorf("a fetch without a base merged:\n%s", lines)
}
for _, key := range []string{"base", "merge", "conflicts", "changed", "trees", "beside"} {
if strings.Contains(string(st.Result), `"`+key+`"`) {
t.Errorf("the answer carries %q without a base: %s", key, st.Result)
}
}
}
func TestABaseThatIsNotACommitIsRefusedBeforeAnythingRuns(t *testing.T) {
f := &fakeStep{}
m, states := fetchModule(t, f)
fetchTool(t, m)(map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "base": "main"})
if st := states.waitEnd(t); st.State != delivery.StepFailed || len(f.lines()) != 0 {
t.Errorf("the step ended %s after %d commands", st.State, len(f.lines()))
}
}
// 1677/AC1: each sibling is fetched into beside/<name> of the workspace, and the tests of the workspace are given
// MESH_CHECK_BESIDE naming that directory, at the unit level; a workspace without siblings is given none.
func TestSiblingsArePlacedBesideAndTheUnitLevelIsGivenTheirDirectory(t *testing.T) {
f := &fakeStep{gomods: "go.mod\n"}
m, states := fetchModule(t, f)
args := map[string]any{"repository": "novox/mesh-controller", "commit": commitSHA, "beside": []any{
map[string]any{"repository": "novox/mesh-catalog", "commit": commitSHA},
map[string]any{"repository": "novox/mesh-host", "commit": commitSHA}}}
if _, err := fetchTool(t, m)(args); err != nil {
t.Fatal(err)
}
st := states.waitEnd(t)
var res FetchResult
json.Unmarshal(st.Result, &res)
if st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
if len(res.Beside) != 2 || res.Beside[0].Dir != "mesh-catalog" || res.Beside[1].Dir != "mesh-host" {
t.Fatalf("result = %s", st.Result)
}
lines := strings.Join(f.lines(), "\n")
for _, d := range []string{"beside/mesh-catalog", "beside/mesh-host"} {
if !strings.Contains(lines, filepath.Join(m.Work, res.Workspace, d)+" init") && !strings.Contains(lines, "-C "+filepath.Join(m.Work, res.Workspace, d)) {
t.Errorf("nothing was fetched into %s:\n%s", d, lines)
}
}
if !strings.Contains(lines, "remote add origin ssh://mesh-forge/novox/mesh-catalog.git") {
t.Errorf("the sibling was not fetched over ssh:\n%s", lines)
}
calls := &[]cmdRecord{}
m.Run = throughBubblewrap(t, capturedRunner(t, "failing", calls), &[]shutOff{})
if err := os.MkdirAll(filepath.Join(m.Work, res.Workspace, "tree"), 0o755); err != nil {
t.Fatal(err)
}
if _, err := m.testToolRun(t, res.Workspace); err != nil {
t.Fatal(err)
}
states.waitEnd(t)
want := "MESH_CHECK_BESIDE=" + filepath.Join(m.Work, res.Workspace, "beside")
if !envHas(*calls, "go", want) {
t.Errorf("no go command was given %s", want)
}
}
func (m *Module) testToolRun(t *testing.T, ws string) (any, error) {
t.Helper()
for _, tool := range m.Tools(m.StepTools()) {
if tool.Name == "build_go_test" {
return tool.Run(map[string]any{"workspace": ws, "level": "unit"})
}
}
t.Fatal("no tool build_go_test")
return nil, nil
}
func envHas(calls []cmdRecord, name, want string) bool {
for _, c := range calls {
if strings.HasPrefix(c.line, name+" ") {
for _, e := range c.env {
if e == want {
return true
}
}
}
}
return false
}
func TestAWorkspaceWithoutSiblingsIsGivenNoBesideVariable(t *testing.T) {
m, states, ws, calls := workspaceFor(t, "failing")
runUnit(t, m, states, ws)
for _, c := range *calls {
for _, e := range c.env {
if strings.HasPrefix(e, "MESH_CHECK_BESIDE=") {
t.Errorf("%s was given %s", c.line, e)
}
}
}
}
func TestSiblingsThatAreNotRepositoriesOrCommitsAreRefused(t *testing.T) {
for name, beside := range map[string]any{
"not a list": "x",
"bad repository": []any{map[string]any{"repository": "../x/y", "commit": baseSHA}},
"bad commit": []any{map[string]any{"repository": "novox/a", "commit": "main"}},
"twice": []any{map[string]any{"repository": "novox/a", "commit": baseSHA}, map[string]any{"repository": "other/a", "commit": baseSHA}},
} {
f := &fakeStep{}
m, states := fetchModule(t, f)
fetchTool(t, m)(map[string]any{"repository": "novox/mesh-sdk", "commit": commitSHA, "beside": beside})
if st := states.waitEnd(t); st.State != delivery.StepFailed || len(f.lines()) != 0 {
t.Errorf("%s: the step ended %s after %d commands", name, st.State, len(f.lines()))
}
}
}
// 1288/AC5 (the checker's contract): an import path runs only in the go.mod directory whose module holds it; a
// relative pattern runs in every directory; no pattern is ./... everywhere.
func TestPackagesRunOnlyInTheModuleThatHoldsThem(t *testing.T) {
tree := t.TempDir()
for dir, mod := range map[string]string{".": "example.org/root", "tools": "example.org/root/tools", "other": "example.org/other"} {
os.MkdirAll(filepath.Join(tree, dir), 0o755)
os.WriteFile(filepath.Join(tree, dir, "go.mod"), []byte("module "+mod+"\n\ngo 1.26\n"), 0o644)
}
dirs := []string{".", "other", "tools"}
got, err := packagesByDir(tree, dirs, []string{"example.org/root/a", "example.org/root/tools/b", "example.org/other"})
if err != nil {
t.Fatal(err)
}
want := map[string]string{".": "example.org/root/a", "tools": "example.org/root/tools/b", "other": "example.org/other"}
for d, w := range want {
if strings.Join(got[d], ",") != w {
t.Errorf("%s runs %v, want %s", d, got[d], w)
}
}
got, _ = packagesByDir(tree, dirs, []string{"./x/..."})
if len(got["."]) != 1 || len(got["tools"]) != 1 || len(got["other"]) != 1 {
t.Errorf("a relative pattern = %v", got)
}
got, _ = packagesByDir(tree, dirs, nil)
if got["tools"][0] != "./..." {
t.Errorf("no pattern = %v", got)
}
if _, err := packagesByDir(tree, dirs, []string{"example.org/nowhere/x"}); err == nil || !strings.Contains(err.Error(), "example.org/nowhere/x") {
t.Errorf("a package no module holds: %v", err)
}
}
func sha256Hex(b []byte) string { h := sha256.Sum256(b); return hex.EncodeToString(h[:]) }
// 1677/AC (integration level): the integration level's go test is given MESH_CHECK_BESIDE when the workspace has siblings
// beside it, with the store's and the bus's addresses, and is given none when it has not.
func TestTheIntegrationLevelIsGivenTheBesideDirectoryOnlyWhenSiblingsWerePlaced(t *testing.T) {
for _, siblings := range []bool{true, false} {
f := newIntegFake(t, "slow")
m, states := integModule(t, f)
beside := filepath.Join(m.Work, "ws1", "beside")
if siblings {
if err := os.MkdirAll(filepath.Join(beside, "mesh-catalog"), 0o755); err != nil {
t.Fatal(err)
}
}
if _, st := runLevelTool(t, m, states, "integration"); st.State != delivery.StepPassed {
t.Fatalf("the step ended %s: %s", st.State, st.Reason)
}
tested := 0
for _, c := range f.calls {
if !strings.HasPrefix(c.line, "go ") || !strings.Contains(c.line, "-tags integration") {
continue
}
tested++
got := ""
hasNATS := false
for _, e := range c.env {
if strings.HasPrefix(e, "MESH_CHECK_BESIDE=") {
got = strings.TrimPrefix(e, "MESH_CHECK_BESIDE=")
}
hasNATS = hasNATS || strings.HasPrefix(e, "MESH_TEST_NATS=")
}
want := ""
if siblings {
want = beside
}
if got != want || !hasNATS {
t.Errorf("siblings %v: %s was given MESH_CHECK_BESIDE=%q (want %q), MESH_TEST_NATS present %v", siblings, c.line, got, want, hasNATS)
}
}
if tested == 0 {
t.Errorf("siblings %v: no integration go test ran", siblings)
}
}
}