Merge pull request 'The Go fetch takes zero-version requirements from the module graph and proves them with a shut-off load; mesh-build-image and -node name nodebuildgo (issue 1966)' (#654) from fix/1966-fetch-module-graph into main

This commit was merged in pull request #654.
This commit is contained in:
2026-11-10 14:48:41 +00:00
9 changed files with 263 additions and 34 deletions
@@ -13,6 +13,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"slices"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@@ -179,6 +180,11 @@ func (m *Module) fetchInto(ctx context.Context, req sdkstep.FetchRequest, id str
return return
} }
var more []TakenVersion var more []TakenVersion
// Then what only a module the directory replaces names, which the download would not miss (pins.go).
more, errs[i] = m.takeReplacedZeroPins(ctx, root, repository, filepath.Join(tree, d), d, req.Packages, taken[i], &pins)
if taken[i] = append(taken[i], more...); errs[i] != nil {
return
}
more, errs[i] = m.downloadModules(ctx, root, repository, filepath.Join(tree, d), d, &pins, req.Packages) more, errs[i] = m.downloadModules(ctx, root, repository, filepath.Join(tree, d), d, &pins, req.Packages)
taken[i] = append(taken[i], more...) taken[i] = append(taken[i], more...)
}() }()
@@ -207,7 +213,9 @@ func (m *Module) downloadModules(ctx context.Context, root, repository, dir, d s
for { for {
failure := m.downloadAndVerify(ctx, dir, d) failure := m.downloadAndVerify(ctx, dir, d)
if failure == nil { if failure == nil {
return taken, nil // The proof that what was downloaded is all the shut-off steps load: else the fetch fails here, naming the
// package, and not a vet, a test or a build later with less to say (issue 1966).
return taken, m.shutOffLoad(ctx, m.goEnvFor(dir), dir, d)
} }
// A requirement at the zero version names no commit: it is taken from the asked versions or the trunk's latest. // A requirement at the zero version names no commit: it is taken from the asked versions or the trunk's latest.
took, err := m.takeZeroPins(ctx, root, repository, dir, d, failure.Error(), asked, pins) took, err := m.takeZeroPins(ctx, root, repository, dir, d, failure.Error(), asked, pins)
@@ -227,6 +235,27 @@ func (m *Module) downloadModules(ctx context.Context, root, repository, dir, d s
} }
} }
// shutOffLoadFormat is what go list -e says of each package it loads: nothing, or the package and Go's words of its error.
const shutOffLoadFormat = `{{with .Error}}{{$.ImportPath}}: {{.Err}}{{"\n"}}{{end}}`
// shutOffLoad loads the packages of dir, with their tests and every package they import, as a shut-off step does
// (GOPROXY=off, the step's own environment env), and fails naming each package whose load would reach the network, in
// Go's words. A package that fails to load for another cause (a compile error, an import of a package of the module
// that does not exist) is the shut-off steps' to say, with their own verdict lines, and is not the fetch's.
func (m *Module) shutOffLoad(ctx context.Context, env []string, dir, d string) error {
r := m.Run(ctx, append(env, "GOPROXY=off"), "go", "-C", dir, "list", "-e", "-deps", "-test", "-f", shutOffLoadFormat, "./...")
var lost []string
for _, l := range strings.Split(r.Stdout+"\n"+r.Stderr, "\n") {
if l = strings.TrimSpace(l); strings.Contains(l, "GOPROXY=off") && !slices.Contains(lost, l) {
lost = append(lost, l)
}
}
if len(lost) > 0 {
return fmt.Errorf("the packages of %s load shut off only with what the fetch did not download: %s", d, strings.Join(lost, "; "))
}
return nil
}
// verifyLocks hold one mutex per digest of a go.mod and go.sum, so two directories with the same pair verify once. // verifyLocks hold one mutex per digest of a go.mod and go.sum, so two directories with the same pair verify once.
var verifyLocks sync.Map var verifyLocks sync.Map
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"os" "os"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -211,22 +212,54 @@ func TestNoCommandOfTheStepSeesAPasswordOrAToken(t *testing.T) {
// 1485/AC3: go mod verify failing ends the step failed naming the directory and the first line of its output. // 1485/AC3: go mod verify failing ends the step failed naming the directory and the first line of its output.
func TestAVerifyThatFailsEndsTheStepNamingTheDirectoryAndTheLine(t *testing.T) { func TestAVerifyThatFailsEndsTheStepNamingTheDirectoryAndTheLine(t *testing.T) {
f := &fakeStep{gomods: "go.mod\ntools/go.mod\n", goRule: func(line string) forgekey.Ran { const seatPkg = "git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo"
if strings.HasSuffix(line, "tools mod verify") || strings.Contains(line, "/tools mod verify") { for _, c := range []struct {
return forgekey.Ran{Status: 1, Stdout: "\nexample.org/x v1.0.0: dir has been modified\nsecond line\n"} name, ran, out string
} want []string
return forgekey.Ran{} notWant string
}} }{
m, states := fetchModule(t, f) {name: "go mod verify fails", ran: "tools mod verify", out: "\nexample.org/x v1.0.0: dir has been modified\nsecond line\n",
if _, err := fetchTool(t, m)(fetchArgs); err != nil { want: []string{"tools", "example.org/x v1.0.0: dir has been modified"}, notWant: "second line"},
t.Fatal(err) // 1966/AC178-2: what the fetch downloaded misses a package the directory's code imports: the shut-off package load
} // fails the fetch naming the package in Go's words, not a shut-off step later.
st := states.waitEnd(t) {name: "the shut-off package load would reach the network", ran: "tools list -e -deps -test",
if st.State != sdkstep.StepFailed { out: "fixture/tools/cmd: \n" + seatPkg + ": module lookup disabled by GOPROXY=off\n",
t.Fatalf("the step ended %s, want failed", st.State) want: []string{"tools", "shut off", seatPkg + ": module lookup disabled by GOPROXY=off"}, notWant: "fixture/tools/cmd"},
} } {
if !strings.Contains(st.Reason, "tools") || !strings.Contains(st.Reason, "example.org/x v1.0.0: dir has been modified") || strings.Contains(st.Reason, "second line") { t.Run(c.name, func(t *testing.T) {
t.Errorf("reason = %q", st.Reason) f := &fakeStep{gomods: "go.mod\ntools/go.mod\n", goRule: func(line string) forgekey.Ran {
if strings.Contains(line, "/"+c.ran) {
if strings.Contains(line, " list ") {
return forgekey.Ran{Stdout: c.out} // go list -e says a package's error and exits 0
}
return forgekey.Ran{Status: 1, Stdout: c.out}
}
return forgekey.Ran{}
}}
m, states := fetchModule(t, f)
if _, err := fetchTool(t, m)(fetchArgs); err != nil {
t.Fatal(err)
}
st := states.waitEnd(t)
if st.State != sdkstep.StepFailed {
t.Fatalf("the step ended %s, want failed", st.State)
}
for _, w := range c.want {
if !strings.Contains(st.Reason, w) {
t.Errorf("reason = %q, want it to hold %q", st.Reason, w)
}
}
if strings.Contains(st.Reason, c.notWant) {
t.Errorf("reason = %q holds %q", st.Reason, c.notWant)
}
if c.ran == "tools list -e -deps -test" {
for _, call := range f.calls {
if strings.Contains(call.line, " list -e -deps") && !slices.Contains(call.env, "GOPROXY=off") {
t.Errorf("the package load ran with the network: %s", call.env)
}
}
}
})
} }
} }
+119 -13
View File
@@ -259,16 +259,117 @@ func absoluteReplaces(body []byte, dir string) []byte {
return []byte(strings.Join(lines, "\n")) return []byte(strings.Join(lines, "\n"))
} }
// takeZeroPins takes, for each requirement of dir's go.mod at the zero version that a go failure names, the version it is // zeroRequirement is a requirement at the zero version in the module graph of a directory: by is "" when the directory's
// built against (ADR 0573 §2, §4: whatever go.mod names): the commit the ask names for the package; else the head of // own go.mod names it, else the directory (relative to dir) of a module the directory replaces by a directory of the
// the package's repository among the asked versions (the check's or try's members: another package of the same // tree, whose go.mod names it.
// repository names it); else the latest published version, the trunk's head. It never looks for the commit type zeroRequirement struct {
// 000000000000 among the pull request heads. It answers what it took; none when no zero requirement is named. pin
func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, failure string, asked sdkstep.PackageVersions, pins *sync.Mutex) ([]TakenVersion, error) { by string
}
// zeroRequirementsOf are the requirements at the zero version of dir's module graph as Go builds it (ADR 0573 §2:
// whatever go.mod names): those of dir's go.mod, then those of each module dir's go.mod replaces by a directory
// (./x, ../x), each module once, own first. A module dir replaces is left out, since its replace decides it; a replaced
// module's own replaces are not followed, since Go reads replaces only of the directory it builds (issue 1966: buildstep,
// replaced by ../mesh-build-go/buildstep, imports a seat package mesh-build-image's go.mod did not name).
func zeroRequirementsOf(dir string) []zeroRequirement {
raw, err := os.ReadFile(filepath.Join(dir, "go.mod")) raw, err := os.ReadFile(filepath.Join(dir, "go.mod"))
if err != nil || !strings.Contains(failure, zeroShort) { if err != nil {
return nil
}
_, replaced := goModRequirements(string(raw))
seen := map[string]bool{}
var out []zeroRequirement
add := func(gomod, by string) {
for _, p := range forgePins(gomod) {
if p.short != zeroShort || seen[p.module] || (by != "" && replaced[p.module]) {
continue
}
seen[p.module] = true
out = append(out, zeroRequirement{pin: p, by: by})
}
}
add(string(raw), "")
local := localReplaces(string(raw))
for _, module := range slices.Sorted(maps.Keys(local)) {
by := local[module]
b, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(by), "go.mod"))
if err != nil {
continue // a replace to a directory with no go.mod: Go says so in its own words
}
add(string(b), by)
}
return out
}
// localReplaces are the replaces of a go.mod to a directory (., .., ./x, ../x), by module: the directory as written.
func localReplaces(gomod string) map[string]string {
out := map[string]string{}
inBlock := false
for _, line := range strings.Split(gomod, "\n") {
if i := strings.Index(line, "//"); i >= 0 {
line = line[:i]
}
f := strings.Fields(line)
switch {
case len(f) == 0:
continue
case inBlock && f[0] == ")":
inBlock = false
continue
case !inBlock && f[0] == "replace" && len(f) == 2 && f[1] == "(":
inBlock = true
continue
case !inBlock && f[0] != "replace":
continue
case !inBlock:
f = f[1:]
}
at := slices.Index(f, "=>")
if at < 1 || at+1 >= len(f) {
continue
}
target := strings.Trim(f[at+1], `"`)
if target == "." || target == ".." || strings.HasPrefix(target, "./") || strings.HasPrefix(target, "../") {
out[strings.Trim(f[0], `"`)] = filepath.ToSlash(filepath.Clean(target))
}
}
return out
}
// takeZeroPins takes, for each requirement at the zero version of dir's module graph (zeroRequirementsOf) when a go
// failure names the zero version, the version it is built against (takeZeroRequirements). It answers what it took; none
// when no zero requirement is named.
func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, failure string, asked sdkstep.PackageVersions, pins *sync.Mutex) ([]TakenVersion, error) {
if !strings.Contains(failure, zeroShort) {
return nil, nil return nil, nil
} }
return m.takeZeroRequirements(ctx, root, repository, dir, d, zeroRequirementsOf(dir), asked, pins)
}
// takeReplacedZeroPins takes, before anything is downloaded, the requirements at the zero version that only a module
// dir replaces by a directory names (zeroRequirementsOf): go mod download loads only what dir's own go.mod names, so it
// passes without them, and the shut-off steps would look them up on the network (issue 1966). One the asked versions
// took (before, takeVersions) is left as it is.
func (m *Module) takeReplacedZeroPins(ctx context.Context, root, repository, dir, d string, asked sdkstep.PackageVersions, before []TakenVersion, pins *sync.Mutex) ([]TakenVersion, error) {
var replaced []zeroRequirement
for _, r := range zeroRequirementsOf(dir) {
if r.by != "" && !slices.ContainsFunc(before, func(tv TakenVersion) bool { return tv.Package == r.module }) {
replaced = append(replaced, r)
}
}
if len(replaced) == 0 {
return nil, nil
}
return m.takeZeroRequirements(ctx, root, repository, dir, d, replaced, asked, pins)
}
// takeZeroRequirements takes, for each requirement at the zero version, the version it is built against (ADR 0573 §2,
// §4: whatever go.mod names): the commit the ask names for the package; else the head of the package's repository among
// the asked versions (the check's or try's members: another package of the same repository names it); else the latest
// published version, the trunk's head. It never looks for the commit 000000000000 among the pull request heads. One
// taken before (dir's modfile requires or replaces it) is left as it is.
func (m *Module) takeZeroRequirements(ctx context.Context, root, repository, dir, d string, reqs []zeroRequirement, asked sdkstep.PackageVersions, pins *sync.Mutex) ([]TakenVersion, error) {
resolved := map[string]string{} resolved := map[string]string{}
if modfile, ok := m.pinModfile(dir); ok { if modfile, ok := m.pinModfile(dir); ok {
if b, err := os.ReadFile(modfile); err == nil { if b, err := os.ReadFile(modfile); err == nil {
@@ -281,14 +382,16 @@ func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, fai
} }
} }
var taken []TakenVersion var taken []TakenVersion
for _, p := range forgePins(string(raw)) { for _, r := range reqs {
if p.short != zeroShort { p := r.pin
continue
}
if v := resolved[p.module]; v != "" && !strings.HasSuffix(v, zeroShort) { if v := resolved[p.module]; v != "" && !strings.HasSuffix(v, zeroShort) {
continue // taken before continue // taken before
} }
named := "v0.0.0-00010101000000-" + zeroShort named := "v0.0.0-00010101000000-" + zeroShort
namedBy := ""
if r.by != "" {
namedBy = "; " + namedBySaid(r.by)
}
if "novox/"+p.repo == repository { if "novox/"+p.repo == repository {
// A module of the checked repository itself: it is in the fetched tree and nowhere else, since nothing of the // A module of the checked repository itself: it is in the fetched tree and nowhere else, since nothing of the
// repository is published before its merge. Every module of the tree goes in, as one may require another. // repository is published before its merge. Every module of the tree goes in, as one may require another.
@@ -299,7 +402,7 @@ func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, fai
if !slices.Contains(own, p.module) { if !slices.Contains(own, p.module) {
return taken, fmt.Errorf("the requirement of %s at the zero version: no module of that path is in the fetched tree of %s, and nothing of a repository is published before its merge", p.module, repository) return taken, fmt.Errorf("the requirement of %s at the zero version: no module of that path is in the fetched tree of %s, and nothing of a repository is published before its merge", p.module, repository)
} }
tv := TakenVersion{Dir: d, Package: p.module, Named: named, Built: "the fetched tree", Said: "a module of the checked repository: built from its tree"} tv := TakenVersion{Dir: d, Package: p.module, Named: named, Built: "the fetched tree", Said: "a module of the checked repository: built from its tree" + namedBy}
if m.Say != nil { if m.Say != nil {
m.Say("%s: %s names no commit; built from the fetched tree of %s", d, p.module, repository) m.Say("%s: %s names no commit; built from the fetched tree of %s", d, p.module, repository)
} }
@@ -316,7 +419,7 @@ func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, fai
if err := m.takeVersion(ctx, root, dir, &tv, pins); err != nil { if err := m.takeVersion(ctx, root, dir, &tv, pins); err != nil {
return taken, fmt.Errorf("the requirement of %s at the zero version: %w", p.module, err) return taken, fmt.Errorf("the requirement of %s at the zero version: %w", p.module, err)
} }
tv.Said = strings.TrimPrefix(tv.Said+"; "+why, "; ") tv.Said = strings.TrimPrefix(tv.Said+"; "+why+namedBy, "; ")
if m.Say != nil { if m.Say != nil {
m.Say("%s: %s names no commit; built at %s (%s)", d, p.module, tv.Built, why) m.Say("%s: %s names no commit; built at %s (%s)", d, p.module, tv.Built, why)
} }
@@ -325,6 +428,9 @@ func (m *Module) takeZeroPins(ctx context.Context, root, repository, dir, d, fai
return taken, nil return taken, nil
} }
// namedBySaid says that a module dir replaces by the directory by names a package, and dir's go.mod does not.
func namedBySaid(by string) string { return "named by " + by + ", not by this go.mod" }
// askedFor is the commit the ask names for the package p: its own, else that of the first package of the same repository // askedFor is the commit the ask names for the package p: its own, else that of the first package of the same repository
// in order (the members' heads are by repository), else none. // in order (the members' heads are by repository), else none.
func askedFor(asked sdkstep.PackageVersions, p pin) string { func askedFor(asked sdkstep.PackageVersions, p pin) string {
@@ -305,6 +305,9 @@ func TestARequirementAtTheZeroVersionIsTakenFromTheAskOrTheTrunk(t *testing.T) {
download string // what go mod download -json answers download string // what go mod download -json answers
want string // the version asked of go want string // the version asked of go
head bool // the asked commit is on no trunk: only a pull request head carries it head bool // the asked commit is on no trunk: only a pull request head carries it
// replaced: the directory's go.mod names no mesh package; a module it replaces by a directory of the tree requires
// it at the zero version, so go mod download passes and only a shut-off package load would look it up (1966/AC178-1)
replaced bool
}{ }{
{name: "the ask names the package", packages: map[string]any{sdk: trunk}, want: sdk + "@" + trunk, {name: "the ask names the package", packages: map[string]any{sdk: trunk}, want: sdk + "@" + trunk,
download: `{"Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip="}`}, download: `{"Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip="}`},
@@ -316,13 +319,31 @@ func TestARequirementAtTheZeroVersionIsTakenFromTheAskOrTheTrunk(t *testing.T) {
{name: "the ask names nothing of its repository", packages: map[string]any{"git.novox.be/novox/mesh-tools/go": trunk}, want: sdk + "@latest", {name: "the ask names nothing of its repository", packages: map[string]any{"git.novox.be/novox/mesh-tools/go": trunk}, want: sdk + "@latest",
download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`}, download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`},
{name: "no ask", want: sdk + "@latest", download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`}, {name: "no ask", want: sdk + "@latest", download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`},
{name: "named only by a module the directory replaces, the ask names it", packages: map[string]any{sdk: trunk}, want: sdk + "@" + trunk, replaced: true,
download: `{"Version":"v0.1.15-0.20261101000000-aaaaaaaaaaaa","Sum":"h1:zip="}`},
{name: "named only by a module the directory replaces, no ask", want: sdk + "@latest", replaced: true,
download: `{"Version":"v0.1.20-0.20261104000000-bbbbbbbbbbbb","Sum":"h1:zip="}`},
} { } {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
h, states := newPinHarness(t, zeroMod, false, pullList) gomod := zeroMod
if c.replaced {
gomod = "module fixture/member\n\ngo 1.26\n\nrequire fixture/sib v0.0.0-00010101000000-" + zeroShort + "\n\nreplace fixture/sib => ./sib\n"
}
h, states := newPinHarness(t, gomod, false, pullList)
base := h.m.Run base := h.m.Run
h.m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { h.m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
line := name + " " + strings.Join(args, " ") line := name + " " + strings.Join(args, " ")
switch { switch {
case c.replaced && name == "git" && strings.Contains(line, " checkout "):
r := base(ctx, env, name, args...)
trees, _ := filepath.Glob(filepath.Join(h.m.Work, "*", "tree"))
for _, tree := range trees {
os.MkdirAll(filepath.Join(tree, "sib"), 0o755)
os.WriteFile(filepath.Join(tree, "sib", "go.mod"), []byte(strings.Replace(zeroMod, "fixture/member", "fixture/sib", 1)), 0o644)
}
return r
case c.replaced && name == "go" && strings.Contains(line, " mod download") && !strings.Contains(line, "-json"):
return forgekey.Ran{} // go.mod names nothing missing: the download passes
case name == "go" && strings.Contains(line, " mod download -json "): case name == "go" && strings.Contains(line, " mod download -json "):
h.mu.Lock() h.mu.Lock()
h.cmds = append(h.cmds, cmdRecord{env: env, line: line}) h.cmds = append(h.cmds, cmdRecord{env: env, line: line})
@@ -357,6 +378,13 @@ func TestARequirementAtTheZeroVersionIsTakenFromTheAskOrTheTrunk(t *testing.T) {
if h.ran("pinned/mesh-sdk@" + zeroShort) { if h.ran("pinned/mesh-sdk@" + zeroShort) {
t.Error("a repository was placed at the commit of zeros") t.Error("a repository was placed at the commit of zeros")
} }
if c.replaced {
var res FetchResult
json.Unmarshal(st.Result, &res)
if len(res.Packages) != 1 || res.Packages[0].Package != sdk || !strings.Contains(res.Packages[0].Said, "named by sib, not by this go.mod") {
t.Errorf("the taken versions = %+v, want %s said as named by sib", res.Packages, sdk)
}
}
}) })
} }
} }
@@ -66,6 +66,13 @@ func (m *Module) takeVersions(ctx context.Context, root, dir, d string, asked sd
return nil, nil return nil, nil
} }
required, replaced := goModRequirements(string(raw)) required, replaced := goModRequirements(string(raw))
// A package only a module dir replaces by a directory names is in dir's module graph too (zeroRequirementsOf).
namedBy := map[string]string{}
for _, r := range zeroRequirementsOf(dir) {
if _, ok := required[r.module]; !ok && r.by != "" {
required[r.module], namedBy[r.module] = zeroVersion, r.by
}
}
var taken []TakenVersion var taken []TakenVersion
for _, pkg := range slices.Sorted(maps.Keys(asked)) { for _, pkg := range slices.Sorted(maps.Keys(asked)) {
named, ok := required[pkg] named, ok := required[pkg]
@@ -81,6 +88,9 @@ func (m *Module) takeVersions(ctx context.Context, root, dir, d string, asked sd
return taken, err return taken, err
} }
} }
if by := namedBy[pkg]; by != "" {
tv.Said = strings.TrimPrefix(tv.Said+"; "+namedBySaid(by), "; ")
}
if m.Say != nil { if m.Say != nil {
m.Say("%s: %s is built at %s (asked %.12s, go.mod names %s)%s", d, pkg, tv.Built, tv.Asked, tv.Named, saidSuffix(tv.Said)) m.Say("%s: %s is built at %s (asked %.12s, go.mod names %s)%s", d, pkg, tv.Built, tv.Asked, tv.Named, saidSuffix(tv.Said))
} }
@@ -171,6 +171,14 @@ func (m *Module) downloadBeside(ctx context.Context, root, tree string, dirs []s
if res := m.Run(ctx, env, "go", "-C", r, "mod", "download"); res.Status != 0 || res.Err != "" { if res := m.Run(ctx, env, "go", "-C", r, "mod", "download"); res.Status != 0 || res.Err != "" {
return fmt.Errorf("go mod download of the graph of %s beside: %s", filepath.Base(r), causeLine(res.Err+"\n"+res.Stdout+"\n"+res.Stderr)) return fmt.Errorf("go mod download of the graph of %s beside: %s", filepath.Base(r), causeLine(res.Err+"\n"+res.Stdout+"\n"+res.Stderr))
} }
// What the packages' load needs beyond the module graph's download (the go.mod of a version a dependency requires,
// which a build of the composer reads: issue 1966), fetched with the network, then proven shut off.
if res := m.Run(ctx, env, "go", "-C", r, "list", "-e", "-deps", "-test", "-f", shutOffLoadFormat, "./..."); res.Status != 0 || res.Err != "" {
return fmt.Errorf("go list of the packages of %s beside: %s", filepath.Base(r), causeLine(res.Err+"\n"+res.Stdout+"\n"+res.Stderr))
}
if err := m.shutOffLoad(ctx, env, r, "beside "+filepath.Base(r)); err != nil {
return err
}
} }
return nil return nil
} }
@@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"slices"
"strings" "strings"
"testing" "testing"
@@ -72,7 +73,7 @@ func TestTheFetchDownloadsTheGraphOfASiblingModuleLaidWithTheTreesItRequires(t *
put(filepath.Join(root, "beside", "mesh-sdk", "go"), "module git.novox.be/novox/mesh-sdk/go\n\ngo 1.26.0\n") put(filepath.Join(root, "beside", "mesh-sdk", "go"), "module git.novox.be/novox/mesh-sdk/go\n\ngo 1.26.0\n")
var said []string var said []string
var lines, envs []string var lines, envs []string
fail := false fail, lost := false, ""
m := &Module{Say: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }} m := &Module{Say: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}
m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran { m.Run = func(ctx context.Context, env []string, name string, args ...string) forgekey.Ran {
lines = append(lines, name+" "+strings.Join(args, " ")) lines = append(lines, name+" "+strings.Join(args, " "))
@@ -80,14 +81,26 @@ func TestTheFetchDownloadsTheGraphOfASiblingModuleLaidWithTheTreesItRequires(t *
if fail { if fail {
return forgekey.Ran{Status: 1, Stderr: "dial tcp: network is unreachable"} return forgekey.Ran{Status: 1, Stderr: "dial tcp: network is unreachable"}
} }
if slices.Contains(env, "GOPROXY=off") {
return forgekey.Ran{Stdout: lost}
}
return forgekey.Ran{} return forgekey.Ran{}
} }
if err := m.downloadBeside(context.Background(), root, tree, []string{"modules/x"}); err != nil { if err := m.downloadBeside(context.Background(), root, tree, []string{"modules/x"}); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(lines) != 1 || lines[0] != "go -C "+host+" mod download" || !strings.Contains(envs[0], "GOWORK="+filepath.Join(root, "beside-0.work")) { // The graph's download, the packages' load with the network, then the same load shut off (issue 1966).
t.Fatalf("commands %q env %q, want one go mod download of the node-engine's module over its workspace", lines, envs) load := "go -C " + host + " list -e -deps -test -f " + shutOffLoadFormat + " ./..."
if len(lines) != 3 || lines[0] != "go -C "+host+" mod download" || lines[1] != load || lines[2] != load ||
!strings.Contains(envs[0], "GOWORK="+filepath.Join(root, "beside-0.work")) || strings.HasSuffix(envs[1], "GOPROXY=off") || !strings.HasSuffix(envs[2], "GOPROXY=off") {
t.Fatalf("commands %q env %q, want go mod download of the node-engine's module over its workspace, then its packages loaded with the network and shut off", lines, envs)
} }
// 1966/AC178-3 (its unit half): the shut-off load misses a go.mod the composer's build reads: the fetch fails naming it.
lines, lost = nil, "github.com/nats-io/nkeys: github.com/nats-io/nkeys@v0.4.16 requires golang.org/x/crypto@v0.52.0: module lookup disabled by GOPROXY=off\n"
if err := m.downloadBeside(context.Background(), root, tree, []string{"modules/x"}); err == nil || !strings.Contains(err.Error(), "beside mesh-host") || !strings.Contains(err.Error(), "golang.org/x/crypto@v0.52.0: module lookup disabled") {
t.Errorf("a shut-off load that would reach the network = %v, want the fetch failed naming it", err)
}
lost = ""
work, _ := os.ReadFile(filepath.Join(root, "beside-0.work")) work, _ := os.ReadFile(filepath.Join(root, "beside-0.work"))
for _, want := range []string{filepath.Join(tree, "modules", "x"), filepath.Join(root, "beside", "mesh-sdk", "go"), host} { for _, want := range []string{filepath.Join(tree, "modules", "x"), filepath.Join(root, "beside", "mesh-sdk", "go"), host} {
if !strings.Contains(string(work), want) { if !strings.Contains(string(work), want) {
+1
View File
@@ -41,6 +41,7 @@ require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
) )
+1
View File
@@ -101,6 +101,7 @@ require (
git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/clients/ociartifactregistryclient v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/nodebuildgo v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000 // indirect
git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect git.novox.be/novox/mesh-delivery/contracts/go/seats/ociartifactregistry v0.0.0-00010101000000-000000000000 // indirect
) )