What the running builds serve is read when a changed build module is met: the node-engine's haves, the builder's build of each, its manifest. The modules it builds are composed from their manifests, as for any changed build module, and the composition's warnings name the verb. Claude-Session: https://claude.ai/code/session_01JG73KG7AfSdh6FP8VrBqCn
268 lines
13 KiB
Go
268 lines
13 KiB
Go
package check
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"maps"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// fakeCompositions answers a check's composition from memory, recording the keys asked.
|
|
type fakeCompositions struct {
|
|
v delivery.CompositionVerdict
|
|
err error
|
|
asked []string
|
|
// waits answers only when its context ends.
|
|
waits bool
|
|
}
|
|
|
|
func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) {
|
|
f.asked = append(f.asked, c.Key)
|
|
if f.waits {
|
|
<-ctx.Done()
|
|
return delivery.CompositionVerdict{}, ctx.Err()
|
|
}
|
|
return f.v, f.err
|
|
}
|
|
|
|
// 1297/AC3 (its unit half): a check whose tests pass takes the assigner's composition of it, by the check's own key,
|
|
// into its verdict: a pass says it passed with the mesh-sdk and machines it composed, and is no longer "not run".
|
|
func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) {
|
|
t.Run("a change to a build module or base names the modules built on it", aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt)
|
|
b := newBench(t)
|
|
f := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "v0.1.14-x", Machines: []string{"g14"}, Took: 40}}
|
|
b.ex.Compositions = f
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
cs.FPSet = "fps-of-the-candidate"
|
|
v := b.judge(t, cs).Verdict
|
|
if v.State != delivery.VerdictPass || v.Composition == nil || v.Composition.State != delivery.CompositionPass || v.Composition.SDK != "v0.1.14-x" || v.Took.Composition != 40 {
|
|
t.Fatalf("verdict = %+v, composition %+v", v, v.Composition)
|
|
}
|
|
if len(f.asked) != 1 || f.asked[0] != cs.Key {
|
|
t.Errorf("asked %v, want the check's key %s", f.asked, cs.Key)
|
|
}
|
|
if !compositionMentions(v, "passed") {
|
|
t.Errorf("the line says %q", compositionWords(v.Composition, v.Took.Composition))
|
|
}
|
|
if v.Composition.FPSet == "" || v.Composition.FPSet != cs.FPSet {
|
|
t.Errorf("the composition names the fingerprint set %q, want the candidate's %q (ADR 0548 §5)", v.Composition.FPSet, cs.FPSet)
|
|
}
|
|
// ADR 0548 §5: a carry whose fingerprints did not change keeps the composition it passed and asks the assigner nothing.
|
|
carry := caseOf(t, pullOf(repoA, 7, head1))
|
|
carry.CarriedFrom, carry.Composition = map[string]string{repoA: trunkA2}, v.Composition
|
|
if kept := b.judge(t, carry).Verdict; kept.Composition == nil || kept.Composition.SDK != "v0.1.14-x" || len(f.asked) != 1 {
|
|
t.Errorf("the carry's composition is %+v and the assigner was asked %d times; want the kept one and no second ask", kept.Composition, len(f.asked))
|
|
}
|
|
}
|
|
|
|
func compositionMentions(v delivery.Verdict, word string) bool {
|
|
return strings.Contains(compositionWords(v.Composition, v.Took.Composition), word)
|
|
}
|
|
|
|
// 1297/AC2 at the checker: a failed composition fails the check, naming the machine it failed at and its words.
|
|
func TestAFailedCompositionFailsTheCheck(t *testing.T) {
|
|
b := newBench(t)
|
|
b.ex.Compositions = &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x",
|
|
Machine: "g14", Words: "g14 runs web, which the change removes from its source"}}
|
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
if v.State != delivery.VerdictFail || v.Failed.Machine != "g14" || v.Composition == nil || v.Composition.State != delivery.CompositionFail {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
v.Group, v.Heads = "feat/x", "0123456789ab" // the checker names them when it writes the verdict
|
|
if _, err := delivery.EncodeVerdict(v); err != nil {
|
|
t.Errorf("the verdict cannot be written: %v", err)
|
|
}
|
|
}
|
|
|
|
// 1297/B3d: a composition that cannot be read leaves it not run; a try is not composed.
|
|
func TestACompositionNotHadIsNotRun(t *testing.T) {
|
|
b := newBench(t)
|
|
f := &fakeCompositions{err: errors.New("the check's bound passed")}
|
|
b.ex.Compositions = f
|
|
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
|
|
if v.State != delivery.VerdictPass || v.Composition == nil || v.Composition.State != delivery.CompositionNotRun {
|
|
t.Fatalf("verdict = %+v", v)
|
|
}
|
|
b2 := newBench(t)
|
|
f2 := &fakeCompositions{v: delivery.CompositionVerdict{State: delivery.CompositionFail, Words: "w", SDK: "x"}}
|
|
b2.ex.Compositions = f2
|
|
cs := caseOf(t, pullOf(repoA, 7, head1))
|
|
cs.Try = &TryOptions{}
|
|
b2.judge(t, cs)
|
|
if len(f2.asked) != 0 {
|
|
t.Errorf("a try asked for a composition: %v", f2.asked)
|
|
}
|
|
}
|
|
|
|
// fixedTrunks are trunk heads that never move.
|
|
type fixedTrunks struct{ heads map[string]string }
|
|
|
|
func (f fixedTrunks) Heads() map[string]string { return f.heads }
|
|
|
|
// fakeEnds answers the builder's builds of a candidate as fakeBuilds holds them, recording the builds waited on; the
|
|
// composition must not have been asked yet when it is (ADR 0603 §10: the check composes with the builds).
|
|
type fakeEnds struct {
|
|
builds fakeBuilds
|
|
asked map[string]string
|
|
composed *fakeCompositions
|
|
early bool
|
|
}
|
|
|
|
func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) {
|
|
f.asked = builds
|
|
f.early = f.composed != nil && len(f.composed.asked) > 0
|
|
out := map[string]delivery.BuilderBuild{}
|
|
for module, fp := range builds {
|
|
if b, ok := f.builds[module+" "+fp]; ok && (b.State == delivery.BuildDone || b.State == delivery.BuildFailed) {
|
|
out[module] = b
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeEnds) Done(context.Context, map[string]string) bool { return false }
|
|
|
|
// fixedServing are the verbs the running builds serve.
|
|
type fixedServing []string
|
|
|
|
func (f fixedServing) Served(context.Context) (func(string) bool, error) {
|
|
return func(v string) bool { return slices.Contains(f, v) }, nil
|
|
}
|
|
|
|
// knownRuns are the modules the trunk holds a record of (the indexer's modules): any commit.
|
|
type knownRuns []string
|
|
|
|
func (k knownRuns) CommitOf(module string) string {
|
|
if slices.Contains(k, module) {
|
|
return trunkA
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// ADR 0603 §10: before it composes, the check waits on the builder's builds of the candidate's changed modules and composes
|
|
// with them; a failed build fails the check naming it, and one not ended within the bound is named. It does not wait on a
|
|
// module built by a build module the change alters, nor on one standing on a base the change alters, nor on one the trunk
|
|
// holds no record of, and names each in the composition's warnings (and so the check's line); a change that alters none of
|
|
// these says nothing.
|
|
func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
|
|
dig := func(c string) string { return strings.Repeat(c, 64) }
|
|
fp := "src2:" + dig("f")
|
|
goBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "bin", Kind: catalogue.ArtifactBundle, Language: "go"}}}
|
|
tsBuild := &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "app", Kind: catalogue.ArtifactBundle, Language: "typescript"}}}
|
|
done := delivery.BuilderBuild{State: delivery.BuildDone}
|
|
for _, row := range []struct {
|
|
name string
|
|
manifest map[string]catalogue.Manifest
|
|
builds map[string]delivery.BuilderBuild // by module, at fp; nil leaves the check without the builds' ends
|
|
runs knownRuns // nil: the trunk holds every module
|
|
want []string
|
|
waited string // the modules waited on, sorted
|
|
failed string // the failure's test, when the check fails on a build
|
|
tests bool // the tests fail: the check fails before it would wait
|
|
served []string // what the running builds serve (ADR 0614 §7); nil reads nothing of what runs
|
|
}{
|
|
{name: "the Go build module invokes a verb no running build serves",
|
|
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild,
|
|
Invokes: []string{"seat:package-registry.publish", "seat:artifact-store.put"}}, "web": {Module: "web", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"mesh-build-go": done}, served: []string{"seat:artifact-store.put"},
|
|
want: []string{"the changed build module mesh-build-go invokes seat:package-registry.publish, which no running build serves, " +
|
|
"so the check cannot run it: web, which it builds, are composed from their manifests (ADR 0614 §7)"}, waited: "mesh-build-go"},
|
|
{name: "the Go build module changes",
|
|
manifest: map[string]catalogue.Manifest{"mesh-build-go": {Module: "mesh-build-go", Build: goBuild}, "web": {Module: "web", Build: goBuild},
|
|
"mail": {Module: "mail", Build: goBuild}, "docs": {Module: "docs"}},
|
|
builds: map[string]delivery.BuilderBuild{"mesh-build-go": done, "docs": done},
|
|
want: []string{"the build module mesh-build-go: mail, web, which it builds"}, waited: "docs, mesh-build-go"},
|
|
{name: "the Node build module changes",
|
|
manifest: map[string]catalogue.Manifest{"mesh-build-node": {Module: "mesh-build-node", Build: goBuild}, "panel": {Module: "panel", Build: tsBuild},
|
|
"web": {Module: "web", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"mesh-build-node": done, "web": done},
|
|
want: []string{"the build module mesh-build-node: panel, which it builds"}, waited: "mesh-build-node, web"},
|
|
{name: "a base changes",
|
|
manifest: map[string]catalogue.Manifest{"image": {Module: "image"},
|
|
"web": {Module: "web", Build: &catalogue.Build{On: []catalogue.BuildsOn{{Arg: "BASE", Module: "image", Artifact: "img"}}}}},
|
|
builds: map[string]delivery.BuilderBuild{"image": done},
|
|
want: []string{"alters image, a base of web", "waits on the build of image alone"}, waited: "image"},
|
|
{name: "neither changes", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"web": done}, waited: "web"},
|
|
{name: "a build fails", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "mail": {Module: "mail", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildFailed, Error: "go build: undefined: x"}, "mail": done},
|
|
waited: "mail, web", failed: "build: web"},
|
|
{name: "the tests fail", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}}, tests: true,
|
|
want: []string{"composes at once, without waiting on the builds of web"}},
|
|
{name: "a build does not end within the bound", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"web": {State: delivery.BuildBuilding}},
|
|
want: []string{"the builds of web did not end within the build-wait-bound"}, waited: "web"},
|
|
{name: "a module the trunk does not hold", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}, "new": {Module: "new", Build: goBuild}},
|
|
builds: map[string]delivery.BuilderBuild{"web": done}, runs: knownRuns{"web"},
|
|
want: []string{"new: the trunk holds no record of it yet"}, waited: "web"},
|
|
{name: "the builds cannot be heard", manifest: map[string]catalogue.Manifest{"web": {Module: "web", Build: goBuild}},
|
|
want: []string{"the builds of web cannot be heard"}},
|
|
} {
|
|
b := candidateBench(t, true)
|
|
if row.tests {
|
|
b.test = func(c stepCall) (any, error) {
|
|
r := captured[Tested](t, "go-test-failing")
|
|
r.Level, _ = c.args["level"].(string)
|
|
return r, nil
|
|
}
|
|
}
|
|
comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}}
|
|
b.ex.Compositions = comps
|
|
var ends *fakeEnds
|
|
if row.builds != nil {
|
|
ends = &fakeEnds{builds: fakeBuilds{}, composed: comps}
|
|
for module, build := range row.builds {
|
|
ends.builds[module+" "+fp] = build
|
|
}
|
|
b.ex.BuildEnds = ends
|
|
}
|
|
if row.runs != nil {
|
|
b.ex.Runs = row.runs
|
|
}
|
|
if row.served != nil {
|
|
b.ex.Serving = fixedServing(row.served)
|
|
}
|
|
read, fps := fixedManifests{}, map[string]delivery.CandidateModule{}
|
|
for module, m := range row.manifest {
|
|
d := dig(string(rune('a' + len(read))))
|
|
read[d], fps[module] = m, delivery.CandidateModule{FP2: fp, Manifest: d}
|
|
}
|
|
b.ex.Read = read
|
|
p := pullOf(catalogueRepository, 9, head1)
|
|
p.Fps, p.Modules = fps, slices.Sorted(maps.Keys(fps))
|
|
v := b.judge(t, caseOf(t, p)).Verdict
|
|
if v.Composition == nil || len(comps.asked) != 1 {
|
|
t.Fatalf("%s: composed %d times: %+v", row.name, len(comps.asked), v)
|
|
}
|
|
if ends != nil {
|
|
if got := strings.Join(slices.Sorted(maps.Keys(ends.asked)), ", "); got != row.waited || ends.early {
|
|
t.Errorf("%s: waited on %q (after the composition: %v), want %q before it composes", row.name, got, ends.early, row.waited)
|
|
}
|
|
}
|
|
switch {
|
|
case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x") ||
|
|
!strings.Contains(v.Failed.Words, "the check asks itself again when the build is done")):
|
|
t.Errorf("%s: the verdict is %s failing %+v, want it failed naming %s, the builder's words and how it is asked again", row.name, v.State, v.Failed, row.failed)
|
|
case row.tests && v.State != delivery.VerdictFail:
|
|
t.Errorf("%s: %+v, want the tests' failure", row.name, v)
|
|
case row.failed == "" && !row.tests && v.State != delivery.VerdictPass:
|
|
t.Errorf("%s: %+v", row.name, v)
|
|
}
|
|
words := compositionWords(v.Composition, v.Took.Composition)
|
|
for _, w := range row.want {
|
|
if !strings.Contains(words, w) {
|
|
t.Errorf("%s: the line says %q, want it to name %q", row.name, words, w)
|
|
}
|
|
}
|
|
if row.want == nil && v.Composition.Words != "" {
|
|
t.Errorf("%s: warned %q though the check waited on every build of the change", row.name, v.Composition.Words)
|
|
}
|
|
}
|
|
}
|