Checker: off mesh-sdk go/delivery onto the seats' types and the clients (verdict, kept, preview, owed, candidates, modules, manifests, packages, source, turns, haves), its holders and buckets its own constants checked against its manifest (D-11), the check ask's fetch typed with the group package and the check answer's fetch read as the Delivery SDK's FetchAnswer, a merge with no program told by merge (B2g; ADR 0634 §1, §2, §6; issue 1966)

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
This commit is contained in:
2026-11-05 17:00:25 +01:00
parent 6b32e8257d
commit 84e617d3a9
60 changed files with 1499 additions and 1226 deletions
@@ -10,13 +10,14 @@ import (
"sync/atomic"
"time"
"mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
"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/modules/mesh-delivery-builder/buildrun"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-checker/internal/check"
)
// reads counts the values this call read from the bus: the runtime launches the tools for one call, so the count is the call's.
@@ -68,7 +69,7 @@ func (r runsOf) CommitOf(module string) string {
if err != nil || len(raw) == 0 {
return ""
}
m, err := delivery.DecodeModule(raw)
m, err := meshcatalogclient.DecodeModule(raw)
if err != nil {
return ""
}
@@ -292,11 +293,11 @@ func main() {
tries := stdio.State(check.TriesState)
source := stdio.State("mesh-delivery-gate.source")
trunks := func() (map[string]string, error) {
e, err := source.Get(delivery.SourceMainKey)
e, err := source.Get(meshdeliveryclient.SourceMainKey)
if err != nil || e == nil {
return nil, err
}
s, err := delivery.DecodeSource(e.Value)
s, err := meshdeliveryclient.DecodeSource(e.Value)
return s.Heads, err
}
r.Trunks = trunks
@@ -330,7 +331,7 @@ func main() {
tools := append(append(append(Tools(r), TryTools(trying)...), SeatTryTools(trying)...), RecoveryTools(r, written)...)
// checker_first_views and checker_first_view_again: where the checker's first views of the indexer's and the assigner's
// states stand, and asking one again by hand (ADR 0615, DLV-M2).
tools = append(tools, views.StatusTools(delivery.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
tools = append(tools, views.StatusTools(check.CheckerModule, "checker_first_views", "checker_first_view_again", stdio.State(views.StatusState), meshhandactsclient.Recorder{Ask: stdio.Ask}, time.Now)...)
if err := stdio.Serve("", logged(tools)); err != nil {
fmt.Fprintf(os.Stderr, "[mesh-delivery-checker-tools] %v\n", err)
os.Exit(1)
@@ -7,8 +7,8 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"mesh-delivery-checker/internal/check"
@@ -110,7 +110,7 @@ func follow(ctx context.Context, w jetstream.KeyWatcher, each func(jetstream.Key
type manifestReader struct{ kv jetstream.KeyValue }
func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.Manifest, error) {
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return catalogue.Manifest{}, err
}
@@ -118,11 +118,11 @@ func (m manifestReader) Manifest(ctx context.Context, digest string) (catalogue.
if err != nil {
return catalogue.Manifest{}, fmt.Errorf("manifests.%s: %w", key, err)
}
kept, err := delivery.DecodeManifest(e.Value())
kept, err := meshcatalogclient.DecodeManifest(e.Value())
if err != nil {
return catalogue.Manifest{}, err
}
return parseManifest(kept.JSON)
return parseManifest(kept.Manifest)
}
// parseManifest decodes a manifest the indexer kept, as every reader does (novox/hq ADR 0578 §3): judged by nothing here,
@@ -5,19 +5,24 @@ import (
"encoding/json"
"errors"
"fmt"
nodeengineseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"sort"
"sync"
"time"
nodeengineseat "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"github.com/nats-io/nats.go/jetstream"
"mesh-delivery-checker/internal/check"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The checker's composition of a check on the bus (novox/hq ADR 0593 §7, §9; check.EngineCompositions): each placed
@@ -65,18 +70,18 @@ func (p placedNodes) Placed() ([]string, error) {
defer cancel()
kv, err := p.kv(ctx)
if err != nil {
return nil, fmt.Errorf("the state %s cannot be read: %w", delivery.PlacementBucket, err)
return nil, fmt.Errorf("the state %s cannot be read: %w", buildrun.PlacementBucket, err)
}
lister, err := kv.ListKeys(ctx)
if errors.Is(err, jetstream.ErrNoKeysFound) {
return nil, nil // a placement that places nothing
}
if err != nil {
return nil, fmt.Errorf("the state %s cannot be listed: %w", delivery.PlacementBucket, err)
return nil, fmt.Errorf("the state %s cannot be listed: %w", buildrun.PlacementBucket, err)
}
seen := map[string]bool{}
for key := range lister.Keys() {
if node, _, _, err := delivery.ParsePlacementKey(key); err == nil {
if node, _, _, err := meshmoduleassignerclient.ParseKey(key); err == nil {
seen[node] = true
}
}
@@ -89,20 +94,20 @@ func (p placedNodes) Placed() ([]string, error) {
}
// seatSandbox runs a compose job on the Go build seat's check stream, as the checker's own ask, and answers the
// composer's verdict from the step's result (delivery.ComposeAnswer).
// composer's verdict from the step's result (buildstep.ComposeAnswer).
type seatSandbox struct{ asker buildrun.Asker }
func (s seatSandbox) Compose(ctx context.Context, job delivery.ComposeJob) (composer.Verdict, error) {
ask := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule,
func (s seatSandbox) Compose(ctx context.Context, job buildstep.ComposeJob) (composer.Verdict, error) {
ask := buildstep.StepAsk{ID: buildstep.NewID(), Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: check.CheckerModule,
At: time.Now().UTC(), Compose: &job}
step, err := s.asker.Ask(ctx, ask)
if err != nil {
return composer.Verdict{}, err
}
if step.State != delivery.StepPassed {
if step.State != buildstep.StepPassed {
return composer.Verdict{}, fmt.Errorf("the compose ask %s ended %s: %s", ask.ID, step.State, step.Reason)
}
var a delivery.ComposeAnswer
var a buildstep.ComposeAnswer
if err := json.Unmarshal(step.Result, &a); err != nil {
return composer.Verdict{}, fmt.Errorf("the compose ask %s answered no compose answer: %w", ask.ID, err)
}
@@ -114,17 +119,17 @@ func (s seatSandbox) Compose(ctx context.Context, job delivery.ComposeJob) (comp
}
// manifestVerdicts are the indexer's refusals of the trunk's manifests, by module, from its modules' values
// (delivery.Module.VerdictOf, ADR 0578 §3): what the sandbox's composer holds a refused module by.
// (meshcatalog.ModulesByModuleValue.VerdictOf, ADR 0578 §3): what the sandbox's composer holds a refused module by.
type manifestVerdicts struct {
mu sync.Mutex
by map[string]delivery.ManifestVerdict
by map[string]meshcatalog.VerdictsByDigestValue
}
func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) {
func (m *manifestVerdicts) put(module string, v *meshcatalog.VerdictsByDigestValue) {
m.mu.Lock()
defer m.mu.Unlock()
if m.by == nil {
m.by = map[string]delivery.ManifestVerdict{}
m.by = map[string]meshcatalog.VerdictsByDigestValue{}
}
if v == nil {
delete(m.by, module)
@@ -132,13 +137,13 @@ func (m *manifestVerdicts) put(module string, v *delivery.ManifestVerdict) {
}
// The sandbox's composer may be the node-engine of an older delivery, which does not know the verdict waits: it is handed a
// wait as the refusal it is held as.
m.by[module] = v.AsRefused()
m.by[module] = meshcatalogclient.VerdictAsRefused(*v)
}
func (m *manifestVerdicts) all() map[string]delivery.ManifestVerdict {
func (m *manifestVerdicts) all() map[string]meshcatalog.VerdictsByDigestValue {
m.mu.Lock()
defer m.mu.Unlock()
out := make(map[string]delivery.ManifestVerdict, len(m.by))
out := make(map[string]meshcatalog.VerdictsByDigestValue, len(m.by))
for k, v := range m.by {
out[k] = v
}
@@ -22,18 +22,30 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"mesh-delivery-checker/internal/check"
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
"git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"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/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/views/viewstest"
"mesh-delivery-checker/internal/check"
)
// busConf is the shape of the composed accounts file (mesh-controller internal/broker): one account with JetStream, the
@@ -98,7 +110,7 @@ type fakeBuildModules struct {
// names are the nodes' names the last check ask carried, for the names check (ADR 0567 §12).
names []string
// packages are, by repository, the package versions the last Go check ask of it carried (StepAsk.Packages).
packages map[string]delivery.PackageVersions
packages map[string]buildstep.PackageVersions
}
func (f *fakeBuildModules) count(tool string) int {
@@ -158,7 +170,7 @@ func (f *fakeBuildModules) result(t *testing.T, tool string, args map[string]any
// does since R4g (ADR 0525; issue 1887 A5): each ask's job runs with step bodies answering the captured results, and a
// check takes the kept verdicts of the checker's kept bucket by their digests.
func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.KeyValue) {
kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: delivery.StepsBucket(module), History: 1})
kv, err := f.js.CreateKeyValue(f.ctx, jetstream.KeyValueConfig{Bucket: buildrun.StepsBucket(module), History: 1})
if err != nil {
t.Fatal(err)
}
@@ -178,7 +190,7 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key
}
steps := gobuild.AskSteps{Fetch: call("fetch"), Test: call("test"),
Kept: func(ctx context.Context, level, digest string) (int, bool, error) {
key, err := delivery.KeptKey(level, digest)
key, err := meshdeliverycheckerclient.KeptKey(level, digest)
if err != nil {
return 0, false, err
}
@@ -189,8 +201,8 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key
if err != nil {
return 0, false, err
}
v, err := delivery.DecodeKept(e.Value())
return v.Tests, err == nil, err
v, err := meshdeliverycheckerclient.DecodeKept(e.Value())
return int(v.Tests), err == nil, err
}}
if module == "mesh-build-go" {
steps.Digest = call("digest")
@@ -201,13 +213,13 @@ func (f *fakeBuildModules) serve(t *testing.T, module string, kept jetstream.Key
}
p := &gobuild.Puller{Source: &gobuild.JetStreamAsks{JS: f.js, Language: language}, Language: language, Node: "n1", Put: put,
Capacity: func() int { return 2 }, PullWait: 200 * time.Millisecond,
Run: func(ctx context.Context, a delivery.StepAsk) (any, error) {
Run: func(ctx context.Context, a buildstep.StepAsk) (any, error) {
if a.Check != nil {
f.mu.Lock()
f.names = a.Check.NodeNames
if repository, _ := a.Check.Fetch["repository"].(string); a.Language == delivery.AskGo {
if repository := a.Check.Fetch.Repository; a.Language == buildstep.AskGo {
if f.packages == nil {
f.packages = map[string]delivery.PackageVersions{}
f.packages = map[string]buildstep.PackageVersions{}
}
f.packages[repository] = a.Packages
}
@@ -311,13 +323,13 @@ func (b *bed) said(t *testing.T, words string) {
// the seat verb's caller-named and plain subjects); the others never answer. It answers what it was asked.
func (b *bed) engine(t *testing.T, nodes ...string) func() []ne.ComposeCandidateInput {
t.Helper()
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: delivery.PlacementBucket, History: 1})
kv, err := b.js.CreateKeyValue(b.ctx, jetstream.KeyValueConfig{Bucket: buildrun.PlacementBucket, History: 1})
if err != nil {
t.Fatal(err)
}
for _, n := range nodes {
pk, _ := delivery.PlacementNodeKey(n)
raw, _ := json.Marshal(delivery.Placement{Node: n})
pk, _ := meshmoduleassignerclient.NodeKey(n)
raw, _ := meshmoduleassignerclient.EncodeNode(meshmoduleassigner.PlacementByNodeNodeValue{Node: n})
if _, err := kv.Put(b.ctx, pk, raw); err != nil {
t.Fatal(err)
}
@@ -412,7 +424,7 @@ func startBed(t *testing.T) *bed {
if _, err := js.CreateOrUpdateConsumer(ctx, stream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
t.Fatal(err)
}
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: delivery.ToolchainsBucket(module), History: 1})
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: buildrun.ToolchainsBucket(module), History: 1})
if err != nil {
t.Fatal(err)
}
@@ -424,11 +436,11 @@ func startBed(t *testing.T) *bed {
}
// The checker's streams as its manifest declares them (ADR 0619, ADR 0620): checks, which the indexer and the forge
// bridge hear, and asks, with verdict and tag-wanted at the subjects the mesh-delivery-checker definition names.
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{meshdeliverychecker.HolderSubjectPreviewWritten(delivery.CheckerModule)}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "CHECKS", Subjects: []string{meshdeliverychecker.HolderSubjectPreviewWritten(check.CheckerModule)}}); err != nil {
t.Fatal(err)
}
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ASKS", Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule),
meshdeliverychecker.HolderSubjectTagWanted(delivery.CheckerModule)}}); err != nil {
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "ASKS", Subjects: []string{meshdeliverychecker.HolderSubjectVerdict(check.CheckerModule),
meshdeliverychecker.HolderSubjectTagWanted(check.CheckerModule)}}); err != nil {
t.Fatal(err)
}
// The shared events stream with the seats' and modules' events (the package registry seats' published among them), and the checker's durable consumer of it.
@@ -438,11 +450,11 @@ func startBed(t *testing.T) *bed {
if _, err := js.CreateOrUpdateConsumer(ctx, process.EventsStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
t.Fatal(err)
}
for name, history := range map[string]int{delivery.CheckerVerdictsBucket: delivery.CheckerVerdictsHistory, delivery.CheckerKeptBucket: delivery.CheckerKeptHistory,
delivery.CheckerOwedBucket: delivery.CheckerOwedHistory, delivery.PreviewsBucket: delivery.PreviewsHistory, delivery.CandidatesBucket: delivery.CandidatesHistory,
delivery.ModulesBucket: delivery.ModulesHistory, delivery.ManifestsBucket: delivery.ManifestsHistory, delivery.SourceBucket: delivery.SourceHistory,
delivery.PresenceBucket: delivery.PresenceHistory, delivery.HaveBucket: delivery.HaveHistory, check.TriesBucket: 1,
delivery.TurnsBucket: delivery.TurnsHistory, delivery.BuilderBuildsBucket: 1} {
for name, history := range map[string]int{check.CheckerVerdictsBucket: 5, check.CheckerKeptBucket: 1,
check.CheckerOwedBucket: 5, check.PreviewsBucket: 5, check.CandidatesBucket: 10,
check.ModulesBucket: 20, check.ManifestsBucket: 1, check.SourceBucket: 64,
check.PresenceBucket: 20, check.HaveBucket: 20, check.TriesBucket: 1,
check.TurnsBucket: 5, builds.BuilderBuildsBucket: 1} {
kv, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: uint8(history)})
if err != nil {
t.Fatal(err)
@@ -451,17 +463,17 @@ func startBed(t *testing.T) *bed {
}
// The indexer and the assigner as the owners of the states the checker follows: their read tools and views families
// (ADR 0615, DLV-M2).
for _, st := range []string{candidatesState, modulesState, manifestsState, delivery.PackagesState, delivery.IndexerPredefinedState, seatDefinitionsState} {
viewstest.Serve(t, ctx, js, ctl, delivery.IndexerModule, st, delivery.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+delivery.CheckerModule)
for _, st := range []string{candidatesState, modulesState, manifestsState, meshcatalog.StatePackages, check.IndexerPredefinedState, seatDefinitionsState} {
viewstest.Serve(t, ctx, js, ctl, check.IndexerModule, st, check.IndexerModule+".indexer_read_"+strings.ReplaceAll(st, "-", "_"), "n1_"+check.CheckerModule)
}
viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, delivery.PlacementState, delivery.AssignerModule+".assigner_read_placement", "n1_"+delivery.CheckerModule)
viewstest.Serve(t, ctx, js, ctl, buildrun.AssignerModule, meshmoduleassigner.StatePlacement, buildrun.AssignerModule+".assigner_read_placement", "n1_"+check.CheckerModule)
// 1929/AC1: the indexer's value of the repository's module, with no TypeScript manifest, so the Go build module alone
// judges the repository (an unknown repository is judged by both, issue 1887 A12).
known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"})
known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", None2: "the fingerprint is computed with its chain"})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[delivery.ModulesBucket].Put(ctx, "m", known); err != nil {
if _, err := b.buckets[check.ModulesBucket].Put(ctx, "m", known); err != nil {
t.Fatal(err)
}
b.setOnline(t, "n1", true)
@@ -471,8 +483,8 @@ func startBed(t *testing.T) *bed {
}
b.serveForge(t, ctl)
b.fake = &fakeBuildModules{js: js, ctx: ctx, calls: map[string]int{}}
b.fake.serve(t, "mesh-build-go", b.buckets[delivery.CheckerKeptBucket])
b.fake.serve(t, "mesh-build-node", b.buckets[delivery.CheckerKeptBucket])
b.fake.serve(t, "mesh-build-go", b.buckets[check.CheckerKeptBucket])
b.fake.serve(t, "mesh-build-node", b.buckets[check.CheckerKeptBucket])
b.report(t, "mesh-build-go", "n1", "go", "go1.27.1", "1")
b.report(t, "mesh-build-node", "n1", "typescript", "tsc5", "1")
b.setTrunk(t, map[string]string{repo: trunk})
@@ -481,19 +493,19 @@ func startBed(t *testing.T) *bed {
func (b *bed) setTrunk(t *testing.T, heads map[string]string) {
t.Helper()
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads})
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[delivery.SourceBucket].Put(b.ctx, delivery.SourceMainKey, data); err != nil {
if _, err := b.buckets[check.SourceBucket].Put(b.ctx, meshdeliveryclient.SourceMainKey, data); err != nil {
t.Fatal(err)
}
}
func (b *bed) setOnline(t *testing.T, node string, online bool) {
t.Helper()
data, _ := delivery.EncodePresence(delivery.Presence{Online: online})
if _, err := b.buckets[delivery.PresenceBucket].Put(b.ctx, node, data); err != nil {
data, _ := json.Marshal(meshwatchdogs.PresenceByNodeValue{V: keys.Version, Online: online})
if _, err := b.buckets[check.PresenceBucket].Put(b.ctx, node, data); err != nil {
t.Fatal(err)
}
}
@@ -502,35 +514,35 @@ func (b *bed) setOnline(t *testing.T, node string, online bool) {
// check ask (ADR 0567 §12); with no record the checker asks no check.
func (b *bed) setNode(t *testing.T, node string) {
t.Helper()
data, err := delivery.EncodeNodeRecord(delivery.NodeRecord{V: 1, Node: node})
data, err := nodeengineclient.EncodeHaveNode(nodeengine.HaveByNodeNodeValue{Record: &nodeengine.NodeRecord{Node: node}})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.HaveNodeKey(node)
if _, err := b.buckets[delivery.HaveBucket].Put(b.ctx, key, data); err != nil {
key, _ := nodeengineclient.HaveNodeKey(node)
if _, err := b.buckets[check.HaveBucket].Put(b.ctx, key, data); err != nil {
t.Fatal(err)
}
}
func (b *bed) setHealth(t *testing.T, node, module string) {
t.Helper()
data, err := delivery.EncodeHave(delivery.Have{Phase: delivery.PhaseRunning, Health: delivery.HealthOK})
data, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.HaveKey(node, module)
if _, err := b.buckets[delivery.HaveBucket].Put(b.ctx, key, data); err != nil {
key, _ := nodeengineclient.HaveKey(node, module)
if _, err := b.buckets[check.HaveBucket].Put(b.ctx, key, data); err != nil {
t.Fatal(err)
}
}
func (b *bed) report(t *testing.T, module, node, language, version, recipe string) {
t.Helper()
data, err := delivery.EncodeToolchain(delivery.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()})
data, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: language, Version: version, Recipe: recipe, At: time.Now().UTC()})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.ToolchainKey(node, language)
key, _ := buildstep.ToolchainKey(node, language)
if _, err := b.tools[module].Put(b.ctx, key, data); err != nil {
t.Fatal(err)
}
@@ -540,30 +552,30 @@ func (b *bed) report(t *testing.T, module, node, language, version, recipe strin
}
// candidate writes candidates.<group> as the indexer does (the SDK's EncodeCandidate is the indexer's own encoder).
func (b *bed) candidate(t *testing.T, group string, pulls ...delivery.CandidatePull) string {
func (b *bed) candidate(t *testing.T, group string, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) string {
t.Helper()
heads := make([]delivery.Head, len(pulls))
heads := make([]meshdeliveryclient.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
hash, err := delivery.HeadsHash(heads)
hash, err := meshdeliveryclient.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
data, err := delivery.EncodeCandidate(delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)})
data, err := json.Marshal(meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: group, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)})
if err != nil {
t.Fatal(err)
}
key, _ := delivery.CandidatesKey(group)
if _, err := b.buckets[delivery.CandidatesBucket].Put(b.ctx, key, data); err != nil {
key, _ := meshcatalogclient.CandidatesKey(group)
if _, err := b.buckets[check.CandidatesBucket].Put(b.ctx, key, data); err != nil {
t.Fatal(err)
}
vk, _ := delivery.CheckerVerdictKey(group, hash)
vk, _ := meshdeliverycheckerclient.VerdictKey(group, hash)
return vk
}
func pull(repository string, number int, head string) delivery.CandidatePull {
return delivery.CandidatePull{Head: delivery.Head{Repository: repository, Number: number, Head: head}, Modules: []string{}}
func pull(repository string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem {
return meshcatalog.CandidatesByGroupValuePullsItem{Repository: repository, Number: int64(number), Head: head, Modules: []string{}}
}
// start runs the program as the checker of a node.
@@ -586,15 +598,15 @@ func (b *bed) start(t *testing.T, env map[string]string) {
}
// verdict reads the verdict key until pred holds of it, within 20 s.
func (b *bed) verdict(t *testing.T, key string, pred func(delivery.Verdict) bool) delivery.Verdict {
func (b *bed) verdict(t *testing.T, key string, pred func(meshdeliverychecker.VerdictsByGroupHeadsValue) bool) meshdeliverychecker.VerdictsByGroupHeadsValue {
t.Helper()
var last delivery.Verdict
var last meshdeliverychecker.VerdictsByGroupHeadsValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
e, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, key)
e, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, key)
if err != nil {
continue
}
if last, err = delivery.DecodeVerdict(e.Value()); err == nil && pred(last) {
if last, err = meshdeliverycheckerclient.DecodeVerdict(e.Value()); err == nil && pred(last) {
return last
}
}
@@ -602,22 +614,24 @@ func (b *bed) verdict(t *testing.T, key string, pred func(delivery.Verdict) bool
return last
}
func ended(v delivery.Verdict) bool { return v.State != delivery.VerdictRunning }
func ended(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State != meshdeliverycheckerclient.VerdictRunning
}
// owed reads the status the checker owes on a head.
func (b *bed) owed(t *testing.T, number int, head, state string) delivery.Owed {
func (b *bed) owed(t *testing.T, number int, head, state string) meshdeliverychecker.OwedByRepositoryNumberHeadActValue {
t.Helper()
key, err := delivery.CheckerOwedKey(repo, number, head)
key, err := check.CheckerOwedKey(repo, number, head)
if err != nil {
t.Fatal(err)
}
var last delivery.Owed
var last meshdeliverychecker.OwedByRepositoryNumberHeadActValue
for deadline := time.Now().Add(20 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
e, err := b.buckets[delivery.CheckerOwedBucket].Get(b.ctx, key)
e, err := b.buckets[check.CheckerOwedBucket].Get(b.ctx, key)
if err != nil {
continue
}
if last, err = delivery.DecodeCheckerOwed(e.Value()); err == nil && last.State == state {
if last, err = check.DecodeCheckerOwed(e.Value()); err == nil && last.State == state {
return last
}
}
@@ -633,25 +647,25 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
// ADR 0603 §10: the candidate changes the module m, whose manifest the trunk holds; the builder has not built m's
// candidate fingerprint yet, so the check waits on the builder's key before it composes.
manifest := `{"module":"m"}`
digest := delivery.ManifestDigest([]byte(manifest))
stored, err := delivery.EncodeManifest(delivery.Manifest{Module: "m", JSON: manifest})
digest := meshcatalogclient.ManifestDigest([]byte(manifest))
stored, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "m", Manifest: manifest})
if err != nil {
t.Fatal(err)
}
mkey, _ := delivery.ManifestsKey(digest)
if _, err := b.buckets[delivery.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil {
mkey, _ := meshcatalogclient.ManifestsKey(digest)
if _, err := b.buckets[check.ManifestsBucket].Put(b.ctx, mkey, stored); err != nil {
t.Fatal(err)
}
known, err := delivery.EncodeModule(delivery.Module{Repository: repo, Commit: trunk, Path: "modules/m", Manifest: digest, None2: "the fingerprint is computed with its chain"})
known, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repo, Commit: trunk, Path: "modules/m", Manifest: digest, None2: "the fingerprint is computed with its chain"})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[delivery.ModulesBucket].Put(b.ctx, "m", known); err != nil {
if _, err := b.buckets[check.ModulesBucket].Put(b.ctx, "m", known); err != nil {
t.Fatal(err)
}
fp := "src2:" + strings.Repeat("f", 64)
p := pull(repo, 7, head1)
p.Modules, p.Fps = []string{"m"}, map[string]delivery.CandidateModule{"m": {FP2: fp, Manifest: digest}}
p.Modules, p.Fps = []string{"m"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: fp, Manifest: digest}}
key := b.candidate(t, "feat/x", p)
// ADR 0593 §7, §9 (D12): n1 and n2 are placed; n1's engine answers compose-candidate on the bus, n2's never does.
asked := b.engine(t, "n1", "n2")
@@ -660,34 +674,36 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
if got := asked(); len(got) != 0 {
t.Fatalf("the engines were asked %+v before m's build ended", got)
}
bkey, _ := delivery.BuilderBuildKey("m", fp)
building, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildBuilding})
if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil {
bkey, _ := builds.BuilderBuildKey("m", fp)
building, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildBuilding})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, building); err != nil {
t.Fatal(err)
}
if v := b.verdict(t, key, func(delivery.Verdict) bool { return true }); v.State != delivery.VerdictRunning || len(asked()) != 0 {
if v := b.verdict(t, key, func(meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return true }); v.State != meshdeliverycheckerclient.VerdictRunning || len(asked()) != 0 {
t.Fatalf("verdict %s and %d asks while m builds: the check composes only once its build ended", v.State, len(asked()))
}
// m's build fails: the check fails naming it. Built again (builder_build_again writes the key done), the check asks
// itself again for the same heads, with no recheck, and passes.
failedBuild, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildFailed, Error: "go build: undefined: x"})
if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil {
failedBuild, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildFailed, Error: "go build: undefined: x"})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, failedBuild); err != nil {
t.Fatal(err)
}
if v := b.verdict(t, key, ended); v.State != delivery.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" {
if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Test != check.BuildFailedTest+"m" {
t.Fatalf("verdict %+v, want it failed on m's build", v)
}
built, _ := json.Marshal(delivery.BuilderBuild{V: 1, State: delivery.BuildDone, Manifest: digest})
if _, err := b.buckets[delivery.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil {
built, _ := json.Marshal(builds.BuilderBuild{V: 1, State: builds.BuildDone, Manifest: digest})
if _, err := b.buckets[builds.BuilderBuildsBucket].Put(b.ctx, bkey, built); err != nil {
t.Fatal(err)
}
b.said(t, "the builds of m it failed on are done again, so it is checked again")
v := b.verdict(t, key, func(v delivery.Verdict) bool { return v.State == delivery.VerdictPass })
v := b.verdict(t, key, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictPass
})
// The check asked again takes the packages the failed check ran from their kept verdicts.
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Kept != 2 || v.Trunk[repo] != trunk {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Kept != 2 || v.Trunk[repo] != trunk {
t.Fatalf("verdict = %+v", v)
}
if c := v.Composition; c == nil || c.State != delivery.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) ||
if c := v.Composition; c == nil || c.State != meshdeliverycheckerclient.CompositionPass || !slices.Equal(c.Machines, []string{"n1"}) ||
!strings.Contains(c.Words, "n2: "+nec.NoAnswer) || len(v.Machines) != 1 || v.Machines[0] != "n1" {
t.Errorf("composition %+v machines %v", v.Composition, v.Machines)
}
@@ -704,7 +720,7 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
t.Errorf("the check ask named the nodes %v, want [n1] from the have's node record", names)
}
o := b.owed(t, 7, head1, "success")
if o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 0 run, 2 kept") {
if o.Context != meshdeliveryclient.StatusContextCheck || !strings.HasPrefix(o.Text, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(o.Text, "unit 0 run, 2 kept") {
t.Errorf("owed = %+v", o)
}
// Two checks of the same heads: the one failed on m's build and the one it asked itself again; the second runs no test.
@@ -715,17 +731,17 @@ func TestCheckerJudgesACandidateAndOwesItsStatus(t *testing.T) { // N6/AC5, 1300
}
// What passed is kept once under each level's digest.
for _, k := range []string{"unit." + digU, "integration." + digI} {
if _, err := b.buckets[delivery.CheckerKeptBucket].Get(b.ctx, k); err != nil {
if _, err := b.buckets[check.CheckerKeptBucket].Get(b.ctx, k); err != nil {
t.Errorf("kept.%s: %v", k, err)
}
}
// The preview of the head merged onto the trunk, with the tree and the manifest digest the fetch named.
pk, _ := delivery.PreviewKey(repo, head1, trunk)
e, err := b.buckets[delivery.PreviewsBucket].Get(b.ctx, pk)
pk, _ := meshdeliverycheckerclient.PreviewKey(repo, head1, trunk)
e, err := b.buckets[check.PreviewsBucket].Get(b.ctx, pk)
if err != nil {
t.Fatalf("previews.%s: %v", pk, err)
}
pv, err := delivery.DecodePreview(e.Value())
pv, err := meshdeliverycheckerclient.DecodePreview(e.Value())
if err != nil || pv.Merge != merged || pv.Modules["modules/m"].Tree != "1111111111111111111111111111111111111111" {
t.Errorf("preview = %+v, %v", pv, err)
}
@@ -744,7 +760,7 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
tests, kept := b.fake.count("mesh-build-go.build_go_test"), keptCount(t, b)
second := b.candidate(t, "feat/y", pull(repo, 8, head2))
v := b.verdict(t, second, ended)
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 0 || v.Levels[delivery.LevelUnit].Kept != 2 || v.Levels[delivery.LevelIntegration].Run != 0 {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept != 2 || v.Levels[buildstep.LevelIntegration].Run != 0 {
t.Fatalf("verdict = %+v, want everything taken from the kept verdicts", v)
}
if got := b.fake.count("mesh-build-go.build_go_test"); got != tests {
@@ -756,22 +772,22 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
// §3: the gate's turn on the repository holds a new check off; its delete claims it.
at := time.Now().UTC()
turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: "fix/other", Heads: "0123456789ab", Approved: at, Since: at}}})
turn, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repo, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{{Group: "fix/other", Heads: "0123456789ab", Approved: at, Since: at}}})
if err != nil {
t.Fatal(err)
}
tk, _ := delivery.TurnsKey(repo)
turnRev, err := b.buckets[delivery.TurnsBucket].Put(b.ctx, tk, turn)
tk, _ := meshdeliveryclient.TurnsKey(repo)
turnRev, err := b.buckets[check.TurnsBucket].Put(b.ctx, tk, turn)
if err != nil {
t.Fatal(err)
}
b.said(t, fmt.Sprintf("merge turn %s changed (revision %d)", tk, turnRev)) // the checker heard the turn
third := b.candidate(t, "feat/z", pull(repo, 9, "3333333333333333333333333333333333333333"))
b.said(t, third+": not claimed: the approved merge of fix/other") // it held the check off
if _, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) {
if _, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, third); !errors.Is(err, jetstream.ErrKeyNotFound) {
t.Fatalf("verdicts.%s exists (%v) while a merge turn holds its repository", third, err)
}
if err := b.buckets[delivery.TurnsBucket].Delete(b.ctx, tk); err != nil {
if err := b.buckets[check.TurnsBucket].Delete(b.ctx, tk); err != nil {
t.Fatal(err)
}
b.verdict(t, third, ended)
@@ -780,8 +796,8 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
tests = b.fake.count("mesh-build-go.build_go_test")
const moved = "abababababababababababababababababababab"
b.setTrunk(t, map[string]string{repo: moved})
c := b.verdict(t, first, func(v delivery.Verdict) bool { return ended(v) && v.Trunk[repo] == moved })
if c.State != delivery.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[delivery.LevelUnit].Run != 0 || c.Levels[delivery.LevelIntegration].Run != 0 {
c := b.verdict(t, first, func(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool { return ended(v) && v.Trunk[repo] == moved })
if c.State != meshdeliverycheckerclient.VerdictPass || c.CarriedFrom[repo] != trunk || c.Levels[buildstep.LevelUnit].Run != 0 || c.Levels[buildstep.LevelIntegration].Run != 0 {
t.Fatalf("the carried verdict = %+v, want a pass carried from %s with no test run", c, trunk[:12])
}
if got := b.fake.count("mesh-build-go.build_go_test"); got != tests {
@@ -794,7 +810,7 @@ func TestACheckOrACarryOfEqualInputsRunsNothing(t *testing.T) { // 1287/AC5, 128
func keptCount(t *testing.T, b *bed) int {
t.Helper()
keys, err := b.buckets[delivery.CheckerKeptBucket].Keys(b.ctx)
keys, err := b.buckets[check.CheckerKeptBucket].Keys(b.ctx)
if err != nil {
return 0
}
@@ -809,7 +825,7 @@ func TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest(t *testing.T) { /
key := b.candidate(t, "feat/x", pull(repo, 7, head1))
b.start(t, nil)
v := b.verdict(t, key, ended)
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, "files in conflict: a.go, sub/b.go") {
if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, "files in conflict: a.go, sub/b.go") {
t.Fatalf("verdict = %+v", v)
}
if o := b.owed(t, 7, head1, "failure"); !strings.HasPrefix(o.Text, "failed: n1: ") || !strings.Contains(o.Text, "files in conflict: a.go, sub/b.go") {
@@ -818,10 +834,10 @@ func TestAHeadThatDoesNotMergeFailsNamingTheFilesAndRunsNoTest(t *testing.T) { /
if n := b.fake.count("mesh-build-go.build_go_digest") + b.fake.count("mesh-build-go.build_go_test"); n != 0 {
t.Errorf("%d digest and test steps ran after a conflict", n)
}
pk, _ := delivery.PreviewKey(repo, head1, trunk)
if e, err := b.buckets[delivery.PreviewsBucket].Get(b.ctx, pk); err != nil {
pk, _ := meshdeliverycheckerclient.PreviewKey(repo, head1, trunk)
if e, err := b.buckets[check.PreviewsBucket].Get(b.ctx, pk); err != nil {
t.Errorf("previews.%s: %v", pk, err)
} else if pv, err := delivery.DecodePreview(e.Value()); err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" {
} else if pv, err := meshdeliverycheckerclient.DecodePreview(e.Value()); err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" {
t.Errorf("preview = %+v, %v, want the files in conflict alone", pv, err)
}
}
@@ -834,13 +850,13 @@ func TestAnHqPullRequestGetsAVerdictAndAStatus(t *testing.T) {
b.setTrunk(t, map[string]string{repo: trunk, "novox/hq": trunk})
hq := b.candidate(t, "feat/hq", pull("novox/hq", 3, head1))
b.start(t, nil)
if v := b.verdict(t, hq, ended); v.State != delivery.VerdictPass {
if v := b.verdict(t, hq, ended); v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("hq's verdict = %+v", v)
}
key, _ := delivery.CheckerOwedKey("novox/hq", 3, head1)
key, _ := check.CheckerOwedKey("novox/hq", 3, head1)
for deadline := time.Now().Add(20 * time.Second); ; time.Sleep(50 * time.Millisecond) {
if e, err := b.buckets[delivery.CheckerOwedBucket].Get(b.ctx, key); err == nil {
if o, err := delivery.DecodeCheckerOwed(e.Value()); err == nil && o.State == "success" {
if e, err := b.buckets[check.CheckerOwedBucket].Get(b.ctx, key); err == nil {
if o, err := check.DecodeCheckerOwed(e.Value()); err == nil && o.State == "success" {
return
}
}
@@ -855,16 +871,16 @@ func TestAnHqPullRequestGetsAVerdictAndAStatus(t *testing.T) {
func TestACheckLeftRunningIsTakenOverWhenTheCheckerStarts(t *testing.T) { // N6/AC2
b := startBed(t)
key := b.candidate(t, "feat/x", pull(repo, 7, head1))
group, heads, _ := delivery.ParseCheckerVerdictKey(key)
left, err := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, Trunk: map[string]string{repo: trunk}, State: delivery.VerdictRunning})
group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(key)
left, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, Trunk: map[string]string{repo: trunk}, State: meshdeliverycheckerclient.VerdictRunning})
if err != nil {
t.Fatal(err)
}
if _, err := b.buckets[delivery.CheckerVerdictsBucket].Create(b.ctx, key, left); err != nil {
if _, err := b.buckets[check.CheckerVerdictsBucket].Create(b.ctx, key, left); err != nil {
t.Fatal(err)
}
b.start(t, nil)
if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass {
if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("verdict = %+v", v)
}
}
@@ -877,7 +893,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
b.stalePin.Store(true)
key := b.candidate(t, "feat/x", pull(repo, 7, head1))
b.start(t, nil)
if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass {
if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
@@ -888,12 +904,12 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
b.setTrunk(t, map[string]string{repo: trunk, sdkRepo: trunk})
// The member carries no preview of its merge yet, so its manifest is read at its head (the package new in the group).
b.heads.Store(sdkRepo+" module.json "+head2, `{"module":"mesh-sdk","build":{"artifacts":[{"name":"go","kind":"package","language":"go","paths":["go"]}]}}`)
asks, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectTagWanted(delivery.CheckerModule)}})
asks, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectTagWanted(check.CheckerModule)}})
if err != nil {
t.Fatal(err)
}
sdk := pull(sdkRepo, 12, head2)
sdk.Modules, sdk.Fps = []string{"mesh-sdk"}, map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("e", 64)}}
sdk.Modules, sdk.Fps = []string{"mesh-sdk"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("e", 64)}}
key = b.candidate(t, "feat/x", pull(repo, 8, head1), sdk)
msg, err := asks.Next(jetstream.FetchMaxWait(20 * time.Second))
if err != nil {
@@ -908,7 +924,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
if want := (meshdeliverychecker.TagWantedHolderEvent{Repository: sdkRepo, Branch: "feat/x", Head: head2, Module: "mesh-sdk", Package: "go", Pull: 12}); wanted != want {
t.Errorf("tag-wanted = %+v, want %+v", wanted, want)
}
if v, err := b.buckets[delivery.CheckerVerdictsBucket].Get(b.ctx, key); err == nil && !strings.Contains(string(v.Value()), `"running"`) {
if v, err := b.buckets[check.CheckerVerdictsBucket].Get(b.ctx, key); err == nil && !strings.Contains(string(v.Value()), `"running"`) {
t.Fatalf("the group was judged before its tagged version was published: %s", v.Value())
}
published, _ := json.Marshal(gopackageregistry.PublishedEvent{Holder: "gitea", Kind: "package", Name: "mesh-sdk/go", Module: check.SDKModulePath, Commit: head2, Tag: "feat-x", Version: "0.1.15-feat-x.1",
@@ -916,7 +932,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
if _, err := b.js.Publish(b.ctx, gopackageregistry.SubjectPublished, published); err != nil {
t.Fatal(err)
}
if v := b.verdict(t, key, ended); v.State != delivery.VerdictPass {
if v := b.verdict(t, key, ended); v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("the group's verdict = %+v", v)
}
b.fake.mu.Lock()
@@ -928,7 +944,7 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
// DLV-C2, DLV-C6 (design 26 *Out of step*, ADR 0620): each check's end says one verdict per pull request on the stream
// asks, at the definition's subject and in its body (decoded strictly): the lone pull request's, then the group's two.
verdicts, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectVerdict(delivery.CheckerModule)}})
verdicts, err := b.js.OrderedConsumer(b.ctx, "ASKS", jetstream.OrderedConsumerConfig{FilterSubjects: []string{meshdeliverychecker.HolderSubjectVerdict(check.CheckerModule)}})
if err != nil {
t.Fatal(err)
}
@@ -949,8 +965,8 @@ func TestAMemberNamingAMeshSDKCommitOffTheTrunkIsJudgedAndNotRefused(t *testing.
}
said[e.Pull] = e
}
for pull, want := range map[int64]meshdeliverychecker.VerdictHolderEvent{7: {Repository: repo, Pull: 7, Head: head1, Verdict: delivery.VerdictPass},
8: {Repository: repo, Pull: 8, Head: head1, Verdict: delivery.VerdictPass}, 12: {Repository: sdkRepo, Pull: 12, Head: head2, Verdict: delivery.VerdictPass}} {
for pull, want := range map[int64]meshdeliverychecker.VerdictHolderEvent{7: {Repository: repo, Pull: 7, Head: head1, Verdict: meshdeliverycheckerclient.VerdictPass},
8: {Repository: repo, Pull: 8, Head: head1, Verdict: meshdeliverycheckerclient.VerdictPass}, 12: {Repository: sdkRepo, Pull: 12, Head: head2, Verdict: meshdeliverycheckerclient.VerdictPass}} {
if got := said[pull]; got.Repository != want.Repository || got.Head != want.Head || got.Verdict != want.Verdict || len(got.OutOfStep) != 0 {
t.Errorf("verdict of pull %d = %+v, want %+v", pull, got, want)
}
@@ -1009,7 +1025,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) {
if b.fake.count("mesh-build-go.build_go_fetch") == 0 {
t.Errorf("the try fetched nothing")
}
for _, name := range []string{delivery.CheckerOwedBucket, delivery.CheckerVerdictsBucket, delivery.PreviewsBucket} {
for _, name := range []string{check.CheckerOwedBucket, check.CheckerVerdictsBucket, check.PreviewsBucket} {
if keys, _ := b.buckets[name].Keys(b.ctx); len(keys) != 0 {
t.Errorf("a try wrote %s: %v", name, keys)
}
@@ -1021,7 +1037,7 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) {
if err != nil {
t.Fatal(err)
}
steps, err := b.js.KeyValue(b.ctx, delivery.StepsBucket("mesh-build-go"))
steps, err := b.js.KeyValue(b.ctx, buildrun.StepsBucket("mesh-build-go"))
if err != nil {
t.Fatal(err)
}
@@ -13,16 +13,19 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"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/delivery"
"git.novox.be/novox/mesh-sdk/go/keys"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-delivery/sdk/graph"
"mesh-delivery-checker/internal/check"
)
// judgeDeps are the parts the judge of a check is made of.
@@ -57,22 +60,22 @@ type judgeDeps struct {
logf func(format string, args ...any)
}
// keptPredefined reads the indexer's key predefined.list (delivery.PredefinedKept), bound once its state appears.
// keptPredefined reads the indexer's key predefined.list (the seat mesh-catalog's state predefined), bound once its state appears.
type keptPredefined struct{ kv jetstream.KeyValue }
func (k keptPredefined) PredefinedList(ctx context.Context) (json.RawMessage, error) {
e, err := k.kv.Get(ctx, delivery.PredefinedListKey)
e, err := k.kv.Get(ctx, meshcatalog.PredefinedByListKey())
if errors.Is(err, jetstream.ErrKeyNotFound) {
return nil, nil
}
if err != nil {
return nil, err
}
var kept delivery.PredefinedKept
if err := json.Unmarshal(e.Value(), &kept); err != nil {
kept, err := meshcatalog.DecodePredefinedByListValue(e.Value())
if err != nil {
return nil, err
}
return kept.List, nil
return json.Marshal(kept.List)
}
// keptSeatDefinitions reads a pull request's entries of the indexer's seat definitions from its first view: each whose
@@ -103,7 +106,7 @@ func seatDefinitionsOf(kv jetstream.KeyValue) keptSeatDefinitions {
}
func (k keptSeatDefinitions) Unparsed(ctx context.Context, repository string, pull int) ([]check.UnparsedSeat, error) {
suffix := ".pull." + delivery.EscapeName(repository) + "." + strconv.Itoa(pull)
suffix := ".pull." + keys.EscapeName(repository) + "." + strconv.Itoa(pull)
keys, err := k.keys(ctx)
if err != nil {
return nil, err
@@ -143,14 +146,14 @@ type nodeNames struct{ kv jetstream.KeyValue }
// openNodeNames opens the node-engine's have for the nodes' names alone: it is not watched, so no check waits on a node.
func openNodeNames(ctx context.Context, js process.BucketOpener, logf func(string, ...any)) *nodeNames {
return &nodeNames{kv: process.OptionalBucket(ctx, js, delivery.HaveBucket, "no check is asked until it is: the names check needs the nodes' names", logf)}
return &nodeNames{kv: process.OptionalBucket(ctx, js, check.HaveBucket, "no check is asked until it is: the names check needs the nodes' names", logf)}
}
func (n nodeNames) Names(ctx context.Context) ([]string, error) {
if n.kv == nil {
return nil, fmt.Errorf("the bucket %s could not be opened at the checker's start", delivery.HaveBucket)
return nil, fmt.Errorf("the bucket %s could not be opened at the checker's start", check.HaveBucket)
}
lister, err := n.kv.ListKeysFiltered(ctx, "*."+delivery.HaveNodeWord)
lister, err := n.kv.ListKeysFiltered(ctx, "*."+check.HaveNodeWord)
if err != nil {
return nil, err
}
@@ -264,7 +267,7 @@ func (r *repoModules) Manifest(ctx context.Context, digest string) (catalogue.Ma
if known {
return m, nil
}
key, err := delivery.ManifestsKey(digest)
key, err := meshcatalogclient.ManifestsKey(digest)
if err != nil {
return catalogue.Manifest{}, err
}
@@ -275,11 +278,11 @@ func (r *repoModules) Manifest(ctx context.Context, digest string) (catalogue.Ma
if !found {
return catalogue.Manifest{}, fmt.Errorf("the indexer keeps no manifest %s", digest)
}
kept, err := delivery.DecodeManifest(raw)
kept, err := meshcatalogclient.DecodeManifest(raw)
if err != nil {
return catalogue.Manifest{}, err
}
if m, err = parseManifest(kept.JSON); err != nil {
if m, err = parseManifest(kept.Manifest); err != nil {
return catalogue.Manifest{}, err
}
r.mu.Lock()
@@ -311,19 +314,19 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
return
}
r.mu.Unlock()
m, err := delivery.DecodeModule(value)
m, err := meshcatalogclient.DecodeModule(value)
if err != nil {
return
}
if r.verdicts != nil {
r.verdicts.put(key, m.VerdictOf(key))
r.verdicts.put(key, meshcatalogclient.ModuleVerdict(key, m))
}
r.mu.Lock()
r.repoOf[key] = m.Repository
r.typed[key] = m.Manifest
r.commit[key] = m.Commit
r.path[key] = m.Path
r.named[key] = m.ArtifactPaths
r.named[key] = m.Paths
_, known := r.langs[m.Manifest]
r.mu.Unlock()
if m.Manifest == "" || known {
@@ -331,10 +334,10 @@ func (r *repoModules) Put(ctx context.Context, key string, value []byte, deleted
}
set := map[string]bool{}
var pkgs []string
if mk, err := delivery.ManifestsKey(m.Manifest); err == nil {
if mk, err := meshcatalogclient.ManifestsKey(m.Manifest); err == nil {
if raw, _, found, err := r.manifests.Get(ctx, mk); err == nil && found {
if kept, err := delivery.DecodeManifest(raw); err == nil {
if man, err := parseManifest(kept.JSON); err == nil && man.Build != nil {
if kept, err := meshcatalogclient.DecodeManifest(raw); err == nil {
if man, err := parseManifest(kept.Manifest); err == nil && man.Build != nil {
for _, a := range man.Build.Artifacts {
if a.Language != "" {
set[a.Language] = true
@@ -365,7 +368,7 @@ func (r *repoModules) ModuleDir(module string) (string, bool) {
}
// PackagePaths are the paths, relative to the repository, that the TypeScript package artifacts of a repository's modules
// are built from, sorted: the named paths the indexer keeps in each module's value (delivery.Module.ArtifactPaths; only the
// are built from, sorted: the named paths the indexer keeps in each module's value (meshcatalog.ModulesByModuleValue.ArtifactPaths; only the
// indexer reads them from a manifest, the lead's decision L8), under the module's directory. None for a module whose value
// holds no paths.
func (r *repoModules) PackagePaths(repository string) []string {
@@ -380,7 +383,7 @@ func (r *repoModules) PackagePaths(repository string) []string {
for _, name := range r.tsPkgs[r.typed[module]] {
for _, p := range r.named[module][name] {
p = strings.Trim(strings.TrimPrefix(p, "./"), "/")
if dir != "" && dir != delivery.RootPath {
if dir != "" && dir != meshcatalogclient.RootPath {
p = dir + "/" + p
}
out = append(out, p)
@@ -420,16 +423,16 @@ func (r *repoModules) BuildModules(repository string) []string {
// buildReader reads the builder's build of a module at a fingerprint (mesh-delivery-builder_builds).
type buildReader struct{ kv kvStore }
func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) {
key, err := delivery.BuilderBuildKey(module, fp2)
func (r buildReader) Build(ctx context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) {
key, err := builds.BuilderBuildKey(module, fp2)
if err != nil {
return delivery.BuilderBuild{}, false, err
return builds.BuilderBuild{}, false, err
}
value, _, found, err := r.kv.Get(ctx, key)
if err != nil || !found {
return delivery.BuilderBuild{}, false, err
return builds.BuilderBuild{}, false, err
}
b, err := delivery.DecodeBuilderBuild(value)
b, err := builds.DecodeBuilderBuild(value)
return b, err == nil, err
}
@@ -438,30 +441,30 @@ func (r buildReader) Build(ctx context.Context, module, fp2 string) (delivery.Bu
// the fact and polls nothing (novox/hq ADR 0603 §10).
type buildEnds struct{ kv jetstream.KeyValue }
func (e buildEnds) Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) {
return e.until(ctx, builds, func(b delivery.BuilderBuild) bool {
return b.State == delivery.BuildDone || b.State == delivery.BuildFailed
func (e buildEnds) Ended(ctx context.Context, asked map[string]string) (map[string]builds.BuilderBuild, error) {
return e.until(ctx, asked, func(b builds.BuilderBuild) bool {
return b.State == builds.BuildDone || b.State == builds.BuildFailed
})
}
func (e buildEnds) Done(ctx context.Context, builds map[string]string) bool {
done, err := e.until(ctx, builds, func(b delivery.BuilderBuild) bool { return b.State == delivery.BuildDone })
return err == nil && len(done) == len(builds)
func (e buildEnds) Done(ctx context.Context, asked map[string]string) bool {
done, err := e.until(ctx, asked, func(b builds.BuilderBuild) bool { return b.State == builds.BuildDone })
return err == nil && len(done) == len(asked)
}
// until hears the builds' keys until each holds a build that ended holds, or ctx ends, and answers those that do, by module.
func (e buildEnds) until(ctx context.Context, builds map[string]string, ended func(delivery.BuilderBuild) bool) (map[string]delivery.BuilderBuild, error) {
func (e buildEnds) until(ctx context.Context, asked map[string]string, ended func(builds.BuilderBuild) bool) (map[string]builds.BuilderBuild, error) {
byKey := map[string]string{}
keys := make([]string, 0, len(builds))
for module, fp2 := range builds {
key, err := delivery.BuilderBuildKey(module, fp2)
keys := make([]string, 0, len(asked))
for module, fp2 := range asked {
key, err := builds.BuilderBuildKey(module, fp2)
if err != nil {
return nil, err
}
byKey[key] = module
keys = append(keys, key)
}
out := map[string]delivery.BuilderBuild{}
out := map[string]builds.BuilderBuild{}
if len(keys) == 0 {
return out, nil
}
@@ -485,7 +488,7 @@ func (e buildEnds) until(ctx context.Context, builds map[string]string, ended fu
if !ours {
continue
}
b, err := delivery.DecodeBuilderBuild(entry.Value())
b, err := builds.DecodeBuilderBuild(entry.Value())
if err == nil && ended(b) {
out[module] = b
} else {
@@ -497,7 +500,7 @@ func (e buildEnds) until(ctx context.Context, builds map[string]string, ended fu
}
// ManifestDigests is the manifest digest of every module the indexer holds, by module: the catalogue the indexer's judge
// judges a change's manifests in (novox/hq ADR 0578 §2).
// judges a change's manifests in (novox/hq ADR 0578 §2).8 §2).
func (r *repoModules) ManifestDigests() map[string]string {
r.mu.Lock()
defer r.mu.Unlock()
@@ -12,8 +12,11 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/keys"
"mesh-delivery-checker/internal/check"
)
@@ -27,7 +30,7 @@ func (m memManifests) Get(_ context.Context, key string) ([]byte, uint64, bool,
func putModule(t *testing.T, r *repoModules, name, repository, manifest string) {
t.Helper()
data, err := delivery.EncodeModule(delivery.Module{Repository: repository, Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64), Manifest: delivery.ManifestDigest([]byte(manifest))})
data, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: repository, Commit: strings.Repeat("a", 40), Fp: "src1:" + strings.Repeat("a", 64), Manifest: meshcatalogclient.ManifestDigest([]byte(manifest))})
if err != nil {
t.Fatal(err)
}
@@ -58,11 +61,11 @@ func TestARepositoryWithATypeScriptModuleIsJudgedByTheNodeBuildModuleToo(t *test
tsManifest := string(tsRaw)
store := memManifests{}
for _, m := range []string{goManifest, tsManifest} {
data, err := delivery.EncodeManifest(delivery.Manifest{Module: "demo", JSON: m})
data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "demo", Manifest: m})
if err != nil {
t.Fatal(err)
}
store[delivery.ManifestDigest([]byte(m))] = data
store[meshcatalogclient.ManifestDigest([]byte(m))] = data
}
r := &repoModules{manifests: store}
putModule(t, r, "mesh-delivery-checker", "novox/mesh-delivery", goManifest)
@@ -89,15 +92,15 @@ const sdkManifest = `{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"a
func TestTheTypeScriptPackagesPathsComeFromTheIndexersModulesNotTheManifest(t *testing.T) { // ADR 0500 §5 with lead decision L8
store := memManifests{}
data, err := delivery.EncodeManifest(delivery.Manifest{Module: "mesh-sdk", JSON: sdkManifest})
data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "mesh-sdk", Manifest: sdkManifest})
if err != nil {
t.Fatal(err)
}
store[delivery.ManifestDigest([]byte(sdkManifest))] = data
store[meshcatalogclient.ManifestDigest([]byte(sdkManifest))] = data
r := &repoModules{manifests: store}
put := func(path string, paths map[string][]string) {
v, err := delivery.EncodeModule(delivery.Module{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), FP: "src1:" + strings.Repeat("a", 64),
Manifest: delivery.ManifestDigest([]byte(sdkManifest)), Path: path, ArtifactPaths: paths})
v, err := meshcatalogclient.EncodeModule(meshcatalog.ModulesByModuleValue{Repository: "novox/mesh-sdk", Commit: strings.Repeat("a", 40), Fp: "src1:" + strings.Repeat("a", 64),
Manifest: meshcatalogclient.ManifestDigest([]byte(sdkManifest)), Path: path, Paths: paths})
if err != nil {
t.Fatal(err)
}
@@ -133,10 +136,11 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) {
t.Errorf("read and empty, the known packages = %#v, want an empty map", got)
}
put := func(module, repository, dir, commit string) {
key, _ := delivery.PackagesKey(module)
raw, err := delivery.EncodePublishedPackages(delivery.PublishedPackages{Module: module, Repository: repository, Dir: dir,
key := meshcatalog.PackagesByModuleKey(keys.EscapeName(module))
now := time.Now().UTC()
raw, err := json.Marshal(meshcatalog.PackagesByModuleValue{V: keys.Version, Module: module, Repository: repository, Dir: dir,
Commit: commit, Requires: strings.Repeat("e", 64),
Packages: []delivery.PackageDigest{{Package: module, Digest: strings.Repeat("d", 64)}}, At: time.Now().UTC()})
Packages: []meshcatalog.PackagesByModuleValuePackagesItem{{Package: module, Digest: strings.Repeat("d", 64)}}, At: &now})
if err != nil {
t.Fatal(err)
}
@@ -146,30 +150,31 @@ func TestThePublishedVersionsAreTheIndexersPackagesState(t *testing.T) {
put(sdk, "novox/mesh-sdk", "go", strings.Repeat("a", 40))
put(build, "novox/mesh-catalog", "modules/mesh-build-go", strings.Repeat("b", 40))
p.Put("broken", []byte("{"), false)
if got := p.LatestPackages(); !reflect.DeepEqual(got, delivery.PackageVersions{sdk: strings.Repeat("a", 40), build: strings.Repeat("b", 40)}) {
if got := p.LatestPackages(); !reflect.DeepEqual(got, buildstep.PackageVersions{sdk: strings.Repeat("a", 40), build: strings.Repeat("b", 40)}) {
t.Errorf("latest packages = %v", got)
}
key, _ := delivery.PackagesKey(build)
key := meshcatalog.PackagesByModuleKey(keys.EscapeName(build))
p.Put(key, nil, true)
if got := p.LatestPackages(); len(got) != 1 || got[sdk] == "" {
t.Errorf("after a delete: %v", got)
}
// A series key names its package by its ecosystem name, the judge's known packages (DLV-I14, design 32), and is no
// module's published version.
series, err := delivery.EncodeSeriesPackage(delivery.SeriesPackage{Name: sdk, Package: "mesh-sdk/go", Series: "0.1", At: time.Now().UTC()})
now := time.Now().UTC()
series, err := json.Marshal(meshcatalog.PackagesByPackageSeriesValue{V: keys.Version, Name: sdk, Package: "mesh-sdk/go", Series: "0.1", At: &now})
if err != nil {
t.Fatal(err)
}
p.Put(delivery.SeriesPackageKey(sdk, "0.1"), series, false)
p.Put(meshcatalog.PackagesByPackageSeriesKey(keys.EscapeName(sdk), "0.1"), series, false)
if got := p.PackageNames(); !reflect.DeepEqual(got, map[string]string{sdk: "mesh-sdk/go"}) || len(p.LatestPackages()) != 1 {
t.Errorf("the known packages = %v, the latest %v", got, p.LatestPackages())
}
t.Run("a pull request's unparsed seat definitions are its entries of the indexer's seat-definitions", func(t *testing.T) {
state := map[string]string{
"web-api.trunk": `{"seat":"web-api","source":"trunk","digest":"d"}`,
"web-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"web-api","source":"pull","module":"web-contracts","directory":"go","unparsed":"seat.json: bad"}`,
"mail-api.pull." + delivery.EscapeName("novox/web") + ".7": `{"seat":"mail-api","source":"pull","digest":"e"}`,
"web-api.pull." + delivery.EscapeName("novox/web") + ".17": `{"seat":"web-api","source":"pull","unparsed":"another pull request's"}`,
"web-api.pull." + keys.EscapeName("novox/web") + ".7": `{"seat":"web-api","source":"pull","module":"web-contracts","directory":"go","unparsed":"seat.json: bad"}`,
"mail-api.pull." + keys.EscapeName("novox/web") + ".7": `{"seat":"mail-api","source":"pull","digest":"e"}`,
"web-api.pull." + keys.EscapeName("novox/web") + ".17": `{"seat":"web-api","source":"pull","unparsed":"another pull request's"}`,
}
k := keptSeatDefinitions{keys: func(context.Context) ([]string, error) { return slices.Collect(maps.Keys(state)), nil },
get: func(_ context.Context, key string) ([]byte, error) { return []byte(state[key]), nil }}
@@ -189,11 +194,11 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
t.Fatal(err)
}
manifest := string(raw)
data, err := delivery.EncodeManifest(delivery.Manifest{Module: "xterm", JSON: manifest})
data, err := meshcatalogclient.EncodeManifest(meshcatalog.ManifestsByDigestValue{Module: "xterm", Manifest: manifest})
if err != nil {
t.Fatal(err)
}
store := memManifests{delivery.ManifestDigest([]byte(manifest)): data}
store := memManifests{meshcatalogclient.ManifestDigest([]byte(manifest)): data}
var said []string
r := &repoModules{manifests: store, logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}
putModule(t, r, "xterm", "novox/mesh-catalog", manifest)
@@ -208,8 +213,8 @@ func TestAManifestFallingBackToAPredefinedKeyIsRead(t *testing.T) {
}
// The sandbox's composer may be an older node-engine, which knows no verdict waits: it is handed a wait as a refusal.
var held manifestVerdicts
held.put("nats", &delivery.ManifestVerdict{Module: "nats", State: delivery.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"})
if v := held.all()["nats"]; v.State != delivery.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 {
held.put("nats", &meshcatalog.VerdictsByDigestValue{Module: "nats", State: meshcatalogclient.VerdictManifestWaits, Unknown: []string{"core-credentials"}, Judge: "j"})
if v := held.all()["nats"]; v.State != meshcatalogclient.VerdictRefused || len(v.Problems) != 1 || len(v.Unknown) != 0 {
t.Errorf("a waiting verdict is handed to the sandbox as %+v", v)
}
}
@@ -3,9 +3,9 @@ package main
import (
"sync"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// lazyTurns is the gate's merge turns as the checker reads them (novox/hq ADR 0548 §3), opened when the gate's bucket is
@@ -33,18 +33,18 @@ func (l *lazyTurns) ensure() {
if !l.said {
l.said = true
l.logf("NO MERGE TURN HOLDS A CHECK: the gate's bucket %s cannot be read (%v); every check is claimed as if no approved merge waited, "+
"and the bucket is opened again before each claim until it is there", delivery.TurnsBucket, err)
"and the bucket is opened again before each claim until it is there", check.TurnsBucket, err)
}
return
}
l.opened = true
if l.said {
l.logf("the gate's merge turns are read from %s from now on: an approved merge's turn holds checks off its repositories again", delivery.TurnsBucket)
l.logf("the gate's merge turns are read from %s from now on: an approved merge's turn holds checks off its repositories again", check.TurnsBucket)
}
}
// Holding opens the bucket when it is not yet, then answers what the turns hold.
func (l *lazyTurns) Holding(repositories []string, heads string) (string, delivery.TurnWait, bool) {
func (l *lazyTurns) Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool) {
l.ensure()
return l.turns.Holding(repositories, heads)
}
@@ -10,7 +10,6 @@ import (
"encoding/json"
"errors"
"fmt"
sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings"
"io"
"log"
"os"
@@ -19,23 +18,29 @@ import (
"syscall"
"time"
sdkfindings "git.novox.be/novox/mesh-delivery/sdk/findings"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner"
"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/buildrun"
"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/announce"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-sdk/go/process"
"git.novox.be/novox/mesh-sdk/go/seatlist"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-delivery-checker/internal/check"
)
// eventSubject is the subject of an event of the checker's stream checks (the bus names the stream CHECKS): the indexer's
// watch of the previews bucket and the forge bridge's watch of the owed bucket are woken by them.
func eventSubject(event string) string {
return process.ModuleStreamSubject(delivery.CheckerModule, event)
return process.ModuleStreamSubject(check.CheckerModule, event)
}
const (
@@ -66,11 +71,11 @@ func durationEnv(getenv func(string) string, name string, def time.Duration) (ti
}
// The checker's own states of its first views (views.StatusState) and of its findings, where the condition keeper raises a
// first view unanswered past its bound (ADR 0615 §6); and the indexer's states it follows (delivery.CandidatesBucket,
// first view unanswered past its bound (ADR 0615 §6); and the indexer's states it follows (check.CandidatesBucket,
// ModulesBucket, ManifestsBucket).
var (
firstViewsBucket = delivery.CheckerModule + "_" + views.StatusState
findingsBucket = delivery.CheckerModule + "_findings"
firstViewsBucket = check.CheckerModule + "_" + views.StatusState
findingsBucket = check.CheckerModule + "_findings"
)
const (
@@ -120,7 +125,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
defer conn.Close()
// Its own states: a check without one of them would be unwritten, so each stops the start, naming its grant (issue 1945).
buckets := map[string]jetstream.KeyValue{}
for _, name := range []string{delivery.CheckerVerdictsBucket, delivery.CheckerKeptBucket, delivery.CheckerOwedBucket, delivery.PreviewsBucket,
for _, name := range []string{check.CheckerVerdictsBucket, check.CheckerKeptBucket, check.CheckerOwedBucket, check.PreviewsBucket,
check.TriesBucket} {
if buckets[name], err = process.EssentialBucket(ctx, js, "checker", name, "judge or keep a check"); err != nil {
return err
@@ -146,7 +151,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The module has one durable consumer of EVENTS: one hub pulls it for the views reader and for the registry seats'
// published, started once both are registered.
hub := process.NewEventHub(logger.Printf)
reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
reader := &views.ProcessReader{Module: check.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)}
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) }
@@ -157,14 +162,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
followed = errors.Join(followed, err)
return p
}
candidates := followOwner(delivery.IndexerModule, candidatesState, "indexer_read_candidates", indexerViews)
modules := followOwner(delivery.IndexerModule, modulesState, "indexer_read_modules", indexerViews)
manifests := followOwner(delivery.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews)
packages := followOwner(delivery.IndexerModule, delivery.PackagesState, "indexer_read_packages", indexerViews)
predefined := followOwner(delivery.IndexerModule, delivery.IndexerPredefinedState, "indexer_read_predefined", indexerViews)
placement := followOwner(delivery.AssignerModule, delivery.PlacementState, "assigner_read_placement", assignerViews)
candidates := followOwner(check.IndexerModule, candidatesState, "indexer_read_candidates", indexerViews)
modules := followOwner(check.IndexerModule, modulesState, "indexer_read_modules", indexerViews)
manifests := followOwner(check.IndexerModule, manifestsState, "indexer_read_manifests", indexerViews)
packages := followOwner(check.IndexerModule, meshcatalog.StatePackages, "indexer_read_packages", indexerViews)
predefined := followOwner(check.IndexerModule, check.IndexerPredefinedState, "indexer_read_predefined", indexerViews)
placement := followOwner(buildrun.AssignerModule, meshmoduleassigner.StatePlacement, "assigner_read_placement", assignerViews)
// The seventh: the indexer's seat definitions, so a pull request whose head's definition does not parse fails.
seatDefs := followOwner(delivery.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews)
seatDefs := followOwner(check.IndexerModule, seatDefinitionsState, "indexer_read_seat_definitions", indexerViews)
if followed != nil {
return followed
}
@@ -200,7 +205,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// The state may not be on the bus yet when a delivery started the checker (the grants part creates it, issue 1790): it
// is bound when the bus says it was created, and the latest hearing is written then.
hearingKV := process.AwaitBucket(ctx, js, conn, check.HearingBucket, "checker_status cannot say whether DEAD_LETTERS is heard", logger.Printf)
hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, []string{delivery.AskGo, delivery.AskNode}, dead,
hearing := buildrun.NewDeadLetterHearing(ctx, buildrun.JetStreamDeadLetters{JS: js}, []string{buildstep.AskGo, buildstep.AskNode}, dead,
logger.Printf, buildrun.SayHearing(ctx, hearingKV, logger.Printf))
defer hearing.Stop()
// Each ask's step is heard by its own watch and, at every step end of its build module and every reconnect, read
@@ -211,8 +216,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
asksKV := process.AwaitBucket(ctx, js, conn, check.AsksBucket, "checker_ask_again refuses every check, as it cannot tell whether its ask waits in a stream", logger.Printf)
ch := &check.Checker{
Asks: awaitedKV{asksKV},
Verdicts: kvStore{buckets[delivery.CheckerVerdictsBucket]},
Owed: kvStore{buckets[delivery.CheckerOwedBucket]},
Verdicts: kvStore{buckets[check.CheckerVerdictsBucket]},
Owed: kvStore{buckets[check.CheckerOwedBucket]},
Trunks: trunks,
Turns: lazy,
Bound: checkBound,
@@ -222,10 +227,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
// The builder's builds say how the candidates' fingerprints stand (issue 1684); where its bucket is not there yet the line
// says nothing of the builds, until the checker starts again.
var builds check.BuildStates
var buildStates check.BuildStates
var ends check.BuildEnds
if kv := process.OptionalBucket(ctx, js, delivery.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil {
builds, ends = buildReader{kvStore{kv}}, buildEnds{kv}
if kv := process.OptionalBucket(ctx, js, builds.BuilderBuildsBucket, "the line says nothing of the builds, and a check composes without waiting on them, until the checker starts again", logger.Printf); kv != nil {
buildStates, ends = buildReader{kvStore{kv}}, buildEnds{kv}
}
if ends != nil {
ch.Rebuilds = ends // a check failed on a build is asked again once that build is done again (ADR 0603 §10)
@@ -276,8 +281,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
ch.Judge = newJudge(judgeDeps{
asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks,
kept: kvStore{buckets[delivery.CheckerKeptBucket]}, previews: kvStore{buckets[delivery.PreviewsBucket]},
repos: repos, published: published, builds: builds, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf,
kept: kvStore{buckets[check.CheckerKeptBucket]}, previews: kvStore{buckets[check.PreviewsBucket]},
repos: repos, published: published, builds: buildStates, ends: ends, buildWait: set.buildWait, forge: forgeReads{call: caller.Call}, logf: logger.Printf,
announce: announce, compositions: compositions, tags: tags,
nodes: openNodeNames(ctx, js, logger.Printf),
predefined: keptPredefined{predefined},
@@ -333,9 +338,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// the assigner's own presence says whether a composition is being made (compositionWatch, ADR 0537 §4). A check is no
// longer held for a node: its asks wait in the build seats' streams, which redeliver a lost node's ask (ADR 0525 §6,
// issue 1912), so the node-engine's have is not watched here.
if kv := process.OptionalBucket(ctx, js, delivery.PresenceBucket, "no node counts as online until the checker starts again", logger.Printf); kv != nil {
if kv := process.OptionalBucket(ctx, js, check.PresenceBucket, "no node counts as online until the checker starts again", logger.Printf); kv != nil {
if pw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) { presence.Put(e.Key(), e.Value(), !opOf(e)) }); err != nil {
process.NotWatched(logger.Printf, delivery.PresenceBucket, err, "no node counts as online until the checker starts again")
process.NotWatched(logger.Printf, check.PresenceBucket, err, "no node counts as online until the checker starts again")
} else {
go func() {
errs <- follow(ctx, pw, func(e jetstream.KeyValueEntry) {
@@ -348,18 +353,18 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}
// The gate's source says the trunk head of each repository, which a pull request is merged onto. Where it is not there
// yet no trunk is known, so nothing is checked.
if kv := process.OptionalBucket(ctx, js, delivery.SourceBucket, "no trunk is known, so nothing is checked until the checker starts again", logger.Printf); kv != nil {
if kv := process.OptionalBucket(ctx, js, check.SourceBucket, "no trunk is known, so nothing is checked until the checker starts again", logger.Printf); kv != nil {
sw, err := settle(ctx, kv, func(e jetstream.KeyValueEntry) {
if e.Key() == delivery.SourceMainKey {
if e.Key() == meshdeliveryclient.SourceMainKey {
trunks.Put(e.Value(), !opOf(e))
}
})
if err != nil {
process.NotWatched(logger.Printf, delivery.SourceBucket, err, "no trunk is known, so nothing is checked until the checker starts again")
process.NotWatched(logger.Printf, check.SourceBucket, err, "no trunk is known, so nothing is checked until the checker starts again")
} else {
go func() {
errs <- follow(ctx, sw, func(e jetstream.KeyValueEntry) {
if e.Key() == delivery.SourceMainKey && trunks.Put(e.Value(), !opOf(e)) {
if e.Key() == meshdeliveryclient.SourceMainKey && trunks.Put(e.Value(), !opOf(e)) {
ch.TrunkMoved(ctx) // the trunk moved: its statuses go pending, and a verdict for the old trunk is judged again
}
})
@@ -371,7 +376,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
// Where the gate's bucket is not there yet it is read as empty, and opened again before each claim until it is there
// (lazyTurns), so a gate delivered after the checker started holds checks from its first turn on.
lazy.open = func() error {
kv, err := js.KeyValue(ctx, delivery.TurnsBucket)
kv, err := js.KeyValue(ctx, check.TurnsBucket)
if err != nil {
return err
}
@@ -444,7 +449,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
}()
// The checker's own verdicts, updates only, in one watch: one checker_ask_again wrote asks its check again
// (Checker.VerdictWritten); a deleted key of heads a candidate holds is judged again (Checker.VerdictDeleted, issue 1733).
vw, err := buckets[delivery.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly())
vw, err := buckets[check.CheckerVerdictsBucket].WatchAll(ctx, jetstream.UpdatesOnly())
if err != nil {
return fmt.Errorf("the checker's verdicts cannot be watched, so no check can be asked again by hand and a deleted verdict would never be written again: %w", err)
}
@@ -14,7 +14,7 @@ func TestTheCheckerWatchesNoHave(t *testing.T) {
if err != nil {
t.Fatal(err)
}
for _, s := range []string{"delivery.HaveBucket", "buildrun.HaveSet"} {
for _, s := range []string{"check.HaveBucket", "buildrun.HaveSet"} {
if strings.Contains(string(raw), s) {
t.Errorf("the checker program still reads %s", s)
}
@@ -1,9 +1,11 @@
package main
import (
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// publishedPackages is the indexer's state `packages` as the checker follows it (novox/hq ADR 0593 §2): by mesh Go
@@ -11,13 +13,13 @@ import (
// package's version; the checker reads it, so a Go check builds every mesh package at the version the mesh publishes
// (check.Examiner.Published) and computes none.
//
// Its series keys (delivery.SeriesPackage, decoded with delivery.DecodeSeriesPackage) name each package by its name in its
// Its series keys (meshcatalog.PackagesByPackageSeriesValue, decoded with meshcatalog.DecodePackagesByPackageSeriesValue) name each package by its name in its
// own ecosystem and the `<module>/<artifact>` it is built from (design 32): the known packages the judge takes
// (PackageNames).
type publishedPackages struct {
mu sync.Mutex
byKey map[string]delivery.PublishedPackages
series map[string]delivery.SeriesPackage
byKey map[string]meshcatalog.PackagesByModuleValue
series map[string]meshcatalog.PackagesByPackageSeriesValue
unread map[string]string
// answered is whether the state's first reading ended (Answered): until then the checker knows no package's name.
answered bool
@@ -36,7 +38,7 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) {
p.mu.Lock()
defer p.mu.Unlock()
if p.byKey == nil {
p.byKey, p.series, p.unread = map[string]delivery.PublishedPackages{}, map[string]delivery.SeriesPackage{}, map[string]string{}
p.byKey, p.series, p.unread = map[string]meshcatalog.PackagesByModuleValue{}, map[string]meshcatalog.PackagesByPackageSeriesValue{}, map[string]string{}
}
delete(p.byKey, key)
delete(p.series, key)
@@ -44,8 +46,8 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) {
if deleted {
return
}
if delivery.IsSeriesPackageKey(key) {
s, err := delivery.DecodeSeriesPackage(raw)
if isSeriesPackageKey(key) {
s, err := meshcatalog.DecodePackagesByPackageSeriesValue(raw)
if err != nil {
p.unread[key] = err.Error()
return
@@ -53,7 +55,7 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) {
p.series[key] = s
return
}
v, err := delivery.DecodePublishedPackages(raw)
v, err := meshcatalog.DecodePackagesByModuleValue(raw)
if err != nil {
p.unread[key] = err.Error()
return
@@ -64,10 +66,10 @@ func (p *publishedPackages) Put(key string, raw []byte, deleted bool) {
// LatestPackages is the latest published version of every mesh Go module the indexer holds, by module path: the commit
// its repository was published at. A module the state does not hold is left out: a check builds it at the version
// go.mod names.
func (p *publishedPackages) LatestPackages() delivery.PackageVersions {
func (p *publishedPackages) LatestPackages() buildstep.PackageVersions {
p.mu.Lock()
defer p.mu.Unlock()
out := delivery.PackageVersions{}
out := buildstep.PackageVersions{}
for _, v := range p.byKey {
if v.Module != "" && v.Commit != "" {
out[v.Module] = v.Commit
@@ -93,3 +95,7 @@ func (p *publishedPackages) PackageNames() map[string]string {
}
return out
}
// isSeriesPackageKey says a key of the indexer's state packages is a package's series (`<package>.<series>`): a module's key
// is its path escaped, which holds no dot (keys.EscapeName).
func isSeriesPackageKey(key string) bool { return strings.Contains(key, ".") }
@@ -11,11 +11,10 @@ import (
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
"git.novox.be/novox/mesh-sdk/go/delivery"
"mesh-delivery-checker/internal/check"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry"
)
// publishedSeats are the two package registry seats' published, by subject (their contracts in mesh-contracts): heard on
@@ -46,7 +45,7 @@ func (t tagAsks) Want(ctx context.Context, w check.TagWanted) error {
msg := nats.NewMsg(eventSubject(check.EventTagWanted))
id := fmt.Sprintf("tag-wanted.%s.%s.%s.%s", w.Repository, w.Head, w.Module, w.Package)
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", delivery.CheckerModule)
msg.Header.Set("x-source", check.CheckerModule)
msg.Header.Set("x-node", t.node)
msg.Header.Set("x-time", t.now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
@@ -65,7 +64,7 @@ func (t tagAsks) Say(ctx context.Context, v check.VerdictEvent) error {
msg := nats.NewMsg(eventSubject(check.EventVerdict))
id := fmt.Sprintf("verdict.%s.%d.%s.%s.%x", v.Repository, v.Pull, v.Head, v.Verdict, sha256.Sum256([]byte(strings.Join(v.OutOfStep, "\n"))))
msg.Header.Set("x-event-id", id)
msg.Header.Set("x-source", delivery.CheckerModule)
msg.Header.Set("x-source", check.CheckerModule)
msg.Header.Set("x-node", t.node)
msg.Header.Set("x-time", t.now().UTC().Format(time.RFC3339Nano))
msg.Header.Set("content-type", "application/json")
@@ -12,8 +12,6 @@ import (
"github.com/nats-io/nats.go/jetstream"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
)
@@ -58,7 +56,7 @@ func (w *toolchainWatch) ensure(module string) error {
if w.watched[module] {
return nil
}
name := delivery.ToolchainsBucket(module)
name := buildrun.ToolchainsBucket(module)
kv, err := w.js.KeyValue(w.ctx, name)
switch {
case errors.Is(err, jetstream.ErrBucketNotFound):
+13 -5
View File
@@ -4,21 +4,29 @@ go 1.26.0
require (
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/clients/meshmoduleassignerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/npmpackageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0
)
require git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261104174638-125929dbc459
require (
git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/meshconfig v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry v0.0.0-20261104174344-ae5266e77639
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun v0.0.0-00010101000000-000000000000
github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op // indirect
github.com/google/go-tpm v0.9.8 // indirect
@@ -6,7 +6,11 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"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/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// One check per delivery (novox/hq ADR 0548): the checker's part. A merge turn holds new checks off its repository (§3);
@@ -17,7 +21,7 @@ import (
func turnOf(t testing.TB, repo, group, heads string) []byte {
t.Helper()
at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC)
data, err := delivery.EncodeTurn(delivery.Turn{Repository: repo, Waiting: []delivery.TurnWait{{Group: group, Heads: heads, Approved: at, Since: at}}})
data, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repo, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{{Group: group, Heads: heads, Approved: at, Since: at}}})
if err != nil {
t.Fatal(err)
}
@@ -26,7 +30,7 @@ func turnOf(t testing.TB, repo, group, heads string) []byte {
func turnKey(t testing.TB, repo string) string {
t.Helper()
k, err := delivery.TurnsKey(repo)
k, err := meshdeliveryclient.TurnsKey(repo)
if err != nil {
t.Fatal(err)
}
@@ -45,10 +49,10 @@ func waitFor(t testing.TB, what string, ok func() bool) {
}
// pullWithFP is a pull request touching the module m at the fingerprint `src2:` and 64 times the hex digit d.
func pullWithFP(repo string, number int, head string, d string) delivery.CandidatePull {
func pullWithFP(repo string, number int, head string, d string) meshcatalog.CandidatesByGroupValuePullsItem {
p := pullOf(repo, number, head)
p.Modules = []string{"m"}
p.Fps = map[string]delivery.CandidateModule{"m": {FP2: "src2:" + strings.Repeat(d, 64)}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"m": {Fp2: "src2:" + strings.Repeat(d, 64)}}
return p
}
@@ -76,7 +80,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
}
r.ch.Reconsider(ctx) // what the watch of turns does on the delete
r.ch.Wait()
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass {
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("judged %d times after the turn went, want once", j.count())
}
},
@@ -86,7 +90,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
turns := &Turns{}
r.ch.Turns = turns
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
c, _ := delivery.DecodeCandidate(cand)
c, _ := meshcatalog.DecodeCandidatesByGroupValue(cand)
turns.Put(turnKey(t, repoSDK), turnOf(t, repoSDK, "fix/other", "0123456789ab"), false)
turns.Put(turnKey(t, repoA), turnOf(t, repoA, "feat/x", GateHeads(c)), false)
r.ch.Candidate(ctx, "feat%2Fx", cand)
@@ -113,7 +117,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
r.ch.Reconsider(ctx)
close(release)
r.ch.Wait()
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != delivery.VerdictPass {
if j.count() != 1 || r.verdict(t, keyOf(t, "feat/x", cand)).State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("judged %d times; the running check is not ended by the turn", j.count())
}
},
@@ -144,7 +148,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
if j.count() != 2 || kinds[0] != KindNew || kinds[1] != KindTrunk || from[0] != nil || from[1][repoA] != trunkA {
t.Fatalf("judged %d times as %v carried from %v; want a whole check, then one carry from %s", j.count(), kinds, from, trunkA[:12])
}
if v.State != delivery.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Trunk[repoA] != trunkA2 || v.CarriedFrom[repoA] != trunkA {
t.Errorf("the verdict is %s on %v carried from %v; want a pass on the new trunk carried from the old", v.State, v.Trunk, v.CarriedFrom)
}
if s := owedState(t, r, repoA, 7, head1); s != "success" {
@@ -160,7 +164,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
j := &judgeFunc{f: func(_ context.Context, c *Case, _ int) (Outcome, error) {
kept = append(kept, c.Composition != nil)
v := passing()
v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "v0.1.14-x", FPSet: c.FPSet}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "v0.1.14-x", Fpset: c.FPSet}
return Outcome{Verdict: v}, nil
}}
r := newRig(t, j)
@@ -220,7 +224,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
j := &judgeFunc{f: func(_ context.Context, c *Case, call int) (Outcome, error) {
kinds, from = append(kinds, c.Kind), append(from, c.CarriedFrom)
if call == 2 {
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "ace",
return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "ace",
Words: repoA + "#7 " + ConflictWords + "go.mod"}}}, nil
}
return Outcome{Verdict: passing()}, nil
@@ -260,7 +264,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
t.Errorf("the line names %s on %s first, want the head on the new trunk", head, trunk)
}
cs.CarriedFrom = map[string]string{repoA: trunkA2}
pending := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil)
pending := Line(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, cs, p, nil, nil, nil)
if !strings.HasPrefix(pending, "being carried to 111111111111 on aaaaaaaaaaaa | from abababababab") {
t.Errorf("the pending line of a carry = %q", pending)
}
@@ -274,10 +278,10 @@ func TestOneCheckPerDelivery(t *testing.T) {
r.ch.Candidate(ctx, "feat%2Fa", candA)
r.ch.Candidate(ctx, "feat%2Fc", candC)
r.ch.Wait()
a, _ := delivery.DecodeCandidate(candA)
c, _ := delivery.DecodeCandidate(candC)
a, _ := meshcatalog.DecodeCandidatesByGroupValue(candA)
c, _ := meshcatalog.DecodeCandidatesByGroupValue(candC)
at := time.Date(2026, 10, 26, 9, 0, 0, 0, time.UTC)
turn, err := delivery.EncodeTurn(delivery.Turn{Repository: repoA, Waiting: []delivery.TurnWait{
turn, err := meshdeliveryclient.EncodeTurn(meshdelivery.TurnsByRepositoryValue{Repository: repoA, Waiting: []meshdelivery.TurnsByRepositoryValueWaitingItem{
{Group: "feat/a", Heads: GateHeads(a), Approved: at, Since: at}, {Group: "feat/c", Heads: GateHeads(c), Approved: at.Add(time.Minute), Since: at}}})
if err != nil {
t.Fatal(err)
@@ -322,7 +326,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
if call == 1 {
return Outcome{Verdict: passing()}, nil
}
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil
return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}}, nil
}}
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
@@ -332,7 +336,7 @@ func TestOneCheckPerDelivery(t *testing.T) {
r.ch.TrunkMoved(ctx)
r.ch.Wait()
v := r.verdict(t, keyOf(t, "feat/x", cand))
if v.State != delivery.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") ||
if v.State != meshdeliverycheckerclient.VerdictFail || v.CarriedFrom != nil || !strings.HasPrefix(v.Failed.Words, "want 4") ||
!strings.Contains(v.Failed.Words, repoA+" "+trunkA[:12]+" to "+trunkA2[:12]) {
t.Errorf("the failed carry is %s carried from %v with words %q; want the test's words, then the move", v.State, v.CarriedFrom, v.Failed.Words)
}
@@ -7,7 +7,9 @@ import (
"fmt"
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// **A check is asked again by hand** (the lost check of 2026-10-28; tools-are-how-a-module-is-spoken-to: every recovery
@@ -23,7 +25,7 @@ import (
// AsksState is the checker's state of the asks each running check put, by verdict key (CheckAsks); AsksBucket its bucket.
const (
AsksState = "asks"
AsksBucket = delivery.CheckerModule + "_" + AsksState
AsksBucket = CheckerModule + "_" + AsksState
)
// CheckAsks are the asks a check put, in order, as the state asks keeps them while it runs; its end deletes the key.
@@ -70,8 +72,8 @@ const AskedAgainWords = EndedWords + "asked again by hand (" + AskAgainTool + ")
var errAskedAgain = errors.New("its verdict was asked again by hand (" + AskAgainTool + ")")
// IsAskedAgain says whether a verdict was written by AskAgainTool.
func IsAskedAgain(v delivery.Verdict) bool {
return IsEnded(v) && strings.HasPrefix(v.Failed.Words, AskedAgainWords)
func IsAskedAgain(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return IsEnded(v) && strings.HasPrefix(failedOf(v).Words, AskedAgainWords)
}
// VerdictEntry is the verdicts state as the ask-again tool writes it: a key's value and revision (nil and 0 for none),
@@ -104,7 +106,7 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
if !ok {
return AskedAgain{}, fmt.Errorf("no candidate is held for the group %q", group)
}
key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
if err != nil {
return AskedAgain{}, err
}
@@ -115,11 +117,11 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
if len(raw) == 0 {
return AskedAgain{}, fmt.Errorf("%s holds no verdict: the checker claims its check when it looks at the candidate (%s)", key, StatusReadTool)
}
v, err := delivery.DecodeVerdict(raw)
v, err := meshdeliverycheckerclient.DecodeVerdict(raw)
if err != nil {
return AskedAgain{}, fmt.Errorf("%s holds what is not a verdict: %w", key, err)
}
if v.State != delivery.VerdictRunning {
if v.State != meshdeliverycheckerclient.VerdictRunning {
return AskedAgain{}, fmt.Errorf("%s is %s, not running: a verdict stands until a push or a trunk move asks it again", key, v.State)
}
steps := r.readSteps()
@@ -150,9 +152,9 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
}
}
words := AskedAgainWords + why
ended := delivery.Verdict{Group: v.Group, Heads: v.Heads, Trunk: v.Trunk, State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Machine: "checker", Words: words}}
data, err := delivery.EncodeVerdict(ended)
ended := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: v.Group, Heads: v.Heads, Trunk: v.Trunk, State: meshdeliverycheckerclient.VerdictFail,
Failed: &FailedFirst{Machine: "checker", Words: words}}
data, err := meshdeliverycheckerclient.EncodeVerdict(ended)
if err != nil {
return AskedAgain{}, err
}
@@ -168,7 +170,7 @@ func (r Reader) AskAgain(w VerdictEntry, group, why string) (AskedAgain, error)
// the check of those heads running in this process, which then considers them again; with none running, the candidate
// that holds them is considered at once. Any other verdict is the checker's own, and left.
func (ch *Checker) VerdictWritten(ctx context.Context, key string, value []byte) {
v, err := delivery.DecodeVerdict(value)
v, err := meshdeliverycheckerclient.DecodeVerdict(value)
if err != nil || !IsAskedAgain(v) {
return
}
@@ -177,22 +179,22 @@ func (ch *Checker) VerdictWritten(ctx context.Context, key string, value []byte)
if l, ok := ch.live[key]; ok {
l.end(errAskedAgain)
ch.mu.Unlock()
ch.logf("%s: %s; the check running for it is ended and asked again", key, v.Failed.Words)
ch.logf("%s: %s; the check running for it is ended and asked again", key, failedOf(v).Words)
return
}
var group string
var cand delivery.Candidate
var cand meshcatalog.CandidatesByGroupValue
for k, c := range ch.known {
if vk, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil && vk == key {
if vk, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil && vk == key {
group, cand = k, c
break
}
}
ch.mu.Unlock()
if group == "" {
ch.logf("%s: %s, but no candidate holds these heads, so nothing is asked", key, v.Failed.Words)
ch.logf("%s: %s, but no candidate holds these heads, so nothing is asked", key, failedOf(v).Words)
return
}
ch.logf("%s: %s; asked again", key, v.Failed.Words)
ch.logf("%s: %s; asked again", key, failedOf(v).Words)
ch.consider(ctx, group, cand)
}
@@ -7,28 +7,18 @@ import (
// A change that adds or modifies a compiled program fails (issue 1577): it is build output, `go build` in a module's folder
// writes one beside go.mod, and `git add -A` takes it. The build module's fetch step, which holds the merged clone, reads the
// first bytes of each changed file and reports the programs (mesh-sdk buildstep.MergeInto); the checker judges the answer.
// NoProgramsReported is the warning of a build module whose fetch answered no programs field: an older module, which cannot
// tell, so the check does not refuse a program it did not look for.
const NoProgramsReported = "the build module did not report programs: a compiled program in the change is not refused"
// first bytes of each changed file and reports the programs (the build modules' fetch tools); the checker judges the answer.
// checkBinariesOf fails a pull request whose change adds or modifies a compiled program, naming each file. It judges the
// first fetch answer of the pull request, whichever build module gave it; a build module that reports no programs leaves a
// warning on the check and passes it.
// first fetch answer of the pull request, whichever build module gave it. A fetch that made the merge and names no program has
// none: the answer's `programs` is an omitted empty list (B2g), and `merge` tells a preview was made.
func (st *state) checkBinariesOf(t *target) verdictPtr {
key := fmt.Sprintf("%s#%d", t.pull.Repository, t.pull.Number)
if t.fetched.Merge == "" {
return nil // no merge was made: nothing was changed to judge
}
if t.fetched.Programs == nil {
st.c.note(fmt.Sprintf("%s: %s (%s)", key, NoProgramsReported, t.module))
st.c.warn(key, fmt.Sprintf("a compiled program is not refused: %s reported no programs", t.module))
st.j.logf("%s: %s (%s)", key, NoProgramsReported, t.module)
return nil
}
var found []string
for _, p := range *t.fetched.Programs {
for _, p := range t.fetched.Programs {
if p.Commit != "" {
found = append(found, fmt.Sprintf("%s: a compiled program added in %s and removed later: it stays in history; squash the branch (issue 1577)", p.Path, short12(p.Commit)))
continue
@@ -1,52 +1,46 @@
package check
import (
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
func TestAChangeWhoseFetchReportsProgramsFailsNamingEachFile(t *testing.T) { // 1577
prog := func(p ...string) *[]ProgramFile {
out := []ProgramFile{}
prog := func(p ...string) []buildstep.Program {
var out []buildstep.Program // no program is the omitted empty list of the typed answer
for _, f := range p {
path, commit, _ := strings.Cut(f, "@") // `path@commit`: added by a commit of the branch and removed later
out = append(out, ProgramFile{Path: path, Kind: "ELF", Commit: commit})
out = append(out, buildstep.Program{Path: path, Kind: "ELF", Commit: commit})
}
return &out
return out
}
for _, c := range []struct {
name string
programs *[]ProgramFile
programs []buildstep.Program
fail []string // paths named; none passes
warned bool // the build module reported no programs field
}{
{"none found", prog(), nil, false},
{"one program", prog("modules/m/m"), []string{"modules/m/m"}, false},
{"two programs", prog("a/x", "b/y"), []string{"a/x", "b/y"}, false},
{"added and removed in the branch", prog("gone/prog@0123456789abcdef0123"), []string{"gone/prog: a compiled program added in 0123456789ab and removed later: it stays in history; squash the branch (issue 1577)"}, false},
{"an older build module reports nothing", nil, nil, true},
{"none found", prog(), nil},
{"one program", prog("modules/m/m"), []string{"modules/m/m"}},
{"two programs", prog("a/x", "b/y"), []string{"a/x", "b/y"}},
{"added and removed in the branch", prog("gone/prog@0123456789abcdef0123"), []string{"gone/prog: a compiled program added in 0123456789ab and removed later: it stays in history; squash the branch (issue 1577)"}},
} {
b := newBench(t)
b.fetch.Merge, b.fetch.Programs = "mmmm", c.programs
cs := caseOf(t, pullOf(repoA, 7, head1))
v := b.judge(t, cs).Verdict
if got := slices.ContainsFunc(cs.Notes(), func(n string) bool { return strings.Contains(n, NoProgramsReported) }); got != c.warned {
t.Errorf("%s: warned = %v, want %v (%v)", c.name, got, c.warned, cs.Notes())
}
line := Line(v, cs, cs.Pulls[0], nil, nil, nil)
if got := strings.Contains(line, "a compiled program is not refused: mesh-build-go reported no programs"); got != c.warned {
t.Errorf("%s: the status line holds the warning = %v, want %v: %s", c.name, got, c.warned, line)
if len(cs.Notes()) > 0 {
t.Errorf("%s: notes = %v: a merge with no program is not a fetch that reported none", c.name, cs.Notes())
}
if len(c.fail) == 0 {
if v.State != delivery.VerdictPass {
if v.State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("%s: verdict = %+v", c.name, v)
}
continue
}
if v.State != delivery.VerdictFail {
if v.State != meshdeliverycheckerclient.VerdictFail {
t.Errorf("%s: verdict = %+v, want a failure", c.name, v)
continue
}
@@ -6,7 +6,7 @@ import (
"sync"
"sync/atomic"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// ReadWorkers is how many values a read tool asks the bus for at once. Each value is a round trip from the tool through the
@@ -60,7 +60,7 @@ func capKeys(keys []string, narrow string) ([]string, string) {
// longer id is the later and equal lengths compare as text. A key that is no step key goes last.
func newestFirst(keys []string) {
id := func(k string) (string, bool) {
_, id, err := delivery.ParseStepKey(k)
_, id, err := buildstep.ParseStepKey(k)
return id, err == nil
}
sort.Slice(keys, func(i, j int) bool {
@@ -41,10 +41,10 @@ func (st *state) buildsOfTheCheck(ctx context.Context) (map[string]string, []str
readable := true
for _, p := range st.c.Pulls {
for module, cm := range p.Fps {
if cm.FP2 == "" || cm.Manifest == "" {
if cm.Fp2 == "" || cm.Manifest == "" {
continue // the builder builds no fingerprint of it (candidates.go: none of the new form, or no manifest)
}
wait[module] = cm.FP2
wait[module] = cm.Fp2
if !readable || st.j.Read == nil {
readable = false
continue
@@ -9,7 +9,9 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
)
// The states of a build the status line says (ADR 0484 clause 7).
@@ -22,18 +24,18 @@ const (
// BuildStates reads the builder's build of a module at a fingerprint of the new form (builds.<module>.<fingerprint>): the
// value and whether the key exists.
type BuildStates interface {
Build(ctx context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error)
Build(ctx context.Context, module, fp2 string) (builds.BuilderBuild, bool, error)
}
// NoteOf is the state of a build as the line says it: done is built, failed is failed, and one queued, building or not
// begun is waiting.
func NoteOf(b delivery.BuilderBuild, found bool) string {
func NoteOf(b builds.BuilderBuild, found bool) string {
switch {
case !found:
return BuildWaiting
case b.State == delivery.BuildDone:
case b.State == builds.BuildDone:
return BuildBuilt
case b.State == delivery.BuildFailed:
case b.State == builds.BuildFailed:
return BuildFailed
}
return BuildWaiting
@@ -48,8 +50,8 @@ func (j *Examiner) buildNotes(ctx context.Context, c *Case) []BuildNote {
fps := map[string]string{}
for _, p := range c.Pulls {
for module, cm := range p.Fps {
if cm.FP2 != "" {
fps[module] = cm.FP2
if cm.Fp2 != "" {
fps[module] = cm.Fp2
}
}
}
@@ -79,7 +81,7 @@ const DefaultBuildWait = 30 * time.Minute
type BuildEnds interface {
// Ended waits until the build of every module at its fingerprint (module to fingerprint) is done or failed, or ctx ends,
// and answers the builds that ended, by module. An error is only builds that cannot be heard at all.
Ended(ctx context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error)
Ended(ctx context.Context, builds map[string]string) (map[string]builds.BuilderBuild, error)
// Done waits until the build of every module at its fingerprint is done (a failed one built again, builder_build_again),
// and says whether they all are; false when ctx ends first or they cannot be heard.
Done(ctx context.Context, builds map[string]string) bool
@@ -91,19 +93,19 @@ const BuildFailedTest = "build: "
// failedBuilds are the candidate's builds a failed verdict names as failed (BuildFailedTest), by module, at the fingerprint
// the case carries for it; none for a verdict that names no failed build.
func failedBuilds(v delivery.Verdict, cs *Case) map[string]string {
if v.State != delivery.VerdictFail {
func failedBuilds(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case) map[string]string {
if v.State != meshdeliverycheckerclient.VerdictFail {
return nil
}
out := map[string]string{}
for _, f := range append([]delivery.FailedFirst{v.Failed}, v.Failures...) {
for _, f := range append([]FailedFirst{failedOf(v)}, failedList(v.Failures)...) {
module, named := strings.CutPrefix(f.Test, BuildFailedTest)
if !named {
continue
}
for _, p := range cs.Pulls {
if cm, ok := p.Fps[module]; ok && cm.FP2 != "" {
out[module] = cm.FP2
if cm, ok := p.Fps[module]; ok && cm.Fp2 != "" {
out[module] = cm.Fp2
}
}
}
@@ -124,13 +126,13 @@ func (j *Examiner) buildWait() time.Duration {
// builds of the candidate and composes with it. A failed build fails the check, naming the module and the builder's words;
// a build not ended within the bound is named, and the check composes without it. It answers the words of the composition's
// warnings: what the check composes without waiting on its build, and why.
func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string {
func (st *state) awaitBuilds(ctx context.Context, v *meshdeliverychecker.VerdictsByGroupHeadsValue) []string {
wait, words := st.buildsOfTheCheck(ctx)
if len(wait) == 0 {
return words
}
modules := slices.Sorted(maps.Keys(wait))
if v.State == delivery.VerdictFail {
if v.State == meshdeliverycheckerclient.VerdictFail {
// The check has failed already: its red status is never held back by a build (the lead, 2026-11-03).
return append(words, fmt.Sprintf("the check failed before its builds were waited on, so it composes at once, without waiting on the builds of %s (ADR 0603 §10)", strings.Join(modules, ", ")))
}
@@ -154,10 +156,10 @@ func (st *state) awaitBuilds(ctx context.Context, v *delivery.Verdict) []string
switch {
case !done:
late = append(late, module)
case b.State == delivery.BuildFailed:
f := delivery.FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)}
if v.State != delivery.VerdictFail {
v.State, v.Failed = delivery.VerdictFail, f
case b.State == builds.BuildFailed:
f := FailedFirst{Test: BuildFailedTest + module, Words: fmt.Sprintf("the candidate's build at %s failed (rebuild with builder_build_again; the check asks itself again when the build is done): %s", wait[module], b.Error)}
if v.State != meshdeliverycheckerclient.VerdictFail {
v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f
continue
}
addFailure(v, f)
@@ -9,7 +9,11 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"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/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// Pull is one pull request the checker judges: its head, the trunk head of its repository it is merged onto and the
@@ -20,7 +24,7 @@ type Pull struct {
Head string
Trunk string
Modules []string
Fps map[string]delivery.CandidateModule
Fps map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue
}
// Case is one check: a candidate's pull requests that are judged (those of a repository whose trunk is not known are left
@@ -48,7 +52,7 @@ type Case struct {
// seriesOf are, by pull request, the changes of a package series it makes (series.go), named in its status line.
seriesOf map[string][]string
// seriesChanges are, by pull request, the same changes as structures (series.go), named in the verdict and its event.
seriesChanges map[string][]delivery.SeriesChange
seriesChanges map[string][]meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem
notes []string
// asks are the ids of the asks the check put on the build seats' streams, in order: a try names them (Try.Asks).
asks []string
@@ -56,7 +60,7 @@ type Case struct {
// the check waits for it: the node that took it, the phase it says, its end. A try writes them into its record, so a
// running try names its asks and where each stands (issue 1890). Called from the check's goroutines; it must not block
// for long.
Watch func(askID string, step *delivery.Step)
Watch func(askID string, step *buildstep.Step)
// Progress says what the check is doing, so a check that ran past its bound names the step it was stuck at.
Progress *Progress
// Kind is what asked the check (KindNew, KindNodes, KindTrunk): a carry after a trunk or sibling move is asked on the
@@ -65,11 +69,11 @@ type Case struct {
// CarriedFrom are, for the carry of a verdict that passed, the trunk and sibling commits it was judged on (ADR 0548 §5);
// nil for a whole check.
CarriedFrom map[string]string
// FPSet is the candidate's fingerprint set (Candidate.FPSet): a composition names the set it judged.
// FPSet is the candidate's fingerprint set (Candidate.Fpset): a composition names the set it judged.
FPSet string
// Composition is, for a carry, the composition that passed on the same fingerprint set: it is kept and not asked again,
// since no fingerprint of a module it composed changed (ADR 0548 §5). Nil composes.
Composition *delivery.Composition
Composition *meshdeliverychecker.VerdictsByGroupHeadsValueComposition
}
// Asked records the id of an ask the check put.
@@ -110,18 +114,18 @@ func (p *Progress) Now() (stage, node string) {
// The checker judges pull requests into main only (ADR 0484 clause 11): the pull message carries no base, a merge into
// another branch is a closed message, so every candidate is into the trunk; a repository whose trunk head is not known has
// none to merge onto. It answers nil when no pull request is left to judge.
func CaseOf(c delivery.Candidate, trunk map[string]string) *Case {
key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
func CaseOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) *Case {
key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
if err != nil {
return nil
}
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.HeadsHash, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}, FPSet: c.FPSet}
cs := &Case{Key: key, Group: c.Group, HeadsHash: c.Heads, Heads: GateHeads(c), Trunk: map[string]string{}, Progress: &Progress{}, FPSet: c.Fpset}
for _, p := range c.Pulls {
switch head, known := trunk[p.Repository]; {
case !known || head == "":
cs.Left = append(cs.Left, fmt.Sprintf("%s#%d: %s", p.Repository, p.Number, UntrunkedWordsOf([]string{p.Repository})))
default:
cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps})
cs.Pulls = append(cs.Pulls, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Trunk: head, Modules: p.Modules, Fps: p.Fps})
cs.Trunk[p.Repository] = head
}
}
@@ -151,8 +155,8 @@ func UntrunkedWordsOf(repos []string) string {
}
// IsUntrunked says whether a verdict says its heads cannot be judged for want of a trunk head (UntrunkedWords).
func IsUntrunked(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && v.Failed.Machine == "checker" && strings.HasPrefix(v.Failed.Words, UntrunkedWords)
func IsUntrunked(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && failedOf(v).Machine == "checker" && strings.HasPrefix(failedOf(v).Words, UntrunkedWords)
}
// **A candidate outside the mesh is not the checker's** (sweep 2026-10-30 item 7): the forge carries repositories the mesh
@@ -168,7 +172,7 @@ const OutsideWords = "outside the mesh: the gate's source.main names no trunk he
// Outside says every pull request of c is of a repository trunk names no head of and touches no module. Never with
// trunk unknown (nil): before the gate's source is read nothing is outside.
func Outside(c delivery.Candidate, trunk map[string]string) bool {
func Outside(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) bool {
if trunk == nil || len(c.Pulls) == 0 {
return false
}
@@ -182,11 +186,11 @@ func Outside(c delivery.Candidate, trunk map[string]string) bool {
// leftOf are the pull requests of a candidate whose repository has no trunk head known, with Trunk empty, in repository and
// number order; nil when every repository's is known.
func leftOf(c delivery.Candidate, trunk map[string]string) []Pull {
func leftOf(c meshcatalog.CandidatesByGroupValue, trunk map[string]string) []Pull {
var out []Pull
for _, p := range c.Pulls {
if trunk[p.Repository] == "" {
out = append(out, Pull{Repository: p.Repository, Number: p.Number, Head: p.Head.Head, Modules: p.Modules, Fps: p.Fps})
out = append(out, Pull{Repository: p.Repository, Number: int(p.Number), Head: p.Head, Modules: p.Modules, Fps: p.Fps})
}
}
sort.SliceStable(out, func(i, j int) bool {
@@ -199,15 +203,15 @@ func leftOf(c delivery.Candidate, trunk map[string]string) []Pull {
}
// GateHeads is the hash the gate matches the members an Approve covers by (mesh-catalog mesh-delivery-gate onestatus.go
// headsOf): delivery.HeadsHash over every pull request of the candidate, hq's included, as
// headsOf): meshdeliveryclient.Heads over every pull request of the candidate, hq's included, as
// `<owner/name lowercase>#<number>=<full head sha lowercase>`. A lone pull request is a group of one. Empty for a candidate
// with no pull request.
func GateHeads(c delivery.Candidate) string {
heads := make([]delivery.Head, 0, len(c.Pulls))
func GateHeads(c meshcatalog.CandidatesByGroupValue) string {
heads := make([]meshdeliveryclient.Head, 0, len(c.Pulls))
for _, p := range c.Pulls {
heads = append(heads, delivery.Head{Repository: strings.ToLower(p.Repository), Number: p.Number, Head: strings.ToLower(p.Head.Head)})
heads = append(heads, meshdeliveryclient.Head{Repository: strings.ToLower(p.Repository), Number: int(p.Number), Head: strings.ToLower(p.Head)})
}
hash, err := delivery.HeadsHash(heads)
hash, err := meshdeliveryclient.HeadsHash(heads)
if err != nil {
return ""
}
@@ -227,7 +231,7 @@ type Trunks struct {
func (t *Trunks) Put(value []byte, deleted bool) bool {
var heads map[string]string
if !deleted {
s, err := delivery.DecodeSource(value)
s, err := meshdeliveryclient.DecodeSource(value)
if err != nil {
return false
}
@@ -11,7 +11,14 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"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/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// DefaultBound is how long a check may run before it is ended failed naming the step it was stuck at (ADR 0484, issue 1678):
@@ -38,7 +45,7 @@ type Announcer func(ctx context.Context, event, key string)
// Outcome is what judging a case gives: the verdict (state, failure, levels, times, composition; the checker adds the
// group, the heads and the trunk) and, for the line, what the check knows of the builds of the modules it touches.
type Outcome struct {
Verdict delivery.Verdict
Verdict meshdeliverychecker.VerdictsByGroupHeadsValue
Builds []BuildNote
// Slowest names the slowest tests of a level that went over its bound ("unit pkg.Test 1.2s").
Slowest []string
@@ -80,7 +87,7 @@ type Checker struct {
// Turns are the gate's merge turns (ADR 0548 §3): no check is claimed of a repository a turn names, but the check of the
// waiting delivery's own heads; nil holds none.
Turns interface {
Holding(repositories []string, heads string) (string, delivery.TurnWait, bool)
Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool)
}
// Bound is how long one check may run; none when 0 (DefaultBound).
Bound time.Duration
@@ -105,17 +112,17 @@ type Checker struct {
once sync.Once
wg sync.WaitGroup
mu sync.Mutex
known map[string]delivery.Candidate // by candidates key
inflight map[string]bool // by verdict key
again map[string]bool // by verdict key: asked again while it ran
tried map[string]bool // by verdict key: looked at since this process started
epoch uint64 // changes of the nodes seen since this process started
nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at
held map[string]string // by verdict key: the turn that holds its check off, as last said
live map[string]liveCheck // by verdict key: the check running in this process, ended when its heads go
settling map[string]*window // by candidates key: the window its heads settle in
rebuilt map[string]bool // by verdict key: every failed build its verdict names is done again
awaiting map[string]bool // by verdict key: its failed builds are heard (awaitRebuilt)
known map[string]meshcatalog.CandidatesByGroupValue // by candidates key
inflight map[string]bool // by verdict key
again map[string]bool // by verdict key: asked again while it ran
tried map[string]bool // by verdict key: looked at since this process started
epoch uint64 // changes of the nodes seen since this process started
nodesFail map[string]uint64 // by verdict key: the epoch a failure of the nodes was written at
held map[string]string // by verdict key: the turn that holds its check off, as last said
live map[string]liveCheck // by verdict key: the check running in this process, ended when its heads go
settling map[string]*window // by candidates key: the window its heads settle in
rebuilt map[string]bool // by verdict key: every failed build its verdict names is done again
awaiting map[string]bool // by verdict key: its failed builds are heard (awaitRebuilt)
// outsideSaid are the candidates outside the mesh said in the log, by group and heads.
outsideSaid map[string]bool
}
@@ -155,8 +162,8 @@ const DefaultSettle = 15 * time.Second
const EndedWords = "ended: "
// IsEnded says whether a verdict was ended without being judged (EndedWords).
func IsEnded(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && v.Failed.Machine == "checker" && strings.HasPrefix(v.Failed.Words, EndedWords)
func IsEnded(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && failedOf(v).Machine == "checker" && strings.HasPrefix(failedOf(v).Words, EndedWords)
}
// superseded is the cause a running check is ended with: the heads that replaced its own, or none when its group went.
@@ -171,7 +178,7 @@ func (s superseded) Error() string {
func (ch *Checker) init() {
ch.once.Do(func() {
ch.known, ch.inflight, ch.again, ch.tried = map[string]delivery.Candidate{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
ch.known, ch.inflight, ch.again, ch.tried = map[string]meshcatalog.CandidatesByGroupValue{}, map[string]bool{}, map[string]bool{}, map[string]bool{}
ch.nodesFail, ch.held = map[string]uint64{}, map[string]string{}
ch.live, ch.settling = map[string]liveCheck{}, map[string]*window{}
ch.rebuilt, ch.awaiting = map[string]bool{}, map[string]bool{}
@@ -216,19 +223,19 @@ func (ch *Checker) Wait() { ch.wg.Wait() }
// checked. A value that is not a candidate is said and left.
func (ch *Checker) Candidate(ctx context.Context, key string, value []byte) {
ch.init()
c, err := delivery.DecodeCandidate(value)
c, err := meshcatalog.DecodeCandidatesByGroupValue(value)
if err != nil {
ch.logf("candidates.%s holds what is not a candidate, left: %v", key, err)
return
}
vkey, _ := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
vkey, _ := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
ch.mu.Lock()
prev, had := ch.known[key]
ch.known[key] = c
// A check of heads this candidate no longer holds is ended at once (issue 1890): a member's new head, a member joining
// or leaving.
ch.endOthers(key, vkey, GateHeads(c))
moved := !had || prev.HeadsHash != c.HeadsHash
moved := !had || prev.Heads != c.Heads
if ch.Settle > 0 && (moved || ch.settling[key] != nil) {
// The group's heads changed: its check waits until they have been the same for Settle, so a group whose members
// are announced one after another is checked once, whole. A later change starts the window again. The wait is a
@@ -279,9 +286,9 @@ func (ch *Checker) VerdictDeleted(ctx context.Context, key string) {
return
}
var group string
var cand delivery.Candidate
var cand meshcatalog.CandidatesByGroupValue
for k, c := range ch.known {
if vk, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil && vk == key {
if vk, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil && vk == key {
group, cand = k, c
break
}
@@ -324,7 +331,7 @@ func (ch *Checker) NodesChanged(ctx context.Context) {
func (ch *Checker) Reconsider(ctx context.Context) {
ch.init()
ch.mu.Lock()
known := make(map[string]delivery.Candidate, len(ch.known))
known := make(map[string]meshcatalog.CandidatesByGroupValue, len(ch.known))
for k, c := range ch.known {
known[k] = c
}
@@ -341,7 +348,7 @@ func (ch *Checker) heads() map[string]string {
return ch.Trunks.Heads()
}
func (ch *Checker) consider(ctx context.Context, key string, c delivery.Candidate) {
func (ch *Checker) consider(ctx context.Context, key string, c meshcatalog.CandidatesByGroupValue) {
heads := ch.heads()
if Outside(c, heads) {
ch.outside(ctx, c)
@@ -422,10 +429,10 @@ func (ch *Checker) consider(ctx context.Context, key string, c delivery.Candidat
// UntrunkedWords, the members whose trunk is known too, as their group is not judged. It is logged once per head: when a
// status or the verdict is written, never at a look that finds them already said. Once the trunk head is known the verdict
// is stale (it names no trunk) and the heads are judged whole (prepare, claim).
func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []Pull) {
key, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
func (ch *Checker) untrunked(ctx context.Context, c meshcatalog.CandidatesByGroupValue, left []Pull) {
key, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
if err != nil {
ch.logf("candidates of %s: no verdict key for heads %q, so it cannot be said that they cannot be judged: %v", c.Group, c.HeadsHash, err)
ch.logf("candidates of %s: no verdict key for heads %q, so it cannot be said that they cannot be judged: %v", c.Group, c.Heads, err)
return
}
repos := make([]string, 0, len(left))
@@ -433,37 +440,37 @@ func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []P
repos = append(repos, p.Repository)
}
words := UntrunkedWordsOf(repos)
v := delivery.Verdict{Group: c.Group, Heads: GateHeads(c), Trunk: map[string]string{}, State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Machine: "checker", Words: words}}
data, err := delivery.EncodeVerdict(v)
v := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: c.Group, Heads: GateHeads(c), Trunk: map[string]string{}, State: meshdeliverycheckerclient.VerdictFail,
Failed: &FailedFirst{Machine: "checker", Words: words}}
data, err := meshdeliverycheckerclient.EncodeVerdict(v)
if err != nil {
ch.logf("%s: %s, and the verdict cannot say so: %v", key, words, err)
return
}
said, err := putOnce(ctx, ch.Verdicts, key, data, func(cur []byte) bool {
held, err := delivery.DecodeVerdict(cur)
return err == nil && IsUntrunked(held) && held.Failed.Words == words // the same words stand; anything else is replaced
held, err := meshdeliverycheckerclient.DecodeVerdict(cur)
return err == nil && IsUntrunked(held) && failedOf(held).Words == words // the same words stand; anything else is replaced
})
if err != nil {
ch.logf("%s: %s, and the verdict cannot say so: %v", key, words, err)
}
for _, cp := range c.Pulls {
okey, err := delivery.CheckerOwedKey(cp.Repository, cp.Number, cp.Head.Head)
okey, err := CheckerOwedKey(cp.Repository, int(cp.Number), cp.Head)
if err != nil {
ch.logf("%s: %v", key, err)
continue
}
o := delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "error",
o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "error",
Target: strings.TrimSuffix(ch.Link, "/") + "/" + key, Text: clipLine(words, MaxLine)}
data, err := delivery.EncodeCheckerOwed(o)
data, err := EncodeCheckerOwed(o)
if err != nil {
ch.logf("%s: the status of %s cannot be written: %v", key, okey, err)
continue
}
wrote, err := putOnce(ctx, ch.Owed, okey, data, func(cur []byte) bool {
held, err := delivery.DecodeCheckerOwed(cur)
o.V = delivery.Version
return err == nil && delivery.SameOwed(held, o)
held, err := DecodeCheckerOwed(cur)
o.V = keys.Version
return err == nil && held == o
})
if err != nil {
ch.logf("%s: the status of %s cannot be written: %v", key, okey, err)
@@ -488,11 +495,11 @@ func (ch *Checker) untrunked(ctx context.Context, c delivery.Candidate, left []P
// outside takes a candidate outside the mesh (case.go Outside): no verdict, and every status owed for its pull requests
// taken away, so the forge module stops writing one into a repository the mesh's forge account may not write. Said once
// per heads in the log.
func (ch *Checker) outside(ctx context.Context, c delivery.Candidate) {
func (ch *Checker) outside(ctx context.Context, c meshcatalog.CandidatesByGroupValue) {
var names []string
for _, p := range c.Pulls {
names = append(names, fmt.Sprintf("%s#%d@%s", p.Repository, p.Number, short12(p.Head.Head)))
key, err := delivery.CheckerOwedKey(p.Repository, p.Number, p.Head.Head)
names = append(names, fmt.Sprintf("%s#%d@%s", p.Repository, p.Number, short12(p.Head)))
key, err := CheckerOwedKey(p.Repository, int(p.Number), p.Head)
if err != nil {
continue
}
@@ -508,7 +515,7 @@ func (ch *Checker) outside(ctx context.Context, c delivery.Candidate) {
ch.logf("%s: the status owed of a pull request outside the mesh could not be taken away: %v", key, err)
}
}
said := c.Group + "@" + c.HeadsHash
said := c.Group + "@" + c.Heads
ch.mu.Lock()
first := !ch.outsideSaid[said]
ch.outsideSaid[said] = true
@@ -538,9 +545,9 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) {
if err != nil || !found {
return
}
v, err := delivery.DecodeVerdict(value)
v, err := meshdeliverycheckerclient.DecodeVerdict(value)
switch {
case err != nil || v.State == delivery.VerdictRunning:
case err != nil || v.State == meshdeliverycheckerclient.VerdictRunning:
return
case IsNodesFailure(v):
cs.Kind = KindNodes
@@ -555,9 +562,9 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) {
return // ended unjudged when its heads went (issue 1890), or never judged for want of a trunk head (issue 1422): checked whole
}
cs.Kind = KindTrunk
if v.State == delivery.VerdictPass && ch.stale(v, cs) {
if v.State == meshdeliverycheckerclient.VerdictPass && ch.stale(v, cs) {
cs.CarriedFrom = judgedOn(v)
if c := v.Composition; c != nil && c.State == delivery.CompositionPass && c.FPSet != "" && c.FPSet == cs.FPSet {
if c := v.Composition; c != nil && c.State == meshdeliverycheckerclient.CompositionPass && c.Fpset != "" && c.Fpset == cs.FPSet {
kept := *c
cs.Composition = &kept
}
@@ -566,7 +573,7 @@ func (ch *Checker) prepare(ctx context.Context, group string, cs *Case) {
// stale says whether a verdict was judged on commits that are no longer the case's: a trunk it merged moved, or a sibling it
// read at its trunk's head (ADR 0548 §6).
func (ch *Checker) stale(v delivery.Verdict, cs *Case) bool {
func (ch *Checker) stale(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case) bool {
return !sameTrunk(v.Trunk, cs.Trunk) || besideMoved(v.Beside, ch.besideNow())
}
@@ -603,7 +610,7 @@ func besideMoved(beside, now map[string]string) bool {
}
// judgedOn are the trunk and sibling commits a verdict was judged on, by repository.
func judgedOn(v delivery.Verdict) map[string]string {
func judgedOn(v meshdeliverychecker.VerdictsByGroupHeadsValue) map[string]string {
out := make(map[string]string, len(v.Trunk)+len(v.Beside))
for r, c := range v.Beside {
out[r] = c
@@ -616,7 +623,7 @@ func judgedOn(v delivery.Verdict) map[string]string {
// carried is what a carry's verdict names as carried from (ADR 0548 §5): the commits it was judged on before, of the
// repositories the new verdict still judges or reads; nil when none of them moved.
func carried(from map[string]string, v delivery.Verdict) map[string]string {
func carried(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue) map[string]string {
out, moved := map[string]string{}, false
for r, c := range from {
now, ok := v.Trunk[r]
@@ -636,7 +643,7 @@ func carried(from map[string]string, v delivery.Verdict) map[string]string {
// moveWords say a carry's moves, for the words of a failure: "carried from <old12> to <new12> of <repository>", each moved
// repository, sorted.
func moveWords(from map[string]string, v delivery.Verdict) string {
func moveWords(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue) string {
var moves []string
for r, c := range from {
now, ok := v.Trunk[r]
@@ -651,7 +658,7 @@ func moveWords(from map[string]string, v delivery.Verdict) string {
return strings.Join(moves, ", ")
}
func (ch *Checker) candidate(key string, fallback delivery.Candidate) delivery.Candidate {
func (ch *Checker) candidate(key string, fallback meshcatalog.CandidatesByGroupValue) meshcatalog.CandidatesByGroupValue {
ch.mu.Lock()
defer ch.mu.Unlock()
if c, ok := ch.known[key]; ok {
@@ -670,8 +677,8 @@ type claimed struct{ rev uint64 }
// - a pass or a failure for the trunk heads the case has stands; one for other trunk heads (the trunk moved) is taken
// again by compare-and-set on the same key, and so is a failure of the nodes after the nodes changed.
func (ch *Checker) claim(ctx context.Context, cs *Case, first bool, epoch uint64) (*claimed, error) {
running := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictRunning}
data, err := delivery.EncodeVerdict(running)
running := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictRunning}
data, err := meshdeliverycheckerclient.EncodeVerdict(running)
if err != nil {
return nil, err
}
@@ -692,22 +699,22 @@ func (ch *Checker) claim(ctx context.Context, cs *Case, first bool, epoch uint64
}
return &claimed{rev}, nil
}
cur, err := delivery.DecodeVerdict(value)
cur, err := meshdeliverycheckerclient.DecodeVerdict(value)
if err != nil {
return nil, fmt.Errorf("%s holds what is not a verdict: %w", cs.Key, err)
}
retake := false
switch {
case cur.State == delivery.VerdictRunning:
case cur.State == meshdeliverycheckerclient.VerdictRunning:
if first {
return &claimed{rev}, nil
}
return nil, nil
case IsEnded(cur), ch.stale(cur, cs):
retake = true // heads held again after their check was ended unjudged (issue 1890), or a trunk that moved
case cur.State == delivery.VerdictFail && ch.takeRebuilt(cs.Key):
case cur.State == meshdeliverycheckerclient.VerdictFail && ch.takeRebuilt(cs.Key):
retake = true // every failed build it names is done again (ADR 0603 §10)
case cur.State == delivery.VerdictFail && IsNodesFailure(cur):
case cur.State == meshdeliverycheckerclient.VerdictFail && IsNodesFailure(cur):
ch.mu.Lock()
at, ok := ch.nodesFail[cs.Key]
ch.mu.Unlock()
@@ -775,15 +782,15 @@ const ConflictWords = "does not merge into the trunk: files in conflict: "
// IsConflictFailure says whether a verdict failed because its head did not merge onto its trunk (ADR 0548 §7): it has no
// pass to carry, so its next check, at a push or at a later trunk move, is a whole check of its head.
func IsConflictFailure(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, ConflictWords)
func IsConflictFailure(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, ConflictWords)
}
// IsNodesFailure says whether a verdict failed because a part of the mesh the check reads was not heard (UnheardWords):
// it is judged again when the nodes change. No check fails for want of a build node any more: its asks wait in the build
// seats' streams (ADR 0525 §6, issue 1912).
func IsNodesFailure(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, UnheardWords)
func IsNodesFailure(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, UnheardWords)
}
// try claims the check, judges it within its bound under run and writes the verdict and the owed statuses. A check that ends
@@ -808,14 +815,14 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) {
}
began := ch.now()
if cs.Watch == nil && ch.Asks != nil {
cs.Watch = func(_ string, step *delivery.Step) {
cs.Watch = func(_ string, step *buildstep.Step) {
if step == nil { // put: kept before its wait starts
ch.recordAsks(ctx, cs.Key, cs.Asks())
}
}
defer ch.forgetAsks(ctx, cs.Key)
}
ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{})
ch.writeOwed(ctx, cs, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{})
out := ch.judge(run, cs)
// An ask the build seat gave up on, or whose step the build module's next process ended because it was replaced under
// it, judged nothing (issue 734, IsLostAsk): the check is asked again under the same claim, its status pending, and after
@@ -823,11 +830,11 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) {
// read at the next nudge (HeardEnds) and judged.
for try := 1; IsLostAsk(out.Verdict) && run.Err() == nil; try++ {
if try >= LostAskTries {
out.Verdict.Failed.Words = strings.TrimSpace(out.Verdict.Failed.Words + "\nasked " + fmt.Sprint(LostAskTries) +
out.Verdict.Failed.Words = strings.TrimSpace(failedOf(out.Verdict).Words + "\nasked " + fmt.Sprint(LostAskTries) +
" times and given up each time: " + UnheardWords)
break
}
ch.logf("%s: %s; asked again (%d of %d)", cs.Key, out.Verdict.Failed.Words, try+1, LostAskTries)
ch.logf("%s: %s; asked again (%d of %d)", cs.Key, failedOf(out.Verdict).Words, try+1, LostAskTries)
out = ch.judge(run, cs)
}
if ctx.Err() != nil || errors.Is(context.Cause(run), errAskedAgain) {
@@ -851,23 +858,26 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) {
if cs.CarriedFrom != nil {
// A carry (ADR 0548 §5): a pass names the commits it was carried from; a failure names the move it failed on.
switch v.State {
case delivery.VerdictPass:
case meshdeliverycheckerclient.VerdictPass:
v.CarriedFrom = carried(cs.CarriedFrom, v)
case delivery.VerdictFail:
case meshdeliverycheckerclient.VerdictFail:
if moves := moveWords(cs.CarriedFrom, v); moves != "" {
v.Failed.Words = strings.TrimSpace(v.Failed.Words + "\nfailed on the carry over the move of " + moves + ": the move changed what these tests read")
v.Failed.Words = strings.TrimSpace(failedOf(v).Words + "\nfailed on the carry over the move of " + moves + ": the move changed what these tests read")
}
}
}
if v.Took.Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) {
if tookOf(v).Queue == 0 && !cs.Queued.IsZero() && began.After(cs.Queued) {
if v.Took == nil {
v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{}
}
v.Took.Queue = began.Sub(cs.Queued).Milliseconds()
}
data, err := delivery.EncodeVerdict(v)
data, err := meshdeliverycheckerclient.EncodeVerdict(v)
if err != nil {
ch.logf("%s: the verdict cannot be written: %v", cs.Key, err)
v = delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Machine: "checker", Words: "the verdict could not be written: " + err.Error()}}
if data, err = delivery.EncodeVerdict(v); err != nil {
v = meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictFail,
Failed: &FailedFirst{Machine: "checker", Words: "the verdict could not be written: " + err.Error()}}
if data, err = meshdeliverycheckerclient.EncodeVerdict(v); err != nil {
return
}
}
@@ -902,9 +912,9 @@ func (ch *Checker) try(ctx, run context.Context, key string, cs *Case) {
// be taken back (buildrun.Asker has no withdraw), and are no longer waited for. No status is written: the heads that
// replaced these have their own check, whose pending status stands.
func (ch *Checker) end(ctx context.Context, cs *Case, cl *claimed, why superseded) {
v := delivery.Verdict{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Machine: "checker", Words: why.Error()}}
data, err := delivery.EncodeVerdict(v)
v := meshdeliverychecker.VerdictsByGroupHeadsValue{Group: cs.Group, Heads: cs.Heads, Trunk: cs.Trunk, State: meshdeliverycheckerclient.VerdictFail,
Failed: &FailedFirst{Machine: "checker", Words: why.Error()}}
data, err := meshdeliverycheckerclient.EncodeVerdict(v)
if err == nil {
_, err = ch.Verdicts.Update(ctx, cs.Key, data, cl.rev)
}
@@ -926,32 +936,32 @@ func (ch *Checker) judge(ctx context.Context, cs *Case) Outcome {
var errBound = errors.New("the check ran past its bound")
func failure(machine, words string) delivery.Verdict {
return delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: machine, Words: words}}
func failure(machine, words string) meshdeliverychecker.VerdictsByGroupHeadsValue {
return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: machine, Words: words}}
}
// writeOwed writes, for each pull request judged, the status the forge bridge performs: pending while the check runs, success
// or failure at its end (ADR 0484 clause 2). A key already holding the same value is not written again, so the bridge
// performs one act for each change; an older head's key is never touched.
func (ch *Checker) writeOwed(ctx context.Context, cs *Case, v delivery.Verdict, out Outcome) {
func (ch *Checker) writeOwed(ctx context.Context, cs *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue, out Outcome) {
for _, p := range cs.Pulls {
key, err := delivery.CheckerOwedKey(p.Repository, p.Number, p.Head)
key, err := CheckerOwedKey(p.Repository, int(p.Number), p.Head)
if err != nil {
ch.logf("%s: %v", cs.Key, err)
continue
}
state := map[string]string{delivery.VerdictRunning: "pending", delivery.VerdictPass: "success", delivery.VerdictFail: "failure"}[v.State]
o := delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: state,
state := map[string]string{meshdeliverycheckerclient.VerdictRunning: "pending", meshdeliverycheckerclient.VerdictPass: "success", meshdeliverycheckerclient.VerdictFail: "failure"}[v.State]
o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: state,
Target: strings.TrimSuffix(ch.Link, "/") + "/" + cs.Key, Text: Line(v, cs, p, out.Builds, out.Slowest, out.Judged)}
data, err := delivery.EncodeCheckerOwed(o)
data, err := EncodeCheckerOwed(o)
if err != nil {
ch.logf("%s: the status of %s cannot be written: %v", cs.Key, key, err)
continue
}
wrote, err := putOnce(ctx, ch.Owed, key, data, func(cur []byte) bool {
held, err := delivery.DecodeCheckerOwed(cur)
o.V = delivery.Version
return err == nil && delivery.SameOwed(held, o)
held, err := DecodeCheckerOwed(cur)
o.V = keys.Version
return err == nil && held == o
})
if err != nil {
ch.logf("%s: the status of %s cannot be written: %v", cs.Key, key, err)
@@ -997,7 +1007,7 @@ func (ch *Checker) Sweep(ctx context.Context) error {
ch.mu.Lock()
current := map[string]bool{}
for _, c := range ch.known {
if k, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash); err == nil {
if k, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads); err == nil {
current[k] = true
}
}
@@ -1010,13 +1020,13 @@ func (ch *Checker) Sweep(ctx context.Context) error {
if err != nil || !found {
continue
}
cur, err := delivery.DecodeVerdict(value)
if err != nil || cur.State != delivery.VerdictRunning {
cur, err := meshdeliverycheckerclient.DecodeVerdict(value)
if err != nil || cur.State != meshdeliverycheckerclient.VerdictRunning {
continue
}
cur.State = delivery.VerdictFail
cur.Failed = delivery.FailedFirst{Machine: "checker", Words: EndedWords + "closed at start: left running, and no candidate holds these heads any more, so nothing will check them"}
data, err := delivery.EncodeVerdict(cur)
cur.State = meshdeliverycheckerclient.VerdictFail
cur.Failed = &FailedFirst{Machine: "checker", Words: EndedWords + "closed at start: left running, and no candidate holds these heads any more, so nothing will check them"}
data, err := meshdeliverycheckerclient.EncodeVerdict(cur)
if err != nil {
return err
}
@@ -1035,7 +1045,7 @@ func (ch *Checker) Sweep(ctx context.Context) error {
func (ch *Checker) TrunkMoved(ctx context.Context) {
ch.init()
ch.mu.Lock()
known := make(map[string]delivery.Candidate, len(ch.known))
known := make(map[string]meshcatalog.CandidatesByGroupValue, len(ch.known))
for k, c := range ch.known {
known[k] = c
}
@@ -1050,23 +1060,23 @@ func (ch *Checker) TrunkMoved(ctx context.Context) {
if err != nil || !found {
continue // never judged: no status to put out of date
}
v, err := delivery.DecodeVerdict(value)
v, err := meshdeliverycheckerclient.DecodeVerdict(value)
if err == nil && !ch.stale(v, cs) {
continue
}
if err == nil && v.State == delivery.VerdictPass {
if err == nil && v.State == meshdeliverycheckerclient.VerdictPass {
cs.CarriedFrom = judgedOn(v) // the status says it is being carried to the new commit (ADR 0548 §6)
}
ch.writeOwed(ctx, cs, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{})
ch.writeOwed(ctx, cs, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{})
}
ch.Reconsider(ctx)
}
// owePending owes mesh/check pending on the heads of a verdict judged on commits that moved while it ran: a pass says it is
// being carried to the trunk's head now (ADR 0548 §6), anything else that it is checked again.
func (ch *Checker) owePending(ctx context.Context, judged *Case, v delivery.Verdict) {
func (ch *Checker) owePending(ctx context.Context, judged *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue) {
next := judged
ckey, err := delivery.CandidatesKey(v.Group)
ckey, err := meshcatalogclient.CandidatesKey(v.Group)
ch.mu.Lock()
c, ok := ch.known[ckey]
ch.mu.Unlock()
@@ -1075,10 +1085,10 @@ func (ch *Checker) owePending(ctx context.Context, judged *Case, v delivery.Verd
next = now
}
}
if v.State == delivery.VerdictPass {
if v.State == meshdeliverycheckerclient.VerdictPass {
next.CarriedFrom = judgedOn(v)
}
ch.writeOwed(ctx, next, delivery.Verdict{State: delivery.VerdictRunning}, Outcome{})
ch.writeOwed(ctx, next, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, Outcome{})
}
// movedSince says whether a trunk the case was judged on is no longer the trunk's head.
@@ -1134,7 +1144,7 @@ func (ch *Checker) hearFailedBuilds(ctx context.Context, group string, cs *Case)
if err != nil || !found {
return
}
if v, err := delivery.DecodeVerdict(value); err == nil {
if v, err := meshdeliverycheckerclient.DecodeVerdict(value); err == nil {
if builds := failedBuilds(v, cs); builds != nil {
ch.awaitRebuilt(ctx, group, cs.Key, builds)
}
@@ -9,8 +9,14 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"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/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/keys"
)
const hqRepo = "novox/hq"
@@ -27,30 +33,30 @@ const (
)
// candidateOf is a candidate of a group with the given pull requests, as the indexer writes one.
func candidateOf(t testing.TB, group string, pulls ...delivery.CandidatePull) []byte {
func candidateOf(t testing.TB, group string, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) []byte {
t.Helper()
heads := make([]delivery.Head, len(pulls))
heads := make([]meshdeliveryclient.Head, len(pulls))
for i, p := range pulls {
heads[i] = p.Head
heads[i] = meshdeliveryclient.Head{Repository: p.Repository, Number: int(p.Number), Head: p.Head}
}
hash, err := delivery.HeadsHash(heads)
hash, err := meshdeliveryclient.HeadsHash(heads)
if err != nil {
t.Fatal(err)
}
c := delivery.Candidate{V: delivery.Version, Group: group, HeadsHash: hash, Pulls: pulls, FPSet: delivery.FingerprintSet(pulls)}
data, err := delivery.EncodeCandidate(c)
c := meshcatalog.CandidatesByGroupValue{V: keys.Version, Group: group, Heads: hash, Pulls: pulls, Fpset: meshcatalogclient.FingerprintSet(pulls)}
data, err := json.Marshal(c)
if err != nil {
t.Fatal(err)
}
return data
}
func pullOf(repo string, number int, head string) delivery.CandidatePull {
return delivery.CandidatePull{Head: delivery.Head{Repository: repo, Number: number, Head: head}, Modules: []string{}}
func pullOf(repo string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem {
return meshcatalog.CandidatesByGroupValuePullsItem{Repository: repo, Number: int64(number), Head: head, Modules: []string{}}
}
// newRepoPullOf is a pull request of a new repository of the mesh: its trunk not named yet, its first module added.
func newRepoPullOf(repo string, number int, head string) delivery.CandidatePull {
func newRepoPullOf(repo string, number int, head string) meshcatalog.CandidatesByGroupValuePullsItem {
p := pullOf(repo, number, head)
p.Modules = []string{"mod-u"}
return p
@@ -73,9 +79,9 @@ func (j *judgeFunc) Judge(ctx context.Context, c *Case) (Outcome, error) {
func (j *judgeFunc) count() int { j.mu.Lock(); defer j.mu.Unlock(); return j.calls }
func passing() delivery.Verdict {
return delivery.Verdict{State: delivery.VerdictPass, Levels: map[string]delivery.LevelResult{
delivery.LevelUnit: {Run: 2, Kept: 3}, delivery.LevelIntegration: {Run: 1}}}
func passing() meshdeliverychecker.VerdictsByGroupHeadsValue {
return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass, Levels: map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{
buildstep.LevelUnit: {Run: 2, Kept: 3}, buildstep.LevelIntegration: {Run: 1}}}
}
func passJudge() *judgeFunc {
@@ -99,20 +105,20 @@ func newRig(t testing.TB, j Judge) *rig {
func (r *rig) setTrunks(t testing.TB, heads map[string]string) bool {
t.Helper()
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads})
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads})
if err != nil {
t.Fatal(err)
}
return r.trunks.Put(data, false)
}
func (r *rig) verdict(t testing.TB, key string) delivery.Verdict {
func (r *rig) verdict(t testing.TB, key string) meshdeliverychecker.VerdictsByGroupHeadsValue {
t.Helper()
raw, _, found, _ := r.verdicts.Get(context.Background(), key)
if !found {
t.Fatalf("no verdict %s; the bucket holds %v", key, r.verdicts.vals)
}
v, err := delivery.DecodeVerdict(raw)
v, err := meshdeliverycheckerclient.DecodeVerdict(raw)
if err != nil {
t.Fatal(err)
}
@@ -121,11 +127,11 @@ func (r *rig) verdict(t testing.TB, key string) delivery.Verdict {
func keyOf(t testing.TB, group string, value []byte) string {
t.Helper()
c, err := delivery.DecodeCandidate(value)
c, err := meshcatalog.DecodeCandidatesByGroupValue(value)
if err != nil {
t.Fatal(err)
}
k, err := delivery.CheckerVerdictKey(group, c.HeadsHash)
k, err := meshdeliverycheckerclient.VerdictKey(group, c.Heads)
if err != nil {
t.Fatal(err)
}
@@ -139,7 +145,7 @@ func TestACandidateNotYetCheckedGetsItsVerdictKeyCreatedOnceAndIsJudged(t *testi
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
key := keyOf(t, "feat/x", cand)
if v := r.verdict(t, key); v.State != delivery.VerdictPass || v.Group != "feat/x" || v.Trunk[repoA] != trunkA {
if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictPass || v.Group != "feat/x" || v.Trunk[repoA] != trunkA {
t.Fatalf("verdict = %+v", v)
}
if r.verdicts.creates[key] != 1 || j.count() != 1 {
@@ -171,14 +177,14 @@ func TestASecondInstancesCreateIsRefusedAndItDoesNothing(t *testing.T) { // N6/A
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
other, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], State: delivery.VerdictRunning})
other, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], State: meshdeliverycheckerclient.VerdictRunning})
r.ch.Verdicts = racyKV{r.verdicts, other}
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if j.count() != 0 {
t.Errorf("the instance that lost the create judged %d times, want none", j.count())
}
if v := r.verdict(t, key); v.State != delivery.VerdictRunning {
if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictRunning {
t.Errorf("the other instance's key was changed: %+v", v)
}
}
@@ -188,13 +194,13 @@ func TestARestartWithARunningKeyJudgesItAgain(t *testing.T) { // N6/AC2
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
key := keyOf(t, "feat/x", cand)
left, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], Trunk: map[string]string{repoA: trunkA}, State: delivery.VerdictRunning})
left, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: strings.SplitN(key, ".", 2)[1], Trunk: map[string]string{repoA: trunkA}, State: meshdeliverycheckerclient.VerdictRunning})
if _, err := r.verdicts.Create(ctx, key, left); err != nil {
t.Fatal(err)
}
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
if j.count() != 1 || r.verdict(t, key).State != delivery.VerdictPass {
if j.count() != 1 || r.verdict(t, key).State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("judged %d times, verdict %+v, want the left check judged once and passed", j.count(), r.verdict(t, key))
}
}
@@ -212,7 +218,7 @@ func TestACheckPastItsBoundFailsNamingTheStuckStep(t *testing.T) { // N6/AC3
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
v := r.verdict(t, keyOf(t, "feat/x", cand))
if v.State != delivery.VerdictFail || v.Failed.Machine != "shanks" || !strings.Contains(v.Failed.Words, "stuck at: test the unit level of mesh-delivery") {
if v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Machine != "shanks" || !strings.Contains(v.Failed.Words, "stuck at: test the unit level of mesh-delivery") {
t.Fatalf("verdict = %+v, want a failure naming shanks and the stuck step", v)
}
}
@@ -242,7 +248,7 @@ func TestAnUnheardForgeFailsWithTheReasonAndIsJudgedAgainWhenTheNodesChange(t *t
}
r.ch.NodesChanged(ctx)
r.ch.Wait()
if j.count() != 2 || r.verdict(t, key).State != delivery.VerdictPass {
if j.count() != 2 || r.verdict(t, key).State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("after the nodes changed: judged %d times, verdict %+v", j.count(), r.verdict(t, key))
}
}
@@ -275,7 +281,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
t.Errorf("said %d times in the log, want once for the head: %q", n, logged)
}
reader := func() Reader {
ckey, _ := delivery.CandidatesKey("feat/y")
ckey, _ := meshcatalogclient.CandidatesKey("feat/y")
return Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState(r.verdicts.vals).read(),
Trunks: func() (map[string]string, error) { return r.trunks.Heads(), nil }}
}
@@ -294,7 +300,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
r.setTrunks(t, map[string]string{repoA: trunkA, repoSDK: trunkS, unknown: trunkA2})
r.ch.TrunkMoved(ctx)
r.ch.Wait()
if v := r.verdict(t, key); j.count() != 1 || v.State != delivery.VerdictPass || v.Trunk[unknown] != trunkA2 {
if v := r.verdict(t, key); j.count() != 1 || v.State != meshdeliverycheckerclient.VerdictPass || v.Trunk[unknown] != trunkA2 {
t.Errorf("judged %d times, verdict %+v, want one pass on %s", j.count(), v, trunkA2)
}
if s := owedState(t, r, unknown, 4, head1); s != "success" {
@@ -306,15 +312,15 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
// running verdict left for heads that cannot be judged is replaced; a verdict key deleted is judged again on its delete.
for _, row := range []struct {
name string
pulls []delivery.CandidatePull
pulls []meshcatalog.CandidatesByGroupValuePullsItem
running bool // a running verdict is left under the key first
deleted bool // the key is deleted after its verdict
judged int
want string // the verdict's state, and for a failure the repository it names
}{
{name: "a member the checker cannot claim", pulls: []delivery.CandidatePull{pullOf(repoA, 9, head2), pullOf("novox/other", 2, head2)}, want: "novox/other"},
{name: "a running verdict left for heads that cannot be judged", pulls: []delivery.CandidatePull{newRepoPullOf(unknown, 5, head2)}, running: true, want: unknown},
{name: "a verdict key deleted", pulls: []delivery.CandidatePull{pullOf(repoA, 5, head2)}, deleted: true, judged: 2, want: delivery.VerdictPass},
{name: "a member the checker cannot claim", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 9, head2), pullOf("novox/other", 2, head2)}, want: "novox/other"},
{name: "a running verdict left for heads that cannot be judged", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{newRepoPullOf(unknown, 5, head2)}, running: true, want: unknown},
{name: "a verdict key deleted", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 5, head2)}, deleted: true, judged: 2, want: meshdeliverycheckerclient.VerdictPass},
} {
t.Run(row.name, func(t *testing.T) {
j := passJudge()
@@ -322,8 +328,8 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
cand := candidateOf(t, "feat/z", row.pulls...)
key := keyOf(t, "feat/z", cand)
if row.running {
group, heads, _ := delivery.ParseCheckerVerdictKey(key)
raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, State: delivery.VerdictRunning})
group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(key)
raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.VerdictRunning})
_, _ = r.verdicts.Create(ctx, key, raw)
}
r.ch.Candidate(ctx, "feat%2Fz", cand)
@@ -334,8 +340,8 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
r.ch.Wait()
}
v := r.verdict(t, key)
if row.want == delivery.VerdictPass {
if v.State != delivery.VerdictPass || j.count() != row.judged {
if row.want == meshdeliverycheckerclient.VerdictPass {
if v.State != meshdeliverycheckerclient.VerdictPass || j.count() != row.judged {
t.Errorf("verdict %s after %d judgings, want a pass after %d", v.State, j.count(), row.judged)
}
return
@@ -344,7 +350,7 @@ func TestARepositoryWithNoTrunkCannotBeJudgedLoudlyUntilItsTrunkIsKnown(t *testi
t.Errorf("verdict %+v after %d judgings, want a failure naming %s and none judged", v, j.count(), row.want)
}
for _, p := range row.pulls {
if s := owedState(t, r, p.Repository, p.Number, p.Head.Head); s != "error" {
if s := owedState(t, r, p.Repository, int(p.Number), p.Head); s != "error" {
t.Errorf("%s#%d owes %q, want error", p.Repository, p.Number, s)
}
}
@@ -391,7 +397,7 @@ func TestATrunkMoveRewritesTheSameVerdictKeyAndTheHeadsStatus(t *testing.T) { //
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
_, rev1, _, _ := r.verdicts.Get(ctx, key)
owedKey, _ := delivery.CheckerOwedKey(repoA, 7, head1)
owedKey, _ := CheckerOwedKey(repoA, 7, head1)
_, orev1, _, _ := r.owed.Get(ctx, owedKey)
if !r.setTrunks(t, map[string]string{repoA: trunkA2, repoSDK: trunkS}) {
t.Fatal("the trunks did not change")
@@ -427,12 +433,12 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
var seen []string
var r *rig
j := &judgeFunc{f: func(ctx context.Context, c *Case, _ int) (Outcome, error) {
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
key, _ := CheckerOwedKey(repoA, 7, head1)
raw, _, _, _ := r.owed.Get(ctx, key)
o, _ := delivery.DecodeCheckerOwed(raw)
o, _ := DecodeCheckerOwed(raw)
seen = append(seen, o.State)
c.series(repoA+"#7", []string{"web/pkg series 0.1 → 0.2"}, []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}})
return Outcome{Verdict: delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "m/a.TestX"}},
c.series(repoA+"#7", []string{"web/pkg series 0.1 → 0.2"}, []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}})
return Outcome{Verdict: meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "m/a.TestX"}},
OutOfStep: map[string][]string{repoA + "#7": {"go/seats/x/contract.go"}}}, nil
}}
r = newRig(t, j)
@@ -440,19 +446,19 @@ func TestTheStatusIsPendingWhileTheCheckRunsAndEndsSuccessOrFailure(t *testing.T
r.ch.Says = says
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
r.ch.Wait()
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
key, _ := CheckerOwedKey(repoA, 7, head1)
raw, _, _, _ := r.owed.Get(ctx, key)
o, err := delivery.DecodeCheckerOwed(raw)
o, err := DecodeCheckerOwed(raw)
if err != nil {
t.Fatal(err)
}
if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != delivery.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") {
if len(seen) != 1 || seen[0] != "pending" || o.State != "failure" || o.Context != meshdeliveryclient.StatusContextCheck || !strings.HasPrefix(o.Target, "https://view.invalid/checks/") {
t.Errorf("while running %v, at the end %+v", seen, o)
}
// DLV-C2 (design 26 *Out of step*): the check's end says one verdict event for the pull request, naming its out-of-step
// files; the running status says none.
if len(says.said) != 1 || says.said[0].Repository != repoA || says.said[0].Pull != 7 || says.said[0].Head != head1 ||
says.said[0].Verdict != delivery.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" ||
says.said[0].Verdict != meshdeliverycheckerclient.VerdictFail || strings.Join(says.said[0].OutOfStep, ",") != "go/seats/x/contract.go" ||
// The join (A2j finding 2): and its series changes, as the seat's event names them.
len(says.said[0].Series) != 1 || says.said[0].Series[0] != (meshdeliverychecker.VerdictHolderEventSeriesItem{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}) {
t.Errorf("verdict events %+v", says.said)
@@ -480,14 +486,14 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
r := newRig(t, j)
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
live := keyOf(t, "feat/x", cand)
dead, _ := delivery.CheckerVerdictKey("feat/gone", "0123456789ab")
dead, _ := meshdeliverycheckerclient.VerdictKey("feat/gone", "0123456789ab")
for _, k := range []string{live, dead} {
group, heads, _ := delivery.ParseCheckerVerdictKey(k)
raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: group, Heads: heads, State: delivery.VerdictRunning})
group, heads, _ := meshdeliverycheckerclient.ParseVerdictKey(k)
raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.VerdictRunning})
_, _ = r.verdicts.Create(ctx, k, raw)
}
r.ch.init()
var c delivery.Candidate
var c meshcatalog.CandidatesByGroupValue
_ = json.Unmarshal(cand, &c)
r.ch.known["feat%2Fx"] = c
if err := r.ch.Sweep(ctx); err != nil {
@@ -496,7 +502,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
if v := r.verdict(t, dead); !IsEnded(v) || !strings.Contains(v.Failed.Words, "closed at start") {
t.Errorf("the dead key = %+v", v)
}
if v := r.verdict(t, live); v.State != delivery.VerdictRunning {
if v := r.verdict(t, live); v.State != meshdeliverycheckerclient.VerdictRunning {
t.Errorf("the live key = %+v, want it left for the check to take over", v)
}
@@ -519,7 +525,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
r.ch.Candidate(ctx, "feat%2Fx", alone)
<-started
r.ch.Candidate(ctx, "feat%2Fx", both)
var bothC delivery.Candidate
var bothC meshcatalog.CandidatesByGroupValue
_ = json.Unmarshal(both, &bothC)
select {
case err := <-ended:
@@ -533,10 +539,10 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
if v := r.verdict(t, live); !IsEnded(v) || v.Failed.Words != EndedWords+"superseded by "+GateHeads(bothC) {
t.Errorf("the superseded verdict = %+v", v.Failed)
}
if v := r.verdict(t, keyOf(t, "feat/x", both)); v.State != delivery.VerdictPass {
if v := r.verdict(t, keyOf(t, "feat/x", both)); v.State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("the group's verdict = %s", v.State)
}
okey, _ := delivery.CheckerOwedKey(repoA, 7, head1)
okey, _ := CheckerOwedKey(repoA, 7, head1)
if raw, _, _, _ := r.owed.Get(ctx, okey); !strings.Contains(string(raw), `"success"`) {
t.Errorf("the head's status = %s, want the group's success, never the ended check's", raw)
}
@@ -554,7 +560,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
// The member leaves again: the ended heads are held again and checked whole.
r.ch.Candidate(ctx, "feat%2Fx", alone)
r.ch.Wait()
if v := r.verdict(t, live); v.State != delivery.VerdictPass || j.count() != 3 {
if v := r.verdict(t, live); v.State != meshdeliverycheckerclient.VerdictPass || j.count() != 3 {
t.Errorf("the heads held again = %s after %d judgings, want a pass of a third", v.State, j.count())
}
@@ -575,7 +581,7 @@ func TestHeadsNoCandidateHoldsAreNeverLeftRunning(t *testing.T) {
if _, _, found, _ := r.verdicts.Get(ctx, keyOf(t, "feat/y", sdk)); found || j.count() != 1 {
t.Errorf("the partial set has a verdict (%v) or the judge ran %d times, want the whole set alone", found, j.count())
}
if v := r.verdict(t, keyOf(t, "feat/y", whole)); v.State != delivery.VerdictPass {
if v := r.verdict(t, keyOf(t, "feat/y", whole)); v.State != meshdeliverycheckerclient.VerdictPass {
t.Errorf("the whole set = %s", v.State)
}
}
@@ -588,7 +594,7 @@ func TestTheForgeBridgeIsToldOnceForEachChangeOfAStatus(t *testing.T) { // 1683
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
key, _ := CheckerOwedKey(repoA, 7, head1)
want := []string{EventOwedWritten + " " + key, EventOwedWritten + " " + key} // pending, then success
if len(told) != 2 || told[0] != want[0] || told[1] != want[1] {
t.Fatalf("told %v, want the status said when pending and when success", told)
@@ -609,8 +615,8 @@ func TestACandidateOutsideTheMeshIsJudgedNothingAndOwesNothing(t *testing.T) {
var logged []string
r.ch.Logf = func(f string, a ...any) { logged = append(logged, fmt.Sprintf(f, a...)) }
const theirs = "someone/own-app"
okey, _ := delivery.CheckerOwedKey(theirs, 55, head1)
before, _ := delivery.EncodeCheckerOwed(delivery.Owed{Act: delivery.OwedStatus, Context: delivery.StatusContextCheck, State: "error", Text: "cannot be judged"})
okey, _ := CheckerOwedKey(theirs, 55, head1)
before, _ := EncodeCheckerOwed(meshdeliverychecker.OwedByRepositoryNumberHeadActValue{Act: meshdeliveryclient.OwedStatus, Context: meshdeliveryclient.StatusContextCheck, State: "error", Text: "cannot be judged"})
if _, err := r.owed.Create(ctx, okey, before); err != nil {
t.Fatal(err)
}
@@ -629,7 +635,7 @@ func TestACandidateOutsideTheMeshIsJudgedNothingAndOwesNothing(t *testing.T) {
if n := countWith(logged, "outside the mesh"); n != 1 {
t.Errorf("said %d times in the log, want once: %q", n, logged)
}
ckey, _ := delivery.CandidatesKey("feat/theirs")
ckey, _ := meshcatalogclient.CandidatesKey("feat/theirs")
st, err := Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState(r.verdicts.vals).read(),
Trunks: func() (map[string]string, error) { return r.trunks.Heads(), nil }}.Status()
if err != nil {
@@ -3,26 +3,40 @@ package check
import (
"strings"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// withComposition is v with the check's composition taken in: a failed one fails the check, naming its machine.
func withComposition(v *delivery.Verdict, comp delivery.CompositionVerdict) {
v.Composition = comp.Composition()
func withComposition(v *meshdeliverychecker.VerdictsByGroupHeadsValue, comp meshwants.CompositionsByGroupHeadsValue) {
v.Composition = compositionOf(comp)
if v.Took == nil {
v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{}
}
v.Took.Composition = comp.Took
if comp.State == delivery.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil {
if comp.State == meshdeliverycheckerclient.CompositionPass && len(comp.Warnings) > 0 && v.Composition != nil {
v.Composition.Words = strings.Join(comp.Warnings, "; ") // passed with a warning (ADR 0593 §9)
}
if comp.State == delivery.CompositionFail {
if comp.State == meshdeliverycheckerclient.CompositionFail {
machine := comp.Machine
if machine == "" {
machine = "the composition"
}
f := delivery.FailedFirst{Machine: machine, Words: comp.Words}
if v.State == delivery.VerdictFail {
f := FailedFirst{Machine: machine, Words: comp.Words}
if v.State == meshdeliverycheckerclient.VerdictFail {
addFailure(v, f) // the tests failed too: the composition is named after them (the operator, 2026-11-02)
return
}
v.State, v.Failed = delivery.VerdictFail, f
v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &f
}
}
// compositionOf is what the checker writes into its verdict from the assigner's composition answer c: nil while c waits (the
// check is still running).
func compositionOf(c meshwants.CompositionsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValueComposition {
if c.State == compositionWaits {
return nil
}
return &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: c.State, Words: c.Words, Sdk: c.Sdk, Machines: c.Machines}
}
@@ -8,24 +8,28 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
buildsrec "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/delivery"
)
// fakeCompositions answers a check's composition from memory, recording the keys asked.
type fakeCompositions struct {
v delivery.CompositionVerdict
v meshwants.CompositionsByGroupHeadsValue
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) {
func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) {
f.asked = append(f.asked, c.Key)
if f.waits {
<-ctx.Done()
return delivery.CompositionVerdict{}, ctx.Err()
return meshwants.CompositionsByGroupHeadsValue{}, ctx.Err()
}
return f.v, f.err
}
@@ -35,12 +39,12 @@ func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (delivery.Compo
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}}
f := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.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 {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.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 {
@@ -49,32 +53,32 @@ func TestAPassingCheckTakesTheAssignersComposition(t *testing.T) {
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)
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 {
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 {
func compositionMentions(v meshdeliverychecker.VerdictsByGroupHeadsValue, 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",
b.ex.Compositions = &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.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 {
if v.State != meshdeliverycheckerclient.VerdictFail || v.Failed.Machine != "g14" || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.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 {
if _, err := meshdeliverycheckerclient.EncodeVerdict(v); err != nil {
t.Errorf("the verdict cannot be written: %v", err)
}
}
@@ -85,11 +89,11 @@ func TestACompositionNotHadIsNotRun(t *testing.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 {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun {
t.Fatalf("verdict = %+v", v)
}
b2 := newBench(t)
f2 := &fakeCompositions{v: delivery.CompositionVerdict{State: delivery.CompositionFail, Words: "w", SDK: "x"}}
f2 := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{State: meshdeliverycheckerclient.CompositionFail, Words: "w", Sdk: "x"}}
b2.ex.Compositions = f2
cs := caseOf(t, pullOf(repoA, 7, head1))
cs.Try = &TryOptions{}
@@ -113,12 +117,12 @@ type fakeEnds struct {
early bool
}
func (f *fakeEnds) Ended(_ context.Context, builds map[string]string) (map[string]delivery.BuilderBuild, error) {
f.asked = builds
func (f *fakeEnds) Ended(_ context.Context, asked map[string]string) (map[string]buildsrec.BuilderBuild, error) {
f.asked = asked
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 := map[string]buildsrec.BuilderBuild{}
for module, fp := range asked {
if b, ok := f.builds[module+" "+fp]; ok && (b.State == buildsrec.BuildDone || b.State == buildsrec.BuildFailed) {
out[module] = b
}
}
@@ -154,12 +158,12 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
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}
done := buildsrec.BuilderBuild{State: buildsrec.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
builds map[string]buildsrec.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
@@ -169,37 +173,37 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
{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"},
builds: map[string]buildsrec.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},
builds: map[string]buildsrec.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},
builds: map[string]buildsrec.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},
builds: map[string]buildsrec.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"},
builds: map[string]buildsrec.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},
builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.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,
builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.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}},
builds: map[string]buildsrec.BuilderBuild{"web": {State: buildsrec.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"},
builds: map[string]buildsrec.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"}},
@@ -212,7 +216,7 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
return r, nil
}
}
comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionPass, SDK: "x", Machines: []string{"g14"}}}
comps := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionPass, Sdk: "x", Machines: []string{"g14"}}}
b.ex.Compositions = comps
var ends *fakeEnds
if row.builds != nil {
@@ -228,10 +232,10 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
if row.served != nil {
b.ex.Serving = fixedServing(row.served)
}
read, fps := fixedManifests{}, map[string]delivery.CandidateModule{}
read, fps := fixedManifests{}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{}
for module, m := range row.manifest {
d := dig(string(rune('a' + len(read))))
read[d], fps[module] = m, delivery.CandidateModule{FP2: fp, Manifest: d}
read[d], fps[module] = m, meshcatalog.CandidatesByGroupValuePullsItemFpsValue{Fp2: fp, Manifest: d}
}
b.ex.Read = read
p := pullOf(catalogueRepository, 9, head1)
@@ -246,12 +250,12 @@ func aChangeToABuildModuleOrBaseNamesTheModulesBuiltOnIt(t *testing.T) {
}
}
switch {
case row.failed != "" && (v.State != delivery.VerdictFail || v.Failed.Test != row.failed || !strings.Contains(v.Failed.Words, "undefined: x") ||
case row.failed != "" && (v.State != meshdeliverycheckerclient.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:
case row.tests && v.State != meshdeliverycheckerclient.VerdictFail:
t.Errorf("%s: %+v, want the tests' failure", row.name, v)
case row.failed == "" && !row.tests && v.State != delivery.VerdictPass:
case row.failed == "" && !row.tests && v.State != meshdeliverycheckerclient.VerdictPass:
t.Errorf("%s: %+v", row.name, v)
}
words := compositionWords(v.Composition, v.Took.Composition)
@@ -5,7 +5,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// 1930/AC3: given a check ask whose stored end keeps the digest's packages as a list of {package, digest} (issue 1930),
@@ -19,17 +20,17 @@ func TestKeptVerdictsAreReadFromTheDigestsListForm(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
b := newBench(t)
var stored []string
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
s := b.answer(a)
if tc.old && s.State == delivery.StepPassed {
if tc.old && s.State == buildstep.StepPassed {
s.Result = oldDigestForm(t, s.Result)
}
stored = append(stored, string(s.Result))
return s
}
first := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
u := first.Levels[delivery.LevelUnit]
if first.State != delivery.VerdictPass || u.Run == 0 || u.Kept != 0 {
u := first.Levels[buildstep.LevelUnit]
if first.State != meshdeliverycheckerclient.VerdictPass || u.Run == 0 || u.Kept != 0 {
t.Fatalf("the first check = %+v", first)
}
if len(stored) == 0 {
@@ -41,7 +42,7 @@ func TestKeptVerdictsAreReadFromTheDigestsListForm(t *testing.T) {
}
b.steps.calls = nil
second := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
if s := second.Levels[delivery.LevelUnit]; second.State != delivery.VerdictPass || s.Kept != u.Run || s.Run != 0 {
if s := second.Levels[buildstep.LevelUnit]; second.State != meshdeliverycheckerclient.VerdictPass || s.Kept != u.Run || s.Run != 0 {
t.Fatalf("the second check's unit level = %+v, want %d kept and none run", s, u.Run)
}
if n := len(b.steps.tool("test")); n != 0 {
@@ -9,10 +9,15 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// **A check's composition is the checker's, made from what each node's engine answers** (novox/hq ADR 0593 §7, §9).
@@ -35,7 +40,7 @@ type EngineAsker interface {
// Sandbox runs a compose job on the Go build seat and answers the composer's verdict.
type Sandbox interface {
Compose(ctx context.Context, job delivery.ComposeJob) (composer.Verdict, error)
Compose(ctx context.Context, job buildstep.ComposeJob) (composer.Verdict, error)
}
// EngineCompositions is the checker's composition of a check (Examiner.Compositions).
@@ -54,7 +59,7 @@ type EngineCompositions struct {
// Manifests are the indexer's trunk manifest digests by module, and Verdicts its refusals of them: the catalogue the
// sandbox composes with, before the case's own manifests are laid over.
Manifests TrunkManifests
Verdicts func() map[string]delivery.ManifestVerdict
Verdicts func() map[string]meshcatalog.VerdictsByDigestValue
}
// composerHead is the node-engine member's head of a case, or empty.
@@ -66,16 +71,16 @@ func composerHead(c *Case) string {
}
// Compose asks every placed node and judges their answers (JudgeEngines). An error is only a case it cannot ask at all.
func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error) {
func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) {
began := time.Now()
key, err := delivery.CompositionKey(c.Group, c.HeadsHash)
key, err := nodeengineclient.CompositionKey(c.Group, c.Heads)
if err != nil {
return delivery.CompositionVerdict{}, err
return meshwants.CompositionsByGroupHeadsValue{}, err
}
var nodes []string
if e.Placed != nil {
if nodes, err = e.Placed(); err != nil {
return delivery.CompositionVerdict{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err)
return meshwants.CompositionsByGroupHeadsValue{}, fmt.Errorf("the placement cannot be read, so no node can be asked: %w", err)
}
}
bound := e.Bound
@@ -126,7 +131,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp
} else if e.Composer != nil && e.Composer() != "" {
composerName = e.Composer()
}
v := JudgeEngines(c.Group, c.HeadsHash, composerName, nodes, answers, silent)
v := JudgeEngines(c.Group, c.Heads, composerName, nodes, answers, silent)
if head != "" && e.Sandbox == nil {
v.Warnings = append(v.Warnings, "the group changes the node-engine's composition code and was composed by the running engines")
}
@@ -138,8 +143,8 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (delivery.Comp
// answer is refused, naming each node and each module refused with its words (the first refusing node is the machine);
// passed otherwise. Every node in nodes without an answer (silent, by node with why, for the journal only) is named
// with nec.NoAnswer as a warning. composer names what composed, for the line. Pure.
func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]ne.ComposeCandidateAnswer, silent map[string]string) delivery.CompositionVerdict {
v := delivery.CompositionVerdict{Group: group, Heads: heads, State: delivery.CompositionPass, SDK: composerName}
func JudgeEngines(group, heads, composerName string, nodes []string, answers map[string]ne.ComposeCandidateAnswer, silent map[string]string) meshwants.CompositionsByGroupHeadsValue {
v := meshwants.CompositionsByGroupHeadsValue{Group: group, Heads: heads, State: meshdeliverycheckerclient.CompositionPass, Sdk: composerName}
sorted := append([]string(nil), nodes...)
sort.Strings(sorted)
var fails []string
@@ -177,7 +182,7 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map
fails = append(fails, "not composed on "+node+": "+strings.Join(words, "; "))
}
if len(fails) > 0 {
v.State, v.Words = delivery.CompositionFail, strings.Join(fails, "; ")
v.State, v.Words = meshdeliverycheckerclient.CompositionFail, strings.Join(fails, "; ")
}
if len(sorted) == 0 {
v.Warnings = append(v.Warnings, "no node is placed, so the change was composed on none")
@@ -232,7 +237,7 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s
if err != nil {
return a, err
}
job := delivery.ComposeJob{Fetch: map[string]any{"repository": nodeEngineRepo, "commit": head}, Dir: delivery.RootPath,
job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: nodeEngineRepo, Commit: head}, Dir: meshcatalogclient.RootPath,
Package: composer.Package, Request: raw, Reader: "the node-engine head of " + c.Group}
verdict, err := e.Sandbox.Compose(ctx, job)
if err != nil {
@@ -9,10 +9,15 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// fakeEngines answers compose-candidate by node from memory; a node it holds no answer for never answers.
@@ -45,12 +50,12 @@ func (f *fakeEngines) ComposeCandidate(ctx context.Context, node string, ask ne.
// fakeSandbox answers a compose job with a verdict, recording the job.
type fakeSandbox struct {
mu sync.Mutex
jobs []delivery.ComposeJob
jobs []buildstep.ComposeJob
v func(node string) composer.Verdict
err error
}
func (f *fakeSandbox) Compose(_ context.Context, job delivery.ComposeJob) (composer.Verdict, error) {
func (f *fakeSandbox) Compose(_ context.Context, job buildstep.ComposeJob) (composer.Verdict, error) {
var req composer.Request
if err := json.Unmarshal(job.Request, &req); err != nil {
return composer.Verdict{}, err
@@ -80,8 +85,8 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
Own: ne.Module{Module: nec.Own, State: nec.ModuleComposed, Digest: "sha256:" + strings.Repeat("b", 64)}}
}
cs := caseOf(t, pullOf(repoA, 7, head1))
key, _ := delivery.CompositionKey(cs.Group, cs.HeadsHash)
compose := func(e *fakeEngines, nodes ...string) delivery.CompositionVerdict {
key, _ := nodeengineclient.CompositionKey(cs.Group, cs.HeadsHash)
compose := func(e *fakeEngines, nodes ...string) meshwants.CompositionsByGroupHeadsValue {
t.Helper()
ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return nodes, nil }, Bound: 50 * time.Millisecond,
Composer: func() string { return strings.Repeat("7", 40) }}
@@ -99,16 +104,16 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
{Module: "mail", State: nec.ModuleHeld, Words: "held as today"}}
e := &fakeEngines{answers: map[string]ne.ComposeCandidateAnswer{"anchor": composed("anchor"), "laptop": refusing}}
v := compose(e, "anchor", "laptop")
if v.State != delivery.CompositionFail || v.Machine != "laptop" || !strings.Contains(v.Words, "not composed on laptop: web: a required value is unset") ||
if v.State != meshdeliverycheckerclient.CompositionFail || v.Machine != "laptop" || !strings.Contains(v.Words, "not composed on laptop: web: a required value is unset") ||
strings.Contains(v.Words, "mail") || len(v.Machines) != 2 {
t.Errorf("verdict = %+v", v)
}
if e.asked["anchor"].Candidate != key || e.asked["laptop"].Input {
t.Errorf("asked %+v, want the check's candidate %s and no input", e.asked, key)
}
var ver delivery.Verdict
var ver meshdeliverychecker.VerdictsByGroupHeadsValue
withComposition(&ver, v)
if ver.State != delivery.VerdictFail || ver.Failed.Machine != "laptop" {
if ver.State != meshdeliverycheckerclient.VerdictFail || ver.Failed.Machine != "laptop" {
t.Errorf("the check's verdict = %+v", ver)
}
})
@@ -122,7 +127,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
if took := time.Since(began); took > 2*time.Second {
t.Errorf("the nodes were not asked in parallel within the bound: %s", took)
}
if v.State != delivery.CompositionPass || v.SDK != strings.Repeat("7", 40) || len(v.Machines) != 1 {
if v.State != meshdeliverycheckerclient.CompositionPass || v.Sdk != strings.Repeat("7", 40) || len(v.Machines) != 1 {
t.Fatalf("verdict = %+v", v)
}
for _, node := range []string{"ace", "g14", "laptop"} {
@@ -130,16 +135,16 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
t.Errorf("%s is not named: %v", node, v.Warnings)
}
}
var ver delivery.Verdict
ver.State = delivery.VerdictPass
var ver meshdeliverychecker.VerdictsByGroupHeadsValue
ver.State = meshdeliverycheckerclient.VerdictPass
withComposition(&ver, v)
if ver.State != delivery.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+nec.NoAnswer) {
if ver.State != meshdeliverycheckerclient.VerdictPass || !strings.Contains(composition(ver.Composition, 0, false), "laptop: "+nec.NoAnswer) {
t.Errorf("the line = %q", composition(ver.Composition, 0, false))
}
})
t.Run("a node-engine member is composed by the composer at its head over each node's input", func(t *testing.T) {
c, err := delivery.DecodeCandidate(candidateOf(t, "feat/x", pullOf(nodeEngineRepo, 3, head2), pullOf(repoA, 7, head1)))
c, err := meshcatalog.DecodeCandidatesByGroupValue(candidateOf(t, "feat/x", pullOf(nodeEngineRepo, 3, head2), pullOf(repoA, 7, head1)))
if err != nil {
t.Fatal(err)
}
@@ -163,11 +168,11 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
job := sb.jobs[0]
var req composer.Request
_ = json.Unmarshal(job.Request, &req)
if job.Fetch["repository"] != nodeEngineRepo || job.Fetch["commit"] != head2 || job.Package != composer.Package ||
if job.Fetch.Repository != nodeEngineRepo || job.Fetch.Commit != head2 || job.Package != composer.Package ||
string(req.Input) != `{"nodes":[{"name":"anchor"}]}` || req.Cases[0].Manifests["web"].Digest == "" {
t.Errorf("job = %+v, request %+v", job, req)
}
if v.State != delivery.CompositionFail || v.SDK != head2 || !strings.Contains(v.Words, "the new composer refuses it") {
if v.State != meshdeliverycheckerclient.CompositionFail || v.Sdk != head2 || !strings.Contains(v.Words, "the new composer refuses it") {
t.Errorf("verdict = %+v", v)
}
// ADR 0593 §9 (ADR 0601 §4's kind): a node the composer did not judge is named as no answer with its words, by the
@@ -185,7 +190,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
}
v, err = ec.Compose(context.Background(), host)
named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+nec.NoAnswer+" (its node-engine sends no running state for web)")
if err != nil || (v.State == delivery.CompositionPass) != row.pass || named != row.pass {
if err != nil || (v.State == meshdeliverycheckerclient.CompositionPass) != row.pass || named != row.pass {
t.Errorf("%+v: verdict %+v, %v", row.m, v, err)
}
}
@@ -193,7 +198,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
// run fails the composition, naming the head, never passing as a silent node.
sb.err = errors.New("the build of mesh-compose failed")
v, err = ec.Compose(context.Background(), host)
if err != nil || v.State != delivery.CompositionFail || !strings.Contains(v.Words, head2) ||
if err != nil || v.State != meshdeliverycheckerclient.CompositionFail || !strings.Contains(v.Words, head2) ||
!strings.Contains(v.Words, "the build of mesh-compose failed") || len(v.Warnings) != 0 {
t.Errorf("a composer that cannot run: %+v, %v", v, err)
}
@@ -208,7 +213,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) {
b := newBench(t)
b.ex.Compositions = ec
v := b.judge(t, cs).Verdict
if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun || !strings.Contains(v.Composition.Words, "no such bucket") {
if v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun || !strings.Contains(v.Composition.Words, "no such bucket") {
t.Errorf("the verdict's composition = %+v", v.Composition)
}
})
@@ -6,23 +6,26 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// failingResult is a level's test result in which the named tests failed, each in its own package.
func failingResult(t *testing.T, tests int, failed ...Failure) json.RawMessage {
t.Helper()
raw, err := json.Marshal(Tested{Level: delivery.LevelIntegration, Tests: tests, Elapsed: 2, Failed: failed, Passed: len(failed) == 0})
raw, err := json.Marshal(Tested{Level: buildstep.LevelIntegration, Tests: tests, Elapsed: 2, Failed: failed, Passed: len(failed) == 0})
if err != nil {
t.Fatal(err)
}
return raw
}
func judgeLevel(t *testing.T, tests ...json.RawMessage) (*delivery.Verdict, *state) {
func judgeLevel(t *testing.T, tests ...json.RawMessage) (*meshdeliverychecker.VerdictsByGroupHeadsValue, *state) {
t.Helper()
st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{}}
v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, delivery.LevelAnswer{Level: delivery.LevelIntegration, Tests: tests})
st := &state{j: &Examiner{}, c: &Case{}, levels: map[string]*acc{buildstep.LevelUnit: {}, buildstep.LevelIntegration: {}}, nodes: map[string]bool{}}
v, _ := st.level(t.Context(), &target{pull: Pull{Repository: "novox/x"}}, buildstep.LevelAnswer{Level: buildstep.LevelIntegration, Tests: tests})
return settled(joinFailures(v)), st
}
@@ -48,29 +51,29 @@ func TestTwoFailingTestsInTwoPackagesAreTwoEntries(t *testing.T) {
if len(v.Failures) != 2 || v.Failed.Test != "integration: x/a.TestA" || !strings.Contains(v.Failed.Words, "a_test.go:3: logged") {
t.Errorf("%s: verdict %+v", name, v)
}
if run := st.levels[delivery.LevelIntegration].run; run != 10 {
if run := st.levels[buildstep.LevelIntegration].run; run != 10 {
t.Errorf("%s: %d tests counted, want all 10", name, run)
}
if _, err := delivery.EncodeVerdict(delivery.Verdict{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil {
if _, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "g", Heads: "0123456789ab", State: v.State, Failed: v.Failed, Failures: v.Failures}); err != nil {
t.Errorf("%s: the verdict is not written: %v", name, err)
}
}
// Design 26 *Out of step* (DLV-C2): the out-of-step files the compare step names, in each result, are the outcome's for
// the pull request, each once and sorted; a level naming none leaves the outcome with none.
step := func(files ...string) json.RawMessage {
raw, _ := json.Marshal(Tested{Level: delivery.LevelUnit, Tests: 1, Passed: false, Why: "out of step", OutOfStep: files})
raw, _ := json.Marshal(Tested{Level: buildstep.LevelUnit, Tests: 1, Passed: false, Why: "out of step", OutOfStep: files})
return raw
}
_, st := judgeLevel(t, step("go/seats/b/contract.go", "go/seats/a/contract.go"), step("go/seats/a/contract.go"))
if got := st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep["novox/x#0"]; strings.Join(got, ",") != "go/seats/a/contract.go,go/seats/b/contract.go" {
if got := st.outcome(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}).OutOfStep["novox/x#0"]; strings.Join(got, ",") != "go/seats/a/contract.go,go/seats/b/contract.go" {
t.Errorf("out of step %v", got)
}
if _, st := judgeLevel(t, failingResult(t, 2, a)); st.outcome(delivery.Verdict{State: delivery.VerdictFail}).OutOfStep != nil {
if _, st := judgeLevel(t, failingResult(t, 2, a)); st.outcome(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}).OutOfStep != nil {
t.Errorf("a level naming no out-of-step file has %v", st.outOfStep)
}
}
// Issue 1921: past delivery.MaxFailures the list names the first fifty and how many more, the status line says how many
// Issue 1921: past meshdeliverycheckerclient.MaxFailures the list names the first fifty and how many more, the status line says how many
// failed and names the first, cut so "and N more" stays whole, and each entry's line is cut to MaxFailureLine.
func TestFailuresAreCappedWithHowManyMore(t *testing.T) {
var fs []Failure
@@ -78,7 +81,7 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) {
fs = append(fs, Failure{Package: fmt.Sprintf("x/p%d", i), Test: "TestT", Cause: strings.Repeat("y", 500)})
}
v, _ := judgeLevel(t, failingResult(t, 60, fs...))
if len(v.Failures) != delivery.MaxFailures+1 || v.Failures[delivery.MaxFailures].Test != "and 10 more" {
if len(v.Failures) != meshdeliverycheckerclient.MaxFailures+1 || v.Failures[meshdeliverycheckerclient.MaxFailures].Test != "and 10 more" {
t.Fatalf("%d entries, last %+v", len(v.Failures), v.Failures[len(v.Failures)-1])
}
if len(v.Failures[0].Words) > MaxFailureLine {
@@ -93,14 +96,14 @@ func TestFailuresAreCappedWithHowManyMore(t *testing.T) {
t.Errorf("line %q", line)
}
// A composition failing after them is counted in the last entry (the operator, 2026-11-02).
withComposition(v, delivery.CompositionVerdict{State: delivery.CompositionFail, Machine: "g14", Words: "route-proxy is defined twice"})
withComposition(v, meshwants.CompositionsByGroupHeadsValue{State: meshdeliverycheckerclient.CompositionFail, Machine: "g14", Words: "route-proxy is defined twice"})
if n := failedCount(*v); n != 61 || v.Failed.Test != "integration: x/p0.TestT" {
t.Errorf("after the composition failedCount = %d, failed %+v", n, v.Failed)
}
// A verdict that names no list (a conflict, a refused step, a verdict written before issue 1921) is one failed line, as
// before.
one := delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "g14", Words: "files in conflict: go.mod\nmore"}}
one := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "g14", Words: "files in conflict: go.mod\nmore"}}
if got := failedLines(one); len(got) != 1 || got[0] != "g14: files in conflict: go.mod" {
t.Fatalf("%q", got)
}
@@ -20,10 +20,17 @@ import (
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"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/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// captured reads a result the build module's own code marshalled (testdata/captured/README).
@@ -80,10 +87,10 @@ func (f *fakeSteps) tool(name string) []stepCall {
return out
}
type entries []delivery.NodeToolchain
type entries []buildstep.NodeToolchain
func (e entries) Entries(language string) []delivery.NodeToolchain {
var out []delivery.NodeToolchain
func (e entries) Entries(language string) []buildstep.NodeToolchain {
var out []buildstep.NodeToolchain
for _, x := range e {
if x.Language == language {
out = append(out, x)
@@ -104,8 +111,8 @@ func (l langs) BuildModules(repository string) []string {
func goEntries(nodes ...string) entries {
var e entries
for _, n := range nodes {
e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}})
e = append(e, delivery.NodeToolchain{Node: n, Toolchain: delivery.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}})
e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "go", Version: "go1.27.1", Recipe: "7"}})
e = append(e, buildstep.NodeToolchain{Node: n, Toolchain: buildstep.Toolchain{Language: "typescript", Version: "node24", Recipe: "3"}})
}
return e
}
@@ -140,8 +147,8 @@ var toolPrefix = map[string]string{buildrun.GoBuildModule: "build_go_", buildrun
// answer runs a check ask as the build module that pulls it does (gobuild.RunAsk), with the fake tools on shanks and the
// bench's kept verdicts, and answers the step it writes.
func (b *bench) answer(a delivery.StepAsk) delivery.Step {
module, _ := delivery.BuildModuleOf(a.Language)
func (b *bench) answer(a buildstep.StepAsk) buildstep.Step {
module, _ := buildstep.BuildModuleOf(a.Language)
const node = "shanks"
tool := func(name string) gobuild.StepFunc {
return func(ctx context.Context, args map[string]any) (any, error) {
@@ -155,7 +162,7 @@ func (b *bench) answer(a delivery.StepAsk) delivery.Step {
steps := gobuild.AskSteps{Fetch: tool("fetch"), Test: tool("test"),
Kept: func(ctx context.Context, level, digest string) (int, bool, error) {
k, found, err := (Kept{Store: b.kept}).Lookup(ctx, level, digest)
return k.Tests, found, err
return int(k.Tests), found, err
}}
if module == buildrun.GoBuildModule {
steps.Digest = tool("digest")
@@ -164,16 +171,16 @@ func (b *bench) answer(a delivery.StepAsk) delivery.Step {
if len(a.Needs) > 0 {
if b.declines > 0 {
b.declines--
result, _ := json.Marshal(delivery.StepDeclined{Declined: true, Needs: a.Needs, Lacks: a.Needs})
return delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "ace", State: delivery.StepRefused, Started: began, Ended: began,
result, _ := json.Marshal(buildstep.StepDeclined{Declined: true, Needs: a.Needs, Lacks: a.Needs})
return buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: "ace", State: buildstep.StepRefused, Started: began, Ended: began,
Reason: "declined", Result: result}
}
steps.Has = func(string) bool { return true }
}
out, err := gobuild.RunAsk(context.Background(), steps, a)
step := delivery.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: delivery.StepPassed, Started: began, Ended: began.Add(2 * time.Second)}
step := buildstep.Step{ID: a.ID, Tool: a.Kind + " ask", Node: node, State: buildstep.StepPassed, Started: began, Ended: began.Add(2 * time.Second)}
if err != nil {
step.State, step.Reason = delivery.StepFailed, err.Error()
step.State, step.Reason = buildstep.StepFailed, err.Error()
return step
}
step.Result, _ = json.Marshal(out)
@@ -188,8 +195,8 @@ func newBench(t testing.TB) *bench {
b.fetch.Modules, b.fetch.Changed, b.fetch.Trees = []string{"."}, []string{"README.md"}, nil
one := captured[Digested](t, "go-digest")
b.digest = map[string]Digested{
delivery.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}},
delivery.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}},
buildstep.LevelUnit: {Level: "unit", Packages: map[string]string{"fixture/slow": digA}},
buildstep.LevelIntegration: {Level: "integration", Packages: map[string]string{"fixture/slow": digB}},
}
_ = one
slow := captured[Tested](t, "go-test-slow")
@@ -212,10 +219,10 @@ func newBench(t testing.TB) *bench {
return b
}
func caseOf(t testing.TB, pulls ...delivery.CandidatePull) *Case {
func caseOf(t testing.TB, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) *Case {
t.Helper()
raw := candidateOf(t, "feat/x", pulls...)
c, err := delivery.DecodeCandidate(raw)
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
if err != nil {
t.Fatal(err)
}
@@ -257,10 +264,10 @@ func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679
cs := caseOf(t, pullOf(repoA, 7, head1))
out := b.judge(t, cs)
v := out.Verdict
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Run != 2 || v.Levels[delivery.LevelIntegration].Run != 2 {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Run != 2 || v.Levels[buildstep.LevelIntegration].Run != 2 {
t.Fatalf("verdict = %+v", v)
}
if v.Composition == nil || v.Composition.State != delivery.CompositionNotRun {
if v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun {
t.Errorf("composition = %+v, want not run", v.Composition)
}
if len(v.Machines) != 1 || v.Machines[0] != "shanks" {
@@ -292,7 +299,7 @@ func TestACleanMergeIsFetchedAtItsTrunkAndBothLevelsPass(t *testing.T) { // 1679
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
asks := b.asks.Asks()
if c.fails != "" {
if out.Verdict.State != delivery.VerdictFail || len(asks) != 0 || !strings.Contains(out.Verdict.Failed.Words, c.fails) {
if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || len(asks) != 0 || !strings.Contains(out.Verdict.Failed.Words, c.fails) {
t.Errorf("names %v, %v: verdict %+v, %d asks", c.names, c.err, out.Verdict, len(asks))
}
} else if len(asks) == 0 || strings.Join(asks[0].Check.NodeNames, ",") != "anchor,laptop" {
@@ -311,7 +318,7 @@ func (n namesOf) Names(context.Context) ([]string, error) { return n.names, n.er
func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
b := newBench(t)
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}}
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/slow": digA, "fixture/other": digC}}
if _, err := (Kept{Store: b.kept}).Keep(context.Background(), "unit", digC, keptValue("fixture/other", 40)); err != nil {
t.Fatal(err)
}
@@ -320,7 +327,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
if len(tests) != 2 || strings.Join(packagesOf(tests[0]), ",") != "fixture/slow" || tests[0].args["level"] != "unit" {
t.Fatalf("test calls = %+v, want the unit level of fixture/slow alone, then the integration level", tests)
}
if u := out.Verdict.Levels[delivery.LevelUnit]; u.Run != 2 || u.Kept != 40 {
if u := out.Verdict.Levels[buildstep.LevelUnit]; u.Run != 2 || u.Kept != 40 {
t.Errorf("unit = %+v, want 2 run and 40 kept", u)
}
@@ -340,11 +347,11 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
}
return d, nil
}
b.digest[delivery.LevelIntegration] = Digested{Level: "integration",
Needs: []delivery.PackageNeeds{{Package: "fixture/node", Needs: []string{delivery.NeedNodeBuildNode}}}}
b.digest[buildstep.LevelIntegration] = Digested{Level: "integration",
Needs: []buildstep.PackageNeeds{{Package: "fixture/node", Needs: []string{buildstep.NeedNodeBuildNode}}}}
out = b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
asks := b.asks.Asks()
if out.Verdict.State != delivery.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 {
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(asks) != 3 || len(asks[0].Needs) != 0 {
t.Fatalf("verdict %+v, asks %+v: want a pass of three asks, the first naming no need", out.Verdict, asks)
}
for _, a := range asks { // ADR 0584: every check ask names the agreed toolchain, so a node off it declines
@@ -353,7 +360,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
}
}
for _, a := range asks[1:] {
if strings.Join(a.Needs, ",") != delivery.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != delivery.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" {
if strings.Join(a.Needs, ",") != buildstep.NeedNodeBuildNode || strings.Join(a.Check.Levels, ",") != buildstep.LevelIntegration || strings.Join(a.Check.Packages, ",") != "fixture/node" {
t.Errorf("a placed ask %+v, want the integration level needing node-build-node", a)
}
}
@@ -362,13 +369,13 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
t.Errorf("test calls = %+v, want the unit level, the integration level of fixture/slow and fixture/outside, then fixture/node alone", tests)
}
slow := captured[Tested](t, "go-test-slow")
if i := out.Verdict.Levels[delivery.LevelIntegration]; i.Run != 2*slow.Tests || i.Kept != 0 {
if i := out.Verdict.Levels[buildstep.LevelIntegration]; int(i.Run) != 2*slow.Tests || i.Kept != 0 {
t.Errorf("integration = %+v, want %d run and nothing counted kept", i, 2*slow.Tests)
}
// And where no node has the need, the check fails once the declines outlast the bound, naming the need.
b.kept, b.declines = newMemKV(), 1000
b.ex.Kept, b.ex.DeclineBound = Kept{Store: b.kept}, 200*time.Millisecond
if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict; v.State != delivery.VerdictFail || !strings.Contains(fmt.Sprint(v), "needs a node with node-build-node") {
if v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict; v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(failedOf(v).Words, "needs a node with node-build-node") {
t.Errorf("with no node having the need: %+v", v)
}
}
@@ -376,7 +383,7 @@ func TestOnlyThePackageWithoutAKeptVerdictRuns(t *testing.T) { // 1288/AC1
func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
b := newBench(t)
failing := captured[Tested](t, "go-test-failing")
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}}
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digB}}
b.test = func(c stepCall) (any, error) {
if c.args["level"] == "unit" {
return failing, nil
@@ -385,7 +392,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
}
cs := caseOf(t, pullOf(repoA, 7, head1))
first := b.judge(t, cs).Verdict
if first.State != delivery.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" {
if first.State != meshdeliverycheckerclient.VerdictFail || first.Failed.Test != "unit: fixture/failing/bad.TestThreeIsFour" {
t.Fatalf("first = %+v", first)
}
if _, _, found, _ := b.kept.Get(context.Background(), "unit."+digB); !found {
@@ -402,7 +409,7 @@ func TestAReAskRunsOnlyWhatFailedAndWhatChanged(t *testing.T) { // 1288/AC2, AC3
t.Fatalf("the re-ask ran %+v, want only fixture/failing/bad", tests)
}
// The trunk moved and one digest changed: that package runs, the unchanged one is taken.
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"fixture/failing/bad": digA, "fixture/failing/good": digD}}
b.steps.calls = nil
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
tests = b.steps.tool("test")
@@ -421,7 +428,7 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1
cs := caseOf(t, pullOf(repoA, 7, head1))
for i := 0; i < 2; i++ {
v := b.judge(t, cs).Verdict
if v.State != delivery.VerdictPass || v.Levels[delivery.LevelUnit].Kept != 0 || v.Levels[delivery.LevelUnit].Run != 1 {
if v.State != meshdeliverycheckerclient.VerdictPass || v.Levels[buildstep.LevelUnit].Kept != 0 || v.Levels[buildstep.LevelUnit].Run != 1 {
t.Fatalf("round %d: verdict = %+v", i, v)
}
}
@@ -445,11 +452,11 @@ func TestANodePackageIsNeverKeptAndRunsEveryCheck(t *testing.T) { // 1288/AC4, 1
func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
for _, conflicts := range []bool{false, true} {
b := newBench(t)
comps := &fakeCompositions{v: delivery.CompositionVerdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.CompositionFail, SDK: "x",
comps := &fakeCompositions{v: meshwants.CompositionsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.CompositionFail, Sdk: "x",
Machine: "g14", Words: "route-proxy is defined twice"}, waits: conflicts}
b.ex.Compositions = comps
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go"}
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
conflict := captured[Fetched](t, "go-fetch-conflict")
b.steps.handlers["build_go_fetch"] = func(c stepCall) (any, error) {
if conflicts && c.args["repository"] == repoSDK {
@@ -467,9 +474,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
return captured[Tested](t, "go-test-slow"), nil
}
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
done := make(chan delivery.Verdict, 1)
done := make(chan meshdeliverychecker.VerdictsByGroupHeadsValue, 1)
go func() { done <- b.judge(t, cs).Verdict }()
var v delivery.Verdict
var v meshdeliverychecker.VerdictsByGroupHeadsValue
select {
case v = <-done:
case <-time.After(5 * time.Second):
@@ -477,14 +484,14 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
}
var got []string
for _, f := range v.Failures {
got = append(got, firstFailed(delivery.Verdict{Failed: f}))
got = append(got, firstFailed(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Test: f.Test, Machine: f.Machine, Words: f.Words}}))
}
tests := []string{"unit: fixture/failing/bad.TestThreeIsFour", "integration: fixture/failing/bad.TestThreeIsFour"}
want := append(append(slices.Clone(tests), tests...), "g14: route-proxy is defined twice") // both members fail their tests
if conflicts {
want = append([]string{"shanks: novox/mesh-sdk#12 does not merge into the trunk: files in conflict: a.go"}, tests...)
}
if v.State != delivery.VerdictFail || len(got) != len(want) {
if v.State != meshdeliverycheckerclient.VerdictFail || len(got) != len(want) {
t.Fatalf("conflicts %v: verdict %+v: failures\n%s\nwant\n%s", conflicts, v, strings.Join(got, "\n"), strings.Join(want, "\n"))
}
for i := range want {
@@ -494,9 +501,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
}
keys, _ := b.prev.Keys(context.Background())
switch {
case !conflicts && (len(keys) != 2 || v.Composition == nil || v.Composition.State != delivery.CompositionFail):
case !conflicts && (len(keys) != 2 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionFail):
t.Errorf("tests alone failed: previews %v, composition %+v, want both previews and the composition read", keys, v.Composition)
case conflicts && (len(comps.asked) != 0 || v.Composition == nil || v.Composition.State != delivery.CompositionNotRun ||
case conflicts && (len(comps.asked) != 0 || v.Composition == nil || v.Composition.State != meshdeliverycheckerclient.CompositionNotRun ||
!strings.Contains(v.Composition.Words, "novox/mesh-sdk#12")):
t.Errorf("a member did not merge: asked %v, composition %+v, want not run naming novox/mesh-sdk#12", comps.asked, v.Composition)
}
@@ -510,7 +517,7 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
t.Errorf("the unit level ran %v and %d digests, want the changed test's package, then the rest, at both levels", unit, len(b.steps.tool("digest")))
}
v.Group, v.Heads = "feat/x", "0123456789ab"
if _, err := delivery.EncodeVerdict(v); err != nil {
if _, err := meshdeliverycheckerclient.EncodeVerdict(v); err != nil {
t.Errorf("conflicts %v: the verdict cannot be written: %v", conflicts, err)
}
if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); !strings.HasPrefix(line, fmt.Sprintf("failed (%d): %s", len(want), want[0][:20])) ||
@@ -523,9 +530,9 @@ func TestEveryFailureOfEveryMemberLevelAndTheCompositionIsNamed(t *testing.T) {
func TestTheChangesOwnPassingTestRunsFirstAndTheRestFollow(t *testing.T) { // 1289/AC3
b := newBench(t)
b.fetch.Changed = []string{"modules/m/internal/b/b_test.go", "modules/m/internal/a/a.go"}
b.digest[delivery.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
b.digest[buildstep.LevelUnit] = Digested{Level: "unit", Packages: map[string]string{"m/internal/a": digA, "m/internal/b": digB}}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictPass {
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
var unit []string
@@ -544,7 +551,7 @@ func TestAHeadThatDoesNotMergeEndsFailedNamingTheFilesAndRunsNoTest(t *testing.T
b.fetch = captured[Fetched](t, "go-fetch-conflict")
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
f := out.Verdict.Failed
if out.Verdict.State != delivery.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") {
if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || f.Machine != "shanks" || !strings.Contains(f.Words, "files in conflict: a.go, sub/b.go") {
t.Fatalf("verdict = %+v", out.Verdict)
}
if n := len(b.steps.tool("digest")) + len(b.steps.tool("test")); n != 0 {
@@ -556,7 +563,7 @@ func TestALevelWhoseDigestIsNotNamedRunsWholeAndKeepsNothing(t *testing.T) { //
b := newBench(t)
b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, fmt.Errorf("go list failed") }
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if out.Verdict.State != delivery.VerdictPass {
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
tests := b.steps.tool("test")
@@ -580,7 +587,7 @@ func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndKeepsItsVerdict(t *testi
}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
v := out.Verdict
if v.State != delivery.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" {
if v.State != meshdeliverycheckerclient.VerdictPass || strings.Join(v.OverBound, ",") != "unit,integration" {
t.Fatalf("verdict = %+v", v)
}
if len(out.Slowest) == 0 || !strings.Contains(out.Slowest[0], "TestTakesAWhile") {
@@ -594,7 +601,7 @@ func TestSkippedTestsAreCountedPerLevel(t *testing.T) { // 1300/AC6, 1251
return testedWith(t, "go-test-slow", func(r *Tested) { r.Level = c.args["level"].(string); r.Skipped = 3 }), nil
}
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
if v.Levels[delivery.LevelUnit].Skipped != 3 || v.Levels[delivery.LevelIntegration].Skipped != 3 {
if v.Levels[buildstep.LevelUnit].Skipped != 3 || v.Levels[buildstep.LevelIntegration].Skipped != 3 {
t.Errorf("levels = %+v", v.Levels)
}
}
@@ -603,7 +610,7 @@ func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testi
b := newBench(t)
cs := caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))
v := b.judge(t, cs).Verdict
if v.State != delivery.VerdictPass {
if v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
fetch := b.steps.tool("fetch")
@@ -618,7 +625,7 @@ func TestAGroupIsOneVerdictAndEachMembersTestsReadTheOthersAtTheirHeads(t *testi
}
found := false
for _, r := range forA {
if r.Repository == repoSDK && r.Commit == head2 && r.Ref == "refs/pull/12/head" {
if r.Repository == repoSDK && r.Commit == head2 {
found = true
}
}
@@ -637,17 +644,17 @@ const (
func TestACleanMergeWritesItsPreviewWithTheTreesManifestsAndLocks(t *testing.T) { // 1445/AC1
b := newBench(t)
b.fetch.Trees = map[string]MergeTree{
b.fetch.Trees = map[string]buildstep.ModuleTree{
"modules/avahi": {Tree: "7777777777777777777777777777777777777777", Manifest: manA, Locks: map[string]string{"go.sum": "ff00"}},
"modules/bare": {Tree: "8888888888888888888888888888888888888888"}, // no manifest digest: left out
}
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
key, _ := delivery.PreviewKey(repoA, head1, trunkA)
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
raw, _, found, _ := b.prev.Get(context.Background(), key)
if !found {
t.Fatalf("no preview %s; the bucket holds %v", key, b.prev.vals)
}
pv, err := delivery.DecodePreview(raw)
pv, err := meshdeliverycheckerclient.DecodePreview(raw)
if err != nil {
t.Fatal(err)
}
@@ -661,12 +668,12 @@ func TestAHeadThatDoesNotMergeWritesAPreviewOfItsConflictsAlone(t *testing.T) {
b := newBench(t)
b.fetch = captured[Fetched](t, "go-fetch-conflict")
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
key, _ := delivery.PreviewKey(repoA, head1, trunkA)
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
raw, _, found, _ := b.prev.Get(context.Background(), key)
if !found {
t.Fatalf("no preview for a head that does not merge; the bucket holds %v", b.prev.vals)
}
pv, err := delivery.DecodePreview(raw)
pv, err := meshdeliverycheckerclient.DecodePreview(raw)
if err != nil || strings.Join(pv.Conflicts, ",") != "a.go,sub/b.go" || pv.Merge != "" || len(pv.Modules) != 0 {
t.Errorf("preview = %+v, %v, want the files in conflict and no merge", pv, err)
}
@@ -677,12 +684,12 @@ func TestAPreviewIsWrittenOnceAndAnExistingKeyStands(t *testing.T) {
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
b.fetch.Merge = "5656565656565656565656565656565656565656"
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
key, _ := delivery.PreviewKey(repoA, head1, trunkA)
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
if b.prev.creates[key] != 1 {
t.Errorf("the preview was created %d times, want once", b.prev.creates[key])
}
raw, _, _, _ := b.prev.Get(context.Background(), key)
if pv, _ := delivery.DecodePreview(raw); pv.Merge == b.fetch.Merge {
if pv, _ := meshdeliverycheckerclient.DecodePreview(raw); pv.Merge == b.fetch.Merge {
t.Error("the second merge commit replaced the first")
}
}
@@ -693,15 +700,15 @@ func TestTheIndexerIsToldOfAPreviewOnceWhenItIsCreated(t *testing.T) { // 1445/A
b.ex.Announce = func(_ context.Context, event, key string) { told = append(told, event+" "+key) }
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
key, _ := delivery.PreviewKey(repoA, head1, trunkA)
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, trunkA)
if len(told) != 1 || told[0] != EventPreviewWritten+" "+key {
t.Errorf("told %v, want once of %s", told, key)
}
}
type fakeBuilds map[string]delivery.BuilderBuild
type fakeBuilds map[string]builds.BuilderBuild
func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (delivery.BuilderBuild, bool, error) {
func (f fakeBuilds) Build(_ context.Context, module, fp2 string) (builds.BuilderBuild, bool, error) {
b, ok := f[module+" "+fp2]
return b, ok, nil
}
@@ -712,13 +719,13 @@ func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T
fpB := "src2:" + strings.Repeat("b", 64)
fpC := "src2:" + strings.Repeat("c", 64)
b.ex.Builds = fakeBuilds{
"avahi " + fpA: {State: delivery.BuildDone},
"mesh-tools " + fpB: {State: delivery.BuildFailed, Error: "boom"},
"avahi " + fpA: {State: builds.BuildDone},
"mesh-tools " + fpB: {State: builds.BuildFailed, Error: "boom"},
// netcheck has no key yet
}
p := pullOf(repoA, 7, head1)
p.Modules = []string{"avahi", "mesh-tools", "netcheck", "nofp"}
p.Fps = map[string]delivery.CandidateModule{"avahi": {FP2: fpA}, "mesh-tools": {FP2: fpB}, "netcheck": {FP2: fpC}, "nofp": {None2: "toolchain not yet reported"}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"avahi": {Fp2: fpA}, "mesh-tools": {Fp2: fpB}, "netcheck": {Fp2: fpC}, "nofp": {None2: "toolchain not yet reported"}}
out := b.judge(t, caseOf(t, p))
var got []string
for _, n := range out.Builds {
@@ -732,7 +739,7 @@ func TestTheOutcomeSaysIfEachCandidatesModuleIsBuiltFailedOrWaiting(t *testing.T
// trunksOf are trunk heads of every repository, as the gate's source names them.
func trunksOf(t *testing.T, heads map[string]string) *Trunks {
t.Helper()
data, err := delivery.EncodeSource(delivery.Source{Group: "g", Heads: heads})
data, err := meshdeliveryclient.EncodeSource(meshdelivery.SourceByMainValue{Group: "g", Heads: heads})
if err != nil {
t.Fatal(err)
}
@@ -808,8 +815,8 @@ func TestACoreRepositorysPassingPackageIsKeptUnderADigestCoveringItsSiblings(t *
// The same siblings again: everything is taken, nothing runs.
b.steps.calls = nil
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
if n := len(b.steps.tool("test")); n != 0 || v.Levels[delivery.LevelUnit].Run != 0 || v.Levels[delivery.LevelUnit].Kept == 0 {
t.Fatalf("%d test steps, unit %+v, want none run and the kept verdict taken", n, v.Levels[delivery.LevelUnit])
if n := len(b.steps.tool("test")); n != 0 || v.Levels[buildstep.LevelUnit].Run != 0 || v.Levels[buildstep.LevelUnit].Kept == 0 {
t.Fatalf("%d test steps, unit %+v, want none run and the kept verdict taken", n, v.Levels[buildstep.LevelUnit])
}
// A sibling's commit moves: a new digest, so the package runs again and is kept under it.
b.ex.Trunks = trunksOf(t, map[string]string{repoSDK: trunkS2, "novox/mesh-catalog": trunkA})
@@ -917,15 +924,15 @@ func TestNoStepOfACheckCarriesPublish(t *testing.T) { // ADR 0500 §4, 1309 (the
func TestAnAskWhoseStepNeverEndsEndsTheCheckLoudly(t *testing.T) {
b := newBench(t)
// A node took the ask and its step stays running: the bound runs from its start (review round 2 item 1).
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
return delivery.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: delivery.StepRunning, Started: time.Now()}
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
return buildstep.Step{ID: a.ID, Tool: "check ask", Node: "shanks", State: buildstep.StepRunning, Started: time.Now()}
}
b.ex.Asks = buildrun.Asker{Publisher: b.asks, Waiter: b.asks, Bound: 20 * time.Millisecond}
out := b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
if f := out.Verdict.Failed; out.Verdict.State != delivery.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") {
if f := out.Verdict.Failed; out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(f.Words, "on shanks did not end within") {
t.Fatalf("verdict = %+v", out.Verdict)
}
if n := len(b.asks.Asks()); n != 1 || b.asks.Asks()[0].Kind != delivery.AskCheck || b.asks.Asks()[0].Check == nil {
if n := len(b.asks.Asks()); n != 1 || b.asks.Asks()[0].Kind != buildstep.AskCheck || b.asks.Asks()[0].Check == nil {
t.Fatalf("asks = %+v, want one check ask", b.asks.Asks())
}
}
@@ -936,16 +943,20 @@ func TestACheckIsOneAskAndAReJudgeGoesOnTheRecheckStream(t *testing.T) {
b := newBench(t)
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
asks := b.asks.Asks()
if len(asks) != 1 || asks[0].Kind != delivery.AskCheck || asks[0].Language != delivery.AskGo || asks[0].Asker != delivery.CheckerModule {
if len(asks) != 1 || asks[0].Kind != buildstep.AskCheck || asks[0].Language != buildstep.AskGo || asks[0].Asker != CheckerModule {
t.Fatalf("asks = %+v, want one Go check ask", asks)
}
if j := asks[0].Check; j == nil || !j.Kept || strings.Join(j.Levels, ",") != "unit,integration" || j.Fetch["repository"] != repoA {
if j := asks[0].Check; j == nil || !j.Kept || strings.Join(j.Levels, ",") != "unit,integration" || j.Fetch.Repository != repoA {
t.Fatalf("the job = %+v", asks[0].Check)
}
// B2g: the job carries the toolchain the checker agreed (ADR 0584), with its language, so a node on another declines it.
if tc := asks[0].Check.Toolchain; tc == nil || *tc != (buildstep.ToolchainUsed{Language: "go", Version: "go1.27.1", Recipe: "7"}) {
t.Errorf("the job's toolchain = %+v, want the agreed go toolchain", tc)
}
cs := caseOf(t, pullOf(repoA, 7, head1))
cs.Kind = KindTrunk
b.judge(t, cs)
if asks := b.asks.Asks(); len(asks) != 2 || asks[1].Kind != delivery.AskRecheck {
if asks := b.asks.Asks(); len(asks) != 2 || asks[1].Kind != buildstep.AskRecheck {
t.Fatalf("asks = %+v, want the re-judge on recheck", asks)
}
}
@@ -971,7 +982,7 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
default:
}
b.asks.End(answer(b.asks.Asks()[0])) // a node took it and ran it
if out := <-done; out.Verdict.State != delivery.VerdictPass || len(b.asks.Asks()) != 1 {
if out := <-done; out.Verdict.State != meshdeliverycheckerclient.VerdictPass || len(b.asks.Asks()) != 1 {
t.Fatalf("verdict %+v after %d asks, want a pass from the one ask", out.Verdict, len(b.asks.Asks()))
}
}
@@ -981,13 +992,13 @@ func TestACheckAskWaitingInItsStreamIsNotBounded(t *testing.T) {
func TestASecondCheckOfTheSameHeadsKeepsWhatTheFirstPassed(t *testing.T) {
b := newBench(t)
first := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
u := first.Levels[delivery.LevelUnit]
if first.State != delivery.VerdictPass || u.Run == 0 || u.Kept != 0 {
u := first.Levels[buildstep.LevelUnit]
if first.State != meshdeliverycheckerclient.VerdictPass || u.Run == 0 || u.Kept != 0 {
t.Fatalf("the first check = %+v", first)
}
b.steps.calls = nil
second := b.judge(t, caseOf(t, pullOf(repoA, 7, head1))).Verdict
if s := second.Levels[delivery.LevelUnit]; second.State != delivery.VerdictPass || s.Kept != u.Run || s.Run != 0 {
if s := second.Levels[buildstep.LevelUnit]; second.State != meshdeliverycheckerclient.VerdictPass || s.Kept != u.Run || s.Run != 0 {
t.Fatalf("the second check's unit level = %+v, want %d kept and none run", s, u.Run)
}
if n := len(b.steps.tool("test")); n != 0 {
@@ -1171,9 +1182,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
if row.seats != "" {
b.ex.SeatList = fakeSeatList{read: row.seats == "read"}
}
var judged []delivery.ComposeJob
var judged []buildstep.ComposeJob
answer := b.asks.Answer
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
if a.Compose == nil {
return answer(a)
}
@@ -1188,8 +1199,8 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
verdict, _ := json.Marshal(composer.Verdict{V: composer.Version, SDK: "exe:1", Cases: []composer.CaseVerdict{{Name: JudgeCase,
Manifests: map[string]composer.ManifestRead{"web": read, "mail": {Accepted: row.untouched == "", Refused: row.untouched}}},
{Name: JudgeTrunk, Manifests: map[string]composer.ManifestRead{"web": {Accepted: true}, "mail": {Accepted: row.trunkMail == "", Refused: row.trunkMail}}}}})
result, _ := json.Marshal(delivery.ComposeAnswer{Key: "k", Verdict: verdict})
return delivery.Step{ID: a.ID, State: delivery.StepPassed, Result: result}
result, _ := json.Marshal(buildstep.ComposeAnswer{Key: "k", Verdict: verdict})
return buildstep.Step{ID: a.ID, State: buildstep.StepPassed, Result: result}
}
p := pullOf(repoA, 7, head1)
if row.ownJudge {
@@ -1197,15 +1208,15 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
b.fetch.Changed = []string{JudgeDir + "/judge/judge.go"}
}
p.Modules = []string{"web"}
p.Fps = map[string]delivery.CandidateModule{"web": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: web}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: web}}
cs := caseOf(t, p)
v := b.judge(t, cs).Verdict
if line := Line(v, cs, cs.Pulls[0], nil, nil, nil); row.seriesWords != "" && !strings.Contains(line, row.seriesWords) || row.seriesWords == "" && strings.Contains(line, "series") {
t.Errorf("%s: the line %q, want it to name %q", row.name, line, row.seriesWords)
}
// The join (A2j finding 2): the verdict names each series change as {module, artifact, from, to}.
if want := []delivery.SeriesChange(nil); row.series == "0.2" {
if want = []delivery.SeriesChange{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) {
if want := []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem(nil); row.series == "0.2" {
if want = []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{{Module: "web", Artifact: "pkg", From: "0.1", To: "0.2"}}; !reflect.DeepEqual(v.Series, want) {
t.Errorf("%s: the verdict's series %+v, want %+v", row.name, v.Series, want)
}
} else if len(v.Series) != 0 {
@@ -1217,18 +1228,18 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes)
}
if row.seats == "unread" {
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 {
if v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 {
t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails)
}
continue
}
if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) {
if row.ownJudge && (len(judged) != 2 || judged[0].Fetch.Commit != head1 || judged[1].Fetch.Commit != trunk) {
t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged)
}
if row.ownJudge {
judged = judged[1:]
}
if len(judged) != 1 || judged[0].Fetch["repository"] != DeliveryRepository || judged[0].Fetch["commit"] != trunk ||
if len(judged) != 1 || judged[0].Fetch.Repository != DeliveryRepository || judged[0].Fetch.Commit != trunk ||
judged[0].Package != JudgePackage || judged[0].Dir != JudgeDir {
t.Fatalf("%s: judged %+v (verdict %+v)", row.name, judged, v)
}
@@ -1250,9 +1261,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
req.Packages, req.Input, row.known)
}
switch {
case row.fails == "" && v.State != delivery.VerdictPass:
case row.fails == "" && v.State != meshdeliverycheckerclient.VerdictPass:
t.Errorf("%s: %+v", row.name, v)
case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
case row.fails != "" && (v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
t.Errorf("%s: %+v, want a fail saying %q", row.name, v.Failed, row.fails)
}
}
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
)
// The failure the build module reported for group r4g on 2026-10-23: the test logged a line, then failed with a multi-line
@@ -53,7 +53,7 @@ func TestWordsLongerThanTheBoundKeepTheirLastLines(t *testing.T) { // 1901/AC1
if !strings.HasSuffix(v.Failed.Words, "the cause is here") || len(v.Failed.Words) > MaxWords+200 {
t.Errorf("%d bytes, ends %q", len(v.Failed.Words), v.Failed.Words[len(v.Failed.Words)-30:])
}
_ = delivery.VerdictFail
_ = meshdeliverycheckerclient.VerdictFail
}
// An older build module sends no "cause": its answer is still accepted and the verdict's words are its output, as before
@@ -85,7 +85,7 @@ func TestAnOlderBuildModulesFailureIsAcceptedWithItsWholeOutput(t *testing.T) {
} {
t.Run(c.name, func(t *testing.T) {
v := levelFailure(&target{module: c.module}, "unit", Tested{By: c.by, Failed: []Failure{{Package: "modules/gitea", Test: "fails", Cause: c.cause, Output: c.output}}})
if v.State != delivery.VerdictFail {
if v.State != meshdeliverycheckerclient.VerdictFail {
t.Errorf("1901/AC8: the verdict is %q, want it failed", v.State)
}
line := firstFailed(*v)
@@ -5,7 +5,7 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
)
// What the gate reads of the checker (R4e, lead's interface with dev-r4e-gate): the verdict's heads are the gate's hash over
@@ -14,7 +14,7 @@ import (
func owedState(t testing.TB, r *rig, repo string, number int, head string) string {
t.Helper()
key, err := delivery.CheckerOwedKey(repo, number, head)
key, err := CheckerOwedKey(repo, number, head)
if err != nil {
t.Fatal(err)
}
@@ -22,7 +22,7 @@ func owedState(t testing.TB, r *rig, repo string, number int, head string) strin
if !found {
return ""
}
o, err := delivery.DecodeCheckerOwed(raw)
o, err := DecodeCheckerOwed(raw)
if err != nil {
t.Fatal(err)
}
@@ -37,7 +37,7 @@ func TestTheVerdictsHeadsAreTheGatesHashOverLowercaseHeads(t *testing.T) { // ga
cand := candidateOf(t, "feat/x", pullOf("Novox/Mesh-Delivery", 7, upper))
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
want, _ := delivery.HeadsHash([]delivery.Head{{Repository: repoA, Number: 7, Head: head1}})
want, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: repoA, Number: 7, Head: head1}})
if v := r.verdict(t, keyOf(t, "feat/x", cand)); v.Heads != want {
t.Errorf("heads = %q, want the gate's hash of the lone pull request, a group of one, %q", v.Heads, want)
}
@@ -49,7 +49,7 @@ func TestAGroupsVerdictHeadsCoverEveryMember(t *testing.T) { // gate (b): hq's m
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2), pullOf(hqRepo, 3, head2))
r.ch.Candidate(ctx, "feat%2Fx", cand)
r.ch.Wait()
want, _ := delivery.HeadsHash([]delivery.Head{{Repository: repoA, Number: 7, Head: head1}, {Repository: repoSDK, Number: 12, Head: head2},
want, _ := meshdeliveryclient.HeadsHash([]meshdeliveryclient.Head{{Repository: repoA, Number: 7, Head: head1}, {Repository: repoSDK, Number: 12, Head: head2},
{Repository: hqRepo, Number: 3, Head: head2}})
if v := r.verdict(t, keyOf(t, "feat/x", cand)); v.Heads != want {
t.Errorf("heads = %q, want %q", v.Heads, want)
@@ -7,9 +7,9 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/graph"
)
@@ -29,7 +29,7 @@ type ManifestReader interface {
// refuseCycle fails the check for a cycle of the change's module graph, naming it; nil when the case touches no manifest,
// when nothing reads manifests, or when there is none. A manifest that cannot be read fails the check, said, never passes it.
func (st *state) refuseCycle(ctx context.Context) *delivery.Verdict {
func (st *state) refuseCycle(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.j.Manifests == nil || st.j.Read == nil || st.c.Try != nil {
return nil
}
@@ -10,9 +10,11 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// goModule is the build module the Go repositories are judged by.
@@ -48,20 +50,20 @@ const (
)
// verdictPtr is a verdict of failure a step of the check ends with.
type verdictPtr = *delivery.Verdict
type verdictPtr = *meshdeliverychecker.VerdictsByGroupHeadsValue
func failed(machine, words string) verdictPtr { return fail(machine, words) }
// Asks puts a check's ask on its build seat's stream and waits for its step (buildrun.Asker; ADR 0525 §1, §6): the
// checker chooses no node, starts no step and holds none; whichever build node pulls the ask runs the check.
type Asks interface {
Put(ctx context.Context, a delivery.StepAsk) error
Wait(ctx context.Context, a delivery.StepAsk) (delivery.Step, error)
Put(ctx context.Context, a buildstep.StepAsk) error
Wait(ctx context.Context, a buildstep.StepAsk) (buildstep.Step, error)
}
// ToolchainEntries are the nodes' toolchain entries of a language (buildrun.ToolchainSet).
type ToolchainEntries interface {
Entries(language string) []delivery.NodeToolchain
Entries(language string) []buildstep.NodeToolchain
}
// BuildModules says which build modules judge a repository: the Go one always, the Node one when the repository holds a
@@ -71,7 +73,7 @@ type BuildModules interface {
}
// askLanguage is the build seat language of each build module the checker asks.
var askLanguage = map[string]string{buildrun.GoBuildModule: delivery.AskGo, buildrun.NodeBuildModule: delivery.AskNode}
var askLanguage = map[string]string{buildrun.GoBuildModule: buildstep.AskGo, buildrun.NodeBuildModule: buildstep.AskNode}
// Examiner judges a case through the build seats (ADR 0484 clauses 3, 4, 10, 13; ADR 0525 §1, §3): for each pull request
// and build module it puts one check ask whose job fetches the head merged onto its trunk head, then runs the unit level
@@ -97,9 +99,9 @@ type Examiner struct {
// indexer's modules (novox/hq ADR 0573 §2): a check builds against those, or against a member's head for a package
// its group publishes (§4), whatever go.mod names. Nil names none, and go.mod's versions are built.
Published interface {
LatestPackages() delivery.PackageVersions
LatestPackages() buildstep.PackageVersions
}
// KnownPackages names the packages the indexer's packages state knows (its series keys, delivery.SeriesPackage), by the
// KnownPackages names the packages the indexer's packages state knows (its series keys, meshcatalog.PackagesByPackageSeriesValue), by the
// name in its own ecosystem, each with the `<module>/<artifact>` it is built from: the names a manifest's `packages` is
// judged against (manifests_judge.go, design 32). Nil, or PackageNames answering nil while the state is not read yet,
// hands the judge no names, and it judges none; an empty map is a state read and empty, against which every name fails.
@@ -126,7 +128,7 @@ type Examiner struct {
ModuleDir(module string) (string, bool)
}
// Packages names, of a repository, the paths (repository-relative) its TypeScript package artifacts are built from, from
// the indexer's modules (delivery.Module.ArtifactPaths, lead decision L8); nil serves no group package.
// the indexer's modules (meshcatalog.ModulesByModuleValue.ArtifactPaths, lead decision L8); nil serves no group package.
Packages interface {
PackagePaths(repository string) []string
}
@@ -169,7 +171,7 @@ type Examiner struct {
// CompositionReader makes a check's composition verdict; an error is only a case that cannot be composed at all.
type CompositionReader interface {
Compose(ctx context.Context, c *Case) (delivery.CompositionVerdict, error)
Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error)
}
func (j *Examiner) now() time.Time {
@@ -185,6 +187,15 @@ func (j *Examiner) logf(format string, args ...any) {
}
}
// fetchBeside are the siblings as the fetch is asked to place them.
func fetchBeside(refs []BesideRef) []buildstep.FetchBeside {
out := make([]buildstep.FetchBeside, 0, len(refs))
for _, r := range refs {
out = append(out, buildstep.FetchBeside{Repository: r.Repository, Commit: r.Commit})
}
return out
}
// besideLines are the siblings as the digest step echoes them: `<owner/repo> <commit>`, sorted.
func besideLines(refs []BesideRef) []string {
out := make([]string, 0, len(refs))
@@ -203,10 +214,10 @@ type target struct {
node string
toolchain string
// agreed is the toolchain the ask names, so a node off it declines the check (ADR 0584); nil when none is agreed.
agreed *delivery.ToolchainUsed
agreed *buildstep.ToolchainUsed
fetched Fetched
// fetchArgs are the arguments of the fetch its check ask runs.
fetchArgs map[string]any
fetchArgs buildstep.Fetch
// priority are the Go packages (import paths) or the Node module directories whose test files the merge changes.
changed []string
// beside are the lines `<owner/repo> <commit>` of the siblings the workspace was given, sorted: its tests may read them,
@@ -279,7 +290,7 @@ type state struct {
// Judge runs the case (it is the Judge of a Checker). A failure of the change, of a merge or of a step is a verdict; an error is only a case that cannot
// be judged at all.
func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
st := &state{j: j, c: c, levels: map[string]*acc{delivery.LevelUnit: {}, delivery.LevelIntegration: {}}, nodes: map[string]bool{},
st := &state{j: j, c: c, levels: map[string]*acc{buildstep.LevelUnit: {}, buildstep.LevelIntegration: {}}, nodes: map[string]bool{},
beside: map[string]string{}}
// Every member and level runs to its end, and every manifest the change touches is judged by the indexer's judge (ADR
// 0578 §2), also after a failure: the verdict names every failure, not only the first (the operator, 2026-11-02).
@@ -288,12 +299,12 @@ func (j *Examiner) Judge(ctx context.Context, c *Case) (Outcome, error) {
if v := settled(joinFailures(tests, st.judgeManifests(ctx), st.refuseCycle(ctx), st.judgeRenames(ctx), st.refuseUnparsed(ctx))); v != nil {
return st.finish(ctx, *v), nil
}
return st.finish(ctx, delivery.Verdict{State: delivery.VerdictPass}), nil
return st.finish(ctx, meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass}), nil
}
// finish is the outcome of the verdict with the state of the builds the candidate's modules carry and the assigner's
// composition of it, read also when the tests failed, so a failed composition is named beside them.
func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome {
func (st *state) finish(ctx context.Context, v meshdeliverychecker.VerdictsByGroupHeadsValue) Outcome {
out := st.outcome(v)
st.composed(ctx, &out.Verdict)
out.Builds = st.j.buildNotes(ctx, st.c)
@@ -303,7 +314,7 @@ func (st *state) finish(ctx context.Context, v delivery.Verdict) Outcome {
// composed makes the check's composition from the engines' answers into its verdict (ADR 0593 §7, §9): a failed
// composition fails the check, naming the machine it failed at; a node that did not answer is a warning. A try is not
// composed (it has no candidate); a composition that cannot be made leaves it not run, said.
func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
func (st *state) composed(ctx context.Context, v *meshdeliverychecker.VerdictsByGroupHeadsValue) {
if st.c.Composition != nil && st.c.Try == nil {
// A carry whose candidate's fingerprints did not change keeps the composition it passed (ADR 0548 §5).
kept := *st.c.Composition
@@ -316,7 +327,7 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
}
if len(st.unpreviewed) > 0 {
// An engine would only say it cannot read a manifest the indexer will not have: the check does not ask it.
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "no preview of a merge of " +
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun, Words: "no preview of a merge of " +
strings.Join(st.unpreviewed, ", ") + " (its head does not merge or its fetch did not answer), so the change cannot be composed"}
return
}
@@ -325,41 +336,41 @@ func (st *state) composed(ctx context.Context, v *delivery.Verdict) {
if err != nil {
// Said in the verdict, never left as a composition not yet run (review finding 4).
st.j.logf("%s: the composition could not be made, so it is said as not run: %v", st.c.Key, err)
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun, Words: "not made: " + err.Error()}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun, Words: "not made: " + err.Error()}
return
}
comp.Warnings = append(comp.Warnings, words...) // ADR 0603 §10
withComposition(v, comp)
if v.Composition != nil && v.Composition.State != delivery.CompositionNotRun {
v.Composition.FPSet = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
if v.Composition != nil && v.Composition.State != meshdeliverycheckerclient.CompositionNotRun {
v.Composition.Fpset = st.c.FPSet // a carry keeps it while the fingerprints stay the same (ADR 0548 §5)
}
}
// outcome fills the verdict's levels, times, machines and composition from what ran.
func (st *state) outcome(v delivery.Verdict) Outcome {
func (st *state) outcome(v meshdeliverychecker.VerdictsByGroupHeadsValue) Outcome {
v.Series = st.c.allSeries() // the series changes, as structures (A2j finding 2)
if v.State == delivery.VerdictPass || len(v.Levels) == 0 {
v.Levels = map[string]delivery.LevelResult{}
if v.State == meshdeliverycheckerclient.VerdictPass || len(v.Levels) == 0 {
v.Levels = map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{}
for level, a := range st.levels {
v.Levels[level] = delivery.LevelResult{Run: a.run, Kept: a.kept, Skipped: a.skipped}
v.Levels[level] = meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{Run: int64(a.run), Kept: int64(a.kept), Skipped: int64(a.skipped)}
}
}
ms := func(s float64) int64 { return int64(s * 1000) }
u, i := st.levels[delivery.LevelUnit], st.levels[delivery.LevelIntegration]
v.Took = delivery.Took{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), Lint: ms(u.lint), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)}
u, i := st.levels[buildstep.LevelUnit], st.levels[buildstep.LevelIntegration]
v.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{Clone: st.clone.Milliseconds(), Unit: ms(u.elapsed), Lint: ms(u.lint), IntegrationSetup: ms(i.setup), IntegrationRun: ms(i.elapsed)}
for node := range st.nodes {
v.Machines = append(v.Machines, node)
}
sort.Strings(v.Machines)
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} {
if st.levels[level].over {
v.OverBound = append(v.OverBound, level)
}
}
// The composition is made from the engines' answers (ADR 0593 §7): until it is, a verdict says it was not run (ADR 0484 clause 6).
v.Composition = &delivery.Composition{State: delivery.CompositionNotRun}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun}
var slow []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} {
a := st.levels[level]
if !a.over {
continue
@@ -402,7 +413,7 @@ func (st *state) stepOutOfStep(p Pull, files []string) {
}
// fail is a verdict of failure at a machine.
func fail(machine, words string) *delivery.Verdict {
func fail(machine, words string) *meshdeliverychecker.VerdictsByGroupHeadsValue {
v := failure(machine, words)
return &v
}
@@ -412,14 +423,14 @@ func (st *state) levelsOf() []string {
if st.c.Try != nil && st.c.Try.Level != "" {
return []string{st.c.Try.Level} // a try of one level runs that level alone
}
return []string{delivery.LevelUnit, delivery.LevelIntegration}
return []string{buildstep.LevelUnit, buildstep.LevelIntegration}
}
// judgeAll puts one check ask per pull request and build module the repository needs, the mesh-sdk member first, and
// judges each answer. A head that does not merge, a pin that is not allowed or a level that fails is a failure of that
// member; every other member is still judged, and the verdict names every failure (the operator, 2026-11-02), unjoined
// to its first lines (settled does that). Only a check whose context ended stops.
func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
func (st *state) judgeAll(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
// The mesh-sdk member first: whether its merge changes the TypeScript package's paths decides whether the other members'
// Node fetches take the group's package (ADR 0500 §5).
pulls := slices.Clone(st.c.Pulls)
@@ -437,12 +448,12 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
if v := st.tagged(ctx); v != nil {
return v
}
var groupPackage map[string]any
var failures []*delivery.Verdict
var groupPackage *buildstep.FetchPackage
var failures []*meshdeliverychecker.VerdictsByGroupHeadsValue
for _, p := range pulls {
if ctx.Err() != nil {
if len(failures) == 0 { // an ask that ended with the context said it already
failures = append(failures, fail(delivery.CheckerModule, ctx.Err().Error()))
failures = append(failures, fail(CheckerModule, ctx.Err().Error()))
}
break
}
@@ -450,16 +461,16 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
modules := st.j.Modules.BuildModules(p.Repository)
for _, bm := range modules {
t := &target{pull: p, module: bm}
if tc, none := delivery.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
if tc, none := buildstep.AgreedToolchain(st.j.Toolchains.Entries(buildrun.LanguageOf[bm])); none == "" {
t.toolchain = tc.Version
t.agreed = &delivery.ToolchainUsed{Version: tc.Version, Recipe: tc.Recipe}
t.agreed = &buildstep.ToolchainUsed{Language: buildrun.LanguageOf[bm], Version: tc.Version, Recipe: tc.Recipe}
}
args := map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number), "base": p.Trunk}
args := buildstep.Fetch{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number), Base: p.Trunk}
if bm == buildrun.NodeBuildModule {
if np, ok := st.tags.nodePackageFor(p.Repository); ok {
args["package"] = map[string]any{"name": np.Name, "version": np.Version} // the group's tagged version (ADR 0607 §3)
args.Package = &buildstep.FetchPackage{Name: np.Name, Version: np.Version} // the group's tagged version (ADR 0607 §3)
} else if groupPackage != nil && p.Repository != SDKRepository {
args["package"] = groupPackage
args.Package = groupPackage
}
}
if bm == buildrun.GoBuildModule {
@@ -469,7 +480,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
continue
}
if len(beside) > 0 {
args["beside"] = beside
args.Beside = fetchBeside(beside)
t.beside = besideLines(beside)
besideAtTrunk(st.c, beside, st.beside)
}
@@ -497,7 +508,7 @@ func (st *state) judgeAll(ctx context.Context) *delivery.Verdict {
// TypeScript version is heard, the form the running checker hands while the new build modules are delivered. Its
// removal is a subtask of the milestone after this one's delivery (issue 1966).
if p.Repository == SDKRepository && len(st.c.Pulls) > 1 && len(st.tags.node) == 0 && st.j.changesPackage(p, merged) {
groupPackage = map[string]any{"repository": p.Repository, "commit": p.Head, "ref": refOf(p.Number)}
groupPackage = &buildstep.FetchPackage{Repository: p.Repository, Commit: p.Head, Ref: refOf(p.Number)}
}
}
return joinFailures(failures...)
@@ -511,17 +522,17 @@ const DefaultDeclineBound = 30 * time.Minute
// each level's digest, kept verdicts and tests. When the answer leaves packages unmet,
// their tests needing what that node lacks (issue 890), the passes are kept and the levels from the first such one are
// asked again of a node that has it (placed).
func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *delivery.Verdict {
func (st *state) ask(ctx context.Context, t *target, merged *[]Fetched) *meshdeliverychecker.VerdictsByGroupHeadsValue {
return st.askNeeding(ctx, t, merged, st.levelsOf(), nil, nil)
}
// askNeeding is ask with the levels to run, what the node must have and the packages it tests: none for the first ask
// of a target; for the placed one the unmet packages' needs and those packages alone, so no package the first ask judged
// is run or counted again (ADR 0552 §4).
func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *delivery.Verdict {
func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, levels, needs, packages []string) *meshdeliverychecker.VerdictsByGroupHeadsValue {
p := t.pull
placed := len(needs) > 0
job := &delivery.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages, Toolchain: t.agreed}
job := &buildstep.CheckJob{Fetch: t.fetchArgs, Levels: levels, Kept: true, Packages: packages, Toolchain: t.agreed}
if st.j.Nodes != nil {
names, err := st.j.Nodes.Names(ctx)
if err == nil && len(names) == 0 {
@@ -538,16 +549,16 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
if st.c.Try != nil && st.c.Try.Run != "" {
job.Run, job.Kept = st.c.Try.Run, false // a try of some tests runs them alone: a partial run proves no package
}
kind := delivery.AskCheck
kind := buildstep.AskCheck
if st.c.Kind == KindTrunk {
kind = delivery.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
kind = buildstep.AskRecheck // a re-judge after a trunk move goes after every new head's check (ADR 0536 §4)
}
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: delivery.CheckerModule,
a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: kind, Language: askLanguage[t.module], Asker: CheckerModule,
At: st.j.now().UTC(), Check: job, Needs: needs}
if a.Language == delivery.AskGo {
if a.Language == buildstep.AskGo {
a.Packages = st.packagesOf(p.Repository)
}
var step delivery.Step
var step buildstep.Step
var firstDecline time.Time
for {
what := fmt.Sprintf("the %s ask %s of %s#%d", kind, a.ID, p.Repository, p.Number)
@@ -563,7 +574,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
if watch := st.c.Watch; watch != nil {
id := a.ID
watch(id, nil) // put: a try names it at once, not only when it ends (issue 1890)
waiting = buildrun.WithStepSeen(ctx, func(s delivery.Step) { watch(id, &s) })
waiting = buildrun.WithStepSeen(ctx, func(s buildstep.Step) { watch(id, &s) })
}
var err error
step, err = st.j.Asks.Wait(waiting, a)
@@ -573,7 +584,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
}
return fail(t.module, fmt.Sprintf("the check of %s#%d: %v", p.Repository, p.Number, err))
}
d, declined := delivery.DeclinedOf(step)
d, declined := buildstep.DeclinedOf(step)
if !declined {
break
}
@@ -598,7 +609,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
st.j.logf("%s#%d: the ask %s was declined on %s, which lacks %s; asked again", p.Repository, p.Number, a.ID, step.Node, strings.Join(d.Lacks, ", "))
a.ID, a.At = buildstep.NewID(), now.UTC()
}
var failures []*delivery.Verdict
var failures []*meshdeliverychecker.VerdictsByGroupHeadsValue
// The fetch and the digests are what of the ask's time its levels' running did not take (Took.Clone).
took := time.Duration(0)
if !step.Started.IsZero() && step.Ended.After(step.Started) {
@@ -607,15 +618,15 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
t.node = step.Node
st.nodes[step.Node] = true
st.c.Progress.Set(fmt.Sprintf("judge the check of %s#%d", p.Repository, p.Number), step.Node)
if step.State != delivery.StepPassed {
if step.State != buildstep.StepPassed {
return fail(step.Node, fmt.Sprintf("the check of %s#%d %s: %s", p.Repository, p.Number, step.State, step.Reason))
}
var answer delivery.CheckAnswer
var answer buildstep.CheckAnswer
if err := json.Unmarshal(step.Result, &answer); err != nil {
return fail(step.Node, fmt.Sprintf("the check of %s#%d answered what is not a check: %s", p.Repository, p.Number, step.Result))
}
if err := json.Unmarshal(answer.Fetch, &t.fetched); err != nil || t.fetched.Workspace == "" {
return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, answer.Fetch))
if t.fetched = answer.Fetch; t.fetched.Workspace == "" {
return fail(step.Node, fmt.Sprintf("the fetch of %s#%d answered no workspace: %s", p.Repository, p.Number, step.Result))
}
if len(t.fetched.Conflicts) > 0 {
if st.c.Try == nil {
@@ -650,7 +661,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
}
}
st.clone += max(0, took-time.Duration(ran*float64(time.Second)))
if unmet := delivery.UnmetNeeds(answer.Levels); len(unmet) > 0 && !placed {
if unmet := buildstep.UnmetNeeds(answer.Levels); len(unmet) > 0 && !placed {
// Every level ran, its passes kept, and some packages were left to a node with what their tests need: the levels
// from the first that left one are asked of such a node, which takes the kept passes and tests those (issue 890).
var again, packages []string
@@ -673,7 +684,7 @@ func (st *state) askNeeding(ctx context.Context, t *target, merged *[]Fetched, l
}
// unmetNames names the unmet packages and their needs.
func unmetNames(unmet []delivery.PackageNeeds) string {
func unmetNames(unmet []buildstep.PackageNeeds) string {
var out []string
for _, u := range unmet {
out = append(out, u.Package+" (needs "+strings.Join(u.Needs, ", ")+")")
@@ -682,7 +693,7 @@ func unmetNames(unmet []delivery.PackageNeeds) string {
}
// unmetOf names the unmet packages of every level.
func unmetOf(levels []delivery.LevelAnswer) string {
func unmetOf(levels []buildstep.LevelAnswer) string {
var out []string
for _, la := range levels {
if len(la.Unmet) > 0 {
@@ -696,7 +707,7 @@ func unmetOf(levels []delivery.LevelAnswer) string {
// of the packages that passed, the kept ones taken are counted, and every test result is read and counted, also after
// one that did not pass, so the verdict names every failed test of the level (issue 1921). It answers the level's
// running time (its tests' and setup's).
func (st *state) level(ctx context.Context, t *target, la delivery.LevelAnswer) (*delivery.Verdict, float64) {
func (st *state) level(ctx context.Context, t *target, la buildstep.LevelAnswer) (*meshdeliverychecker.VerdictsByGroupHeadsValue, float64) {
level := la.Level
a := st.levels[level]
if a == nil {
@@ -930,18 +941,18 @@ func firstLine(s string) string {
const MaxFailureLine = 300
// levelFailure turns a level that did not pass into a verdict: Failed names what failed first with its words whole,
// and Failures every failed test of the level's results with its first line, at most delivery.MaxFailures and an entry
// and Failures every failed test of the level's results with its first line, at most meshdeliverycheckerclient.MaxFailures and an entry
// saying how many more (issue 1921).
func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict {
v := delivery.Verdict{State: delivery.VerdictFail}
var all []delivery.FailedFirst
func levelFailure(t *target, level string, results ...Tested) *meshdeliverychecker.VerdictsByGroupHeadsValue {
v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail}
var all []FailedFirst
for _, res := range results {
if len(res.Failed) == 0 {
why := res.Why
if why == "" {
why = "the " + level + " level did not pass"
}
all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: why})
all = append(all, FailedFirst{Test: level + ": " + t.pull.Repository, Words: why})
continue
}
for _, f := range res.Failed {
@@ -949,75 +960,76 @@ func levelFailure(t *target, level string, results ...Tested) *delivery.Verdict
if t.module == buildrun.NodeBuildModule && f.Package == "" {
name = res.Module
}
all = append(all, delivery.FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f))})
all = append(all, FailedFirst{Test: level + ": " + name, Words: failureWordsAfter(f, hidWords(t.module, res.By, f))})
}
}
if len(all) == 0 { // no result: a level said failed with nothing to name
all = append(all, delivery.FailedFirst{Test: level + ": " + t.pull.Repository, Words: "the " + level + " level did not pass"})
all = append(all, FailedFirst{Test: level + ": " + t.pull.Repository, Words: "the " + level + " level did not pass"})
}
v.Failed = all[0]
v.Failures = all // whole: settled cuts them to their first lines once every failure of the check is joined
first := all[0]
v.Failed = &first
v.Failures = failureItems(all) // whole: settled cuts them to their first lines once every failure of the check is joined
return &v
}
// joinFailures is one failed verdict of every failure of vs, in the order given (a nil one passed), unsettled: Failed is
// the first whole, and Failures every failure whole, or none when the one failure is no level's (a conflict, a refused
// step: one line, as before issue 1921). Nil when none failed.
func joinFailures(vs ...*delivery.Verdict) *delivery.Verdict {
var out *delivery.Verdict
var all []delivery.FailedFirst
func joinFailures(vs ...*meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue {
var out *meshdeliverychecker.VerdictsByGroupHeadsValue
var all []FailedFirst
listed := false
for _, v := range vs {
if v == nil {
continue
}
if out == nil {
out = &delivery.Verdict{State: delivery.VerdictFail, Failed: v.Failed}
out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: v.Failed}
}
if len(v.Failures) > 0 {
all, listed = append(all, v.Failures...), true
all, listed = append(all, failedList(v.Failures)...), true
continue
}
all = append(all, v.Failed)
all = append(all, failedOf(*v))
}
if out != nil && (listed || len(all) > 1) {
out.Failures = all
out.Failures = failureItems(all)
}
return out
}
// settled is a joined verdict as it is written: each failure of its list cut to its first line, at most
// delivery.MaxFailures and then how many more (failureLines).
func settled(v *delivery.Verdict) *delivery.Verdict {
// meshdeliverycheckerclient.MaxFailures and then how many more (failureLines).
func settled(v *meshdeliverychecker.VerdictsByGroupHeadsValue) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if v != nil && len(v.Failures) > 0 {
v.Failures = failureLines(v.Failures)
v.Failures = failureItems(failureLines(failedList(v.Failures)))
}
return v
}
// addFailure is a settled list of failures with one more at its end: the first failure is put first when the list is
// empty (a verdict of one failure keeps none), and past delivery.MaxFailures the last entry counts one more.
func addFailure(v *delivery.Verdict, f delivery.FailedFirst) {
// empty (a verdict of one failure keeps none), and past meshdeliverycheckerclient.MaxFailures the last entry counts one more.
func addFailure(v *meshdeliverychecker.VerdictsByGroupHeadsValue, f FailedFirst) {
if len(v.Failures) == 0 {
v.Failures = failureLines([]delivery.FailedFirst{v.Failed})
v.Failures = failureItems(failureLines([]FailedFirst{failedOf(*v)}))
}
if n := len(v.Failures); n > delivery.MaxFailures {
v.Failures[n-1].Test = fmt.Sprintf("and %d more", failedCount(*v)-delivery.MaxFailures+1)
if n := len(v.Failures); n > meshdeliverycheckerclient.MaxFailures {
v.Failures[n-1].Test = fmt.Sprintf("and %d more", failedCount(*v)-meshdeliverycheckerclient.MaxFailures+1)
return
}
v.Failures = append(v.Failures, failureLines([]delivery.FailedFirst{f})...)
v.Failures = append(v.Failures, failureItems(failureLines([]FailedFirst{f}))...)
}
// failureLines is every failure with the first line of its words, cut to MaxFailureLine, at most delivery.MaxFailures
// failureLines is every failure with the first line of its words, cut to MaxFailureLine, at most meshdeliverycheckerclient.MaxFailures
// and then one entry saying how many more failed.
func failureLines(all []delivery.FailedFirst) []delivery.FailedFirst {
n := min(len(all), delivery.MaxFailures)
out := make([]delivery.FailedFirst, 0, n+1)
func failureLines(all []FailedFirst) []FailedFirst {
n := min(len(all), meshdeliverycheckerclient.MaxFailures)
out := make([]FailedFirst, 0, n+1)
for _, f := range all[:n] {
out = append(out, delivery.FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)})
out = append(out, FailedFirst{Test: f.Test, Machine: f.Machine, Words: clipLine(firstLine(f.Words), MaxFailureLine)})
}
if more := len(all) - n; more > 0 {
out = append(out, delivery.FailedFirst{Test: fmt.Sprintf("and %d more", more)})
out = append(out, FailedFirst{Test: fmt.Sprintf("and %d more", more)})
}
return out
}
@@ -5,7 +5,8 @@ import (
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The judged_by answer of mesh-build-go's digest and test steps (ADR 0535 §5; the milestone file's interface for issue
@@ -84,7 +85,7 @@ func TestAnInstalledModuleWithoutTheRunnerJudgesAsTodayAndTheLineSaysSo(t *testi
b := candidateBench(t, false)
cs := caseOf(t, pullOf(catalogueRepository, 9, head1))
out := b.judge(t, cs)
if out.Verdict.State != delivery.VerdictPass {
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
// As today: the digests key kept verdicts, and the levels are asked without the runner the module ignored.
@@ -112,7 +113,7 @@ func TestACandidateJudgedCheckTakesNoKeptVerdictAndWritesNone(t *testing.T) { //
t.Fatal(err)
}
out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 9, head1)))
if out.Verdict.State != delivery.VerdictPass {
if out.Verdict.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", out.Verdict)
}
tests := b.steps.tool("test")
@@ -124,7 +125,7 @@ func TestACandidateJudgedCheckTakesNoKeptVerdictAndWritesNone(t *testing.T) { //
t.Errorf("the %v level was asked for %v, want it whole", c.args["level"], c.args["packages"])
}
}
if u := out.Verdict.Levels[delivery.LevelUnit]; u.Kept != 0 {
if u := out.Verdict.Levels[buildstep.LevelUnit]; u.Kept != 0 {
t.Errorf("unit = %+v, want nothing taken", u)
}
if keys, _ := b.kept.Keys(context.Background()); len(keys) != 1 || keys[0] != "unit."+digA {
@@ -148,8 +149,8 @@ func TestTheLineNamesTheCandidateThatJudged(t *testing.T) { // 1898/AC6
}
// A long failing line keeps the program it was judged by; only the lesser parts go.
v := out.Verdict
v.State = delivery.VerdictFail
v.Failed = delivery.FailedFirst{Test: "unit: git.novox.be/novox/mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go.TestADigestFoldsTheSiblings", Words: strings.Repeat("x", 60)}
v.State = meshdeliverycheckerclient.VerdictFail
v.Failed = &FailedFirst{Test: "unit: git.novox.be/novox/mesh-catalog/modules/mesh-build-go/cmd/mesh-build-go.TestADigestFoldsTheSiblings", Words: strings.Repeat("x", 60)}
if l := Line(v, cs, p, out.Builds, out.Slowest, out.Judged); !strings.Contains(l, "judged by the candidate's") {
t.Errorf("the long line %q dropped the program", l)
}
@@ -174,7 +175,7 @@ func TestACandidateThatDoesNotCompileFailsTheCheckWithTheCompilersWords(t *testi
b.steps.handlers["build_go_digest"] = func(stepCall) (any, error) { return nil, errorString(refusal) }
b.test = func(stepCall) (any, error) { return nil, errorString(refusal) }
out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 9, head1)))
if out.Verdict.State != delivery.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") {
if out.Verdict.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(out.Verdict.Failed.Words, "undefined: x") {
t.Fatalf("verdict = %+v, want a failure naming the compile error", out.Verdict)
}
if n := len(b.steps.tool("test")); n != 1 {
@@ -195,9 +196,9 @@ func TestTheStatusTheForgeShowsNamesTheProgramThatJudged(t *testing.T) { // 1898
r := newRig(t, j)
r.ch.Candidate(ctx, "feat/x", candidateOf(t, "feat/x", pullOf(repoA, 7, head1)))
r.ch.Wait()
key, _ := delivery.CheckerOwedKey(repoA, 7, head1)
key, _ := CheckerOwedKey(repoA, 7, head1)
raw, _, _, _ := r.owed.Get(ctx, key)
o, err := delivery.DecodeCheckerOwed(raw)
o, err := DecodeCheckerOwed(raw)
if err != nil {
t.Fatal(err)
}
@@ -7,7 +7,8 @@ import (
"sort"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// Kept is the checker's bucket of kept verdicts (ADR 0444, ADR 0484 clause 13): a passing verdict per package and level,
@@ -17,31 +18,31 @@ import (
type Kept struct{ Store KV }
// Lookup is the kept verdict of a level under a digest, or none.
func (k Kept) Lookup(ctx context.Context, level, digest string) (delivery.Kept, bool, error) {
key, err := delivery.KeptKey(level, digest)
func (k Kept) Lookup(ctx context.Context, level, digest string) (meshdeliverychecker.KeptByLevelDigestValue, bool, error) {
key, err := meshdeliverycheckerclient.KeptKey(level, digest)
if err != nil {
return delivery.Kept{}, false, err
return meshdeliverychecker.KeptByLevelDigestValue{}, false, err
}
value, _, found, err := k.Store.Get(ctx, key)
if err != nil || !found {
return delivery.Kept{}, false, err
return meshdeliverychecker.KeptByLevelDigestValue{}, false, err
}
v, err := delivery.DecodeKept(value)
v, err := meshdeliverycheckerclient.DecodeKept(value)
if err != nil {
return delivery.Kept{}, false, fmt.Errorf("%s holds what is not a kept verdict: %w", key, err)
return meshdeliverychecker.KeptByLevelDigestValue{}, false, fmt.Errorf("%s holds what is not a kept verdict: %w", key, err)
}
return v, true, nil
}
// Keep writes v under level and digest by a create. It answers false, and writes nothing, when the key exists: the first
// value stands.
func (k Kept) Keep(ctx context.Context, level, digest string, v delivery.Kept) (bool, error) {
key, err := delivery.KeptKey(level, digest)
func (k Kept) Keep(ctx context.Context, level, digest string, v meshdeliverychecker.KeptByLevelDigestValue) (bool, error) {
key, err := meshdeliverycheckerclient.KeptKey(level, digest)
if err != nil {
return false, err
}
v.Level = level
data, err := delivery.EncodeKept(v)
data, err := meshdeliverycheckerclient.EncodeKept(v)
if err != nil {
return false, err
}
@@ -77,8 +78,8 @@ func (k Kept) KeepPasses(ctx context.Context, repository, level, toolchain strin
if failedIn(res, p.Package) {
continue
}
made, err := k.Keep(ctx, level, digest, delivery.Kept{State: delivery.VerdictPass, Repository: repository, Package: p.Package,
Level: level, Tests: p.Tests, Took: int64(p.Elapsed * 1000), Toolchain: toolchain, At: at})
made, err := k.Keep(ctx, level, digest, meshdeliverychecker.KeptByLevelDigestValue{State: meshdeliverycheckerclient.VerdictPass, Repository: repository, Package: p.Package,
Level: level, Tests: int64(p.Tests), Took: int64(p.Elapsed * 1000), Toolchain: toolchain, At: at.UTC().Format(time.RFC3339)})
if err != nil {
return kept, err
}
@@ -6,7 +6,9 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
var at0 = time.Date(2026, 10, 21, 19, 0, 0, 0, time.UTC)
@@ -16,23 +18,23 @@ const (
digB = "bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22bb22"
)
func keptValue(pkg string, tests int) delivery.Kept {
return delivery.Kept{State: delivery.VerdictPass, Repository: "novox/mesh-delivery", Package: pkg, Tests: tests, Took: 120, Toolchain: "go1.26.0", At: at0}
func keptValue(pkg string, tests int) meshdeliverychecker.KeptByLevelDigestValue {
return meshdeliverychecker.KeptByLevelDigestValue{State: meshdeliverycheckerclient.VerdictPass, Repository: "novox/mesh-delivery", Package: pkg, Tests: int64(tests), Took: 120, Toolchain: "go1.26.0", At: at0.UTC().Format(time.RFC3339)}
}
func TestAPassIsKeptByACreateUnderItsLevelAndDigest(t *testing.T) { // 1287/AC1
ctx, st := context.Background(), newMemKV()
k := Kept{Store: st}
made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3))
made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3))
if err != nil || !made {
t.Fatalf("Keep = %v, %v, want made", made, err)
}
key, _ := delivery.KeptKey(delivery.LevelUnit, digA)
key, _ := meshdeliverycheckerclient.KeptKey(buildstep.LevelUnit, digA)
if st.creates[key] != 1 {
t.Fatalf("the key %s was created %d times, want once", key, st.creates[key])
}
got, found, err := k.Lookup(ctx, delivery.LevelUnit, digA)
if err != nil || !found || got.Package != "m/a" || got.Tests != 3 || got.Level != delivery.LevelUnit || got.Toolchain != "go1.26.0" {
got, found, err := k.Lookup(ctx, buildstep.LevelUnit, digA)
if err != nil || !found || got.Package != "m/a" || got.Tests != 3 || got.Level != buildstep.LevelUnit || got.Toolchain != "go1.26.0" {
t.Fatalf("Lookup = %+v, %v, %v", got, found, err)
}
}
@@ -40,14 +42,14 @@ func TestAPassIsKeptByACreateUnderItsLevelAndDigest(t *testing.T) { // 1287/AC1
func TestASecondKeepUnderTheSameDigestIsRefusedAndTheFirstStands(t *testing.T) { // 1287/AC2
ctx, st := context.Background(), newMemKV()
k := Kept{Store: st}
if made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3)); err != nil || !made {
if made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3)); err != nil || !made {
t.Fatal(made, err)
}
made, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/other", 9))
made, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/other", 9))
if err != nil || made {
t.Fatalf("the second Keep = %v, %v, want refused without an error", made, err)
}
got, _, _ := k.Lookup(ctx, delivery.LevelUnit, digA)
got, _, _ := k.Lookup(ctx, buildstep.LevelUnit, digA)
if got.Package != "m/a" || got.Tests != 3 {
t.Errorf("the first value did not stand: %+v", got)
}
@@ -103,14 +105,14 @@ func TestOnlyTheSkippingPackageOfAResultIsWithheld(t *testing.T) { // issue 890
func TestAKeptValueOfAnotherDigestIsNotRead(t *testing.T) { // 1287/AC7
ctx, st := context.Background(), newMemKV()
k := Kept{Store: st}
if _, err := k.Keep(ctx, delivery.LevelUnit, digA, keptValue("m/a", 3)); err != nil {
if _, err := k.Keep(ctx, buildstep.LevelUnit, digA, keptValue("m/a", 3)); err != nil {
t.Fatal(err)
}
// A toolchain change changes the digest, so the new digest finds nothing and the old key is not deleted.
if _, found, _ := k.Lookup(ctx, delivery.LevelUnit, digB); found {
if _, found, _ := k.Lookup(ctx, buildstep.LevelUnit, digB); found {
t.Error("a digest that was not kept was found")
}
if _, found, _ := k.Lookup(ctx, delivery.LevelIntegration, digA); found {
if _, found, _ := k.Lookup(ctx, buildstep.LevelIntegration, digA); found {
t.Error("a kept unit verdict was found at the integration level")
}
if keys, _ := st.Keys(ctx); len(keys) != 1 {
@@ -121,8 +123,8 @@ func TestAKeptValueOfAnotherDigestIsNotRead(t *testing.T) { // 1287/AC7
func TestAKeptVerdictThatIsNotAPassIsRefused(t *testing.T) {
k := Kept{Store: newMemKV()}
v := keptValue("m/a", 1)
v.State = delivery.VerdictFail
if _, err := k.Keep(context.Background(), delivery.LevelUnit, digA, v); err == nil {
v.State = meshdeliverycheckerclient.VerdictFail
if _, err := k.Keep(context.Background(), buildstep.LevelUnit, digA, v); err == nil {
t.Error("a failure was kept")
}
}
@@ -6,7 +6,9 @@ import (
"strings"
"unicode/utf8"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// MaxLine is the most characters a status's line has: the forge shows a status's description in a short space, and the full
@@ -33,8 +35,8 @@ func short12(s string) string {
}
// firstFailed is what failed first, in words: a conflict, a refused step or a test, named before anything else.
func firstFailed(v delivery.Verdict) string {
f := v.Failed
func firstFailed(v meshdeliverychecker.VerdictsByGroupHeadsValue) string {
f := failedOf(v)
switch {
case f.Test != "":
if w := firstWordsLine(f.Words); w != "" {
@@ -51,7 +53,7 @@ func firstFailed(v delivery.Verdict) string {
// failedLines is everything a verdict names as failed, one line each: every failed test with its first line (issue
// 1921), or what failed first when the verdict names no list (a conflict, a refused step, an older verdict).
func failedLines(v delivery.Verdict) []string {
func failedLines(v meshdeliverychecker.VerdictsByGroupHeadsValue) []string {
if len(v.Failures) == 0 {
if f := firstFailed(v); f != "" {
return []string{f}
@@ -60,7 +62,7 @@ func failedLines(v delivery.Verdict) []string {
}
lines := make([]string, 0, len(v.Failures))
for _, f := range v.Failures {
lines = append(lines, firstFailed(delivery.Verdict{Failed: f}))
lines = append(lines, firstFailed(meshdeliverychecker.VerdictsByGroupHeadsValue{Failed: &FailedFirst{Machine: f.Machine, Test: f.Test, Words: f.Words}}))
}
return lines
}
@@ -68,7 +70,7 @@ func failedLines(v delivery.Verdict) []string {
// failedHead is the head of a failed verdict's line: what failed first, and when more did, how many failed and "and N
// more" after it (the operator, 2026-11-02), so a line cut to MaxLine still says that more failed. The words of the first
// are cut to fit room characters when room is not negative; the count and "and N more" are never cut.
func failedHead(v delivery.Verdict, room int) string {
func failedHead(v meshdeliverychecker.VerdictsByGroupHeadsValue, room int) string {
first := firstFailed(v)
n := failedCount(v)
if n <= 1 {
@@ -93,13 +95,13 @@ func clipRunes(s string, n int) string {
return string(r[:n-1]) + "…"
}
// failedCount is how many failures a verdict names, the ones after delivery.MaxFailures counted too.
func failedCount(v delivery.Verdict) int {
// failedCount is how many failures a verdict names, the ones after meshdeliverycheckerclient.MaxFailures counted too.
func failedCount(v meshdeliverychecker.VerdictsByGroupHeadsValue) int {
n := len(v.Failures)
if n > delivery.MaxFailures {
if n > meshdeliverycheckerclient.MaxFailures {
var more int
if _, err := fmt.Sscanf(v.Failures[n-1].Test, "and %d more", &more); err == nil {
return delivery.MaxFailures + more
return meshdeliverycheckerclient.MaxFailures + more
}
}
return n
@@ -150,7 +152,7 @@ func (p pieces) join(drop int) string {
return joinSet(parts...)
}
func levelWords(name string, r delivery.LevelResult) string {
func levelWords(name string, r meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue) string {
s := fmt.Sprintf("%s %d run, %d kept", name, r.Run, r.Kept)
if r.Skipped > 0 {
s += fmt.Sprintf(", %d skipped", r.Skipped)
@@ -160,14 +162,16 @@ func levelWords(name string, r delivery.LevelResult) string {
// compositionWords says the composition: not yet run, or once the engines answered (ADR 0593 §7) passed, with the nodes
// that gave no answer, or failed, with the time it took and the machines it composed (issue 1300).
func compositionWords(c *delivery.Composition, took int64) string { return composition(c, took, false) }
func compositionWords(c *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, took int64) string {
return composition(c, took, false)
}
// composition is compositionWords; short counts the machines and leaves the SDK commit to the report.
func composition(c *delivery.Composition, took int64, short bool) string {
if c != nil && c.State == delivery.CompositionNotRun && c.Words != "" {
func composition(c *meshdeliverychecker.VerdictsByGroupHeadsValueComposition, took int64, short bool) string {
if c != nil && c.State == meshdeliverycheckerclient.CompositionNotRun && c.Words != "" {
return "composition not judged: " + c.Words
}
if c == nil || c.State == delivery.CompositionNotRun {
if c == nil || c.State == meshdeliverycheckerclient.CompositionNotRun {
return "composition not yet run"
}
in := ""
@@ -186,14 +190,14 @@ func composition(c *delivery.Composition, took int64, short bool) string {
}
}
warned := ""
if c.State == delivery.CompositionPass && c.Words != "" {
if c.State == meshdeliverycheckerclient.CompositionPass && c.Words != "" {
warned = " (" + c.Words + ")" // a node that did not answer (ADR 0593 §9)
}
switch {
case c.State == delivery.CompositionPass && short:
case c.State == meshdeliverycheckerclient.CompositionPass && short:
return "composition passed" + in + on + warned // what it composed with is in the report
case c.State == delivery.CompositionPass:
return "composition passed" + in + " with " + short12(c.SDK) + on + warned
case c.State == meshdeliverycheckerclient.CompositionPass:
return "composition passed" + in + " with " + short12(c.Sdk) + on + warned
}
return "composition failed" + in + on + ": " + c.Words
}
@@ -211,7 +215,7 @@ func buildWords(builds []BuildNote) string {
// carriedWords say the commits a verdict was carried from, for the line of pull request p (ADR 0548 §8): its own trunk's
// commit as twelve characters, a sibling's with its name before it; only those that moved, sorted. Empty when none did.
func carriedWords(from map[string]string, v delivery.Verdict, cs *Case, p Pull) string {
func carriedWords(from map[string]string, v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull) string {
var own string
var others []string
for r, old := range from {
@@ -243,7 +247,7 @@ func carriedWords(from map[string]string, v delivery.Verdict, cs *Case, p Pull)
}
// parts makes the pieces of the line of a pull request of a case.
func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces {
func parts(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) pieces {
var pc pieces
pc.judged = judged[pullKey(p)]
pc.series = seriesWords(cs, p)
@@ -252,7 +256,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
}
commits := short12(p.Head) + " on " + short12(p.Trunk)
switch v.State {
case delivery.VerdictRunning:
case meshdeliverycheckerclient.VerdictRunning:
pc.head = "checking " + commits
if cs != nil && cs.CarriedFrom != nil {
// Out of date and being carried to the new commit (ADR 0548 §6): pending, and saying so.
@@ -261,7 +265,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
pc.carried = "from " + w
}
}
case delivery.VerdictPass:
case meshdeliverycheckerclient.VerdictPass:
pc.head = "pass " + commits
if w := carriedWords(v.CarriedFrom, v, cs, p); w != "" {
pc.carried = "carried from " + w // the counts that follow say what the carry ran and kept
@@ -271,16 +275,16 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
pc.commits = commits
}
var counts []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} {
if r, ok := v.Levels[level]; ok {
counts = append(counts, levelWords(level, r))
}
}
pc.counts = strings.Join(counts, " | ")
var short []string
for _, level := range []string{delivery.LevelUnit, delivery.LevelIntegration} {
for _, level := range []string{buildstep.LevelUnit, buildstep.LevelIntegration} {
if r, ok := v.Levels[level]; ok {
name := map[string]string{delivery.LevelUnit: "unit", delivery.LevelIntegration: "int"}[level]
name := map[string]string{buildstep.LevelUnit: "unit", buildstep.LevelIntegration: "int"}[level]
w := fmt.Sprintf("%s %d run %d kept", name, r.Run, r.Kept)
if r.Skipped > 0 {
w += fmt.Sprintf(" %d skipped", r.Skipped)
@@ -289,8 +293,8 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
}
}
pc.shortCounts = strings.Join(short, " | ")
if v.State != delivery.VerdictRunning {
t := v.Took
if v.State != meshdeliverycheckerclient.VerdictRunning {
t := tookOf(v)
var times []string
for _, x := range []struct {
name string
@@ -329,13 +333,13 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
}
}
pc.builds = buildWords(builds)
if v.State != delivery.VerdictRunning {
pc.composition = compositionWords(v.Composition, v.Took.Composition)
pc.shortComposition = composition(v.Composition, v.Took.Composition, true)
if v.State != meshdeliverycheckerclient.VerdictRunning {
pc.composition = compositionWords(v.Composition, tookOf(v).Composition)
pc.shortComposition = composition(v.Composition, tookOf(v).Composition, true)
}
if cs != nil && len(cs.Pulls) > 1 {
pc.group = fmt.Sprintf("group %s of %d pull requests", cs.Group, len(cs.Pulls))
if v.State != delivery.VerdictPass {
if v.State != meshdeliverycheckerclient.VerdictPass {
pc.group = fmt.Sprintf("%s#%d in group %s", p.Repository, p.Number, cs.Group)
}
}
@@ -343,7 +347,7 @@ func parts(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []s
}
// Report is the full line of a pull request's status, with no limit: what the read tools and the full report say.
func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
func Report(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
return parts(v, cs, p, builds, slowest, judged).join(0)
}
@@ -355,9 +359,9 @@ func Report(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []
// builds, then the group drop first, then the times are given in their compact form, then dropped, and the line is cut at last.
// The program that judged is never dropped: a line naming it gives the counts in their compact form, then drops them and
// what follows, and is cut in its first part rather than lose it.
func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
func Line(v meshdeliverychecker.VerdictsByGroupHeadsValue, cs *Case, p Pull, builds []BuildNote, slowest []string, judged map[string]string) string {
pc := parts(v, cs, p, builds, slowest, judged)
if v.State == delivery.VerdictFail {
if v.State == meshdeliverycheckerclient.VerdictFail {
// The head gives way first to the commits and what is never dropped: its count and "and N more" stay whole.
kept := joinSet(pc.commits, pc.carried, pc.warning, pc.judged, pc.series)
pc.head = failedHead(v, MaxLine-utf8.RuneCountInString(" | "+kept))
@@ -379,7 +383,7 @@ func Line(v delivery.Verdict, cs *Case, p Pull, builds []BuildNote, slowest []st
return string(head[:room]) + "…" + rest
}
}
if v.State == delivery.VerdictFail {
if v.State == meshdeliverycheckerclient.VerdictFail {
// A failure's head, sized above, and the commits fit: the counts and the composition are in the report.
if s := joinSet(pc.head, pc.commits, pc.carried, pc.warning, pc.judged, pc.series); utf8.RuneCountInString(s) <= MaxLine {
return s
@@ -6,19 +6,22 @@ import (
"testing"
"unicode/utf8"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
func lineCase(t testing.TB, pulls ...delivery.CandidatePull) (*Case, Pull) {
func lineCase(t testing.TB, pulls ...meshcatalog.CandidatesByGroupValuePullsItem) (*Case, Pull) {
cs := caseOf(t, pulls...)
return cs, cs.Pulls[0]
}
func passVerdict() delivery.Verdict {
return delivery.Verdict{State: delivery.VerdictPass,
Levels: map[string]delivery.LevelResult{delivery.LevelUnit: {Run: 12, Kept: 301}, delivery.LevelIntegration: {Run: 3, Kept: 40}},
Took: delivery.Took{Queue: 800, Clone: 4100, Unit: 400, IntegrationSetup: 3200, IntegrationRun: 12000},
Composition: &delivery.Composition{State: delivery.CompositionNotRun}}
func passVerdict() meshdeliverychecker.VerdictsByGroupHeadsValue {
return meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictPass,
Levels: map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{buildstep.LevelUnit: {Run: 12, Kept: 301}, buildstep.LevelIntegration: {Run: 3, Kept: 40}},
Took: &meshdeliverychecker.VerdictsByGroupHeadsValueTook{Queue: 800, Clone: 4100, Unit: 400, IntegrationSetup: 3200, IntegrationRun: 12000},
Composition: &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionNotRun}}
}
func TestAPassOpensWithTheVerdictAndSaysTheCommitsTheCountsTheTimesAndThatTheCompositionWasNotRun(t *testing.T) { // 1300/AC1
@@ -44,17 +47,17 @@ func TestAPassOpensWithTheVerdictAndSaysTheCommitsTheCountsTheTimesAndThatTheCom
func TestAFailureNamesTheTestTheConflictOrTheRefusedStepFirst(t *testing.T) { // 1300/AC2
cs, p := lineCase(t, pullOf(repoA, 7, head1))
long := delivery.FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + "TestSomething", Words: strings.Repeat("because ", 40)}
long := FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + "TestSomething", Words: strings.Repeat("because ", 40)}
for name, c := range map[string]struct {
v delivery.Verdict
v meshdeliverychecker.VerdictsByGroupHeadsValue
want string
}{
"test": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"},
"conflict": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"},
"refused": {delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"},
"test": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "unit: m/a.TestX", Words: "want 4"}}, "failed: unit: m/a.TestX: want 4"},
"conflict": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "shanks", Words: "novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"}}, "failed: shanks: novox/mesh-delivery#7 does not merge into the trunk: files in conflict: go.mod"},
"refused": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Machine: "mesh-build-go", Words: "no go build node qualifies: builders disagree"}}, "failed: mesh-build-go: no go build node qualifies"},
// ADR 0591: a first failure longer than the forge's limit is shortened; "failed (N)" and "and N-1 more" are never cut.
"many, the first too long": {delivery.Verdict{State: delivery.VerdictFail, Failed: long, Failures: []delivery.FailedFirst{long, {Machine: "g14", Words: "route-proxy is defined twice"},
{Test: "unit: m/b.TestY", Words: "boom"}}}, "failed (3): integration: pkg/pkg/"},
"many, the first too long": {meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &long, Failures: failureItems([]FailedFirst{long, {Machine: "g14", Words: "route-proxy is defined twice"},
{Test: "unit: m/b.TestY", Words: "boom"}})}, "failed (3): integration: pkg/pkg/"},
} {
line := Line(c.v, cs, p, nil, nil, nil)
if more := len(c.v.Failures) - 1; more > 0 && (!strings.Contains(line, fmt.Sprintf("…; and %d more | 111111111111", more)) || utf8.RuneCountInString(line) > MaxLine) {
@@ -79,7 +82,7 @@ func TestALevelOverItsBoundIsNamedWithItsSlowestTestsAndTheVerdictIsNotChanged(t
func TestSkippedTestsAreSaidAndNeverAFlatPass(t *testing.T) { // 1300/AC6
cs, p := lineCase(t, pullOf(repoA, 7, head1))
v := passVerdict()
v.Levels[delivery.LevelUnit] = delivery.LevelResult{Run: 12, Kept: 301, Skipped: 2}
v.Levels[buildstep.LevelUnit] = meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue{Run: 12, Kept: 301, Skipped: 2}
if line := Line(v, cs, p, nil, nil, nil); !strings.Contains(line, "unit 12 run, 301 kept, 2 skipped") {
t.Errorf("line = %q", line)
}
@@ -108,7 +111,7 @@ func TestTheBuildsOfTheTouchedModulesAreSaidAndDroppedFirstWhenTheLineIsTooLong(
t.Errorf("line %q is too long", line)
}
short := passVerdict()
short.Took = delivery.Took{}
short.Took = &meshdeliverychecker.VerdictsByGroupHeadsValueTook{}
if l := Line(short, cs, p, builds[:1], nil, nil); !strings.Contains(l, "builds: avahi built") {
t.Errorf("a short line keeps its builds: %q", l)
}
@@ -116,7 +119,7 @@ func TestTheBuildsOfTheTouchedModulesAreSaidAndDroppedFirstWhenTheLineIsTooLong(
func TestALineIsNeverLongerThanTheLimit(t *testing.T) { // 1300/AC1
cs, p := lineCase(t, pullOf(repoA, 7, head1))
v := delivery.Verdict{State: delivery.VerdictFail, Failed: delivery.FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + ".TestSomething", Words: strings.Repeat("because ", 40)}}
v := meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &FailedFirst{Test: "integration: " + strings.Repeat("pkg/", 50) + ".TestSomething", Words: strings.Repeat("because ", 40)}}
if line := Line(v, cs, p, nil, nil, nil); utf8.RuneCountInString(line) > MaxLine || !strings.HasPrefix(line, "failed: integration: ") {
t.Errorf("line = %q (%d)", line, utf8.RuneCountInString(line))
}
@@ -124,7 +127,7 @@ func TestALineIsNeverLongerThanTheLimit(t *testing.T) { // 1300/AC1
func TestARunningCheckIsPending(t *testing.T) {
cs, p := lineCase(t, pullOf(repoA, 7, head1))
if line := Line(delivery.Verdict{State: delivery.VerdictRunning}, cs, p, nil, nil, nil); line != "checking 111111111111 on aaaaaaaaaaaa" {
if line := Line(meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictRunning}, cs, p, nil, nil, nil); line != "checking 111111111111 on aaaaaaaaaaaa" {
t.Errorf("line = %q", line)
}
}
@@ -132,7 +135,7 @@ func TestARunningCheckIsPending(t *testing.T) {
func TestThePassedCompositionIsSaidOnlyWhenItWasRun(t *testing.T) { // ADR 0484 clause 6
cs, p := lineCase(t, pullOf(repoA, 7, head1))
v := passVerdict()
v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "afbe371003a570f088be4a394d83aa7a9458f7ab"}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "afbe371003a570f088be4a394d83aa7a9458f7ab"}
if r := Report(v, cs, p, nil, nil, nil); !strings.Contains(r, "composition passed with afbe371003a5") {
t.Errorf("report = %q", r)
}
@@ -146,7 +149,7 @@ func TestTheCompositionsTimeAndMachinesAreSaidOnceTheAssignerReportsThem(t *test
cs, p := lineCase(t, pullOf(repoA, 7, head1))
v := passVerdict()
v.Took.Composition = 2400
v.Composition = &delivery.Composition{State: delivery.CompositionPass, SDK: "afbe371003a570f088be4a394d83aa7a9458f7ab", Machines: []string{"g14", "ace", "novox", "shanks"}}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionPass, Sdk: "afbe371003a570f088be4a394d83aa7a9458f7ab", Machines: []string{"g14", "ace", "novox", "shanks"}}
r := Report(v, cs, p, nil, nil, nil)
for _, want := range []string{"composition passed in 2.4s with afbe371003a5 on 4 machines: ace, g14, novox, shanks"} {
if !strings.Contains(r, want) {
@@ -156,8 +159,8 @@ func TestTheCompositionsTimeAndMachinesAreSaidOnceTheAssignerReportsThem(t *test
if l := Line(v, cs, p, nil, nil, nil); !strings.Contains(l, "composition passed in 2.4s") || !strings.Contains(l, "on 4 machines") || utf8.RuneCountInString(l) > MaxLine {
t.Errorf("line = %q", l)
}
v.State, v.Failed = delivery.VerdictFail, delivery.FailedFirst{Machine: "novox", Words: "composition refused"}
v.Composition = &delivery.Composition{State: delivery.CompositionFail, Words: "novox: two seats claim dns", Machines: []string{"novox"}}
v.State, v.Failed = meshdeliverycheckerclient.VerdictFail, &FailedFirst{Machine: "novox", Words: "composition refused"}
v.Composition = &meshdeliverychecker.VerdictsByGroupHeadsValueComposition{State: meshdeliverycheckerclient.CompositionFail, Words: "novox: two seats claim dns", Machines: []string{"novox"}}
if r := Report(v, cs, p, nil, nil, nil); !strings.Contains(r, "composition failed in 2.4s on novox: novox: two seats claim dns") {
t.Errorf("failed composition report = %q", r)
}
@@ -169,7 +172,7 @@ func TestTheLineSaysTheLintTime(t *testing.T) {
test := b.test
b.test = func(c stepCall) (any, error) {
r, err := test(c)
if res, ok := r.(Tested); ok && res.Level == delivery.LevelUnit {
if res, ok := r.(Tested); ok && res.Level == buildstep.LevelUnit {
res.Lint = 1.5
return res, err
}
@@ -3,9 +3,10 @@ package check
import (
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
sdkrun "git.novox.be/novox/mesh-delivery/sdk/buildrun"
)
// **An ask the build seat gave up on ends its step** (novox/hq issue 734): mesh-sdk buildrun.LostAskWaiter hands the
@@ -22,18 +23,18 @@ const LostAskTries = 3
// IsLostAsk says whether a verdict failed because its ask judged nothing: the build seat gave it up (buildrun.LostWords), or
// the build module that ran its step was replaced under it, and its next process ended the step it left running
// (buildstep.RestartReason; a delivery of the build module, as on 2026-10-28). Neither is the change's failure: the check
// (sdkrun.RestartReason; a delivery of the build module, as on 2026-10-28). Neither is the change's failure: the check
// is asked again, as for a given-up ask.
func IsLostAsk(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && (strings.Contains(v.Failed.Words, buildrun.LostWords) ||
strings.Contains(v.Failed.Words, buildstep.RestartReason))
func IsLostAsk(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && (strings.Contains(failedOf(v).Words, buildrun.LostWords) ||
strings.Contains(failedOf(v).Words, sdkrun.RestartReason))
}
// IsNotTaken says whether a verdict failed because no build node took its ask within the bound (buildrun.NotTakenWords).
func IsNotTaken(v delivery.Verdict) bool {
return v.State == delivery.VerdictFail && strings.Contains(v.Failed.Words, buildrun.NotTakenWords)
func IsNotTaken(v meshdeliverychecker.VerdictsByGroupHeadsValue) bool {
return v.State == meshdeliverycheckerclient.VerdictFail && strings.Contains(failedOf(v).Words, buildrun.NotTakenWords)
}
// HearingBucket is the checker's state hearing: whether it hears DEAD_LETTERS (buildrun.Hearing under
// buildrun.HearingKey), which checker_status says (dead_letters).
const HearingBucket = delivery.CheckerModule + "_" + buildrun.HearingState
const HearingBucket = CheckerModule + "_" + buildrun.HearingState
@@ -8,9 +8,11 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
sdkrun "git.novox.be/novox/mesh-delivery/sdk/buildrun"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// losingAsks is the build seat over a bus that gives up the first `lose` asks put (DEAD_LETTERS keeps them, as on
@@ -18,7 +20,7 @@ import (
// gives up (an ask no node takes).
type losingAsks struct {
*buildrun.FakeAsks
answer func(delivery.StepAsk) delivery.Step
answer func(buildstep.StepAsk) buildstep.Step
dead *buildrun.DeadLetters
never bool
@@ -47,7 +49,7 @@ func (l *losingAsks) PublishAsk(ctx context.Context, subject, id string, data []
}
go l.dead.Heard(g)
default:
a, err := delivery.DecodeStepAsk(data)
a, err := buildstep.DecodeStepAsk(data)
if err != nil {
return err
}
@@ -146,32 +148,33 @@ func TestAnAskGivenUpOrNeverTakenEndsItsTryAsErrorAndItsHeadIsAskedAnew(t *testi
}
// troubled is the build seat as a check meets it when its step's end does not reach its wait: the first `restarts` asks'
// steps are ended by the build module's next process (buildstep.RestartReason), and the first `deaf` asks' watches hear
// steps are ended by the build module's next process (sdkrun.RestartReason), and the first `deaf` asks' watches hear
// nothing though their steps end (the lost check of 2026-10-28). Its steps state is read through AskSteps and Steps.
type troubled struct {
*buildrun.FakeAsks
answer func(delivery.StepAsk) delivery.Step
answer func(buildstep.StepAsk) buildstep.Step
mu sync.Mutex
restarts, deaf int
deafIDs map[string]bool
steps map[string]delivery.Step
steps map[string]buildstep.Step
}
func (tr *troubled) PublishAsk(ctx context.Context, subject, id string, data []byte) error {
if err := tr.FakeAsks.PublishAsk(ctx, subject, id, data); err != nil {
return err
}
a, err := delivery.DecodeStepAsk(data)
a, err := buildstep.DecodeStepAsk(data)
if err != nil {
return err
}
step := tr.answer(a)
step.What = buildstep.AskWhat(a)
read, _ := buildstep.AskAs[buildstep.Ask](a)
step.What = sdkrun.AskWhat(read)
tr.mu.Lock()
if tr.restarts > 0 {
tr.restarts--
step.State, step.Reason, step.Result = delivery.StepFailed, buildstep.RestartReason, nil
step.State, step.Reason, step.Result = buildstep.StepFailed, sdkrun.RestartReason, nil
}
deaf := tr.deaf > 0
if deaf {
@@ -186,21 +189,21 @@ func (tr *troubled) PublishAsk(ctx context.Context, subject, id string, data []b
return nil
}
func (tr *troubled) WatchAsk(ctx context.Context, module, askID string) (<-chan delivery.Step, error) {
func (tr *troubled) WatchAsk(ctx context.Context, module, askID string) (<-chan buildstep.Step, error) {
tr.mu.Lock()
deaf := tr.deafIDs[askID]
tr.mu.Unlock()
if deaf {
return make(chan delivery.Step), nil // open, and silent for good
return make(chan buildstep.Step), nil // open, and silent for good
}
return tr.FakeAsks.WatchAsk(ctx, module, askID)
}
func (tr *troubled) AskSteps(_ context.Context, _, askID string) ([]delivery.Step, error) {
func (tr *troubled) AskSteps(_ context.Context, _, askID string) ([]buildstep.Step, error) {
tr.mu.Lock()
defer tr.mu.Unlock()
if s, ok := tr.steps[askID]; ok {
return []delivery.Step{s}, nil
return []buildstep.Step{s}, nil
}
return nil, nil
}
@@ -212,9 +215,9 @@ func (tr *troubled) Steps(running bool) StateRead {
vals := map[string]json.RawMessage{}
for id, s := range tr.steps {
if running {
s.State, s.Ended, s.Started = delivery.StepRunning, time.Time{}, time.Now()
s.State, s.Ended, s.Started = buildstep.StepRunning, time.Time{}, time.Now()
}
raw, _ := delivery.EncodeStep(s)
raw, _ := buildstep.EncodeStep(s)
vals[s.Node+"."+id] = raw
}
return StateRead{Keys: func() ([]string, error) {
@@ -262,13 +265,13 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
state string
asks int
}{
{name: "given up once", lose: 1, state: delivery.VerdictPass, asks: 2},
{name: "given up every time", lose: LostAskTries, state: delivery.VerdictFail, asks: LostAskTries},
{name: "its build module replaced under it", restarts: 1, state: delivery.VerdictPass, asks: 2},
{name: "replaced under every ask", restarts: LostAskTries, state: delivery.VerdictFail, asks: LostAskTries},
{name: "its watch deaf, a step of its build module ends", deaf: 1, how: "nudge", state: delivery.VerdictPass, asks: 1},
{name: "its watch deaf, asked again by hand", deaf: 1, how: "hand", state: delivery.VerdictPass, asks: 2},
{name: "its watch deaf, asked again by hand after its build module replaced it", deaf: 1, restarts: 1, how: "hand", state: delivery.VerdictPass, asks: 2},
{name: "given up once", lose: 1, state: meshdeliverycheckerclient.VerdictPass, asks: 2},
{name: "given up every time", lose: LostAskTries, state: meshdeliverycheckerclient.VerdictFail, asks: LostAskTries},
{name: "its build module replaced under it", restarts: 1, state: meshdeliverycheckerclient.VerdictPass, asks: 2},
{name: "replaced under every ask", restarts: LostAskTries, state: meshdeliverycheckerclient.VerdictFail, asks: LostAskTries},
{name: "its watch deaf, a step of its build module ends", deaf: 1, how: "nudge", state: meshdeliverycheckerclient.VerdictPass, asks: 1},
{name: "its watch deaf, asked again by hand", deaf: 1, how: "hand", state: meshdeliverycheckerclient.VerdictPass, asks: 2},
{name: "its watch deaf, asked again by hand after its build module replaced it", deaf: 1, restarts: 1, how: "hand", state: meshdeliverycheckerclient.VerdictPass, asks: 2},
} {
t.Run(row.name, func(t *testing.T) {
b := newBench(t)
@@ -280,7 +283,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
asked = func() int { return len(l.Asks()) }
} else {
tr = &troubled{FakeAsks: &buildrun.FakeAsks{}, answer: b.answer, restarts: row.restarts, deaf: row.deaf,
deafIDs: map[string]bool{}, steps: map[string]delivery.Step{}}
deafIDs: map[string]bool{}, steps: map[string]buildstep.Step{}}
heard = &buildrun.HeardEnds{Inner: tr, Read: tr}
b.ex.Asks = buildrun.Asker{Publisher: tr, Waiter: heard, Bound: 5 * time.Second}
asked = func() int { return len(tr.Asks()) }
@@ -295,7 +298,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
for deadline := time.Now().Add(10 * time.Second); heard.Waiting() == 0 && time.Now().Before(deadline); {
time.Sleep(2 * time.Millisecond)
}
if v := r.verdict(t, key); v.State != delivery.VerdictRunning {
if v := r.verdict(t, key); v.State != meshdeliverycheckerclient.VerdictRunning {
t.Fatalf("a check whose step ended unheard = %s, want running until it is read", v.State)
}
}
@@ -305,7 +308,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
heard.Nudge(buildrun.GoBuildModule)
case "hand":
cands := newMemKV()
ckey, _ := delivery.CandidatesKey("feat/x")
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
_, _ = cands.Create(ctx, ckey, cand)
none := StateRead{Keys: func() ([]string, error) { return nil, nil }, Get: func(string) (json.RawMessage, error) { return nil, nil }}
rd := Reader{CandidateState: kvRead(cands), VerdictState: kvRead(r.verdicts), AskState: kvRead(asksKept),
@@ -336,7 +339,7 @@ func TestACheckWhoseAskJudgedNothingIsAskedAgainOrRead(t *testing.T) {
if _, _, kept, _ := asksKept.Get(ctx, key); kept {
t.Errorf("the asks of the ended check are still kept under %s", key)
}
if row.state == delivery.VerdictFail && (!IsLostAsk(v) || !IsNodesFailure(v)) {
if row.state == meshdeliverycheckerclient.VerdictFail && (!IsLostAsk(v) || !IsNodesFailure(v)) {
t.Errorf("a check that judged nothing every time = %q, want lost and judged again when the nodes change", v.Failed.Words)
}
if row.how == "hand" {
@@ -9,10 +9,11 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/composer"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/packagename"
)
@@ -20,7 +21,7 @@ import (
// A change's manifests are judged by the indexer's judge (novox/hq ADR 0578 §2): the checker asks the Go build seat to
// build the judge's command (mesh-delivery's modules/mesh-delivery-indexer, cmd/mesh-judge) at mesh-delivery's trunk, or
// at the group's mesh-delivery member head when that member changes the judge, and to run it in its sandbox over the
// catalogue as the change leaves it, exactly as the composer is run (delivery.ComposeJob, ADR 0482 §4). A manifest the
// catalogue as the change leaves it, exactly as the composer is run (buildstep.ComposeJob, ADR 0482 §4). A manifest the
// change touches that the judge refuses fails the check, naming the manifest and each problem. The checker links no
// judge of its own.
@@ -106,7 +107,7 @@ func judgeRequest(trunk map[string]string, pulls []Pull) (req composer.Request,
// judgeManifests runs the indexer's judge over the case's manifests and fails the check for each touched manifest it
// refuses. A case that touches no manifest, or an examiner that reads no trunk manifests, judges nothing; a judge that
// cannot be asked or does not answer fails the check, said, never passes it.
func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
func (st *state) judgeManifests(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.j.Manifests == nil || st.c.Try != nil {
return nil
}
@@ -195,14 +196,14 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
// runJudge asks the Go build seat to build the judge at the commit and run it over the request, and reads its change and
// trunk cases; a failed verdict when it cannot be asked or does not answer.
func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string) (change, trunk composer.CaseVerdict, v *delivery.Verdict) {
func (st *state) runJudge(ctx context.Context, req composer.Request, touched []string, commit, at string) (change, trunk composer.CaseVerdict, v *meshdeliverychecker.VerdictsByGroupHeadsValue) {
raw, err := json.Marshal(req)
if err != nil {
return change, trunk, fail(goModule, err.Error())
}
job := delivery.ComposeJob{Fetch: map[string]any{"repository": DeliveryRepository, "commit": commit}, Dir: JudgeDir,
job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: DeliveryRepository, Commit: commit}, Dir: JudgeDir,
Package: JudgePackage, Request: raw, Reader: "the indexer's judge at " + at}
a := delivery.StepAsk{ID: buildstep.NewID(), Kind: delivery.AskCheck, Language: delivery.AskGo, Asker: delivery.CheckerModule,
a := buildstep.StepAsk{ID: buildstep.NewID(), Kind: buildstep.AskCheck, Language: buildstep.AskGo, Asker: CheckerModule,
At: st.j.now().UTC(), Compose: &job}
st.c.Progress.Set(fmt.Sprintf("the judge ask %s of the manifests of %s", a.ID, strings.Join(touched, ", ")), "")
st.c.Asked(a.ID)
@@ -231,7 +232,7 @@ const CatalogSeat = "mesh-catalog"
// as well. The verdicts read are the trunk's manifest validator's: its change case, where refused, held (not accepted
// with no reason) or with a field set aside (a field the running manifest validator does not know waits, ADR 0601 §3)
// each fail it, naming the module and the problems.
func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *delivery.Verdict {
func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Request, touched []string, cv composer.CaseVerdict, judgedAt string) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.j.Read == nil {
return nil
}
@@ -258,7 +259,7 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
if trunk == "" {
return fail(goModule, "the new indexer cannot be judged by the trunk's manifest validator: the trunk head of "+DeliveryRepository+" is not known")
}
var v *delivery.Verdict
var v *meshdeliverychecker.VerdictsByGroupHeadsValue
judgedAt = "mesh-delivery's trunk " + trunk
if cv, _, v = st.runJudge(ctx, req, touched, trunk, judgedAt); v != nil {
return v
@@ -285,11 +286,11 @@ func (st *state) refuseUnmovableIndexer(ctx context.Context, req composer.Reques
}
// judgeAnswer reads the judge's change and trunk cases from its step, or why it cannot.
func judgeAnswer(step delivery.Step) (change, trunk composer.CaseVerdict, why string) {
if step.State != delivery.StepPassed {
func judgeAnswer(step buildstep.Step) (change, trunk composer.CaseVerdict, why string) {
if step.State != buildstep.StepPassed {
return change, trunk, fmt.Sprintf("its step %s: %s", step.State, step.Reason)
}
var a delivery.ComposeAnswer
var a buildstep.ComposeAnswer
if err := json.Unmarshal(step.Result, &a); err != nil {
return change, trunk, "its step's answer cannot be read: " + err.Error()
}
@@ -400,7 +401,7 @@ func (st *state) knownPackages(ctx context.Context) []string {
}
dirs := []string{d.dir}
if d.dir == "" {
dirs = []string{"modules/" + d.module, delivery.RootPath}
dirs = []string{"modules/" + d.module, meshcatalogclient.RootPath}
}
var why []string
for _, dir := range dirs {
@@ -0,0 +1,90 @@
package check
import (
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshwatchdogs"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
checker "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// The holders and buckets the checker names. No contract names a holder (novox/hq ADR 0625 §4, ADR 0634 §1), so these are its
// own constants, each a state of a seat (the seat's state name beside the holder's module name) and each declared in its
// manifest (module.json: its own states, its `reads`); a unit row checks them against the manifest (TestTheNamesAreTheManifests).
const (
// CheckerModule is this part's module; its states verdicts, kept, owed and previews are the seat mesh-delivery-checker's.
CheckerModule = "mesh-delivery-checker"
CheckerVerdictsBucket = CheckerModule + "_" + checker.StateVerdicts
CheckerKeptBucket = CheckerModule + "_" + checker.StateKept
CheckerOwedBucket = CheckerModule + "_" + checker.StateOwed
PreviewsBucket = CheckerModule + "_" + checker.StatePreviews
// GateModule holds the seat mesh-delivery's states source and turns, which the checker reads (reads: mesh-delivery-gate.source,
// mesh-delivery-gate.turns).
GateModule = "mesh-delivery-gate"
SourceBucket = GateModule + "_" + meshdelivery.StateSource
TurnsBucket = GateModule + "_" + meshdelivery.StateTurns
// IndexerModule holds the seat mesh-catalog's states, which the checker follows by their views (consumes:
// mesh-delivery-indexer.views.*).
IndexerModule = meshcatalogclient.Module
CandidatesBucket = IndexerModule + "_" + meshcatalog.StateCandidates
ModulesBucket = IndexerModule + "_" + meshcatalog.StateModules
ManifestsBucket = IndexerModule + "_" + meshcatalog.StateManifests
IndexerPredefinedState = meshcatalog.StatePredefined
// WatchmanModule holds the seat mesh-watchdogs' presence (reads: mesh-delivery-watchman.presence).
WatchmanModule = "mesh-delivery-watchman"
PresenceBucket = WatchmanModule + "_" + meshwatchdogs.StatePresence
// HaveBucket is the node-engine's state have (reads: node-engine.have); a node's own key ends in HaveNodeWord.
HaveBucket = "node-engine_" + nodeengine.StateHave
HaveNodeWord = "node"
// compositionWaits is the state of the assigner's composition answer that waits for what it composes from.
compositionWaits = "waits"
)
// FailedFirst names what failed: a test (its package and name) or a machine, with the words of the failure. It is the shape of
// the verdict's `failed` and of each entry of its `failures` (the seat's two types of it have the same fields), so the check
// judges with one and converts at the verdict's edge (failureItems, failedList).
type FailedFirst = checker.VerdictsByGroupHeadsValueFailed
// failedOf is the first failure a verdict names: the zero value of a verdict that names none.
func failedOf(v checker.VerdictsByGroupHeadsValue) FailedFirst {
if v.Failed == nil {
return FailedFirst{}
}
return *v.Failed
}
// tookOf is the time of each part of a check a verdict names: the zero value of a verdict that names none.
func tookOf(v checker.VerdictsByGroupHeadsValue) checker.VerdictsByGroupHeadsValueTook {
if v.Took == nil {
return checker.VerdictsByGroupHeadsValueTook{}
}
return *v.Took
}
func failureItems(list []FailedFirst) []checker.VerdictsByGroupHeadsValueFailuresItem {
if list == nil {
return nil
}
out := make([]checker.VerdictsByGroupHeadsValueFailuresItem, len(list))
for i, f := range list {
out[i] = checker.VerdictsByGroupHeadsValueFailuresItem(f)
}
return out
}
func failedList(items []checker.VerdictsByGroupHeadsValueFailuresItem) []FailedFirst {
if items == nil {
return nil
}
out := make([]FailedFirst, len(items))
for i, f := range items {
out[i] = FailedFirst(f)
}
return out
}
@@ -0,0 +1,49 @@
package check
import (
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// The checker's owed state holds the one act it owes, the status mesh/check on a pull request at a head: the gate's owed act
// with the context mesh/check. Its value has the shape of the gate's (the seat mesh-delivery's owed), so its rules and its
// codec are the mesh-delivery client's; only the checker reads it, so the checker's own narrowing to its one act lives here.
// CheckerOwedAct is the act the checker owes.
const CheckerOwedAct = meshdeliveryclient.OwedStatus + "=" + meshdeliveryclient.StatusContextCheck
// CheckerOwedKey is the key, in the checker's owed state, of the status the checker owes on a pull request at a head.
func CheckerOwedKey(repository string, number int, head string) (string, error) {
return meshdeliveryclient.OwedKey(repository, number, head, CheckerOwedAct)
}
func checkCheckerOwed(o meshdeliverychecker.OwedByRepositoryNumberHeadActValue) error {
if o.Act != meshdeliveryclient.OwedStatus || o.Context != meshdeliveryclient.StatusContextCheck {
return fmt.Errorf("the checker owes only a status with the context %s, not the act %q with the context %q", meshdeliveryclient.StatusContextCheck, o.Act, o.Context)
}
return nil
}
// EncodeCheckerOwed is o as the gate's owed value, refused for any act but the checker's status.
func EncodeCheckerOwed(o meshdeliverychecker.OwedByRepositoryNumberHeadActValue) ([]byte, error) {
if err := checkCheckerOwed(o); err != nil {
return nil, err
}
return meshdeliveryclient.EncodeOwed(meshdelivery.OwedByRepositoryNumberHeadActValue(o))
}
// DecodeCheckerOwed reads an owed act of the checker's state.
func DecodeCheckerOwed(data []byte) (meshdeliverychecker.OwedByRepositoryNumberHeadActValue, error) {
g, err := meshdeliveryclient.DecodeOwed(data)
if err != nil {
return meshdeliverychecker.OwedByRepositoryNumberHeadActValue{}, err
}
o := meshdeliverychecker.OwedByRepositoryNumberHeadActValue(g)
if err := checkCheckerOwed(o); err != nil {
return meshdeliverychecker.OwedByRepositoryNumberHeadActValue{}, err
}
return o, nil
}
@@ -5,11 +5,12 @@ import (
"errors"
"maps"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/trunk"
)
// **A check builds against the group's own package heads, not against what a member names** (novox/hq ADR 0573 §2, §4;
// issue 1585). Every Go check ask carries the version of each mesh package to build against (delivery.StepAsk.Packages):
// issue 1585). Every Go check ask carries the version of each mesh package to build against (buildstep.StepAsk.Packages):
// the head of the group's member that publishes it, else its latest published version as the indexer holds it. The build
// module writes them into the workspace, whatever version or commit a member's go.mod names, so no member is refused
// for naming no version of the group's package, an older head of it, a merge commit or a commit on no pull request: that
@@ -37,8 +38,8 @@ type Forge interface {
// also a package new in the group, which has no published version yet), else that member's head (mesh-sdk's Go module
// whenever mesh-sdk is a member). A package of repository itself comes with the tree the check fetched, and is left out.
// Nil for none.
func (st *state) packagesOf(repository string) delivery.PackageVersions {
versions := delivery.PackageVersions{}
func (st *state) packagesOf(repository string) buildstep.PackageVersions {
versions := buildstep.PackageVersions{}
if st.j.Published != nil {
maps.Copy(versions, st.j.Published.LatestPackages())
}
@@ -51,8 +52,8 @@ func (st *state) packagesOf(repository string) delivery.PackageVersions {
versions[pkg] = ""
}
for pkg := range versions {
repo, _, ok := delivery.PackageRepository(pkg)
if !ok || delivery.SameRepository(repo, repository) {
repo, _, ok := trunk.PackageRepository(pkg)
if !ok || trunk.SameRepository(repo, repository) {
delete(versions, pkg)
continue
}
@@ -11,21 +11,22 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// published is the latest published version of each mesh package, as the indexer's modules say (Examiner.Published).
type published delivery.PackageVersions
type published buildstep.PackageVersions
func (p published) LatestPackages() delivery.PackageVersions {
return maps.Clone(delivery.PackageVersions(p))
func (p published) LatestPackages() buildstep.PackageVersions {
return maps.Clone(buildstep.PackageVersions(p))
}
// packagesOfFetch is the package versions the fetch of repository was handed (delivery.StepAsk.Packages, issue 1585).
func packagesOfFetch(t *testing.T, b *bench, repository string) delivery.PackageVersions {
// packagesOfFetch is the package versions the fetch of repository was handed (buildstep.StepAsk.Packages, issue 1585).
func packagesOfFetch(t *testing.T, b *bench, repository string) buildstep.PackageVersions {
t.Helper()
for _, f := range b.steps.tool("fetch") {
if f.args["repository"] == repository {
@@ -50,7 +51,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
b.fetch.Modules = []string{"modules/m"}
b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA}
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict
if v.State != delivery.VerdictPass {
if v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
if got := packagesOfFetch(t, b, repoA); len(got) != 1 || got[SDKModulePath] != head2 {
@@ -61,7 +62,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
}
b = newBench(t)
b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA}
if v := b.judge(t, caseOf(t, pullOf("novox/mesh-catalog", 7, head1))).Verdict; v.State != delivery.VerdictPass {
if v := b.judge(t, caseOf(t, pullOf("novox/mesh-catalog", 7, head1))).Verdict; v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("a pull request alone: verdict = %+v", v)
}
if got := packagesOfFetch(t, b, "novox/mesh-catalog"); len(got) != 2 || got[SDKModulePath] != trunkS || got[contract] != trunkA {
@@ -76,27 +77,27 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}},
{Name: "go-gopackageregistry", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go/seats/gopackageregistry"}}}}}
digest, contractsDigest := strings.Repeat("c", 64), strings.Repeat("d", 64)
sdkPull := func() delivery.CandidatePull {
sdkPull := func() meshcatalog.CandidatesByGroupValuePullsItem {
p := pullOf(repoSDK, 12, head2)
p.Modules = []string{"mesh-sdk"}
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: digest}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("f", 64), Manifest: digest}}
return p
}
unpreviewed := func() delivery.CandidatePull { // a package new in the group: no preview carries its manifest yet
unpreviewed := func() meshcatalog.CandidatesByGroupValuePullsItem { // a package new in the group: no preview carries its manifest yet
p := sdkPull()
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64)}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-sdk": {Fp2: "src2:" + strings.Repeat("f", 64)}}
return p
}
contractsPull := func() delivery.CandidatePull {
contractsPull := func() meshcatalog.CandidatesByGroupValuePullsItem {
p := pullOf("novox/mesh-catalog", 4, strings.Repeat("3", 40))
p.Modules = []string{"mesh-contracts"}
p.Fps = map[string]delivery.CandidateModule{"mesh-contracts": {FP2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}}
p.Fps = map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"mesh-contracts": {Fp2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}}
return p
}
const sdkGo, contractsGo, seatGo = SDKModulePath, "git.novox.be/novox/mesh-catalog/go", "git.novox.be/novox/mesh-catalog/go/seats/gopackageregistry"
for _, row := range []struct {
name string
pulls []delivery.CandidatePull
pulls []meshcatalog.CandidatesByGroupValuePullsItem
published []string // the seats that publish the members' heads
wants int
fails string
@@ -107,19 +108,19 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
// noModule: the Go seat's published names no Go module path (ruling 1 on A2d's finding 1: never worked out).
noModule bool
}{
{name: "a group", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
{name: "a group", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2,
built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}, node: map[string]any{"name": "lib", "version": "0.1.15-feat-x.1"}},
{name: "a package new in the group, read at its head", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), unpreviewed()},
{name: "a package new in the group, read at its head", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), unpreviewed()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}},
{name: "one head, two Go packages (as mesh-contracts' head)", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), contractsPull()},
{name: "one head, two Go packages (as mesh-contracts' head)", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), contractsPull()},
published: []string{"go-package-registry"}, wants: 2,
built: map[string]string{contractsGo: "v0.1.15-feat-x.1", seatGo: "v0.1.3-feat-x.1"}},
{name: "a lone pull request", pulls: []delivery.CandidatePull{sdkPull()}},
{name: "a Go published naming no module", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
{name: "a lone pull request", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{sdkPull()}},
{name: "a Go published naming no module", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, noModule: true,
fails: "no published of the tagged version of go of mesh-sdk (novox/mesh-sdk#12) on go-package-registry within 50ms"},
{name: "a package never published", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
{name: "a package never published", pulls: []meshcatalog.CandidatesByGroupValuePullsItem{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry"}, wants: 2,
fails: "no published of the tagged version of lib of mesh-sdk (novox/mesh-sdk#12) on npm-package-registry within 50ms"},
} {
@@ -131,7 +132,7 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
b.ex.Forge = headFiles{repoSDK + " module.json " + head2: string(raw)}
tags := &recordedTags{heard: &PublishedHeard{}, publish: row.published, noModule: row.noModule}
b.ex.Tags, b.ex.PublishBound = tags, 50*time.Millisecond
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
b.asks.Answer = func(a buildstep.StepAsk) buildstep.Step {
if len(tags.wanted) != row.wants {
t.Errorf("%s: a member was asked before every tag-wanted was said: %d of %d", row.name, len(tags.wanted), row.wants)
}
@@ -147,9 +148,9 @@ func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *tes
}
}
switch {
case row.fails == "" && v.State != delivery.VerdictPass:
case row.fails == "" && v.State != meshdeliverycheckerclient.VerdictPass:
t.Errorf("%s: verdict = %+v", row.name, v)
case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
case row.fails != "" && (v.State != meshdeliverycheckerclient.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
t.Errorf("%s: verdict = %+v, want it to fail saying %q", row.name, v, row.fails)
case row.fails != "" && len(b.steps.tool("fetch")) != 0:
t.Errorf("%s: a member was built though a package was not published", row.name)
@@ -247,7 +248,7 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR
t.Errorf("the siblings named for the verdict = %v, want the node-engine at %s", out.Beside, hostRuns[:12])
}
got := besideOf(t, b, SDKRepository)
if c := got[catalogue]; c.Commit != head2 || c.Ref != "refs/pull/12/head" {
if c := got[catalogue]; c.Commit != head2 {
t.Errorf("the catalogue beside mesh-sdk in a group = %+v, want the member's head %s", c, head2)
}
if h := got[host]; h.Commit != hostRuns {
@@ -258,7 +259,7 @@ func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR
b = newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{SDKRepository: trunkS, catalogue: trunkA, host: trunkA2, networking: trunkA2})
b.judge(t, caseOf(t, pullOf(host, 8, head1), pullOf(networking, 3, head2)))
if n := besideOf(t, b, host)[networking]; n.Commit != head2 || n.Ref != "refs/pull/3/head" {
if n := besideOf(t, b, host)[networking]; n.Commit != head2 {
t.Errorf("mesh-networking beside the node-engine in a group = %+v, want the member's head %s", n, head2)
}
// Alone, mesh-sdk's tests read the catalogue's trunk, and the verdict names it.
@@ -327,7 +328,7 @@ func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) {
func TestAMeshLabHeadGetsACheck(t *testing.T) {
const lab = "novox/mesh-lab"
raw := candidateOf(t, "fix/x", pullOf(lab, 3, head1))
c, err := delivery.DecodeCandidate(raw)
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
if err != nil {
t.Fatal(err)
}
@@ -336,7 +337,7 @@ func TestAMeshLabHeadGetsACheck(t *testing.T) {
t.Fatalf("the case = %+v, want mesh-lab#3 judged", cs)
}
b := newBench(t)
if v := b.judge(t, cs).Verdict; v.State != delivery.VerdictPass {
if v := b.judge(t, cs).Verdict; v.State != meshdeliverycheckerclient.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
if fetched := b.steps.tool("fetch"); len(fetched) == 0 || fetched[0].args["repository"] != lab {
@@ -7,7 +7,8 @@ import (
"regexp"
"slices"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
var digest64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
@@ -19,10 +20,10 @@ var digest64 = regexp.MustCompile(`^[0-9a-f]{64}$`)
// indexer gives its module no fingerprint and says why. A head that does not merge has a preview of the files in conflict
// alone, so the indexer says the candidate's modules have no fingerprint because it does not merge (issue 1445). It
// answers false when no fetch was made.
func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) {
func PreviewOf(p Pull, fetched []Fetched) (meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue, bool) {
var merge string
var conflicts []string
mods := map[string]delivery.PreviewModule{}
mods := map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{}
for _, f := range fetched {
if len(f.Conflicts) > 0 {
conflicts = append(conflicts, f.Conflicts...)
@@ -30,7 +31,7 @@ func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) {
}
if len(conflicts) > 0 {
slices.Sort(conflicts)
return delivery.Preview{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Conflicts: slices.Compact(conflicts)}, true
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Conflicts: slices.Compact(conflicts)}, true
}
for _, f := range fetched {
if f.Merge == "" {
@@ -43,13 +44,13 @@ func PreviewOf(p Pull, fetched []Fetched) (delivery.Preview, bool) {
if t.Tree == "" || !digest64.MatchString(t.Manifest) {
continue
}
mods[dir] = delivery.PreviewModule{Tree: t.Tree, Manifest: t.Manifest, Locks: t.Locks, Paths: t.Paths, Packages: t.Packages}
mods[dir] = meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{Tree: t.Tree, Manifest: t.Manifest, Locks: t.Locks, Paths: t.Paths, Packages: t.Packages}
}
}
if merge == "" {
return delivery.Preview{}, false
return meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{}, false
}
pv := delivery.Preview{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Merge: merge}
pv := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: p.Repository, Head: p.Head, Trunk: p.Trunk, Merge: merge}
if len(mods) > 0 {
pv.Modules = mods
}
@@ -67,10 +68,10 @@ func (j *Examiner) writePreview(ctx context.Context, p Pull, fetched []Fetched)
if !ok {
return
}
key, err := delivery.PreviewKey(p.Repository, p.Head, p.Trunk)
key, err := meshdeliverycheckerclient.PreviewKey(p.Repository, p.Head, p.Trunk)
if err == nil {
var data []byte
if data, err = delivery.EncodePreview(pv); err == nil {
if data, err = meshdeliverycheckerclient.EncodePreview(pv); err == nil {
_, err = j.Previews.Create(ctx, key, data)
if errors.Is(err, ErrConflict) {
return
@@ -6,7 +6,10 @@ import (
"sync"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// The checker's ranked start of checks (ADR 0536 §4, issue 1897): a freed place goes to a head never judged (or asked again
@@ -47,7 +50,7 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4
cands := fakeState{}
put := func(n int, head string) {
cand := candidateOf(t, group(n), pullOf(repoA, n, head))
ckey, _ := delivery.CandidatesKey(group(n))
ckey, _ := meshcatalogclient.CandidatesKey(group(n))
cands[ckey] = cand
r.ch.Candidate(ctx, group(n), cand)
}
@@ -58,7 +61,7 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4
put(4, head1) // judged on the new trunk: stands
r.ch.Wait()
cand3 := candidateOf(t, group(3), pullOf(repoA, 3, head1)) // never judged
ckey3, _ := delivery.CandidatesKey(group(3))
ckey3, _ := meshcatalogclient.CandidatesKey(group(3))
cands[ckey3] = cand3
verdicts := fakeState{}
for k, v := range r.verdicts.vals {
@@ -80,10 +83,10 @@ func TestCheckerStatusNamesTheWaitingChecksByKind(t *testing.T) { // 1897/AC4
// is still the one the mesh runs, though the node-engine's trunk has moved on (ADR 0548 §6).
func TestCheckerStatusReadsTheNodeEngineAtItsRunningCommit(t *testing.T) { // 568/21
cand := candidateOf(t, group(1), pullOf(repoA, 1, head1))
ckey, _ := delivery.CandidatesKey(group(1))
ckey, _ := meshcatalogclient.CandidatesKey(group(1))
vkey := keyOf(t, group(1), cand)
dc, _ := delivery.DecodeCandidate(cand)
judged, err := delivery.EncodeVerdict(delivery.Verdict{Group: group(1), Heads: dc.HeadsHash, Trunk: map[string]string{repoA: trunkA}, Beside: map[string]string{nodeEngineRepo: "run111"}, State: delivery.VerdictPass, Levels: passing().Levels})
dc, _ := meshcatalog.DecodeCandidatesByGroupValue(cand)
judged, err := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: group(1), Heads: dc.Heads, Trunk: map[string]string{repoA: trunkA}, Beside: map[string]string{nodeEngineRepo: "run111"}, State: meshdeliverycheckerclient.VerdictPass, Levels: passing().Levels})
if err != nil {
t.Fatal(err)
}
@@ -8,8 +8,13 @@ import (
"strings"
"time"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The read tools (novox/hq ADR 0484 clauses 8 and 13, issue 1686): what the checker stored, as stored, with the line of the
@@ -68,28 +73,28 @@ type VerdictRead struct {
Note string `json:"note,omitempty"`
}
func (r Reader) candidate(group string) (delivery.Candidate, bool) {
key, err := delivery.CandidatesKey(group)
func (r Reader) candidate(group string) (meshcatalog.CandidatesByGroupValue, bool) {
key, err := meshcatalogclient.CandidatesKey(group)
if err != nil || r.CandidateState.Get == nil {
return delivery.Candidate{}, false
return meshcatalog.CandidatesByGroupValue{}, false
}
raw, err := r.CandidateState.Get(key)
if err != nil || len(raw) == 0 {
return delivery.Candidate{}, false
return meshcatalog.CandidatesByGroupValue{}, false
}
c, err := delivery.DecodeCandidate(raw)
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
return c, err == nil
}
func (r Reader) read(key string, raw json.RawMessage) (VerdictRead, error) {
out := VerdictRead{Key: key, Verdict: raw}
v, err := delivery.DecodeVerdict(raw)
v, err := meshdeliverycheckerclient.DecodeVerdict(raw)
if err != nil {
return out, fmt.Errorf("%s holds what is not a verdict: %w", key, err)
}
out.Failed = failedLines(v)
c, ok := r.candidate(v.Group)
if !ok || (c.HeadsHash != v.Heads && GateHeads(c) != v.Heads) {
if !ok || (c.Heads != v.Heads && GateHeads(c) != v.Heads) {
out.Note = "the candidate no longer holds these heads"
return out, nil
}
@@ -98,7 +103,7 @@ func (r Reader) read(key string, raw json.RawMessage) (VerdictRead, error) {
if cs == nil {
if IsUntrunked(v) {
for _, p := range leftOf(c, v.Trunk) {
out.Reports[pullKey(p)] = v.Failed.Words
out.Reports[pullKey(p)] = failedOf(v).Words
}
}
return out, nil
@@ -126,7 +131,7 @@ func (r Reader) Verdicts(group string) (out []VerdictRead, cut string, err error
var matching []string
for _, k := range keys {
if group != "" {
if g, _, err := delivery.ParseCheckerVerdictKey(k); err != nil || g != group {
if g, _, err := meshdeliverycheckerclient.ParseVerdictKey(k); err != nil || g != group {
continue
}
}
@@ -169,14 +174,14 @@ func (r Reader) Pull(repository string, number int) (VerdictRead, error) {
// The candidates are read in batches, each in parallel and taken in key order, so the read stops at the batch that holds it.
for from := 0; from < len(keys); from += ReadWorkers * 4 {
batch := keys[from:min(from+ReadWorkers*4, len(keys))]
cands := make([]delivery.Candidate, len(batch))
cands := make([]meshcatalog.CandidatesByGroupValue, len(batch))
ok := make([]bool, len(batch))
_ = parallel(len(batch), func(i int) error {
raw, err := r.CandidateState.Get(batch[i])
if err != nil || len(raw) == 0 {
return nil
}
c, err := delivery.DecodeCandidate(raw)
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
cands[i], ok[i] = c, err == nil
return nil
})
@@ -185,10 +190,10 @@ func (r Reader) Pull(repository string, number int) (VerdictRead, error) {
continue
}
for _, p := range c.Pulls {
if p.Repository != repository || p.Number != number {
if p.Repository != repository || int(p.Number) != number {
continue
}
vkey, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
vkey, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
if err != nil {
return VerdictRead{}, err
}
@@ -215,7 +220,7 @@ func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, string,
return nil, "", fmt.Errorf("the kept state cannot be read here")
}
if digest != "" {
key, err := delivery.KeptKey(level, digest)
key, err := meshdeliverycheckerclient.KeptKey(level, digest)
if err != nil {
return nil, "", err
}
@@ -235,7 +240,7 @@ func (r Reader) Kept(level, digest string) (map[string]json.RawMessage, string,
sort.Strings(keys)
var matching []string
for _, k := range keys {
if l, _, err := delivery.ParseKeptKey(k); err == nil && (level == "" || l == level) {
if l, _, err := meshdeliverycheckerclient.ParseKeptKey(k); err == nil && (level == "" || l == level) {
matching = append(matching, k)
}
}
@@ -280,7 +285,7 @@ func (r Reader) Previews(repository, head string) ([]PreviewRead, []string, stri
parts := map[string]part{}
var matching, bad []string
for _, k := range keys {
repo, h, trunk, err := delivery.ParsePreviewKey(k)
repo, h, trunk, err := meshdeliverycheckerclient.ParsePreviewKey(k)
if err != nil {
bad = append(bad, k)
continue
@@ -398,7 +403,7 @@ func (r Reader) turns(st *Status) *Turns {
}
// heldBy says the turn that holds a candidate's check off, or "".
func heldBy(t *Turns, c delivery.Candidate, heads map[string]string) string {
func heldBy(t *Turns, c meshcatalog.CandidatesByGroupValue, heads map[string]string) string {
cs := CaseOf(c, heads)
if t == nil || cs == nil {
return ""
@@ -451,7 +456,7 @@ func (r Reader) Status() (Status, error) {
st.Notes = append(st.Notes, fmt.Sprintf("read the first %d candidates of %d", MaxReads, total))
}
type judged struct {
c delivery.Candidate
c meshcatalog.CandidatesByGroupValue
raw json.RawMessage
ok bool
}
@@ -461,11 +466,11 @@ func (r Reader) Status() (Status, error) {
if err != nil || len(raw) == 0 {
return nil
}
c, err := delivery.DecodeCandidate(raw)
c, err := meshcatalog.DecodeCandidatesByGroupValue(raw)
if err != nil {
return nil
}
vkey, err := delivery.CheckerVerdictKey(c.Group, c.HeadsHash)
vkey, err := meshdeliverycheckerclient.VerdictKey(c.Group, c.Heads)
if err != nil {
return nil
}
@@ -501,10 +506,10 @@ func (r Reader) Status() (Status, error) {
}
continue
}
v, err := delivery.DecodeVerdict(raw)
v, err := meshdeliverycheckerclient.DecodeVerdict(raw)
switch {
case err != nil:
case v.State == delivery.VerdictRunning:
case v.State == meshdeliverycheckerclient.VerdictRunning:
if why := read.lostWhy(c); why != "" {
st.Lost[c.Group] = why
}
@@ -545,7 +550,7 @@ type stepsRead struct {
type moduleStep struct {
module string
step delivery.Step
step buildstep.Step
}
// readSteps reads the newest MaxReads keys of each build module's steps state.
@@ -579,14 +584,14 @@ func (r Reader) readSteps() stepsRead {
if cut != "" {
out.notes = append(out.notes, "the steps of "+m+": read the newest "+fmt.Sprint(MaxReads)+" of its keys; a step older than those is not shown")
}
steps := make([]delivery.Step, len(keys))
steps := make([]buildstep.Step, len(keys))
decoded := make([]bool, len(keys))
_ = parallel(len(keys), func(i int) error {
raw, err := sr.Get(keys[i])
if err != nil || len(raw) == 0 {
return nil
}
step, err := delivery.DecodeStep(raw)
step, err := buildstep.DecodeStep(raw)
steps[i], decoded[i] = step, err == nil
return nil
})
@@ -596,7 +601,7 @@ func (r Reader) readSteps() stepsRead {
}
step := steps[i]
out.all = append(out.all, moduleStep{module: m, step: step})
if step.State != delivery.StepRunning {
if step.State != buildstep.StepRunning {
continue
}
rs := RunningStep{Module: m, Node: step.Node, ID: step.ID, Tool: step.Tool, What: step.What}
@@ -615,9 +620,9 @@ func (r Reader) readSteps() stepsRead {
// asksOf says whether a step is an ask of one of the candidate's pull requests: its What names `<repository>@<head12>`
// (buildstep.AskWhat).
func asksOf(c delivery.Candidate, what string) bool {
func asksOf(c meshcatalog.CandidatesByGroupValue, what string) bool {
for _, p := range c.Pulls {
if p.Repository != "" && strings.Contains(what, p.Repository+"@"+short12(p.Head.Head)) {
if p.Repository != "" && strings.Contains(what, p.Repository+"@"+short12(p.Head)) {
return true
}
}
@@ -626,16 +631,16 @@ func asksOf(c delivery.Candidate, what string) bool {
// lostWhy says why a candidate whose verdict is running has a check that nothing will judge, or "": no step of its pull
// requests can still run, and its newest one ended.
func (s stepsRead) lostWhy(c delivery.Candidate) string {
func (s stepsRead) lostWhy(c meshcatalog.CandidatesByGroupValue) string {
for _, rs := range s.running {
if asksOf(c, rs.What) {
return ""
}
}
var last *delivery.Step
var last *buildstep.Step
for i := range s.all {
st := s.all[i].step
if st.State != delivery.StepRunning && asksOf(c, st.What) && (last == nil || st.Ended.After(last.Ended)) {
if st.State != buildstep.StepRunning && asksOf(c, st.What) && (last == nil || st.Ended.After(last.Ended)) {
last = &s.all[i].step
}
}
@@ -653,9 +658,9 @@ const DefaultStepBound = 30 * time.Minute
// leftWhy says why a key in the running state runs no step any more, or "" when it can still be running: its build module
// is not on its node or is not running there (the node-engine's have), or it started longer ago than the step bound, when
// the checker stopped waiting for it.
func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound time.Duration) string {
func (r Reader) leftWhy(module string, step buildstep.Step, now time.Time, bound time.Duration) string {
if r.HaveState.Get != nil {
key, err := delivery.HaveKey(step.Node, module)
key, err := nodeengineclient.HaveKey(step.Node, module)
if err == nil {
raw, err := r.HaveState.Get(key)
switch {
@@ -663,7 +668,7 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound
case len(raw) == 0:
return fmt.Sprintf("%s is not on %s", module, step.Node)
default:
if h, err := delivery.DecodeHave(raw); err == nil && h.Phase != delivery.PhaseRunning {
if h, err := nodeengineclient.DecodeHave(raw); err == nil && h.Phase != nodeengineclient.PhaseRunning {
return fmt.Sprintf("%s on %s is %s", module, step.Node, h.Phase)
}
}
@@ -680,17 +685,17 @@ func (r Reader) leftWhy(module string, step delivery.Step, now time.Time, bound
// buildsNotEnded are the builder's builds of the candidate's changed modules that are neither done nor failed, as
// Status.BuildsNotEnded says them; empty when none, or when the builder's builds are not readable here.
func (r Reader) buildsNotEnded(c delivery.Candidate) string {
func (r Reader) buildsNotEnded(c meshcatalog.CandidatesByGroupValue) string {
if r.BuildState.Get == nil {
return ""
}
var out []string
for _, p := range c.Pulls {
for module, cm := range p.Fps {
if cm.FP2 == "" || cm.Manifest == "" {
if cm.Fp2 == "" || cm.Manifest == "" {
continue
}
key, err := delivery.BuilderBuildKey(module, cm.FP2)
key, err := builds.BuilderBuildKey(module, cm.Fp2)
if err != nil {
continue
}
@@ -700,8 +705,8 @@ func (r Reader) buildsNotEnded(c delivery.Candidate) string {
}
state := "not begun"
if len(raw) > 0 {
b, err := delivery.DecodeBuilderBuild(raw)
if err != nil || b.State == delivery.BuildDone || b.State == delivery.BuildFailed {
b, err := builds.DecodeBuilderBuild(raw)
if err != nil || b.State == builds.BuildDone || b.State == builds.BuildFailed {
continue
}
state = b.State
@@ -9,7 +9,14 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
type fakeState map[string][]byte
@@ -40,7 +47,7 @@ func readerOf(t *testing.T) (Reader, string) {
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1), pullOf(hqRepo, 3, head2))
r.ch.Candidate(context.Background(), "feat%2Fx", cand)
r.ch.Wait()
ckey, _ := delivery.CandidatesKey("feat/x")
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
verdicts, kept := fakeState{}, fakeState{}
for k, v := range r.verdicts.vals {
verdicts[k] = v
@@ -58,7 +65,7 @@ func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest
if err != nil || len(vs) != 1 || vs[0].Key != key {
t.Fatalf("verdicts = %+v, %v", vs, err)
}
if _, err := delivery.DecodeVerdict(vs[0].Verdict); err != nil {
if _, err := meshdeliverycheckerclient.DecodeVerdict(vs[0].Verdict); err != nil {
t.Errorf("the verdict is not as stored: %v", err)
}
if got := vs[0].Reports["novox/mesh-delivery#7"]; !strings.HasPrefix(got, "pass 111111111111 on aaaaaaaaaaaa") || !strings.Contains(got, "composition not yet run") {
@@ -68,9 +75,9 @@ func TestTheVerdictToolsAnswerTheStoredVerdictWithTheFullReportOfEachPullRequest
t.Errorf("another group's verdicts = %+v", only)
}
// A failed verdict is answered with every failure it names, one line each (the operator, 2026-11-02).
raw, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/x", Heads: "0123456789ab", State: delivery.VerdictFail,
Failed: delivery.FailedFirst{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom\nmore"},
Failures: []delivery.FailedFirst{{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom"}, {Machine: "g14", Words: "route-proxy is defined twice"}}})
raw, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/x", Heads: "0123456789ab", State: meshdeliverycheckerclient.VerdictFail,
Failed: &FailedFirst{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom\nmore"},
Failures: failureItems([]FailedFirst{{Test: "unit: m/a.TestA", Words: "a_test.go:3: boom"}, {Machine: "g14", Words: "route-proxy is defined twice"}})})
if got, _ := rd.read(key, raw); strings.Join(got.Failed, " | ") != "unit: m/a.TestA: a_test.go:3: boom | g14: route-proxy is defined twice" {
t.Errorf("failed = %q", got.Failed)
}
@@ -104,7 +111,7 @@ func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { //
t.Fatalf("one kept = %v, %v", one, err)
}
for _, raw := range one {
if k, err := delivery.DecodeKept(raw); err != nil || k.Package != "m/a" {
if k, err := meshdeliverycheckerclient.DecodeKept(raw); err != nil || k.Package != "m/a" {
t.Errorf("kept value = %+v, %v", k, err)
}
}
@@ -118,12 +125,12 @@ func TestTheKeptToolAnswersTheKeysAsStoredByLevelAndByDigest(t *testing.T) { //
// 1680 live acceptance: checker_previews shows the preview key of an open pull request, as stored, narrowed by repository and head.
func TestThePreviewsToolShowsTheStoredPreviewOfAPullRequest(t *testing.T) {
pv := delivery.Preview{Repository: repoA, Head: head1, Trunk: head2, Merge: "m1", Modules: map[string]delivery.PreviewModule{}}
data, err := delivery.EncodePreview(pv)
pv := meshdeliverychecker.PreviewsByRepositoryHeadTrunkValue{Repository: repoA, Head: head1, Trunk: head2, Merge: "m1", Modules: map[string]meshdeliverychecker.PreviewsByRepositoryHeadTrunkValueModulesValue{}}
data, err := meshdeliverycheckerclient.EncodePreview(pv)
if err != nil {
t.Fatal(err)
}
key, _ := delivery.PreviewKey(repoA, head1, head2)
key, _ := meshdeliverycheckerclient.PreviewKey(repoA, head1, head2)
rd := Reader{PreviewState: fakeState{key: data, "junk": []byte("{}")}.read()}
got, bad, _, err := rd.Previews(repoA, head1[:8])
if err != nil || len(got) != 1 || got[0].Key != key || got[0].Head != head1 || got[0].Trunk != head2 || string(got[0].Preview) != string(data) {
@@ -143,29 +150,29 @@ func TestThePreviewsToolShowsTheStoredPreviewOfAPullRequest(t *testing.T) {
// checker_status reads the steps in flight by node and the candidates without a verdict; since R4g nothing holds the checker
// and it has no capacity of its own (ADR 0525 §4, §7; 1685 closed by the hold's deletion).
func TestTheStatusToolReadsRunningSteps(t *testing.T) {
running, _ := delivery.EncodeStep(delivery.Step{ID: "abc1", Tool: "build_go_test", Node: "shanks", State: delivery.StepRunning, Started: time.Date(2026, 10, 22, 15, 0, 0, 0, time.UTC)})
passed, _ := delivery.EncodeStep(delivery.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: delivery.StepPassed})
running, _ := buildstep.EncodeStep(buildstep.Step{ID: "abc1", Tool: "build_go_test", Node: "shanks", State: buildstep.StepRunning, Started: time.Date(2026, 10, 22, 15, 0, 0, 0, time.UTC)})
passed, _ := buildstep.EncodeStep(buildstep.Step{ID: "abc2", Tool: "build_go_test", Node: "ace", State: buildstep.StepPassed})
cand := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
ckey, _ := delivery.CandidatesKey("feat/x")
ckey, _ := meshcatalogclient.CandidatesKey("feat/x")
// ADR 0603 §10: feat/y's check runs and waits on the builds of its changed modules: web building, mail not begun; api
// is done and named nowhere.
fp, man := "src2:"+strings.Repeat("f", 64), strings.Repeat("e", 64)
py := pullOf(repoA, 8, head2)
py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]delivery.CandidateModule{"web": {FP2: fp, Manifest: man}, "mail": {FP2: fp, Manifest: man}, "api": {FP2: fp, Manifest: man}}
py.Modules, py.Fps = []string{"api", "mail", "web"}, map[string]meshcatalog.CandidatesByGroupValuePullsItemFpsValue{"web": {Fp2: fp, Manifest: man}, "mail": {Fp2: fp, Manifest: man}, "api": {Fp2: fp, Manifest: man}}
candY := candidateOf(t, "feat/y", py)
ykey, _ := delivery.CandidatesKey("feat/y")
runningV, _ := delivery.EncodeVerdict(delivery.Verdict{Group: "feat/y", Heads: "0123456789ab", State: delivery.VerdictRunning})
ykey, _ := meshcatalogclient.CandidatesKey("feat/y")
runningV, _ := meshdeliverycheckerclient.EncodeVerdict(meshdeliverychecker.VerdictsByGroupHeadsValue{Group: "feat/y", Heads: "0123456789ab", State: meshdeliverycheckerclient.VerdictRunning})
builds := fakeState{}
for module, state := range map[string]string{"web": delivery.BuildBuilding, "api": delivery.BuildDone} {
k, _ := delivery.BuilderBuildKey(module, fp)
builds[k], _ = json.Marshal(delivery.BuilderBuild{V: 1, State: state})
for module, state := range map[string]string{"web": buildsrec.BuildBuilding, "api": buildsrec.BuildDone} {
k, _ := buildsrec.BuilderBuildKey(module, fp)
builds[k], _ = json.Marshal(buildsrec.BuilderBuild{V: 1, State: state})
}
rd := Reader{
StepStates: map[string]StateRead{"mesh-build-go": fakeState{"shanks.abc1": running, "ace.abc2": passed}.read()},
CandidateState: fakeState{ckey: cand, ykey: candY}.read(),
VerdictState: fakeState{keyOf(t, "feat/y", candY): runningV}.read(),
BuildState: builds.read(),
HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(),
HaveState: fakeState{"shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(),
HearingState: fakeState{"dead-letters": []byte(`{"heard":false,"streams":["SEAT_NODE_BUILD_GO"],` +
`"why":"DEAD_LETTERS is not heard, so an ask a build seat gives up on is waited for until its bound: SEAT_NODE_BUILD_GO: permissions violation",` +
`"at":"2026-10-26T13:00:00Z"}`)}.read(),
@@ -195,9 +202,9 @@ func TestTheStatusToolReadsRunningSteps(t *testing.T) {
}
}
func haveOf(t *testing.T, phase delivery.Phase) []byte {
func haveOf(t *testing.T, phase string) []byte {
t.Helper()
raw, err := delivery.EncodeHave(delivery.Have{Phase: phase, Health: delivery.HealthOK})
raw, err := nodeengineclient.EncodeHave(nodeengine.HaveByNodeModuleValue{Phase: phase, Health: nodeengineclient.HealthOK})
if err != nil {
t.Fatal(err)
}
@@ -206,7 +213,7 @@ func haveOf(t *testing.T, phase delivery.Phase) []byte {
func stepOf(t *testing.T, id, node string, started time.Time) []byte {
t.Helper()
raw, err := delivery.EncodeStep(delivery.Step{ID: id, Tool: "build_go_fetch", Node: node, State: delivery.StepRunning, Started: started})
raw, err := buildstep.EncodeStep(buildstep.Step{ID: id, Tool: "build_go_fetch", Node: node, State: buildstep.StepRunning, Started: started})
if err != nil {
t.Fatal(err)
}
@@ -228,8 +235,8 @@ func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *te
}.read(),
},
HaveState: fakeState{
"g14.mesh-build-go": haveOf(t, delivery.PhaseRunning),
"shanks.mesh-build-go": haveOf(t, delivery.PhaseFailed),
"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning),
"shanks.mesh-build-go": haveOf(t, nodeengineclient.PhaseFailed),
}.read(),
StepBound: 30 * time.Minute,
Now: func() time.Time { return now },
@@ -258,7 +265,7 @@ func TestTheStatusCountsOnlyStepsThatCanStillRunAndSaysWhyTheOthersAreLeft(t *te
// 1887/A10: a group of hq's pull requests alone is judged by the checker since R4g, so without a verdict it is unjudged.
func TestTheStatusCountsAnHQOnlyGroupAsUnjudged(t *testing.T) {
cand := candidateOf(t, "pb-x", pullOf(hqRepo, 3, head2))
ckey, _ := delivery.CandidatesKey("pb-x")
ckey, _ := meshcatalogclient.CandidatesKey("pb-x")
st, err := Reader{CandidateState: fakeState{ckey: cand}.read(), VerdictState: fakeState{}.read()}.Status()
if err != nil {
t.Fatal(err)
@@ -268,7 +275,7 @@ func TestTheStatusCountsAnHQOnlyGroupAsUnjudged(t *testing.T) {
}
// ADR 0548 §3: a candidate whose check an approved merge's turn holds off is named with the turn.
held := candidateOf(t, "feat/x", pullOf(repoA, 7, head1))
hkey, _ := delivery.CandidatesKey("feat/x")
hkey, _ := meshcatalogclient.CandidatesKey("feat/x")
st, err = Reader{CandidateState: fakeState{hkey: held}.read(), VerdictState: fakeState{}.read(),
Trunks: func() (map[string]string, error) { return map[string]string{repoA: trunkA}, nil },
TurnState: fakeState{turnKey(t, repoA): turnOf(t, repoA, "fix/other", "0123456789ab")}.read()}.Status()
@@ -311,13 +318,13 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { //
kept := fakeState{}
for i := 0; i < 1800; i++ {
level := map[bool]string{true: "unit", false: "integration"}[i%9 != 0]
k, _ := delivery.KeptKey(level, fmt.Sprintf("%064x", i))
k, _ := meshdeliverycheckerclient.KeptKey(level, fmt.Sprintf("%064x", i))
kept[k] = []byte(`{"v":1}`)
}
previews := fakeState{}
for i := 0; i < 1500; i++ {
repo := map[bool]string{true: repoA, false: repoSDK}[i < 300]
k, _ := delivery.PreviewKey(repo, fmt.Sprintf("%08x%032x", i, 0), trunkA)
k, _ := meshdeliverycheckerclient.PreviewKey(repo, fmt.Sprintf("%08x%032x", i, 0), trunkA)
previews[k] = []byte(`{"v":1}`)
}
for _, row := range []struct {
@@ -372,15 +379,15 @@ func TestReadToolsReadOnlyWhatTheyNeedInParallelAndSayWhenCut(t *testing.T) { //
steps := fakeState{}
for i := 0; i < 1500; i++ {
id := fmt.Sprintf("mvo5%04dabcdef", i)
step := delivery.Step{ID: id, Tool: "t", Node: "g14", State: delivery.StepPassed}
step := buildstep.Step{ID: id, Tool: "t", Node: "g14", State: buildstep.StepPassed}
if i == 1499 {
step.State, step.Started = delivery.StepRunning, now.Add(-time.Minute)
step.State, step.Started = buildstep.StepRunning, now.Add(-time.Minute)
}
raw, _ := delivery.EncodeStep(step)
raw, _ := buildstep.EncodeStep(step)
steps["g14."+id] = raw
}
sb := &slowBucket{vals: steps}
st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, delivery.PhaseRunning)}.read(),
st, err := Reader{StepStates: map[string]StateRead{"mesh-build-go": sb.read()}, HaveState: fakeState{"g14.mesh-build-go": haveOf(t, nodeengineclient.PhaseRunning)}.read(),
Now: func() time.Time { return now }}.Status()
if err != nil || sb.reads.Load() != MaxReads || len(st.Running) != 1 || sb.maxFlight.Load() < 2 {
t.Errorf("status: %d reads (want %d), running %+v, in flight %d, %v", sb.reads.Load(), MaxReads, st.Running, sb.maxFlight.Load(), err)
@@ -9,8 +9,9 @@ import (
"strings"
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// A config value renamed (design 32 *A config value renamed*, DLV-C5): the checker asks the seat mesh-config's verb
@@ -52,15 +53,15 @@ func renamesOf(was, now catalogue.Manifest) renameChange {
}
// judgeRenames fails the check for a rename the seat refuses, a window a change would drop, or an ask that cannot be made.
func (st *state) judgeRenames(ctx context.Context) *delivery.Verdict {
func (st *state) judgeRenames(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.c.Try != nil {
return nil
}
var out *delivery.Verdict
var out *meshdeliverychecker.VerdictsByGroupHeadsValue
failed := func(words string) {
f := delivery.FailedFirst{Machine: meshconfig.Seat, Words: words}
f := FailedFirst{Machine: meshconfig.Seat, Words: words}
if out == nil {
out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f}
out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &f}
return
}
addFailure(out, f)
@@ -1,11 +1,7 @@
package check
import (
"encoding/json"
"strings"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// The shapes of what the build modules' steps answer, as their code writes them (mesh-catalog modules/mesh-build-go
@@ -13,52 +9,10 @@ import (
// cmd/mesh-build-node check.go TestResult, fetch.go FetchResult) with the fields the fetch step adds for a merge (ADR 0484
// clause 3). Only the fields the checker reads are named; the rest is ignored.
// MergeTree is what a fetch that merged says of one module directory: the tree sha, the digest of its manifest, the
// hashes of its lock files by name and the objects of its named paths. A bare string is the tree alone.
type MergeTree struct {
Tree string `json:"tree"`
Manifest string `json:"manifest,omitempty"`
Locks map[string]string `json:"locks,omitempty"`
// Paths are, of a module whose every artifact names its paths, each named path's object; Tree is then their named
// tree (mesh-sdk buildstep.ModuleTree, novox/hq ADR 0499 clause 2).
Paths map[string]string `json:"paths,omitempty"`
// Packages are the mesh packages the directory's go.sum names (mesh-sdk buildstep.ModuleTree, novox/hq ADR 0573 §2).
Packages []string `json:"packages,omitempty"`
}
// UnmarshalJSON takes a string as the tree alone, or an object.
func (t *MergeTree) UnmarshalJSON(data []byte) error {
if s := strings.TrimSpace(string(data)); strings.HasPrefix(s, `"`) {
return json.Unmarshal(data, &t.Tree)
}
type plain MergeTree
return json.Unmarshal(data, (*plain)(t))
}
// Fetched is the answer of a build module's fetch step.
type Fetched struct {
Workspace string `json:"workspace"`
Commit string `json:"commit"`
Modules []string `json:"modules"`
// What a fetch with a base adds: the trunk commit, the merge commit, the files in conflict (a conflict is an answer),
// the paths the merge changes and the trees of the module directories.
Base string `json:"base,omitempty"`
Merge string `json:"merge,omitempty"`
Conflicts []string `json:"conflicts,omitempty"`
Changed []string `json:"changed,omitempty"`
Trees map[string]MergeTree `json:"trees,omitempty"`
// Programs are the changed files whose first bytes are an executable binary, read by the build module in its workspace
// (issue 1577); nil for a build module that does not report it, a pointer to an empty list for one that found none.
Programs *[]ProgramFile `json:"programs,omitempty"`
}
// ProgramFile is a changed file that is a compiled program, and its format (ELF, Mach-O or PE).
type ProgramFile struct {
Path string `json:"path"`
Kind string `json:"kind"`
// Commit is set for a program a commit of the branch added and a later one removed: it stays in history once merged.
Commit string `json:"commit,omitempty"`
}
// Fetched is the answer of a build module's fetch step, whole: the Delivery SDK's FetchAnswer, which a check answer keeps
// (B2g; the build seats' part FetchAnswer): the workspace, the merge preview with its conflicts, changed paths, programs and
// trees. A conflict is an answer.
type Fetched = buildstep.FetchAnswer
// GoPackage is one package of a Go test result.
type GoPackage struct {
@@ -128,5 +82,5 @@ type Digested struct {
// JudgedBy is the program that ran the digest, as Tested.JudgedBy says.
JudgedBy string `json:"judged_by,omitempty"`
// Needs are the packages whose tests need something of the node at this level (issue 890).
Needs []delivery.PackageNeeds `json:"needs,omitempty"`
Needs []buildstep.PackageNeeds `json:"needs,omitempty"`
}
@@ -4,7 +4,8 @@ import (
"context"
"fmt"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// A pull request whose head's seat definition does not parse fails its check, naming the seat and where it lives (design
@@ -25,15 +26,15 @@ type SeatDefinitions interface {
// refuseUnparsed fails the check of each pull request with a seat definition that does not parse; a state that cannot be
// read fails it too, saying so.
func (st *state) refuseUnparsed(ctx context.Context) *delivery.Verdict {
func (st *state) refuseUnparsed(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.j.SeatDefinitions == nil || st.c.Try != nil {
return nil
}
var out *delivery.Verdict
var out *meshdeliverychecker.VerdictsByGroupHeadsValue
failed := func(words string) {
f := delivery.FailedFirst{Machine: SeatDefinitionsMachine, Words: words}
f := FailedFirst{Machine: SeatDefinitionsMachine, Words: words}
if out == nil {
out = &delivery.Verdict{State: delivery.VerdictFail, Failed: f}
out = &meshdeliverychecker.VerdictsByGroupHeadsValue{State: meshdeliverycheckerclient.VerdictFail, Failed: &f}
return
}
addFailure(out, f)
@@ -6,8 +6,8 @@ import (
"slices"
"strings"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// A change of a package series is named in the verdict, so the review of the change sees it (design 32 *A package, its
@@ -19,7 +19,7 @@ import (
func (st *state) seriesChanges(ctx context.Context) {
for _, p := range st.c.Pulls {
var words []string
var changes []delivery.SeriesChange
var changes []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem
for _, module := range p.Modules {
was, now, ok, err := st.manifestPair(ctx, p, module)
if err != nil {
@@ -72,13 +72,13 @@ func (st *state) manifestPair(ctx context.Context, p Pull, module string) (was,
}
// seriesLine is a series change as the status line names it: `<module>/<artifact> series <trunk> → <head>`.
func seriesLine(c delivery.SeriesChange) string {
func seriesLine(c meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) string {
return fmt.Sprintf("%s/%s series %s → %s", c.Module, c.Artifact, c.From, c.To)
}
// seriesMoved are the artifacts of module whose series the head's manifest changes from the trunk's, by artifact. An
// artifact new on the head is no series change (the lead's ruling): it has no series on the trunk.
func seriesMoved(module string, was, now catalogue.Manifest) []delivery.SeriesChange {
func seriesMoved(module string, was, now catalogue.Manifest) []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem {
if was.Build == nil || now.Build == nil {
return nil
}
@@ -86,37 +86,39 @@ func seriesMoved(module string, was, now catalogue.Manifest) []delivery.SeriesCh
for _, a := range was.Build.Artifacts {
before[a.Name] = a.SeriesOf()
}
var out []delivery.SeriesChange
var out []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem
for _, a := range now.Build.Artifacts {
if old, ok := before[a.Name]; ok && old != a.SeriesOf() {
out = append(out, delivery.SeriesChange{Module: module, Artifact: a.Name, From: old, To: a.SeriesOf()})
out = append(out, meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{Module: module, Artifact: a.Name, From: old, To: a.SeriesOf()})
}
}
slices.SortFunc(out, func(a, b delivery.SeriesChange) int { return strings.Compare(a.Artifact, b.Artifact) })
slices.SortFunc(out, func(a, b meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) int {
return strings.Compare(a.Artifact, b.Artifact)
})
return out
}
// series keeps, of a pull request (pullKey), the words of the series changes it makes and the changes themselves.
func (c *Case) series(key string, words []string, changes []delivery.SeriesChange) {
func (c *Case) series(key string, words []string, changes []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) {
c.notesMu.Lock()
defer c.notesMu.Unlock()
if c.seriesOf == nil {
c.seriesOf, c.seriesChanges = map[string][]string{}, map[string][]delivery.SeriesChange{}
c.seriesOf, c.seriesChanges = map[string][]string{}, map[string][]meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem{}
}
c.seriesOf[key], c.seriesChanges[key] = words, changes
}
// SeriesChanges are the series changes a pull request (pullKey) makes, as the verdict and its event name them.
func (c *Case) SeriesChanges(key string) []delivery.SeriesChange {
func (c *Case) SeriesChanges(key string) []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem {
c.notesMu.Lock()
defer c.notesMu.Unlock()
return slices.Clone(c.seriesChanges[key])
}
// allSeries are the series changes of every pull request of the case, each module's artifact once, by module and
// artifact: what the verdict names (delivery.Verdict.Series).
func (c *Case) allSeries() []delivery.SeriesChange {
var out []delivery.SeriesChange
// artifact: what the verdict names (meshdeliverychecker.VerdictsByGroupHeadsValue.Series).
func (c *Case) allSeries() []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem {
var out []meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem
seen := map[string]bool{}
for _, p := range c.Pulls {
for _, s := range c.SeriesChanges(pullKey(p)) {
@@ -126,7 +128,7 @@ func (c *Case) allSeries() []delivery.SeriesChange {
}
}
}
slices.SortFunc(out, func(a, b delivery.SeriesChange) int {
slices.SortFunc(out, func(a, b meshdeliverychecker.VerdictsByGroupHeadsValueSeriesItem) int {
return strings.Compare(a.Module+"/"+a.Artifact, b.Module+"/"+b.Artifact)
})
return out
@@ -10,9 +10,10 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// EventTagWanted is the checker's event that asks the builder for a group member's tagged versions (design 51 *Tagged
@@ -158,11 +159,11 @@ func (st *state) manifestAtHead(ctx context.Context, p Pull, module string) (m c
}
}
if dirs == nil {
dirs = []string{"modules/" + module, delivery.RootPath}
dirs = []string{"modules/" + module, meshcatalogclient.RootPath}
}
for _, d := range dirs {
file := "module.json"
if t := strings.Trim(d, "/"); t != "" && t != delivery.RootPath {
if t := strings.Trim(d, "/"); t != "" && t != meshcatalogclient.RootPath {
file = t + "/module.json"
}
text, err := st.j.Forge.File(ctx, p.Repository, file, p.Head)
@@ -248,7 +249,7 @@ func (st *state) declaredPackages(ctx context.Context) ([]declaredPackage, error
// packages by module path in Go's form (StepAsk.Packages), and the TypeScript packages a member's Node fetch takes from the
// registry, by the repository that publishes them.
type groupVersions struct {
goPackages delivery.PackageVersions
goPackages buildstep.PackageVersions
node map[string]NodePackage
}
@@ -286,7 +287,7 @@ func (g *groupVersions) took(d declaredPackage, p TaggedPublished) {
switch d.language {
case "go":
if g.goPackages == nil {
g.goPackages = delivery.PackageVersions{}
g.goPackages = buildstep.PackageVersions{}
}
g.goPackages[p.Module] = goVersion(p.Version) // Heard took no Go published without its module
case "typescript":
@@ -300,13 +301,13 @@ func (g *groupVersions) took(d declaredPackage, p TaggedPublished) {
// tagged asks a group's tagged versions before any member is built (design 51 *Tagged versions*): one tag-wanted per
// package a member declares, then each package's published awaited within the publish bound. A lone pull request and a
// try ask nothing. A package not published within the bound fails the check naming it.
func (st *state) tagged(ctx context.Context) *delivery.Verdict {
func (st *state) tagged(ctx context.Context) *meshdeliverychecker.VerdictsByGroupHeadsValue {
if st.j.Tags == nil || st.c.Try != nil || len(st.c.Pulls) < 2 {
return nil
}
decl, err := st.declaredPackages(ctx)
if err != nil {
return fail(delivery.CheckerModule, err.Error())
return fail(CheckerModule, err.Error())
}
if len(decl) == 0 {
return nil
@@ -316,7 +317,7 @@ func (st *state) tagged(ctx context.Context) *delivery.Verdict {
for _, d := range decl {
t := TagWanted{Repository: d.pull.Repository, Branch: st.c.Group, Head: d.pull.Head, Module: d.module, Package: d.name, Pull: int64(d.pull.Number)}
if err := st.j.Tags.Want(ctx, t); err != nil {
return fail(delivery.CheckerModule, fmt.Sprintf("the tagged version of %s of %s (%s) cannot be asked: %v", d.name, d.module, pullKey(d.pull), err))
return fail(CheckerModule, fmt.Sprintf("the tagged version of %s of %s (%s) cannot be asked: %v", d.name, d.module, pullKey(d.pull), err))
}
names = append(names, d.module+"/"+d.name)
}
@@ -341,9 +342,9 @@ func (st *state) tagged(ctx context.Context) *delivery.Verdict {
}
if len(late) > 0 {
if ctx.Err() != nil {
return fail(delivery.CheckerModule, ctx.Err().Error())
return fail(CheckerModule, ctx.Err().Error())
}
return fail(delivery.CheckerModule, fmt.Sprintf("no published of the tagged version of %s within %s, so no member is built (design 51 *Tagged versions*)", strings.Join(late, "; "), bound))
return fail(CheckerModule, fmt.Sprintf("no published of the tagged version of %s within %s, so no member is built (design 51 *Tagged versions*)", strings.Join(late, "; "), bound))
}
return nil
}
@@ -13,8 +13,11 @@ import (
"sync"
"time"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
"git.novox.be/novox/mesh-sdk/go/keys"
)
// A try is a pushed head judged by the checker as a pull request's head is, before its pull request is opened (ADR 0501
@@ -29,7 +32,7 @@ const (
TryEndTool = "checker_try_end"
// TriesState is the checker's state of tries: the bucket mesh-delivery-checker_tries, keys try-<unix nanoseconds>.
TriesState = "tries"
TriesBucket = delivery.CheckerModule + "_" + TriesState
TriesBucket = CheckerModule + "_" + TriesState
// MaxTries and MaxTriesWaiting are checked when a try is asked, mesh-wide: a new try is refused while MaxTries tries run
// on a build node or MaxTriesWaiting wait for one (queued, or their asks not yet taken by a node). It is no bound on
// what runs later: tries let in while waiting may later all run at once, and each node's build capacity bounds what
@@ -78,11 +81,11 @@ type Try struct {
Said string `json:"said,omitempty"`
// Took is the time from its start to its end, as Go writes a duration.
Took string `json:"took,omitempty"`
// Failed is every test that failed, each with its first line, at most delivery.MaxFailures and then how many more
// Failed is every test that failed, each with its first line, at most meshdeliverycheckerclient.MaxFailures and then how many more
// (issue 1921); or what else failed first (a conflict, a refused step).
Failed []string `json:"failed,omitempty"`
Levels map[string]delivery.LevelResult `json:"levels,omitempty"`
Machines []string `json:"machines,omitempty"`
Failed []string `json:"failed,omitempty"`
Levels map[string]meshdeliverychecker.VerdictsByGroupHeadsValueLevelsValue `json:"levels,omitempty"`
Machines []string `json:"machines,omitempty"`
// Asks are the ids of the asks the try put on the build seats' streams, each ending on the step key <node>.<id> of its
// build module: the delivery view counts those steps as a try's, not a candidate's check (build_usage).
Asks []string `json:"asks,omitempty"`
@@ -122,7 +125,7 @@ type tryWriter struct {
}
// heard takes an ask put (step nil) or a value of its step key in, and writes the record when it changed.
func (w *tryWriter) heard(askID string, step *delivery.Step) {
func (w *tryWriter) heard(askID string, step *buildstep.Step) {
w.mu.Lock()
defer w.mu.Unlock()
if w.stopped {
@@ -180,7 +183,7 @@ func (t Try) HoldsANode() bool {
if len(t.Steps) == 0 {
return true
}
return slices.ContainsFunc(t.Steps, func(s TryStep) bool { return s.State == delivery.StepRunning })
return slices.ContainsFunc(t.Steps, func(s TryStep) bool { return s.State == buildstep.StepRunning })
}
// Over says whether the try came to its end.
@@ -199,7 +202,7 @@ func DecodeTry(raw []byte) (Try, error) {
}
func encodeTry(t Try) ([]byte, error) {
t.V = delivery.Version
t.V = keys.Version
return json.Marshal(t)
}
@@ -232,7 +235,7 @@ func AskTry(store TryStore, trunks map[string]string, a TryAsk, now time.Time) (
return "", fmt.Errorf("%q is not a repository, <owner>/<name>", a.Repository)
case !tryHeadOK.MatchString(head):
return "", fmt.Errorf("the head %q is not a full commit, 40 or 64 hex digits", a.Head)
case a.Level != "" && a.Level != delivery.LevelUnit && a.Level != delivery.LevelIntegration:
case a.Level != "" && a.Level != buildstep.LevelUnit && a.Level != buildstep.LevelIntegration:
return "", fmt.Errorf("the level %q is neither unit nor integration", a.Level)
case a.Tests != "" && !tryTestsOK.MatchString(a.Tests):
return "", fmt.Errorf("the tests %q are not a go test -run pattern", a.Tests)
@@ -530,9 +533,9 @@ func (tr *Tryer) run(ctx context.Context, key string) {
// Not the change's: its ask was given up or never taken (issue 734), or a part of the mesh was not heard.
t.State = TryError
if IsLostAsk(v) || IsNotTaken(v) {
t.Failed = []string{v.Failed.Words}
t.Failed = []string{failedOf(v).Words}
}
case v.State == delivery.VerdictPass:
case v.State == meshdeliverycheckerclient.VerdictPass:
t.State = TryPass
default:
t.State = TryFail
@@ -8,7 +8,7 @@ import (
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
)
// memTries is the tries state as the tools see it, over a memKV.
@@ -72,7 +72,7 @@ func TestATryIsAskedQueuedOncePerHeadAndAtMostFourAtOnce(t *testing.T) { // ADR
// their asks not taken) are bounded apart, MaxTries and MaxTriesWaiting; the refusal names both counts.
onNode := func(i int) Try {
return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning,
Steps: []TryStep{{Ask: "a", State: delivery.StepRunning}}}
Steps: []TryStep{{Ask: "a", State: buildstep.StepRunning}}}
}
waitingFor := func(i int) Try {
return Try{ID: fmt.Sprintf("try-%d", i), Repository: repoA, Head: fmt.Sprintf("%040x", i), Trunk: trunkA, State: TryRunning,
@@ -133,7 +133,7 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { /
tr.Put(ctx, id, raw)
tr.Wait()
got := readTry(t, kv, id)
if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[delivery.LevelUnit].Run != 2 {
if got.State != TryPass || got.Took == "" || got.Head != head1 || got.Trunk != trunkA || got.Said == "" || got.Levels[buildstep.LevelUnit].Run != 2 {
t.Fatalf("try = %+v", got)
}
// Issue 1890: while it runs, the try names its ask as soon as it is put, and then where its step stands; the end
@@ -142,17 +142,17 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { /
written.running[2].Steps[0].Node != "shanks" {
t.Errorf("the running try's writes = %+v, want its ask named while it ran, then its node", written.running)
}
if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != delivery.StepPassed {
if len(got.Steps) != 1 || got.Steps[0].Node != "shanks" || got.Steps[0].State != buildstep.StepPassed {
t.Errorf("the ended try's steps = %+v", got.Steps)
}
// The try names the asks it put, so the delivery view counts their steps as a try's (build_usage).
if len(got.Asks) != 1 {
t.Errorf("asks = %v, want the one check ask the try put", got.Asks)
} else if _, err := delivery.AskStepKey("shanks", got.Asks[0]); err != nil {
} else if _, err := buildstep.AskStepKey("shanks", got.Asks[0]); err != nil {
t.Errorf("the try names %q, which is no ask id: %v", got.Asks[0], err)
}
fetch := b.steps.tool("fetch")
if len(fetch) != 1 || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != "" {
if len(fetch) != 1 || fetch[0].args["commit"] != head1 || fetch[0].args["base"] != trunkA || fetch[0].args["ref"] != nil {
t.Errorf("fetch = %+v, want the head merged onto the trunk, with no pull request's ref", fetch)
}
if keys, _ := b.prev.Keys(ctx); len(keys) != 0 {
@@ -170,7 +170,7 @@ func TestATryIsJudgedAsAPullRequestsHeadAndOwesTheForgeNothing(t *testing.T) { /
func TestATryOfOneLevelAndSomeTestsRunsThemAloneAndKeepsNothing(t *testing.T) {
ctx := context.Background()
kv := newMemKV()
id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Level: delivery.LevelIntegration, Tests: "TestBus|TestStore"}, time.Now())
id, err := AskTry(memTries{kv}, tryTrunks, TryAsk{Repository: repoA, Head: head1, Level: buildstep.LevelIntegration, Tests: "TestBus|TestStore"}, time.Now())
if err != nil {
t.Fatal(err)
}
@@ -180,7 +180,7 @@ func TestATryOfOneLevelAndSomeTestsRunsThemAloneAndKeepsNothing(t *testing.T) {
tr.Put(ctx, id, raw)
tr.Wait()
tests := b.steps.tool("test")
if len(tests) != 1 || tests[0].args["level"] != delivery.LevelIntegration || tests[0].args["run"] != "TestBus|TestStore" {
if len(tests) != 1 || tests[0].args["level"] != buildstep.LevelIntegration || tests[0].args["run"] != "TestBus|TestStore" {
t.Fatalf("tests = %+v, want the integration level alone with the run pattern", tests)
}
if n := len(b.steps.tool("digest")); n != 0 {
@@ -5,7 +5,8 @@ import (
"strings"
"sync"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
)
// Turns holds the gate's merge turns (novox/hq ADR 0548 §3), by repository, as a watch of the gate's turns bucket puts them
@@ -13,26 +14,26 @@ import (
// approved merge goes before a check not yet started. A bucket not there is read as empty.
type Turns struct {
mu sync.Mutex
byRepo map[string]delivery.Turn
byRepo map[string]meshdelivery.TurnsByRepositoryValue
}
// Put takes a value of the turns bucket in, or its delete, and answers whether what the checker holds changed. A value that
// is not a turn is dropped as a delete would be: a turn the checker cannot read holds nothing.
func (t *Turns) Put(key string, value []byte, deleted bool) bool {
repository, err := delivery.ParseTurnsKey(key)
repository, err := meshdeliveryclient.ParseTurnsKey(key)
if err != nil {
return false
}
t.mu.Lock()
defer t.mu.Unlock()
if t.byRepo == nil {
t.byRepo = map[string]delivery.Turn{}
t.byRepo = map[string]meshdelivery.TurnsByRepositoryValue{}
}
cur, had := t.byRepo[repository]
if !deleted {
if turn, err := delivery.DecodeTurn(value); err == nil {
if turn, err := meshdeliveryclient.DecodeTurn(value); err == nil {
t.byRepo[repository] = turn
return !had || !delivery.SameTurn(cur, turn)
return !had || !meshdeliveryclient.SameTurn(cur, turn)
}
}
delete(t.byRepo, repository)
@@ -41,9 +42,9 @@ func (t *Turns) Put(key string, value []byte, deleted bool) bool {
// Holding names the first of the repositories (by name) a turn holds off the check of the heads, and the delivery that
// waits first in it; false when none does.
func (t *Turns) Holding(repositories []string, heads string) (string, delivery.TurnWait, bool) {
func (t *Turns) Holding(repositories []string, heads string) (string, meshdelivery.TurnsByRepositoryValueWaitingItem, bool) {
if t == nil {
return "", delivery.TurnWait{}, false
return "", meshdelivery.TurnsByRepositoryValueWaitingItem{}, false
}
sorted := make([]string, 0, len(repositories))
for _, r := range repositories {
@@ -54,7 +55,7 @@ func (t *Turns) Holding(repositories []string, heads string) (string, delivery.T
defer t.mu.Unlock()
for _, r := range sorted {
turn, ok := t.byRepo[r]
if !ok || !delivery.TurnHolds(turn, heads) {
if !ok || !meshdeliveryclient.TurnHolds(turn, heads) {
continue
}
for _, w := range turn.Waiting {
@@ -63,14 +64,14 @@ func (t *Turns) Holding(repositories []string, heads string) (string, delivery.T
}
}
}
return "", delivery.TurnWait{}, false
return "", meshdelivery.TurnsByRepositoryValueWaitingItem{}, false
}
// All is every turn held now, by repository (a copy).
func (t *Turns) All() map[string]delivery.Turn {
func (t *Turns) All() map[string]meshdelivery.TurnsByRepositoryValue {
t.mu.Lock()
defer t.mu.Unlock()
out := make(map[string]delivery.Turn, len(t.byRepo))
out := make(map[string]meshdelivery.TurnsByRepositoryValue, len(t.byRepo))
for r, turn := range t.byRepo {
out[r] = turn
}
@@ -3,8 +3,8 @@ package check
import (
"context"
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliverycheckerclient"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker"
)
// EventVerdict is the checker's event that says a pull request's check ended, with the generated files its steps found out
@@ -23,8 +23,8 @@ type VerdictSayer interface {
// sayVerdict says, for each pull request of a check that ended in pass or fail, one verdict event naming its head and its
// out-of-step files. A try owes no pull request anything and says none (ADR 0501 §5). A publish that fails is said in the
// log: the verdict and the status are written already, and the next check of the heads says it again.
func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v delivery.Verdict, out Outcome) {
if ch.Says == nil || cs.Try != nil || (v.State != delivery.VerdictPass && v.State != delivery.VerdictFail) {
func (ch *Checker) sayVerdict(ctx context.Context, cs *Case, v meshdeliverychecker.VerdictsByGroupHeadsValue, out Outcome) {
if ch.Says == nil || cs.Try != nil || (v.State != meshdeliverycheckerclient.VerdictPass && v.State != meshdeliverycheckerclient.VerdictFail) {
return
}
for _, p := range cs.Pulls {
+25 -1
View File
@@ -10,6 +10,8 @@ import (
"slices"
"strings"
"testing"
"mesh-delivery-checker/internal/check"
)
// The controller's manifest reader is mesh-controller internal/catalogue (Manifest, manifest.go: Consumes :521,
@@ -101,7 +103,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
if m.Module != "mesh-delivery-checker" {
t.Errorf("module = %q", m.Module)
}
// delivery.CheckerKeptHistory, CheckerOwedHistory, PreviewsHistory, CheckerVerdictsHistory; the names follow the SDK's buckets.
// The histories of the checker's four seat states (checked against internal/check names below); the names follow the seat mesh-delivery-checker.
if want := []state{{Name: "asks", History: 1}, {Name: "hearing", History: 1}, {Name: "kept", History: 1}, {Name: "owed", History: 5}, {Name: "previews", History: 5}, {Name: "tries", History: 1}, {Name: "verdicts", History: 5}, {Name: "first-views", History: 1}, {Name: "findings", History: 1}}; !reflect.DeepEqual(m.State, want) {
t.Errorf("state = %+v, want %+v", m.State, want)
}
@@ -165,6 +167,28 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
t.Errorf("the manifest declares %q: the checker has none", k)
}
}
// D-11 (ADR 0634 §1): the holders and buckets internal/check names, no contract naming a holder, are the manifest's: a state
// the checker reads is a `reads` (<module>.<state>) and a state of the indexer it follows a `consumes` of its views family.
for bucket, read := range map[string]string{check.SourceBucket: "mesh-delivery-gate.source", check.TurnsBucket: "mesh-delivery-gate.turns",
check.PresenceBucket: "mesh-delivery-watchman.presence", check.HaveBucket: "node-engine.have"} {
if strings.Replace(bucket, "_", ".", 1) != read || !slices.Contains(m.Reads, read) {
t.Errorf("the bucket %s is not the declared read %s", bucket, read)
}
}
for bucket, view := range map[string]string{check.CandidatesBucket: "candidates", check.ModulesBucket: "modules", check.ManifestsBucket: "manifests"} {
if bucket != check.IndexerModule+"_"+view || !slices.Contains(m.Consumes, check.IndexerModule+".views."+view) {
t.Errorf("the bucket %s is not a followed view of the indexer's %s", bucket, view)
}
}
if !slices.Contains(m.Consumes, check.IndexerModule+".views."+check.IndexerPredefinedState) {
t.Errorf("the indexer's state %s is followed, and not declared", check.IndexerPredefinedState)
}
for bucket, history := range map[string]int{check.CheckerVerdictsBucket: 5, check.CheckerKeptBucket: 1, check.CheckerOwedBucket: 5, check.PreviewsBucket: 5} {
own := strings.TrimPrefix(bucket, check.CheckerModule+"_")
if own == bucket || !slices.ContainsFunc(m.State, func(st state) bool { return st.Name == own && st.History == history }) {
t.Errorf("the bucket %s is not a state of the manifest at history %d", bucket, history)
}
}
}
func TestManifestProcess(t *testing.T) { // 1678/AC1