318 lines
12 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|