package main import ( "context" "os" "path/filepath" "slices" "strings" "sync" "testing" "time" "git.novox.be/novox/mesh-sdk/go/forgekey" ) // A fake machine for the step tests: a toolchain directory laid out as `npm ci --no-bin-links` leaves it, a work // directory, a clock that only the fake commands advance, and a runner that records every command and answers by // what the test set. type call struct { dir string // env -C name string // the program after env -C args []string // its arguments env []string } type fakeMachine struct { mu sync.Mutex calls []call now time.Time // cost is how long a program takes, by the base name of its path; answer is what it prints and its status; // effect runs the program's effects on the disk. cost map[string]time.Duration answer map[string]forgekey.Ran effect func(c call) } func (f *fakeMachine) run(_ context.Context, env []string, name string, args ...string) forgekey.Ran { f.mu.Lock() defer f.mu.Unlock() c := call{name: name, args: args, env: env} key := filepath.Base(name) if name == "env" && len(args) >= 3 && args[0] == "-C" { c = call{dir: args[1], name: args[2], args: args[3:], env: env} key = filepath.Base(c.name) if key == "node" && len(c.args) > 0 { key = filepath.Base(c.args[0]) // node } } else if name == "git" { for _, a := range args { if slices.Contains([]string{"init", "remote", "fetch", "checkout", "rev-parse", "ls-files", "merge", "diff"}, a) { key = "git " + a break } } } f.calls = append(f.calls, c) f.now = f.now.Add(f.cost[key]) if f.effect != nil { f.effect(c) } return f.answer[key] } func (f *fakeMachine) clock() time.Time { f.mu.Lock(); defer f.mu.Unlock(); return f.now } // keys of the programs: tsc, esbuild, npm. func newMachine(t *testing.T) (*Module, *fakeMachine, string) { t.Helper() home, cache, work := t.TempDir(), t.TempDir(), t.TempDir() f := &fakeMachine{now: time.Unix(1_000_000, 0), cost: map[string]time.Duration{}, answer: map[string]forgekey.Ran{}} m := &Module{Node: "ace", Recipe: Recipe{Steps: "1", Lock: "lock"}, Toolchain: &Toolchain{Paths: Paths{Home: home, Cache: cache}, Registry: func() (Registry, error) { return Registry{URL: "https://git.novox.be/api/packages/novox/npm/"}, nil }}, Work: work, Run: f.run, Clock: f.clock, Ctx: context.Background()} // The toolchain directory as npm ci leaves it, with no bin links. nm := filepath.Join(m.toolchainDir(), "node_modules") for _, p := range []string{"typescript/bin/tsc", "esbuild/bin/esbuild", "@esbuild/linux-x64/bin/esbuild", "@novox/mesh-sdk/dist/stdio.js", "@types/node/index.d.ts", "nats/lib/nats.js"} { write(t, filepath.Join(nm, p), "x", 0o644) } return m, f, work } func write(t *testing.T, path, body string, mode os.FileMode) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(body), mode); err != nil { t.Fatal(err) } } // workspaceWith makes workspace "w1" of the machine with the files given (path under tree -> body). func workspaceWith(t *testing.T, work string, files map[string]string) string { t.Helper() for p, body := range files { write(t, filepath.Join(work, "w1", "tree", filepath.FromSlash(p)), body, 0o644) } return "w1" } func symlinksUnder(t *testing.T, root string) []string { t.Helper() var out []string filepath.Walk(root, func(p string, info os.FileInfo, err error) error { if err == nil && info.Mode()&os.ModeSymlink != 0 { out = append(out, p) } return nil }) return out } func joined(c call) string { return c.name + " " + strings.Join(c.args, " ") }