185 lines
9.1 KiB
Go
185 lines
9.1 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/composer"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
var testTime = time.Date(2026, 10, 24, 3, 0, 0, 0, time.UTC)
|
|
|
|
const sdkHead = "1111111111111111111111111111111111111111"
|
|
|
|
// raws are the manifests of meshOf as their repositories hold them, by digest, as the indexer keeps them; `web2` is web
|
|
// with a field `shares` that the assigner's own mesh-sdk does not read (the shape of issue 1593).
|
|
var raws = map[string]string{
|
|
"d-mail": `{"module": "mail", "version": "1", "requires": ["postgres-database"]}`,
|
|
"d-store": `{"module": "store", "version": "1", "provides": [{"name": "postgres-database"}]}`,
|
|
"d-web": `{"module": "web", "version": "1"}`,
|
|
"d-web2": `{"module": "web", "version": "2", "shares": ["photos"]}`,
|
|
}
|
|
|
|
// asTheHeadReads is how mesh-sdk at the group's head reads each manifest: as the assigner's own reads it, but for the
|
|
// field it adds, which it accepts. The head's composer is built and run by the Go build module (ADR 0482 clause 4); here
|
|
// its verdict is made by mesh-sdk's composer.Run over the same request, with `web2` read as the head reads it.
|
|
var asTheHeadReads = map[string]string{"d-web2": `{"module": "web", "version": "2"}`}
|
|
|
|
func digestedMesh() Mesh {
|
|
m := meshOf()
|
|
m.Digests = map[string]string{"mail": "d-mail", "store": "d-store", "web": "d-web"}
|
|
return m
|
|
}
|
|
|
|
// webShares is a group of mesh-sdk and mesh-catalog whose catalogue member gives web the field only the SDK head reads:
|
|
// the assigner's own reader refuses it (Unreadable), as the indexer's manifest is read in world.change.
|
|
func webShares(members map[string]string) Change {
|
|
_, err := catalogue.ParseManifest([]byte(raws["d-web2"]))
|
|
return Change{Group: "g", Heads: "0123456789ab", SDKHead: sdkHead, Manifests: map[string]catalogue.Manifest{},
|
|
Unreadable: map[string]string{"web": err.Error()}, Digests: map[string]string{"web": "d-web2"}, Members: members}
|
|
}
|
|
|
|
// run is the composer's step for a job, as the build module answers it: the request filled from the indexer's digests
|
|
// (with read standing in for how that mesh-sdk reads a digest), run, and its verdict put in a passed step.
|
|
func run(t *testing.T, job delivery.ComposeJob, read map[string]string) Ran {
|
|
t.Helper()
|
|
req, err := composer.Decode(job.Request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := composer.Fill(&req, func(d string) ([]byte, error) {
|
|
if r, ok := read[d]; ok {
|
|
return []byte(r), nil
|
|
}
|
|
return []byte(raws[d]), nil
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v, err := composer.Run(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v.SDK = "(devel) " + job.Fetch["commit"].(string)
|
|
vr, _ := json.Marshal(v)
|
|
ans, _ := json.Marshal(delivery.ComposeAnswer{Built: true, Key: "composer-0", Verdict: vr})
|
|
return RanOf(delivery.Step{State: delivery.StepPassed, Result: ans})
|
|
}
|
|
|
|
// 1622/AC1: a group holding a mesh-sdk pull request is composed by the composer built at the member's head: the job
|
|
// fetches mesh-sdk at that head, builds mesh-sdk's composer command in its Go module, and hands it the trunk and the
|
|
// change by digest, never the manifests' bytes, composing the machines the change touches.
|
|
func TestAGroupWithAnSDKMemberAsksTheComposerAtItsHead(t *testing.T) {
|
|
c := webShares(nil)
|
|
job, err := HeadJob(digestedMesh(), c)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if job.Fetch["repository"] != SDKRepository || job.Fetch["commit"] != sdkHead || job.Dir != "go" || job.Package != composer.Package {
|
|
t.Fatalf("job = %+v", job)
|
|
}
|
|
ask := delivery.StepAsk{ID: "mh3k2x9qa1b2c5", Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: "mesh-delivery-assigner",
|
|
At: testTime, Compose: &job}
|
|
if _, err := delivery.EncodeStepAsk(ask); err != nil {
|
|
t.Fatalf("the ask is refused by its layout: %v", err)
|
|
}
|
|
req, err := composer.Decode(job.Request)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(req.Cases) != 2 || req.Cases[0].Name != CaseTrunk || req.Cases[1].Name != CaseChange {
|
|
t.Fatalf("cases = %+v", req.Cases)
|
|
}
|
|
if req.Cases[0].Manifests["web"].Digest != "d-web" || req.Cases[1].Manifests["web"].Digest != "d-web2" || len(req.Cases[1].Manifests["web"].Raw) != 0 {
|
|
t.Errorf("the cases do not name web by its trunk and its change digest: %+v", req.Cases)
|
|
}
|
|
if !slices.Equal(req.Machines, []string{"g14"}) || req.Depth != composer.DepthResolve || !req.HoldingsFromClaims {
|
|
t.Errorf("request = machines %v, depth %q, holdings from claims %v", req.Machines, req.Depth, req.HoldingsFromClaims)
|
|
}
|
|
}
|
|
|
|
// 1622/AC2, AC3: a catalogue member using a field only the SDK head reads passes with the head's composer, and the
|
|
// verdict names the mesh-sdk the composer was built with; with the assigner's own mesh-sdk it fails.
|
|
func TestAFieldTheGroupAddsPassesWithTheHeadsComposer(t *testing.T) {
|
|
c := webShares(map[string]string{"novox/mesh-controller": "2222222222222222222222222222222222222222"})
|
|
job, _ := HeadJob(digestedMesh(), c)
|
|
head := run(t, job, asTheHeadReads)
|
|
v := FromComposer(digestedMesh(), c, head, nil, 0)
|
|
if v.State != delivery.CompositionPass || v.SDK != "(devel) "+sdkHead || !slices.Equal(v.Machines, []string{"g14"}) {
|
|
t.Fatalf("with the head's composer: %+v", v)
|
|
}
|
|
if _, err := delivery.EncodeCompositionVerdict(v); err != nil {
|
|
t.Errorf("the verdict cannot be written: %v", err)
|
|
}
|
|
own := Compose(digestedMesh(), c, "v0.1.14-own", 0)
|
|
if own.State != delivery.CompositionFail || !strings.Contains(own.Words, "shares") {
|
|
t.Fatalf("with the assigner's own mesh-sdk: %+v, want a fail naming the field", own)
|
|
}
|
|
}
|
|
|
|
// 1623/AC1, AC2: the group of mesh-sdk and mesh-catalog adding and using a field, without a controller member, fails
|
|
// naming the field and mesh-controller, whose running build pins an older mesh-sdk; with a controller member pinned to
|
|
// the SDK head (its composer reads what the head reads) it passes.
|
|
func TestAFieldTheRunningControllerCannotReadFailsTheGroup(t *testing.T) {
|
|
c := webShares(nil)
|
|
job, _ := HeadJob(digestedMesh(), c)
|
|
head := run(t, job, asTheHeadReads)
|
|
running := "3333333333333333333333333333333333333333"
|
|
rjob, err := ReaderJob(c, "mesh-controller", "novox/mesh-controller", running)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rjob.Fetch["commit"] != running || rjob.Dir != delivery.RootPath {
|
|
t.Fatalf("reader job = %+v", rjob)
|
|
}
|
|
old := run(t, rjob, nil) // the running controller's mesh-sdk reads web2 as the assigner's own does: refused
|
|
v := FromComposer(digestedMesh(), c, head, []Reader{{Module: "mesh-controller", Commit: running, Ran: old}}, 0)
|
|
if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "shares") || !strings.Contains(v.Words, "move mesh-controller onto mesh-sdk "+sdkHead) {
|
|
t.Fatalf("without a controller member: %+v, want a fail naming the field and mesh-controller", v)
|
|
}
|
|
member := "2222222222222222222222222222222222222222"
|
|
pinned := run(t, rjob, asTheHeadReads)
|
|
v = FromComposer(digestedMesh(), c, head, []Reader{{Module: "mesh-controller", Commit: member, Member: true, Ran: pinned}}, 0)
|
|
if v.State != delivery.CompositionPass {
|
|
t.Fatalf("with a controller member pinned to the head: %+v, want a pass", v)
|
|
}
|
|
}
|
|
|
|
// 1623: a reader whose mesh-sdk has no composer command (before issue 1621) is said, not failed; a reader's composer that
|
|
// failed for another cause fails the group, loud.
|
|
func TestAReaderWithNoComposerIsSaidAndAnotherFailureFails(t *testing.T) {
|
|
c := webShares(nil)
|
|
job, _ := HeadJob(digestedMesh(), c)
|
|
head := run(t, job, asTheHeadReads)
|
|
none := Ran{State: delivery.StepFailed, Reason: "go build: no required module provides package git.novox.be/novox/mesh-sdk/go/cmd/mesh-compose"}
|
|
v := FromComposer(digestedMesh(), c, head, []Reader{{Module: "mesh-host", Commit: "abc", Ran: none}}, 0)
|
|
if v.State != delivery.CompositionPass || !hasWarning(v, "mesh-host at abc") || !hasWarning(v, "before issue 1621") {
|
|
t.Fatalf("a reader with no composer: %+v", v)
|
|
}
|
|
broke := Ran{State: delivery.StepFailed, Reason: "the fetch could not reach the forge"}
|
|
v = FromComposer(digestedMesh(), c, head, []Reader{{Module: "mesh-host", Commit: "abc", Ran: broke}}, 0)
|
|
if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "could not reach the forge") {
|
|
t.Fatalf("a reader whose composer failed: %+v", v)
|
|
}
|
|
}
|
|
|
|
// 1622: the head's composer that did not run fails the composition naming why, never passes it; a trunk manifest the
|
|
// head's mesh-sdk refuses fails it too, since the group would deliver a reader that refuses it.
|
|
func TestTheHeadsComposerFailingOrRefusingTheTrunkFails(t *testing.T) {
|
|
c := webShares(nil)
|
|
v := FromComposer(digestedMesh(), c, Ran{State: delivery.StepRefused, Reason: "the guard refuses it"}, nil, 0)
|
|
if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "the guard refuses it") || !strings.Contains(v.Words, sdkHead) {
|
|
t.Fatalf("a head composer that did not run: %+v", v)
|
|
}
|
|
job, _ := HeadJob(digestedMesh(), c)
|
|
strict := map[string]string{"d-web2": asTheHeadReads["d-web2"], "d-mail": `{"module": "mail", "version": "1", "gone": true}`}
|
|
v = FromComposer(digestedMesh(), c, run(t, job, strict), nil, 0)
|
|
if v.State != delivery.CompositionFail || !strings.Contains(v.Words, "refuses the trunk's manifest of mail") {
|
|
t.Fatalf("a head that refuses a trunk manifest: %+v", v)
|
|
}
|
|
}
|