Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/reserve_test.go
T
jschoubben e313248b5f U2b: the reserved version reaches each artifact's build, and a versioned ask names each artifact's registry seat
The ask carried the reserved version (§573), but the build modules' shared run gave each
artifact's build only the artifact's own version, which no asker sets, so no build got
MESH_VERSION and a Go package was packed as a candidate and never published. The run now
gives every build the ask's version, and the builder names the registry seat each artifact
of a versioned build is published through, so a Go package is published under its
reserved version on go-package-registry.

Builds: design 47 G §370, §573, §458; design 51 *Publishing*

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 16:13:13 +01:00

241 lines
12 KiB
Go

package build
import (
"context"
"encoding/json"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"mesh-delivery-builder/internal/versions"
)
// fakeReservations answers a fill and keeps what the builder reserved and recorded.
type fakeReservations struct {
filled bool
reserved [][]string
series []string
built map[string]string
sources []versions.Source
deps map[string][]meshbuilds.Dependency
}
func (f *fakeReservations) Filled() bool { return f.filled }
func (f *fakeReservations) Reserve(_ context.Context, module, series string, artifacts []string, src versions.Source) (versions.ReservedVersion, error) {
f.reserved, f.series, f.sources = append(f.reserved, artifacts), append(f.series, series), append(f.sources, src)
return versions.ReservedVersion{Module: module, Series: series, Version: series + ".5"}, nil
}
func (f *fakeReservations) Built(_ context.Context, _, version string, digests map[string]string, deps map[string][]meshbuilds.Dependency) error {
f.deps = deps
f.built = map[string]string{version: ""}
for ref, d := range digests {
f.built[ref] = d
}
return nil
}
// TestABuildReservesItsVersionAtItsStartOnceTheCatalogFilled: design 47 G §369, §372 (test 102) — nothing is reserved before
// versions-filled and the build waits saying wait-held naming the fill; after it the build reserves one version in the
// manifest's MAJOR.MINOR for its artifacts and records their digests; a done key reserves nothing again; a manifest whose
// version is no MAJOR.MINOR reserves nothing.
func TestABuildReservesItsVersionAtItsStartOnceTheCatalogFilled(t *testing.T) {
r := newRig(t, goBundle)
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}}
res := &fakeReservations{}
r.b.Reserved = res
var said []builds.BuildLogEntry
r.b.Log = func(e builds.BuildLogEntry) { said = append(said, e) }
r.b.Module(context.Background(), "m", moduleValue(t, "modules/m"))
r.b.Wait()
if _, ok := r.st.vals[r.key]; ok || len(res.reserved) != 0 {
t.Fatalf("before versions-filled the build was claimed (%v) or reserved %v", ok, res.reserved)
}
var waits []string
for _, e := range said {
if e.Event == builds.BuildSaidEvent && e.Why == "wait-held" {
waits = append(waits, e.Line)
}
}
if len(waits) != 1 || !slices.ContainsFunc(waits, func(l string) bool { return strings.Contains(l, "versions-filled") }) {
t.Fatalf("wait-held lines = %q, want one naming versions-filled", waits)
}
res.filled = true
// Design 47 G §573: the build's ask carries the reserved version.
var askedVersions []string
r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
askedVersions = append(askedVersions, a.Version)
return r.answer(a)
}
v := r.consider(t, "modules/m")
if len(askedVersions) == 0 || slices.ContainsFunc(askedVersions, func(v string) bool { return v != "0.1.5" }) {
t.Fatalf("the asks carried versions %q, want the reserved 0.1.5 on each", askedVersions)
}
if v.State != builds.BuildDone || len(res.reserved) != 1 || !slices.Equal(res.reserved[0], []string{"m:tool"}) || res.series[0] != "0.1" {
t.Fatalf("build %+v reserved %v in %v, want done and one reservation of m:tool in 0.1", v.State, res.reserved, res.series)
}
if res.built["m:tool"] != digest || len(res.built) != 2 {
t.Errorf("recorded %v, want the digest of m:tool under the reserved version", res.built)
}
r.consider(t, "modules/m")
if len(res.reserved) != 1 {
t.Errorf("a done key reserved again: %v", res.reserved)
}
// A manifest whose version is no MAJOR.MINOR reserves nothing and is built as before.
r2 := newRig(t, goBundle)
res2 := &fakeReservations{filled: true}
r2.b.Reserved = res2
var unversioned []string
r2.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
unversioned = append(unversioned, a.Version)
return r2.answer(a)
}
var journal []string
r2.b.Logf = func(f string, a ...any) { journal = append(journal, fmt.Sprintf(f, a...)) }
if v := r2.consider(t, "modules/m"); v.State != builds.BuildDone || len(res2.reserved) != 0 {
t.Errorf("a manifest with no MAJOR.MINOR: state %s, reserved %v", v.State, res2.reserved)
}
if len(unversioned) == 0 || slices.ContainsFunc(unversioned, func(v string) bool { return v != "" }) {
t.Errorf("a build reserving none asked with versions %q, want none", unversioned)
}
// G §505: the builder writes in its journal, naming the module, that it reserved nothing.
if !slices.ContainsFunc(journal, func(l string) bool { return strings.HasPrefix(l, "m: ") && strings.Contains(l, "not MAJOR.MINOR") }) {
t.Errorf("journal %q, want a line naming m and saying its version is not MAJOR.MINOR", journal)
}
}
// TestADropOfAReservationIsAReasksValueOfItsOwnKey: design 47 G §369 — builder_reserved_drop asks the builder through
// reasks, under a key of the reservation (never a build key), refusing a module with no MAJOR.MINOR or no why.
func TestADropOfAReservationIsAReasksValueOfItsOwnKey(t *testing.T) {
at := time.Unix(0, 0)
key, data, err := NewReservedDrop("m", "1.4", "never released", "op@x", at)
if err != nil || key != "reserved."+versions.ReservedKey("m", "1.4") {
t.Fatalf("key %q (%v)", key, err)
}
if r, err := DecodeReAsk(data); err != nil || r.Mode != ModeReservedDrop || r.Name != "m" || r.FP != "1.4" || r.By != "op@x" {
t.Errorf("value = %+v (%v)", r, err)
}
for name, args := range map[string][3]string{"no module": {"", "1.4", "why"}, "no series": {"m", "1", "why"}, "three numbers": {"m", "1.4.2", "why"}, "no why": {"m", "1.4", " "}} {
if _, _, err := NewReservedDrop(args[0], args[1], args[2], "op", at); err == nil {
t.Errorf("%s: a drop was made", name)
}
}
}
// TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision: design 47 G §371, A-57 — a build of the registration's
// first watched branch reserves a version and the reservation carries the branch and the source revision of the catalog revision its
// entry came from; a build of another watched branch reserves none and is built and done.
func TestOnlyABuildOfTheFirstWatchedBranchReservesAndItCarriesItsCatalogRevision(t *testing.T) {
for _, c := range []struct {
name string
origin Origin
want []versions.Source
}{
{"the first branch", Origin{Branch: "main", Source: 41, First: true},
[]versions.Source{{Repository: "novox/mesh-catalog", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fingerprint: fp1, Branch: "main", Revision: 41}}},
{"another branch", Origin{Branch: "beta", Source: 42}, nil},
} {
t.Run(c.name, func(t *testing.T) {
r := newRig(t, goBundle)
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}}
res := &fakeReservations{filled: true}
r.b.Reserved = res
r.b.Entry(context.Background(), c.origin, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog", Path: "modules/m",
Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'), Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)}})
r.b.Wait()
if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone {
t.Fatalf("build = %+v, want done", v)
}
if !reflect.DeepEqual(res.sources, c.want) {
t.Errorf("reserved from %+v, want %+v", res.sources, c.want)
}
})
}
}
// TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith: design 47 G §466, §673, A-57 — the `resolved` of each artifact and the
// toolchain digest of the step's result are kept on the reservation as dependencies of kind 1 and 3, their parts the artifact
// references the entry's edges name; a package no edge names is left out and said.
func TestAReservedBuildKeepsWhatItsStepResolvedAndWasBuiltWith(t *testing.T) {
r := newRig(t, goBundle)
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: "0.1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goBundle}}}
res := &fakeReservations{filled: true}
r.b.Reserved = res
var journal []string
r.b.Logf = func(f string, a ...any) { journal = append(journal, fmt.Sprintf(f, a...)) }
r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
st := r.answer(a)
var out buildstep.BuildAnswer
if err := json.Unmarshal(st.Result, &out); err != nil || len(out.Artifacts) == 0 {
return st
}
out.Artifacts[0].Resolved = []buildstep.AskResolved{{Package: "git.novox.be/novox/mesh-sdk/go", Version: "0.1.14", Digest: "sha256:11"},
{Package: "unnamed/pkg", Version: "1.0.0", Digest: "sha256:22"}}
out.Toolchain = &buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "1", Digest: "sha256:tc"}
st.Result, _ = json.Marshal(out)
return st
}
r.b.Entry(context.Background(), Origin{Branch: "main", Source: 41, First: true}, meshcatalog.CatalogEntry{Module: "m", Repository: "novox/mesh-catalog",
Path: "modules/m", Commit: "7a74cee83f88e6585a5511c3cc53bdf0f5303d20", Fp: fp1, ManifestDigest: mdigest('a'),
Inputs: &meshcatalog.EntryInputs{Toolchains: CatalogToolchains(agreedToolchains)},
Edges: []meshcatalog.Edge{{Kind: 1, From: "mesh-sdk:go", To: "m:tool", Why: "git.novox.be/novox/mesh-sdk/go"}, {Kind: 3, From: "go-toolchain:program", To: "m:tool"}}})
r.b.Wait()
want := map[string][]meshbuilds.Dependency{"m:tool": {{Kind: 1, Part: "mesh-sdk:go", Version: "0.1.14", Digest: "sha256:11"},
{Kind: 3, Part: "go-toolchain:program", Version: "go1.27.1", Digest: "sha256:tc"}}}
if !reflect.DeepEqual(res.deps, want) {
t.Errorf("dependencies = %+v, want %+v", res.deps, want)
}
if !slices.ContainsFunc(journal, func(l string) bool {
return strings.Contains(l, "unnamed/pkg") && strings.Contains(l, "names no artifact")
}) {
t.Errorf("journal %q, want the package no edge names said", journal)
}
}
// TestAModuleWhoseOnlyArtifactIsAGoPackageIsBuiltUnderItsReservedVersion: design 47 G §370 — a module whose one artifact is
// a Go package (mesh-wireguard's go-overlay) is planned on the Go build seat, its ask carries the reserved version and names
// the seat go-package-registry the package is published and tagged through, and the build is done; a build reserving none
// names no seat.
func TestAModuleWhoseOnlyArtifactIsAGoPackageIsBuiltUnderItsReservedVersion(t *testing.T) {
goPackage := catalogue.Artifact{Name: "go-overlay", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}
for _, c := range []struct {
name, series, version, seat string
}{
{"reserved", "0.1", "0.1.5", versions.SeatGoPackages},
{"reserving none", "1", "", ""},
} {
t.Run(c.name, func(t *testing.T) {
r := newRig(t, goPackage)
r.mf.byDigest[mdigest('a')] = catalogue.Manifest{Module: "m", Version: c.series, Build: &catalogue.Build{Artifacts: []catalogue.Artifact{goPackage}}}
r.b.Reserved = &fakeReservations{filled: true}
r.bm.packages["go-overlay"] = "git.novox.be/novox/mesh-networking/go@v0.1.5" // the Go build module's published reference
var asked []buildstep.StepAsk
r.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
asked = append(asked, a)
return r.answer(a)
}
v := r.consider(t, "modules/m")
if v.State != builds.BuildDone {
t.Fatalf("state %s (%s), want done", v.State, v.Error)
}
if len(asked) != 1 || asked[0].Language != buildstep.AskGo || asked[0].Version != c.version || asked[0].Build == nil ||
len(asked[0].Build.Artifacts) != 1 || asked[0].Build.Artifacts[0] != (buildstep.AskArtifact{Artifact: "go-overlay", Seat: c.seat}) {
t.Fatalf("asks %+v, want one Go ask with version %q naming go-overlay on seat %q", asked, c.version, c.seat)
}
})
}
}