diff --git a/go/compose/judge/judge.go b/go/compose/judge/judge.go index 1b3cab5..698a3f6 100644 --- a/go/compose/judge/judge.go +++ b/go/compose/judge/judge.go @@ -121,7 +121,7 @@ func Intended(current, laidInput *compose.Input, currentPlaced map[string]delive // shown in the ask's brief. The seed alone fails on any difference. func JudgeAnswers(p delivery.PlacementProposal, intended map[string]map[string]bool, answers map[string]*delivery.CandidateAnswer, revisions map[string]uint64, online map[string]bool, pulsing map[string]bool, now time.Time) delivery.PlacementVerdict { - v := delivery.PlacementVerdict{At: now.UTC(), Intended: map[string][]string{}, Unintended: map[string][]string{}, + v := delivery.PlacementVerdict{At: now.UTC(), Intended: map[string][]string{}, Unintended: map[string][]string{}, Carried: map[string][]string{}, Answers: map[string]delivery.CandidateAnswer{}} for node, set := range intended { for m := range set { @@ -150,7 +150,8 @@ func JudgeAnswers(p delivery.PlacementProposal, intended map[string]map[string]b want := intended[node] differs := func(module, words string) { if p.Seed { - v.Fails = append(v.Fails, fmt.Sprintf("%s %s with the seed, which must be the same as what runs", node, words)) + // The seed carries what its nodes found, each named in the brief for the operator who approves it. + v.Carried[node] = append(v.Carried[node], words) return } if !want[module] { @@ -158,7 +159,7 @@ func JudgeAnswers(p delivery.PlacementProposal, intended map[string]map[string]b } } for _, d := range a.Differs { - differs(d.Module, fmt.Sprintf("composes %s differently (%s)", d.Module, d.Why)) + differs(d.Module, differenceWords(d)) } for _, d := range a.Added { differs(d.Module, "would run "+d.Module) @@ -174,6 +175,9 @@ func JudgeAnswers(p delivery.PlacementProposal, intended map[string]map[string]b if len(v.Unintended) == 0 { v.Unintended = nil } + if len(v.Carried) == 0 { + v.Carried = nil + } if len(v.Answers) == 0 { v.Answers = nil } @@ -228,6 +232,9 @@ func Brief(p delivery.PlacementProposal, v delivery.PlacementVerdict) string { nodes[n] = true } } + for n := range v.Carried { + nodes[n] = true + } names := make([]string, 0, len(nodes)) for n := range nodes { names = append(names, n) @@ -243,6 +250,9 @@ func Brief(p delivery.PlacementProposal, v delivery.PlacementVerdict) string { if m := v.Unintended[n]; len(m) > 0 { fmt.Fprintf(&b, "; also differs, not computed: %s", strings.Join(m, ", ")) } + if m := v.Carried[n]; len(m) > 0 { + fmt.Fprintf(&b, "; the seed carries: %s", strings.Join(m, "; ")) + } if held := v.Answers[n].Held; len(held) > 0 { var names []string for _, h := range held { @@ -253,3 +263,21 @@ func Brief(p delivery.PlacementProposal, v delivery.PlacementVerdict) string { } return b.String() } + +// differenceWords names one module's difference: the module, the resource and the first path that differs in it, and how +// many more, or why when no body is shown. +func differenceWords(d delivery.ModuleDiffers) string { + for _, b := range d.Bodies { + if len(b.Paths) > 0 { + words := fmt.Sprintf("%s: %s at %s", d.Module, b.ID, b.Paths[0]) + if n := len(b.Paths) - 1; n > 0 { + words += fmt.Sprintf(" (and %d more)", n) + } + return words + } + } + if d.Resource != "" { + return fmt.Sprintf("%s: %s (%s)", d.Module, d.Resource, d.Why) + } + return fmt.Sprintf("%s: %s", d.Module, d.Why) +} diff --git a/go/compose/judge/judge_test.go b/go/compose/judge/judge_test.go index 077d644..2cc5454 100644 --- a/go/compose/judge/judge_test.go +++ b/go/compose/judge/judge_test.go @@ -47,7 +47,9 @@ func TestJudge(t *testing.T) { {"a module added where intended", false, map[string]*delivery.CandidateAnswer{"anchor": {Proposal: "p-one", Revision: 7, Verdict: delivery.CandidateDiffers, Added: []delivery.ModuleAdded{{Module: "web", Unissued: []string{"its own secret"}}}}, "laptop": same(8)}, nil, delivery.VerdictPass, "", ""}, {"a difference not computed passes, marked", false, map[string]*delivery.CandidateAnswer{"anchor": differs(7, "mail"), "laptop": same(8)}, nil, delivery.VerdictPass, "", "mail"}, - {"the seed with a difference", true, map[string]*delivery.CandidateAnswer{"anchor": same(7), "laptop": differs(8, "web")}, nil, delivery.VerdictFail, "laptop composes web differently", ""}, + {"the seed carries a difference, named", true, map[string]*delivery.CandidateAnswer{"anchor": same(7), "laptop": {Proposal: "p-one", Revision: 8, + Verdict: delivery.CandidateDiffers, Differs: []delivery.ModuleDiffers{{Module: "node-tools", Resource: "node-tools.runtime", Why: "the resource differs", + Bodies: []delivery.ResourceBodies{{ID: "node-tools.runtime", Paths: []string{"env.MESH_OWNED_PATHS./etc/resolv.conf", "env.MESH_TOOL_MODULES"}}}}}}}, nil, delivery.VerdictPass, "", ""}, {"a refusal", false, map[string]*delivery.CandidateAnswer{"anchor": same(7), "laptop": {Proposal: "p-one", Revision: 8, Verdict: delivery.CandidateRefused, Refusals: []delivery.ModuleRefused{{Module: "web", Why: "no own secret"}}}}, nil, delivery.VerdictFail, "laptop refuses", ""}, {"a node not answering, within its bound", false, map[string]*delivery.CandidateAnswer{"anchor": same(7)}, map[string]bool{"laptop": true}, delivery.VerdictWaits, "laptop (offline)", ""}, @@ -63,6 +65,9 @@ func TestJudge(t *testing.T) { if v.State != row.state || !strings.Contains(all, row.words) { t.Errorf("%s: %s, %q", row.name, v.State, all) } + if row.seed && row.state == delivery.VerdictPass && !strings.Contains(Brief(q, v), "the seed carries: node-tools: node-tools.runtime at env.MESH_OWNED_PATHS./etc/resolv.conf (and 1 more)") { + t.Errorf("%s: the brief does not name it: %s", row.name, Brief(q, v)) + } if strings.HasPrefix(row.name, "a module held as today") && !strings.Contains(Brief(q, v), "stays held as today: spiegel") { t.Errorf("%s: the brief does not show it: %s", row.name, Brief(q, v)) } diff --git a/go/delivery/placement_proposal.go b/go/delivery/placement_proposal.go index 1a99de2..6548705 100644 --- a/go/delivery/placement_proposal.go +++ b/go/delivery/placement_proposal.go @@ -356,15 +356,19 @@ func NewProposalID(at time.Time) string { // is shown by node: Intended is what the holder computed the proposal changes, Unintended what a node answered beside // it, marked as such in the brief. type PlacementVerdict struct { - State string `json:"state"` - At time.Time `json:"at"` - Fails []string `json:"fails,omitempty"` - Silent []string `json:"silent,omitempty"` - Until time.Time `json:"until,omitempty"` - Intended map[string][]string `json:"intended,omitempty"` - Unintended map[string][]string `json:"unintended,omitempty"` - Answers map[string]CandidateAnswer `json:"answers,omitempty"` - Notes []string `json:"notes,omitempty"` + State string `json:"state"` + At time.Time `json:"at"` + Fails []string `json:"fails,omitempty"` + Silent []string `json:"silent,omitempty"` + Until time.Time `json:"until,omitempty"` + Intended map[string][]string `json:"intended,omitempty"` + Unintended map[string][]string `json:"unintended,omitempty"` + // Carried is, for the seed, by node each difference a node found that the seed carries, named for the operator who + // approves it knowingly (": at "): the seed fails on no difference, only on a refusal or + // silence (issue 1126). + Carried map[string][]string `json:"carried,omitempty"` + Answers map[string]CandidateAnswer `json:"answers,omitempty"` + Notes []string `json:"notes,omitempty"` } // PlacementProposal is an open proposal, the value of its key in PlacementProposalsBucket.