Files
mesh-delivery/modules/mesh-delivery-builder/internal/build/reserve_test.go
T

207 lines
9.9 KiB
Go

package build
import (
"context"
"encoding/json"
"fmt"
"reflect"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/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)
}
}