The delivery repository's contracts and SDK live under modules/ as mesh-delivery-contracts and mesh-delivery-sdk, their Go module paths renamed to match the folders (the TypeScript names already did), and every importer in mesh-delivery, mesh-catalog, mesh-host, mesh-tools and mesh-sdk follows in the same group: a design as it should be, one path per module, checked by the group's check building every importer. Builds: design 47 G §427, §88 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
207 lines
10 KiB
Go
207 lines
10 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)
|
|
}
|
|
}
|