diff --git a/go/delivery/toolchains.go b/go/delivery/toolchains.go index 4fa2361..ecaebe4 100644 --- a/go/delivery/toolchains.go +++ b/go/delivery/toolchains.go @@ -99,22 +99,47 @@ type NodeToolchain struct { Toolchain } -// AgreedToolchain is the one Toolchain2 every node's entry of a language agrees on, or none and why: "toolchain not -// yet reported" with no entries, and "builders disagree" naming each differing version and recipe with the nodes -// reporting it when two differ (novox/hq ADR 0405 clause 2). The answer does not depend on the entries' order. The -// entries must all be of one language. -func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { +// 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 (novox/hq ADR 0584). 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 Toolchain2 + 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 Toolchain2{}, ToolchainNoReport + return Agreement{Wait: ToolchainNoReport} } groups := map[Toolchain2][]string{} for _, e := range entries { k := Toolchain2{Language: e.Language, Version: e.Version, Recipe: e.Recipe} groups[k] = append(groups[k], e.Node) } - if len(groups) == 1 { - for k := range groups { - return k, "" + 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)) @@ -123,5 +148,12 @@ func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { parts = append(parts, fmt.Sprintf("%s recipe %s on %s", k.Version, k.Recipe, strings.Join(nodes, ", "))) } sort.Strings(parts) - return Toolchain2{}, "builders disagree: " + strings.Join(parts, "; ") + return Agreement{Wait: "builders disagree: " + strings.Join(parts, "; ")} +} + +// AgreedToolchain is ToolchainAgreement's toolchain, or none and why (its Wait): the version a strict majority of the +// nodes report (novox/hq ADR 0584, which changes ADR 0405 clause 2 and ADR 0477 clause 2). +func AgreedToolchain(entries []NodeToolchain) (tc Toolchain2, none string) { + a := ToolchainAgreement(entries) + return a.Toolchain, a.Wait } diff --git a/go/delivery/toolchains_test.go b/go/delivery/toolchains_test.go index 5b19b13..36fa002 100644 --- a/go/delivery/toolchains_test.go +++ b/go/delivery/toolchains_test.go @@ -55,36 +55,50 @@ func TestToolchainKeyAndValue(t *testing.T) { } } -// 1443/AC2 -func TestAgreedToolchain(t *testing.T) { - tc, none := AgreedToolchain([]NodeToolchain{tcEntry("ace", "go", "1.26.1", "3"), tcEntry("novox", "go", "1.26.1", "3")}) - if tc != (Toolchain2{Language: "go", Version: "1.26.1", Recipe: "3"}) || none != "" { - t.Errorf("%+v %q", tc, none) - } -} - -// 1443/AC3 -func TestAgreedToolchainNoReport(t *testing.T) { - tc, none := AgreedToolchain(nil) - if tc != (Toolchain2{}) || none != "toolchain not yet reported" { - t.Errorf("%+v %q", tc, none) - } -} - -// 1443/AC4 -func TestAgreedToolchainDisagree(t *testing.T) { - a, b, c := tcEntry("ace", "go", "1.26.1", "3"), tcEntry("novox", "go", "1.26.2", "3"), tcEntry("g14", "go", "1.26.1", "4") - _, first := AgreedToolchain([]NodeToolchain{a, b, c}) - _, second := AgreedToolchain([]NodeToolchain{c, b, a}) - if first == "" || first != second { - t.Errorf("order changed the answer: %q vs %q", first, second) - } - for _, want := range []string{"builders disagree", "1.26.1", "1.26.2", "ace", "novox", "g14"} { - if !strings.Contains(first, want) { - t.Errorf("%q does not name %q", first, want) +// 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) } } - if tc, _ := AgreedToolchain([]NodeToolchain{a, b}); tc != (Toolchain2{}) { - t.Errorf("a disagreement gave %+v", tc) - } }