From 0f9dd8a8e3e3615e034222762baf8d34b9e1f9bc Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:11:04 +0100 Subject: [PATCH 1/3] =?UTF-8?q?Delivery=20sdk:=20graph,=20forms=20and=20pa?= =?UTF-8?q?ckagename=20name=20the=20catalog=20client's=20and=20trunk's=20n?= =?UTF-8?q?ames,=20not=20mesh-sdk's=20go/delivery;=20forms=20holds=20Built?= =?UTF-8?q?Artifact=20(ADR=200634=20=C2=A71,=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- sdk/buildlog/buildlog.go | 21 --------------------- sdk/forms/forms.go | 25 ++++++++++++++++++------- sdk/go.mod | 1 + sdk/graph/graph.go | 25 +++++++++++++------------ sdk/graph/graph_test.go | 4 ++-- sdk/packagename/packagename.go | 6 +++--- 6 files changed, 37 insertions(+), 45 deletions(-) delete mode 100644 sdk/buildlog/buildlog.go diff --git a/sdk/buildlog/buildlog.go b/sdk/buildlog/buildlog.go deleted file mode 100644 index ae49fc58..00000000 --- a/sdk/buildlog/buildlog.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package buildlog reads the builder's log as mesh-delivery's parts that follow it read it: the indexer and the view. -package buildlog - -import ( - "encoding/json" - - "git.novox.be/novox/mesh-sdk/go/delivery" -) - -// IsBuildEntry says whether an entry of the builder's log is a build's start or end, read from its event alone, so an -// entry of another kind (a build's narration, build.said) is passed over before its form is read. One whose event -// cannot be read is taken as a build's, so its decoding says what is wrong with it. -func IsBuildEntry(value []byte) bool { - var e struct { - Event *string `json:"event"` - } - if json.Unmarshal(value, &e) != nil || e.Event == nil { - return true - } - return *e.Event == delivery.BuildStartedEvent || *e.Event == delivery.BuildEndedEvent -} diff --git a/sdk/forms/forms.go b/sdk/forms/forms.go index 8f447800..9fee0dd9 100644 --- a/sdk/forms/forms.go +++ b/sdk/forms/forms.go @@ -1,7 +1,7 @@ // Package forms holds the artifact reference forms the builder writes a done build key in and the assigner holds a want // by (novox/hq ADR 0618 §1-§2, ADR 0624): logic only those two modules of mesh-delivery share, so it is mesh-delivery's -// and not mesh-sdk's (ADR 0593 §1, ADR 0608 §5). The build key's fields (delivery.BuilderBuild.Needs, -// delivery.BuiltArtifact.References) are the contract, in mesh-sdk. +// and not mesh-sdk's (ADR 0593 §1, ADR 0608 §5). The build key's fields (BuilderBuild.Needs, +// BuiltArtifact.References) are the builder's, in its package `builds`. package forms import ( @@ -10,10 +10,21 @@ import ( "strings" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) -// artifactStore opens the artifact store's references (delivery.BuiltArtifact). +// BuiltArtifact is one artifact of a done build, as the forms read it: its name, kind, digest and the reference that names +// where it is, with every form it is named in (References, Reference among them; novox/hq ADR 0618, ADR 0624). A reader +// that does not know References reads Reference alone. The builder's build key holds the same fields (its package +// `builds`), whose type is this one, since both read the forms and the forms are logic of the sdk alone. +type BuiltArtifact struct { + Name string `json:"name"` + Kind string `json:"kind,omitempty"` + Digest string `json:"digest"` + Reference string `json:"reference,omitempty"` + References []string `json:"references,omitempty"` +} + +// artifactStore opens the artifact store's references (BuiltArtifact). const artifactStore = "artifact-store://" // The artifact reference forms (novox/hq ADR 0605 §9, ADR 0618 §1-§2, ADR 0624): each form a done key's reference may be @@ -55,7 +66,7 @@ type ArtifactForm struct { // ArtifactForms are the forms a builder knows, oldest first. type ArtifactForms []ArtifactForm -// Current are the forms the builder writes and the assigner knows: the three first forms (delivery.BuiltArtifact's +// Current are the forms the builder writes and the assigner knows: the three first forms (BuiltArtifact's // reference by kind), then the registry seat's form of a bundle or an archive. var Current = ArtifactForms{ // The registry form precedes the artifact-store form: what a build key kept of an image is recorded without the @@ -105,7 +116,7 @@ func (fs ArtifactForms) Reads(stated []string, form string) bool { // Needs are the reader capabilities a done build's artifacts carry (ADR 0618 §1): `form:` of every form any of // its references, single or listed, is written in, sorted. Nil when it names none (no artifact, or no reference). -func (fs ArtifactForms) Needs(artifacts []delivery.BuiltArtifact) []string { +func (fs ArtifactForms) Needs(artifacts []BuiltArtifact) []string { set := map[string]bool{} for _, a := range artifacts { for _, r := range append([]string{a.Reference}, a.References...) { @@ -206,7 +217,7 @@ func (fs ArtifactForms) Kept(kind string, present, counted map[string][]string) // its first answered in Reference, since a key names where its artifact is (ADR 0498 clause 1). An answered reference // in a form that derives an older one (ArtifactForm.Older) answers that older one too, so the build answers one form and // the key keeps each a counted node reads. -func (fs ArtifactForms) Choose(a delivery.BuiltArtifact, answered []string, present, counted map[string][]string) delivery.BuiltArtifact { +func (fs ArtifactForms) Choose(a BuiltArtifact, answered []string, present, counted map[string][]string) BuiltArtifact { kept := fs.Kept(a.Kind, present, counted) answered = fs.withOlder(a.Kind, answered) type ref struct { diff --git a/sdk/go.mod b/sdk/go.mod index 5d0874d0..9b67c9de 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -9,6 +9,7 @@ require git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshcondi require git.novox.be/novox/mesh-contracts/go v0.0.0-20261104174344-ae5266e77639 // indirect require ( + git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-20261104174344-ae5266e77639 github.com/klauspost/compress v1.20.1 // indirect github.com/nats-io/nats.go v1.54.0 // indirect diff --git a/sdk/graph/graph.go b/sdk/graph/graph.go index e5a0cad3..1291e558 100644 --- a/sdk/graph/graph.go +++ b/sdk/graph/graph.go @@ -9,8 +9,9 @@ import ( "slices" "strings" + "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshcatalogclient" + "git.novox.be/novox/mesh-delivery/sdk/trunk" "git.novox.be/novox/mesh-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine" ) @@ -37,9 +38,9 @@ func decode(data []byte, into *Node) error { return nil } -// checkName refuses a module name a key cannot hold (delivery.ModulesKey's rule). +// checkName refuses a module name a key cannot hold (meshcatalogclient.ModulesKey's rule). func checkName(kind, name string) error { - if _, err := delivery.ModulesKey(name); err != nil { + if _, err := meshcatalogclient.ModulesKey(name); err != nil { return fmt.Errorf("graph: the %s name %q: %w", kind, name, err) } return nil @@ -67,15 +68,15 @@ const ( // State is the name of the indexer's state of the module graph, as its manifest declares it, and Bucket its // bucket: one key per module (Key). State = "graph" - Bucket = delivery.IndexerModule + "_" + State + Bucket = meshcatalogclient.Module + "_" + State // Read is the line a reader's manifest names in `reads` to read the graph. - Read = delivery.IndexerModule + "." + State + Read = meshcatalogclient.Module + "." + State // History is the revisions the graph state keeps per key (ADR 0603 §2). History = 5 ) // Key is the key, in Bucket, of a module: ``, as ModulesKey. -func Key(module string) (string, error) { return delivery.ModulesKey(module) } +func Key(module string) (string, error) { return meshcatalogclient.ModulesKey(module) } // ParseKey is the inverse of Key. func ParseKey(key string) (string, error) { @@ -131,8 +132,8 @@ func (g Node) check() error { switch { case (g.FP == "") == (g.None == ""): return fmt.Errorf("graph: a graph node has either a fingerprint or a reason for none, not both or neither") - case g.FP != "" && !strings.HasPrefix(g.FP, delivery.SourcePrefix2): - return fmt.Errorf("graph: the graph node's fingerprint %q does not start with %s", g.FP, delivery.SourcePrefix2) + case g.FP != "" && !strings.HasPrefix(g.FP, meshcatalogclient.SourcePrefix2): + return fmt.Errorf("graph: the graph node's fingerprint %q does not start with %s", g.FP, meshcatalogclient.SourcePrefix2) } if err := checkRepository(g.Repository); err != nil { return err @@ -143,7 +144,7 @@ func (g Node) check() error { } } for _, p := range g.Packages { - if _, _, ok := delivery.PackageRepository(p); !ok { + if _, _, ok := trunk.PackageRepository(p); !ok { return fmt.Errorf("graph: the graph node's package %q is no mesh package's path", p) } } @@ -269,12 +270,12 @@ func (g Graph) Requirers(provision string) []string { // the module whose directory is the package's directory or the nearest one above it (the longest match, a package // published from inside a module's directory belonging to that module), or "" when the graph holds none. func (g Graph) Publisher(pkg string) string { - repository, dir, ok := delivery.PackageRepository(pkg) + repository, dir, ok := trunk.PackageRepository(pkg) if !ok { return "" } if dir == "" { - dir = delivery.RootPath + dir = meshcatalogclient.RootPath } best, bestLen := "", -1 for _, name := range slices.Sorted(maps.Keys(g)) { @@ -285,7 +286,7 @@ func (g Graph) Publisher(pkg string) string { switch { case n.Path == dir: return name - case n.Path == delivery.RootPath: + case n.Path == meshcatalogclient.RootPath: if bestLen < 0 { best, bestLen = name, 0 } diff --git a/sdk/graph/graph_test.go b/sdk/graph/graph_test.go index 85f07596..6e55adf5 100644 --- a/sdk/graph/graph_test.go +++ b/sdk/graph/graph_test.go @@ -9,8 +9,8 @@ import ( "testing" 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-sdk/go/catalogue" - "git.novox.be/novox/mesh-sdk/go/delivery" ) // TestGraphEdges: the module graph's cycle, dependents and providers, each from the edges alone (ADR 0603 §2, ADR 0604 §1). @@ -30,7 +30,7 @@ func TestGraphEdges(t *testing.T) { // A base chain, a package and a build module: app stands on base, imports lib's package, is built by build-go. g := Graph{ "build-go": node("novox/mesh-catalog", "modules/build-go", func(n *Node) { n.Toolchains = gotool }), - "lib": node("novox/lib", delivery.RootPath, nil), + "lib": node("novox/lib", meshcatalogclient.RootPath, nil), "base": node("novox/mesh-catalog", "modules/base", nil), "app": node("novox/mesh-catalog", "modules/app", func(n *Node) { n.Bases, n.Packages, n.Toolchains = []string{"base"}, []string{"git.novox.be/novox/lib"}, gotool diff --git a/sdk/packagename/packagename.go b/sdk/packagename/packagename.go index 86d8214b..93b8ba34 100644 --- a/sdk/packagename/packagename.go +++ b/sdk/packagename/packagename.go @@ -12,15 +12,15 @@ import ( "path" "strings" + "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" ) // ReadFile reads one file of a repository at a commit; a file the commit does not hold is nil with no error. type ReadFile func(ctx context.Context, repository, commit, file string) ([]byte, error) // Of is a package artifact's name in its ecosystem, read at commit from the first of its paths (or the module's directory -// dir, delivery.RootPath or empty for the repository's root) that holds its go.mod (Go) or package.json (TypeScript). A +// dir, meshcatalogclient.RootPath or empty for the repository's root) that holds its go.mod (Go) or package.json (TypeScript). A // file read that fails, a go.mod that names no module and a package.json that names no name are errors, said. func Of(ctx context.Context, read ReadFile, repository, commit, dir string, a catalogue.Artifact) (string, error) { file := "package.json" @@ -28,7 +28,7 @@ func Of(ctx context.Context, read ReadFile, repository, commit, dir string, a ca file = "go.mod" } base := dir - if base == delivery.RootPath { + if base == meshcatalogclient.RootPath { base = "" } roots := a.Paths From d38a0c8607c8e19088239abe2c224f439d6c85f0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:11:11 +0100 Subject: [PATCH 2/3] =?UTF-8?q?Delete=20sdk/buildlog:=20the=20indexer=20an?= =?UTF-8?q?d=20the=20view=20ask=20the=20builder's=20own=20builds.IsBuildEn?= =?UTF-8?q?try=20(the=20view's=20read.go=20is=20the=20only=20file=20of=20t?= =?UTF-8?q?he=20view=20touched;=20ADR=200634=20=C2=A71;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- modules/mesh-delivery-indexer/internal/index/builds.go | 4 ++-- modules/mesh-delivery-indexer/internal/index/imports.go | 4 ++-- modules/mesh-delivery-view/internal/view/read.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/mesh-delivery-indexer/internal/index/builds.go b/modules/mesh-delivery-indexer/internal/index/builds.go index bba38c57..f098a463 100644 --- a/modules/mesh-delivery-indexer/internal/index/builds.go +++ b/modules/mesh-delivery-indexer/internal/index/builds.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "git.novox.be/novox/mesh-delivery/sdk/buildlog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -33,7 +33,7 @@ func NewBuilds() *Builds { return &Builds{records: map[string]delivery.BuildReco // *The node-engine's own build*) is no build and is passed over, whatever its form. A build's entry that does not read, or // names no module or repository, is refused and said. func (b *Builds) Put(value []byte) error { - if !buildlog.IsBuildEntry(value) { + if !builds.IsBuildEntry(value) { return nil } e, err := delivery.DecodeBuildLogEntry(value) diff --git a/modules/mesh-delivery-indexer/internal/index/imports.go b/modules/mesh-delivery-indexer/internal/index/imports.go index b00dd7be..bab18bf6 100644 --- a/modules/mesh-delivery-indexer/internal/index/imports.go +++ b/modules/mesh-delivery-indexer/internal/index/imports.go @@ -7,7 +7,7 @@ import ( "slices" "strings" - "git.novox.be/novox/mesh-delivery/sdk/buildlog" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/delivery" "mesh-delivery-indexer/internal/fingerprint" @@ -25,7 +25,7 @@ import ( // Imports that cannot be resolved (a package the forge cannot list) are kept with no ImportForm: the next recompute that // resolves them writes the import form, one rebuild too many, never a stale module. func (ix *Indexer) ApplyBuildEnd(ctx context.Context, value []byte) error { - if !buildlog.IsBuildEntry(value) { + if !builds.IsBuildEntry(value) { return nil } e, err := delivery.DecodeBuildLogEntry(value) diff --git a/modules/mesh-delivery-view/internal/view/read.go b/modules/mesh-delivery-view/internal/view/read.go index bfc21be3..e886b626 100644 --- a/modules/mesh-delivery-view/internal/view/read.go +++ b/modules/mesh-delivery-view/internal/view/read.go @@ -14,8 +14,8 @@ import ( "sync/atomic" "time" + "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-indexer/takein" - "git.novox.be/novox/mesh-delivery/sdk/buildlog" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/delivery" ) @@ -912,7 +912,7 @@ func (r Reader) BuildHistory(module string, since time.Time) (HistoryAnswer, err } var entries []delivery.BuildLogEntry for _, le := range page.Entries { - if !buildlog.IsBuildEntry(le.Value) { + if !builds.IsBuildEntry(le.Value) { continue // an entry of another kind (the old reference form's close, design 51) is no build } e, err := delivery.DecodeBuildLogEntry(le.Value) From 9350c3807d40e837e44779750b184ec852efed93 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 16:11:41 +0100 Subject: [PATCH 3/3] =?UTF-8?q?Delivery=20SDK:=20the=20toolchain=20a=20lan?= =?UTF-8?q?guage's=20builders=20agree=20on=20(entries,=20majority=20rule)?= =?UTF-8?q?=20over=20a=20seat-neutral=20form,=20so=20the=20builder,=20the?= =?UTF-8?q?=20checker=20and=20the=20indexer=20read=20a=20build=20seat's=20?= =?UTF-8?q?toolchains=20state=20with=20one=20rule=20(ADR=200634=20=C2=A71,?= =?UTF-8?q?=20=C2=A72;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- sdk/buildstep/toolchains.go | 85 ++++++++++++++++++++++++++++++++ sdk/buildstep/toolchains_test.go | 58 ++++++++++++++++++++++ 2 files changed, 143 insertions(+) create mode 100644 sdk/buildstep/toolchains.go create mode 100644 sdk/buildstep/toolchains_test.go diff --git a/sdk/buildstep/toolchains.go b/sdk/buildstep/toolchains.go new file mode 100644 index 00000000..6dc878f7 --- /dev/null +++ b/sdk/buildstep/toolchains.go @@ -0,0 +1,85 @@ +package buildstep + +import ( + "fmt" + "sort" + "strings" +) + +// **The toolchain a language's builders agree on** (novox/hq ADR 0584): the nodes report their toolchain in the state +// `toolchains` of a node-build seat (the seat's generated type holds the entry); this file holds the one rule over the +// entries every reader of that state shares, over a seat-neutral form, so no reader copies it. + +// ToolchainNoReport is the reason AgreedToolchain gives when no node has reported a language's toolchain. +const ToolchainNoReport = "toolchain not yet reported" + +// Toolchain is what a toolchain is identified by: the language, the installed version and the build module's recipe +// version. It has the fields of the mesh-catalog client's Toolchain2, which a caller converts it to. +type Toolchain struct { + Language string + Version string + Recipe string +} + +// NodeToolchain is one node's entry of a language, as read from a build seat's state toolchains. +type NodeToolchain struct { + Node string + Toolchain +} + +// SittingOut is a node whose toolchain of a language is not the agreed one: it builds nothing of that language until +// it reports the agreed version (novox/hq ADR 0584 clause 2). +type SittingOut struct { + Node string + Version string + Recipe string +} + +// Agreement is what the nodes' entries of one language come to. Toolchain is the agreed one, set when Wait is empty. +// Wait is why nothing is agreed: "toolchain not yet reported" with no entries, or "builders disagree" naming each version +// and recipe with the nodes reporting it, when no version has a strict majority. SittingOut lists, sorted by node, the +// nodes that report another version than the agreed one. +type Agreement struct { + Toolchain Toolchain + SittingOut []SittingOut + Wait string +} + +// ToolchainAgreement is the agreed toolchain of a language: the version and recipe a strict majority of the reporting +// nodes report (novox/hq ADR 0584 clause 1). With no strict majority (two against two) or no entry it gives no toolchain +// and says why in Wait. The answer does not depend on the entries' order. The entries must all be of one language. +func ToolchainAgreement(entries []NodeToolchain) Agreement { + if len(entries) == 0 { + return Agreement{Wait: ToolchainNoReport} + } + groups := map[Toolchain][]string{} + for _, e := range entries { + k := Toolchain{Language: e.Language, Version: e.Version, Recipe: e.Recipe} + groups[k] = append(groups[k], e.Node) + } + for k, nodes := range groups { + if 2*len(nodes) > len(entries) { + a := Agreement{Toolchain: k} + for _, e := range entries { + if e.Version != k.Version || e.Recipe != k.Recipe { + a.SittingOut = append(a.SittingOut, SittingOut{Node: e.Node, Version: e.Version, Recipe: e.Recipe}) + } + } + sort.Slice(a.SittingOut, func(i, j int) bool { return a.SittingOut[i].Node < a.SittingOut[j].Node }) + return a + } + } + parts := make([]string, 0, len(groups)) + for k, nodes := range groups { + sort.Strings(nodes) + parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", "))) + } + sort.Strings(parts) + return Agreement{Wait: "builders disagree: " + strings.Join(parts, "; ")} +} + +// AgreedToolchain is ToolchainAgreement's toolchain, or none and why (its Wait). +func AgreedToolchain(entries []NodeToolchain) (tc Toolchain, none string) { + a := ToolchainAgreement(entries) + return a.Toolchain, a.Wait +} diff --git a/sdk/buildstep/toolchains_test.go b/sdk/buildstep/toolchains_test.go new file mode 100644 index 00000000..a1f376ef --- /dev/null +++ b/sdk/buildstep/toolchains_test.go @@ -0,0 +1,58 @@ +package buildstep + +import ( + "strings" + "testing" +) + +func tcEntry(node, lang, version, recipe string) NodeToolchain { + return NodeToolchain{Node: node, Toolchain: Toolchain{Language: lang, Version: version, Recipe: recipe}} +} + +// 1443/AC2-4, ADR 0584: the agreed toolchain is the strict majority's; a minority node sits out; a tie waits. +func TestToolchainAgreement(t *testing.T) { + e := tcEntry + for _, c := range []struct { + name string + in []NodeToolchain + version string + out []SittingOut + wait []string // "" wait means agreed + }{ + {"all agree", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"three against one", []NodeToolchain{e("ace", "typescript", "26.12.1", "3"), e("g14", "typescript", "26.13.0", "3"), e("novox", "typescript", "26.13.0", "3"), e("shanks", "typescript", "26.13.0", "3")}, + "26.13.0", []SittingOut{{"ace", "26.12.1", "3"}}, nil}, + {"two against two", []NodeToolchain{e("ace", "go", "1.26.1", "3"), e("novox", "go", "1.26.2", "3"), e("g14", "go", "1.26.1", "3"), e("shanks", "go", "1.26.2", "3")}, + "", nil, []string{"builders disagree", "1.26.1 recipe 3 on ace, g14", "1.26.2 recipe 3 on novox, shanks"}}, + {"one node alone", []NodeToolchain{e("ace", "go", "1.26.1", "3")}, "1.26.1", nil, nil}, + {"no report", nil, "", nil, []string{ToolchainNoReport}}, + } { + a := ToolchainAgreement(c.in) + rev := make([]NodeToolchain, len(c.in)) + for i, x := range c.in { + rev[len(c.in)-1-i] = x + } + if b := ToolchainAgreement(rev); b.Wait != a.Wait || b.Toolchain != a.Toolchain { + t.Errorf("%s: order changed the answer: %+v vs %+v", c.name, a, b) + } + if a.Toolchain.Version != c.version || len(a.SittingOut) != len(c.out) { + t.Errorf("%s: %+v", c.name, a) + } + for i := range c.out { + if a.SittingOut[i] != c.out[i] { + t.Errorf("%s: sitting out %+v, want %+v", c.name, a.SittingOut, c.out) + } + } + if (len(c.wait) == 0) != (a.Wait == "") { + t.Errorf("%s: wait %q", c.name, a.Wait) + } + for _, w := range c.wait { + if !strings.Contains(a.Wait, w) { + t.Errorf("%s: %q does not name %q", c.name, a.Wait, w) + } + } + if tc, none := AgreedToolchain(c.in); tc != a.Toolchain || none != a.Wait { + t.Errorf("%s: AgreedToolchain %+v %q", c.name, tc, none) + } + } +}