Name in the seed's brief the first differing field's value as it runs and as composed, so the operator sees what changes (issue 1126)
mesh/delivery-group group fix/1126-seed-held rejected: composed together: the checker's verdict at the members' heads (63905bd4b284): the composition
mesh/delivery merged: source.main moved to novox/mesh-sdk@f9afe328881f
mesh/check being carried to 77b48a4f1aff on f9afe328881f | from 53f26493697e
mesh/delivery-group group fix/1126-seed-held rejected: composed together: the checker's verdict at the members' heads (63905bd4b284): the composition
mesh/delivery merged: source.main moved to novox/mesh-sdk@f9afe328881f
mesh/check being carried to 77b48a4f1aff on f9afe328881f | from 53f26493697e
This commit is contained in:
@@ -5,9 +5,11 @@
|
|||||||
package judge
|
package judge
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -264,20 +266,103 @@ func Brief(p delivery.PlacementProposal, v delivery.PlacementVerdict) string {
|
|||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
// differenceWords names one module's difference: the module, the resource and the first path that differs in it, and how
|
// differenceWords names one module's difference: the module, the first field that differs in it with its value as it
|
||||||
// many more, or why when no body is shown.
|
// runs and as the seed composes it, each cut to about 60 characters ("(value cut)" where a body shown is cut before the
|
||||||
|
// field), and how many fields more; or the resource and why when no body is shown.
|
||||||
func differenceWords(d delivery.ModuleDiffers) string {
|
func differenceWords(d delivery.ModuleDiffers) string {
|
||||||
for _, b := range d.Bodies {
|
for _, b := range d.Bodies {
|
||||||
if len(b.Paths) > 0 {
|
if len(b.Paths) == 0 {
|
||||||
words := fmt.Sprintf("%s: %s at %s", d.Module, b.ID, b.Paths[0])
|
continue
|
||||||
|
}
|
||||||
|
path := b.Paths[0]
|
||||||
|
words := fmt.Sprintf("%s: %s %s → %s", d.Module, fieldName(b.Running, b.Composed, path),
|
||||||
|
valueAt(b.Running, path), valueAt(b.Composed, path))
|
||||||
if n := len(b.Paths) - 1; n > 0 {
|
if n := len(b.Paths) - 1; n > 0 {
|
||||||
words += fmt.Sprintf(" (and %d more)", n)
|
words += fmt.Sprintf(" (and %d more fields)", n)
|
||||||
}
|
}
|
||||||
return words
|
return words
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if d.Resource != "" {
|
if d.Resource != "" {
|
||||||
return fmt.Sprintf("%s: %s (%s)", d.Module, d.Resource, d.Why)
|
return fmt.Sprintf("%s: %s (%s)", d.Module, d.Resource, d.Why)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s: %s", d.Module, d.Why)
|
return fmt.Sprintf("%s: %s", d.Module, d.Why)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shownValue bounds a value named in a brief.
|
||||||
|
const shownValue = 60
|
||||||
|
|
||||||
|
// walk follows a differing path (delivery.DifferingPaths: keys joined by dots, a key may hold dots, and a JSON value
|
||||||
|
// written in a string is read as JSON) through a body, answering the keys it passed and the value at its end.
|
||||||
|
func walk(body, path string) (keys []string, value any, ok bool) {
|
||||||
|
var v any
|
||||||
|
if json.Unmarshal([]byte(body), &v) != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
rest := path
|
||||||
|
for rest != "" {
|
||||||
|
if s, isString := v.(string); isString {
|
||||||
|
var inner any
|
||||||
|
if json.Unmarshal([]byte(s), &inner) != nil {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
v = inner
|
||||||
|
}
|
||||||
|
switch t := v.(type) {
|
||||||
|
case map[string]any:
|
||||||
|
best := ""
|
||||||
|
for k := range t {
|
||||||
|
if (rest == k || strings.HasPrefix(rest, k+".")) && len(k) > len(best) {
|
||||||
|
best = k
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if best == "" {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
keys, v, rest = append(keys, best), t[best], strings.TrimPrefix(strings.TrimPrefix(rest, best), ".")
|
||||||
|
case []any:
|
||||||
|
seg, after, _ := strings.Cut(rest, ".")
|
||||||
|
n, err := strconv.Atoi(seg)
|
||||||
|
if err != nil || n < 0 || n >= len(t) {
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
keys, v, rest = append(keys, seg), t[n], after
|
||||||
|
default:
|
||||||
|
return nil, nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return keys, v, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// valueAt is the value at path in a shown body, cut to shownValue, or why it is not shown.
|
||||||
|
func valueAt(body, path string) string {
|
||||||
|
if body == "" {
|
||||||
|
return "(none)"
|
||||||
|
}
|
||||||
|
_, v, ok := walk(body, path)
|
||||||
|
if !ok {
|
||||||
|
return "(value cut)"
|
||||||
|
}
|
||||||
|
s, isString := v.(string)
|
||||||
|
if !isString {
|
||||||
|
raw, _ := json.Marshal(v)
|
||||||
|
s = string(raw)
|
||||||
|
}
|
||||||
|
if r := []rune(s); len(r) > shownValue {
|
||||||
|
s = string(r[:shownValue-1]) + "…"
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// fieldName is the differing field as a person reads it: its parent's name and its own, `PARENT[key]`, from whichever
|
||||||
|
// body reaches it; the path's end when neither does.
|
||||||
|
func fieldName(running, composed, path string) string {
|
||||||
|
for _, body := range []string{running, composed} {
|
||||||
|
if keys, _, ok := walk(body, path); ok && len(keys) > 0 {
|
||||||
|
if len(keys) == 1 {
|
||||||
|
return keys[0]
|
||||||
|
}
|
||||||
|
return keys[len(keys)-2] + "[" + keys[len(keys)-1] + "]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|||||||
@@ -49,7 +49,9 @@ func TestJudge(t *testing.T) {
|
|||||||
{"a difference not computed passes, marked", false, map[string]*delivery.CandidateAnswer{"anchor": differs(7, "mail"), "laptop": same(8)}, nil, delivery.VerdictPass, "", "mail"},
|
{"a difference not computed passes, marked", false, map[string]*delivery.CandidateAnswer{"anchor": differs(7, "mail"), "laptop": same(8)}, nil, delivery.VerdictPass, "", "mail"},
|
||||||
{"the seed carries a difference, named", true, map[string]*delivery.CandidateAnswer{"anchor": same(7), "laptop": {Proposal: "p-one", Revision: 8,
|
{"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",
|
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, "", ""},
|
Bodies: []delivery.ResourceBodies{{ID: "node-tools.runtime", Paths: []string{"env.MESH_OWNED_PATHS./etc/resolv.conf", "env.MESH_TOOL_MODULES"},
|
||||||
|
Running: `{"env":{"MESH_OWNED_PATHS":"{\"/etc/hosts\":\"hostname\",\"/etc/resolv.conf\":\"networkmanager (node-uplink)\"}"}}`,
|
||||||
|
Composed: `{"env":{"MESH_OWNED_PATHS":"{\"/etc/hosts\":\"hostname\",\"/etc/resolv.conf\":\"systemd-resolved (node-resolver)\"}","MESH_TOOL_MODULES":"… (cut at 8192 bytes)`}}}}}}, nil, delivery.VerdictPass, "", ""},
|
||||||
{"a refusal", false, map[string]*delivery.CandidateAnswer{"anchor": same(7), "laptop": {Proposal: "p-one", Revision: 8, Verdict: delivery.CandidateRefused,
|
{"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", ""},
|
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)", ""},
|
{"a node not answering, within its bound", false, map[string]*delivery.CandidateAnswer{"anchor": same(7)}, map[string]bool{"laptop": true}, delivery.VerdictWaits, "laptop (offline)", ""},
|
||||||
@@ -65,9 +67,18 @@ func TestJudge(t *testing.T) {
|
|||||||
if v.State != row.state || !strings.Contains(all, row.words) {
|
if v.State != row.state || !strings.Contains(all, row.words) {
|
||||||
t.Errorf("%s: %s, %q", row.name, v.State, all)
|
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)") {
|
if row.seed && row.state == delivery.VerdictPass && !strings.Contains(Brief(q, v), "the seed carries: node-tools: MESH_OWNED_PATHS[/etc/resolv.conf] networkmanager (node-uplink) → (value cut) (and 1 more fields)") {
|
||||||
t.Errorf("%s: the brief does not name it: %s", row.name, Brief(q, v))
|
t.Errorf("%s: the brief does not name it: %s", row.name, Brief(q, v))
|
||||||
}
|
}
|
||||||
|
if row.seed && row.state == delivery.VerdictPass {
|
||||||
|
whole := row.answers["laptop"].Differs[0]
|
||||||
|
whole.Bodies = []delivery.ResourceBodies{{ID: "r", Paths: []string{"env.MESH_OWNED_PATHS./etc/resolv.conf"},
|
||||||
|
Running: `{"env":{"MESH_OWNED_PATHS":"{\"/etc/resolv.conf\":\"networkmanager (node-uplink)\"}"}}`,
|
||||||
|
Composed: `{"env":{"MESH_OWNED_PATHS":"{\"/etc/resolv.conf\":\"systemd-resolved (node-resolver)\"}"}}`}}
|
||||||
|
if got := differenceWords(whole); got != "node-tools: MESH_OWNED_PATHS[/etc/resolv.conf] networkmanager (node-uplink) → systemd-resolved (node-resolver)" {
|
||||||
|
t.Errorf("the difference is named %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
if strings.HasPrefix(row.name, "a module held as today") && !strings.Contains(Brief(q, v), "stays held as today: spiegel") {
|
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))
|
t.Errorf("%s: the brief does not show it: %s", row.name, Brief(q, v))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user