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