A candidate's module is fetched against each package member of its group by the digest of the member's build (fetch `builds`), the member out of the fetched packages; a member not built holds it with wait-held naming the member, its bound a key of the builder's bounds state with its own time to live (A-77), a failed member or one missing past the bound fails the step naming it. Builds: design 47 G §373, §465 (A-77) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
167 lines
6.3 KiB
Go
167 lines
6.3 KiB
Go
package build
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
|
|
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
|
"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-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
)
|
|
|
|
const memberPackage = "example.org/p"
|
|
|
|
var fpP = "src2:" + hex64('2')
|
|
|
|
// groupValue is a group's candidate: the module m, built against the head of the package member p, and p.
|
|
func groupValue(t *testing.T) []byte {
|
|
t.Helper()
|
|
pulls := []meshcatalog.CandidatesByGroupValuePullsItem{{Repository: "novox/mesh-catalog", Number: 7, Head: headX, Modules: []string{"m", "p"},
|
|
Fps: map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{
|
|
"m": {Fp2: fp1, Manifest: mdigest('a'), Packages: map[string]string{memberPackage: headY, "example.org/q": "v1.2.3"}},
|
|
"p": {Fp2: fpP, Manifest: mdigest('b')}}}}
|
|
hash, err := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: pulls[0].Repository, Number: 7, Head: headX}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: "feat/x", Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return data
|
|
}
|
|
|
|
// memberRig is a rig whose group changes m and its package member p (artifact lib); p has a trunk record when known.
|
|
func memberRig(t *testing.T, known bool) *rig {
|
|
t.Helper()
|
|
r := newRig(t, goBundle)
|
|
lib := catalogue.Artifact{Name: "lib", Kind: "bundle", Language: "go"}
|
|
r.mf.byDigest[mdigest('b')] = catalogue.Manifest{Module: "p", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{lib}}}
|
|
r.b.Members = func(_ context.Context, pkg string) (string, string, bool, error) {
|
|
if pkg == memberPackage {
|
|
return "p", "lib", true, nil
|
|
}
|
|
return "", "", false, nil
|
|
}
|
|
trunkModule(t, r)
|
|
if known {
|
|
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-catalog", Commit: trunkHead, Fp2: "src2:" + hex64('d'),
|
|
Manifest: mdigest('b'), Path: "modules/p", Toolchains: CatalogToolchains(agreedToolchains)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.b.Module(context.Background(), "p", data)
|
|
r.b.Wait()
|
|
}
|
|
return r
|
|
}
|
|
|
|
func TestAGroupRunBuildsADependentAgainstItsPackageMembersBuildByDigest(t *testing.T) { // design 47 G §373, §465; test 103
|
|
t.Run("the member is fetched by its build's digest, and no package of it is fetched by version", func(t *testing.T) {
|
|
r := memberRig(t, true)
|
|
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
|
r.b.Wait()
|
|
if v := valueOf(t, r.st, r.key); v.State != builds.BuildDone {
|
|
t.Fatalf("m = %+v, want built", v)
|
|
}
|
|
var found bool
|
|
for _, c := range r.bm.callsOf("build_go_fetch") {
|
|
f := c.body.(map[string]any)
|
|
bs, _ := f["builds"].([]any)
|
|
if len(bs) == 0 {
|
|
continue
|
|
}
|
|
found = true
|
|
want := map[string]any{"package": memberPackage, "module": "p", "artifact": "lib", "digest": digest}
|
|
if len(bs) != 1 || !jsonEqual(bs[0], want) {
|
|
t.Errorf("builds = %v, want %v", bs, want)
|
|
}
|
|
pk, _ := f["packages"].(map[string]any)
|
|
if _, named := pk[memberPackage]; named || pk["example.org/q"] != "v1.2.3" {
|
|
t.Errorf("packages = %v, want the member out of it and the other package kept", pk)
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("no fetch named the member's build")
|
|
}
|
|
})
|
|
t.Run("a member not built holds the dependent with wait-held naming it, and past the bound fails it", func(t *testing.T) {
|
|
r := memberRig(t, false)
|
|
var mu sync.Mutex
|
|
bounds := &fakeMemberBounds{}
|
|
r.b.MemberBounds = bounds
|
|
var said []builds.BuildLogEntry
|
|
r.b.Log = func(e builds.BuildLogEntry) { mu.Lock(); said = append(said, e); mu.Unlock() }
|
|
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
|
r.b.Wait()
|
|
if _, written := r.st.vals[r.key]; written || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
|
t.Fatal("the dependent was claimed or built while its package member is not built")
|
|
}
|
|
mu.Lock()
|
|
if len(said) != 1 || said[0].Why != "wait-held" || !strings.Contains(said[0].Line, "p/lib") {
|
|
t.Errorf("said = %+v, want one wait-held naming p/lib", said)
|
|
}
|
|
mu.Unlock()
|
|
if got := bounds.armed(); len(got) != 1 || got[0] != r.key+" "+MemberBound.String() {
|
|
t.Fatalf("bounds armed = %v, want one of 60 minutes on the dependent's key", got)
|
|
}
|
|
r.b.MemberBoundEnded(context.Background(), r.key) // the bus's marker of the bound's expiry
|
|
r.b.Wait()
|
|
if got := bounds.ended(); len(got) != 1 || got[0] != r.key {
|
|
t.Errorf("bounds ended = %v, want the wait's bound deleted", got)
|
|
}
|
|
if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed || !strings.Contains(v.Error, "p/lib") || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
|
t.Errorf("m = %+v, want failed naming the member and nothing built", v)
|
|
}
|
|
})
|
|
t.Run("a member whose build failed fails the dependent's step naming it", func(t *testing.T) {
|
|
r := memberRig(t, false)
|
|
mk, _ := builds.BuilderBuildKey("p", fpP)
|
|
data, err := builds.EncodeBuilderBuild(builds.BuilderBuild{State: builds.BuildFailed, Error: "boom"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r.st.put(mk, data)
|
|
r.b.Candidate(context.Background(), "feat%2Fx", groupValue(t))
|
|
r.b.Wait()
|
|
if v := valueOf(t, r.st, r.key); v.State != builds.BuildFailed || !strings.Contains(v.Error, "p/lib") || !strings.Contains(v.Error, "boom") || len(r.bm.callsOf("build_go_fetch")) != 0 {
|
|
t.Errorf("m = %+v, want failed naming the member and its error", v)
|
|
}
|
|
})
|
|
}
|
|
|
|
// fakeMemberBounds keeps the bounds a builder arms and ends.
|
|
type fakeMemberBounds struct {
|
|
mu sync.Mutex
|
|
arms, ends_ []string
|
|
}
|
|
|
|
func (f *fakeMemberBounds) Arm(_ context.Context, key, _ string, bound time.Duration) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.arms = append(f.arms, key+" "+bound.String())
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeMemberBounds) End(_ context.Context, key string) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.ends_ = append(f.ends_, key)
|
|
}
|
|
|
|
func (f *fakeMemberBounds) armed() []string { f.mu.Lock(); defer f.mu.Unlock(); return f.arms }
|
|
func (f *fakeMemberBounds) ended() []string { f.mu.Lock(); defer f.mu.Unlock(); return f.ends_ }
|
|
|
|
func jsonEqual(a, b any) bool {
|
|
x, _ := json.Marshal(a)
|
|
y, _ := json.Marshal(b)
|
|
return string(x) == string(y)
|
|
}
|