The operator's desktop slows while it builds (shanks at 92%, the Go build runtime at 1650% of 24 processors). The build modules declare cpu-cap (default shanks=50,g14=50,ace=75), which the composer turns into the runtime's quota at idle priority; each step's go, node and buildah commands get GOMAXPROCS and UV_THREADPOOL_SIZE for their part of it, and the status tools show the limits and what the kernel holds the runtime to.
310 lines
12 KiB
Go
310 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/forgekey"
|
|
)
|
|
|
|
const commitSHA = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
var fetchArgs = map[string]any{"repository": "novox/mesh-catalog", "commit": commitSHA}
|
|
|
|
// fetchMachine is a machine whose git checks out the given files into the tree it is asked to fetch into (the fake
|
|
// answers the commands of FetchCommit and `git ls-files`), and whose npm ci writes the packages the test names.
|
|
func fetchMachine(t *testing.T, files map[string]string, installs map[string]string) (*Module, *fakeMachine) {
|
|
t.Helper()
|
|
m, f, _ := newMachine(t)
|
|
f.answer["git rev-parse"] = forgekey.Ran{Stdout: commitSHA + "\n"}
|
|
var manifests []string
|
|
for p := range files {
|
|
if strings.HasSuffix(p, "module.json") {
|
|
manifests = append(manifests, p)
|
|
}
|
|
}
|
|
f.answer["git ls-files"] = forgekey.Ran{Stdout: strings.Join(manifests, "\n") + "\n"}
|
|
f.effect = func(c call) {
|
|
switch {
|
|
case c.name == "git" && slices_contains(c.args, "checkout"):
|
|
for i, a := range c.args {
|
|
if a == "-C" {
|
|
for p, body := range files {
|
|
write(t, filepath.Join(c.args[i+1], filepath.FromSlash(p)), body, 0o644)
|
|
}
|
|
}
|
|
}
|
|
case c.name == "npm":
|
|
for i, a := range c.args {
|
|
if a == "--prefix" {
|
|
for p, body := range installs {
|
|
write(t, filepath.Join(c.args[i+1], "node_modules", filepath.FromSlash(p)), body, 0o644)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return m, f
|
|
}
|
|
|
|
func slices_contains(l []string, s string) bool {
|
|
for _, x := range l {
|
|
if x == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func typeScriptModule(pkg string) map[string]string {
|
|
return map[string]string{
|
|
"modules/m/module.json": `{"module":"m","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["tools/index.js"]}]}}`,
|
|
"modules/m/package.json": pkg,
|
|
}
|
|
}
|
|
|
|
func npmCalls(f *fakeMachine) []call {
|
|
var out []call
|
|
for _, c := range f.calls {
|
|
if c.name == "npm" {
|
|
out = append(out, c)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 1489/AC1: a module whose package.json names only @novox/mesh-sdk and @types/node (and typescript) installs nothing
|
|
// and the step passes.
|
|
func TestAModuleNamingOnlyTheSdkAndTypesInstallsNothing(t *testing.T) {
|
|
m, f := fetchMachine(t, typeScriptModule(`{"dependencies":{"@novox/mesh-sdk":"^0.1.6"},"devDependencies":{"@types/node":"^22","typescript":"^5"}}`), nil)
|
|
out, err := m.fetch(context.Background(), fetchArgs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := out.(FetchResult)
|
|
if len(npmCalls(f)) != 0 || len(r.Installed) != 0 || len(r.Modules) != 1 || r.Modules[0] != "modules/m" || r.Commit != commitSHA {
|
|
t.Errorf("result = %+v, npm calls %d, want the module found and nothing installed", r, len(npmCalls(f)))
|
|
}
|
|
if _, err := os.Stat(filepath.Join(m.Work, r.Workspace, "tree", "modules", "m", "module.json")); err != nil {
|
|
t.Errorf("the commit is not checked out in the workspace: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1489/AC2: a module naming lodash with a lock gets npm ci --omit=dev --ignore-scripts (fake runner), its install put
|
|
// at its node_modules without the SDK and without any copy of it; without a lock the step fails naming the directory.
|
|
func TestAModuleNamingLodashWithALockIsInstalledWithoutTheSdkAndWithoutALockFails(t *testing.T) {
|
|
files := typeScriptModule(`{"dependencies":{"@novox/mesh-sdk":"^0.1.6","lodash":"^4"},"devDependencies":{"x":"1"},"scripts":{"test":"y"}}`)
|
|
files["modules/m/package-lock.json"] = `{"lockfileVersion":3}`
|
|
m, f := fetchMachine(t, files, map[string]string{"lodash/index.js": "l", "@novox/mesh-sdk/index.js": "sdk", "a/node_modules/@novox/mesh-sdk/i.js": "nested"})
|
|
out, err := m.fetch(context.Background(), fetchArgs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := out.(FetchResult)
|
|
calls := npmCalls(f)
|
|
if len(calls) != 1 {
|
|
t.Fatalf("npm ran %d times, want once", len(calls))
|
|
}
|
|
line := strings.Join(calls[0].args, " ")
|
|
if !strings.HasPrefix(line, "ci --omit=dev --omit=peer --ignore-scripts --no-bin-links") {
|
|
t.Errorf("ran npm %s", line)
|
|
}
|
|
nm := filepath.Join(m.Work, r.Workspace, "tree", "modules", "m", "node_modules")
|
|
if _, err := os.Stat(filepath.Join(nm, "lodash", "index.js")); err != nil {
|
|
t.Errorf("lodash is not at the module's node_modules: %v", err)
|
|
}
|
|
for _, gone := range []string{"@novox/mesh-sdk", "a/node_modules/@novox/mesh-sdk"} {
|
|
if _, err := os.Stat(filepath.Join(nm, gone)); err == nil {
|
|
t.Errorf("a copy of the SDK is left at %s", gone)
|
|
}
|
|
}
|
|
if got := strings.Join(r.Installed, ","); got != "modules/m" {
|
|
t.Errorf("installed = %q", got)
|
|
}
|
|
// the module's own package.json is untouched: the install worked on a scratch copy.
|
|
if raw, _ := os.ReadFile(filepath.Join(m.Work, r.Workspace, "tree", "modules", "m", "package.json")); !strings.Contains(string(raw), "scripts") {
|
|
t.Errorf("the module's package.json was rewritten: %s", raw)
|
|
}
|
|
var scratch []string
|
|
for _, e := range mustReadDir(t, filepath.Join(m.Work, r.Workspace)) {
|
|
scratch = append(scratch, e)
|
|
}
|
|
if strings.Contains(strings.Join(scratch, ","), "npm-") {
|
|
t.Errorf("the scratch install is left in the workspace: %v", scratch)
|
|
}
|
|
|
|
}
|
|
|
|
// 1489 (the 1459 settlement): a module naming packages with no package-lock.json does not fail the fetch: it passes,
|
|
// installs nothing for that module, and its result lists the refused bundle with its reason; a build of that bundle
|
|
// fails naming it.
|
|
func TestAModuleWithoutALockIsRefusedInTheResultNotTheFetch(t *testing.T) {
|
|
files := typeScriptModule(`{"dependencies":{"@novox/mesh-sdk":"^0.1.6","lodash":"^4"}}`)
|
|
files["modules/ok/module.json"] = `{"module":"ok","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["tools/index.js"]}]}}`
|
|
files["modules/ok/package.json"] = `{"dependencies":{"@novox/mesh-sdk":"^0.1.6"}}`
|
|
m, f := fetchMachine(t, files, nil)
|
|
out, err := m.fetch(context.Background(), fetchArgs)
|
|
if err != nil {
|
|
t.Fatalf("a module without a lock failed the fetch: %v", err)
|
|
}
|
|
r := out.(FetchResult)
|
|
if len(npmCalls(f)) != 0 || len(r.Installed) != 0 || len(r.Modules) != 2 {
|
|
t.Errorf("result = %+v, npm calls %d", r, len(npmCalls(f)))
|
|
}
|
|
if len(r.Refused) != 1 || r.Refused[0].Module != "modules/m" ||
|
|
!strings.Contains(r.Refused[0].Reason, "names packages beyond the SDK and has no package-lock.json") || !strings.Contains(r.Refused[0].Reason, "lodash") {
|
|
t.Fatalf("refused = %+v, want modules/m with its reason", r.Refused)
|
|
}
|
|
// the build of that bundle fails naming it, without running a compiler.
|
|
f.calls = nil
|
|
_, err = m.build(context.Background(), map[string]any{"workspace": r.Workspace, "module": "modules/m", "artifact": "tools"})
|
|
if err == nil || !strings.Contains(err.Error(), "modules/m") || !strings.Contains(err.Error(), "no package-lock.json") || len(f.calls) != 0 {
|
|
t.Errorf("build = %v after %d commands, want the refusal naming modules/m and nothing run", err, len(f.calls))
|
|
}
|
|
}
|
|
|
|
// 1489 (review 4): a bundle depending on another @novox package is installed, the mesh's registry given to npm ci
|
|
// through --userconfig, as the toolchain's install gives it.
|
|
func TestAnotherNovoxPackageIsInstalledFromTheRegistryThroughAUserconfig(t *testing.T) {
|
|
files := typeScriptModule(`{"dependencies":{"@novox/other":"^1","lodash":"^4"}}`)
|
|
files["modules/m/package-lock.json"] = `{}`
|
|
m, f := fetchMachine(t, files, map[string]string{"@novox/other/index.js": "o", "lodash/index.js": "l"})
|
|
m.Toolchain.Registry = func() (Registry, error) {
|
|
return Registry{URL: "https://git.novox.be/api/packages/novox/npm/", Username: "mesh-build-node", Password: "pw"}, nil
|
|
}
|
|
var npmrc string
|
|
inner := f.effect
|
|
f.effect = func(c call) {
|
|
inner(c)
|
|
for i, a := range c.args {
|
|
if c.name == "npm" && a == "--userconfig" {
|
|
raw, _ := os.ReadFile(c.args[i+1])
|
|
npmrc = string(raw)
|
|
}
|
|
}
|
|
}
|
|
out, err := m.fetch(context.Background(), fetchArgs)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(npmrc, "@novox:registry=https://git.novox.be/api/packages/novox/npm/") || !strings.Contains(npmrc, ":_auth=") {
|
|
t.Errorf("npm ci was given the config %q", npmrc)
|
|
}
|
|
if strings.Contains(strings.Join(npmCalls(f)[0].args, " "), "pw") {
|
|
t.Errorf("the credential is on npm's command line")
|
|
}
|
|
r := out.(FetchResult)
|
|
if _, err := os.Stat(filepath.Join(m.Work, r.Workspace, "tree", "modules", "m", "node_modules", "@novox", "other", "index.js")); err != nil {
|
|
t.Errorf("@novox/other was not installed: %v", err)
|
|
}
|
|
for _, e := range mustReadDir(t, filepath.Join(m.Work, r.Workspace)) {
|
|
if strings.HasPrefix(e, "npm-") {
|
|
t.Errorf("the scratch install (and its credential file) is left: %s", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func mustReadDir(t *testing.T, dir string) []string {
|
|
t.Helper()
|
|
es, err := os.ReadDir(dir)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var out []string
|
|
for _, e := range es {
|
|
out = append(out, e.Name())
|
|
}
|
|
return out
|
|
}
|
|
|
|
// 1489/AC3: given a process environment holding a token, no command of the step sees it. The commands the step makes
|
|
// are given PATH, HOME and the locale and their own words; and the real clean runner, run on `env` here, prints only
|
|
// those.
|
|
func TestNoCommandOfTheStepSeesATokenOfTheProcess(t *testing.T) {
|
|
t.Setenv("MESH_BUS_CREDENTIAL", "hunter2")
|
|
t.Setenv("NPM_TOKEN", "hunter3")
|
|
files := typeScriptModule(`{"dependencies":{"lodash":"^4"}}`)
|
|
files["modules/m/package-lock.json"] = `{}`
|
|
m, f := fetchMachine(t, files, map[string]string{"lodash/index.js": "l"})
|
|
if _, err := m.fetch(context.Background(), fetchArgs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(f.calls) < 6 {
|
|
t.Fatalf("only %d commands ran", len(f.calls))
|
|
}
|
|
for _, c := range f.calls {
|
|
for _, e := range c.env {
|
|
if strings.Contains(e, "hunter") || strings.HasPrefix(e, "MESH_") || strings.HasPrefix(e, "NPM_TOKEN") {
|
|
t.Errorf("%s %v was given %s", c.name, c.args[:min(len(c.args), 3)], e)
|
|
}
|
|
}
|
|
if len(c.env) < 3 || !strings.HasPrefix(c.env[0], "PATH=") || !strings.HasPrefix(c.env[1], "HOME=") || c.env[2] != "LC_ALL=C" {
|
|
t.Errorf("%s was given %v, want PATH, HOME and the locale first", c.name, c.env)
|
|
}
|
|
// And its threads, its part of the node's processor cap (issue 1806).
|
|
if !slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "GOMAXPROCS=") }) ||
|
|
!slices.ContainsFunc(c.env, func(e string) bool { return strings.HasPrefix(e, "UV_THREADPOOL_SIZE=") }) {
|
|
t.Errorf("%s was given %v, without its threads", c.name, c.env)
|
|
}
|
|
}
|
|
// The runner main wires: what it hands a command is exactly what the step gave.
|
|
ran := forgekey.ExecRunnerClean(10*time.Second)(context.Background(), append(m.baseEnv(), "ONLY=this"), "env")
|
|
if ran.Status != 0 || strings.Contains(ran.Stdout, "hunter") || !strings.Contains(ran.Stdout, "ONLY=this") {
|
|
t.Errorf("the clean runner printed %q", ran.Stdout)
|
|
}
|
|
}
|
|
|
|
func TestFetchRefusesWhatIsNotARepositoryBeforeAnythingRuns(t *testing.T) {
|
|
m, f := fetchMachine(t, nil, nil)
|
|
for _, repo := range []string{"", "x", "../etc/passwd", "a/b/c", "a/..", "o/r;rm"} {
|
|
if _, err := m.fetch(context.Background(), map[string]any{"repository": repo, "commit": commitSHA}); err == nil {
|
|
t.Errorf("%q was accepted", repo)
|
|
}
|
|
}
|
|
if len(f.calls) != 0 {
|
|
t.Errorf("%d commands ran", len(f.calls))
|
|
}
|
|
}
|
|
|
|
func TestFetchAsksGitForTheSshRemote(t *testing.T) {
|
|
m, f := fetchMachine(t, typeScriptModule(`{}`), nil)
|
|
if _, err := m.fetch(context.Background(), fetchArgs); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var all []string
|
|
for _, c := range f.calls {
|
|
all = append(all, c.name+" "+strings.Join(c.args, " "))
|
|
}
|
|
lines := strings.Join(all, "\n")
|
|
for _, want := range []string{"remote add origin ssh://mesh-forge/novox/mesh-catalog.git", "fetch --no-tags --depth 1 origin " + commitSHA, "checkout -q --detach " + commitSHA} {
|
|
if !strings.Contains(lines, want) {
|
|
t.Errorf("git was not asked %q:\n%s", want, lines)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestWorkspacesOlderThanADayAreRemoved(t *testing.T) {
|
|
m, _, work := newMachine(t)
|
|
old, fresh := filepath.Join(work, "aaaa"), filepath.Join(work, "bbbb")
|
|
for _, d := range []string{old, fresh, filepath.Join(work, "Not-A-Workspace")} {
|
|
os.MkdirAll(d, 0o755)
|
|
}
|
|
os.Chtimes(old, time.Now().Add(-25*time.Hour), time.Now().Add(-25*time.Hour))
|
|
os.Chtimes(filepath.Join(work, "Not-A-Workspace"), time.Now().Add(-48*time.Hour), time.Now().Add(-48*time.Hour))
|
|
m.CleanWorkspaces(time.Now())
|
|
if _, err := os.Stat(old); err == nil {
|
|
t.Errorf("the old workspace is kept")
|
|
}
|
|
if _, err := os.Stat(fresh); err != nil {
|
|
t.Errorf("the fresh workspace was removed")
|
|
}
|
|
if _, err := os.Stat(filepath.Join(work, "Not-A-Workspace")); err != nil {
|
|
t.Errorf("a directory that is no workspace was removed")
|
|
}
|
|
}
|