diff --git a/modules/pacman/cmd/pacman-tools/view.go b/modules/pacman/cmd/pacman-tools/view.go index fc8c2e32..6a1fea92 100644 --- a/modules/pacman/cmd/pacman-tools/view.go +++ b/modules/pacman/cmd/pacman-tools/view.go @@ -118,12 +118,19 @@ func (m *Machine) installedOn(node string) ([]nodeInstalled, error) { } // declaredOn is what each node's modules declare present, by node and package, with the declaring modules, and the source -// that answered for each node: its engine's key, else the controller's mirror; a node neither holds is missing. -func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, map[string]string, error) { +// that answered for each node: its engine's key, else the controller's mirror; a node neither holds is missing. When the +// engine's state cannot be read at all (its bucket is not on the bus yet, or the read is refused) the mirror answers for +// every node and the engine's error is returned as a word to say beside the sources; it is an error only when the mirror +// cannot be read either (ADR 0565 clause 6). +func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, map[string]string, string, error) { out := map[string]map[string][]string{} from := map[string]string{} + engineErr := "" for _, source := range []string{SourceEngine, SourceController} { r := m.States(source) + read := map[string]map[string][]string{} + readFrom := map[string]string{} + var failed error for _, n := range nodes { if from[n] != "" { continue @@ -131,21 +138,32 @@ func (m *Machine) declaredOn(nodes []string) (map[string]map[string][]string, ma var v declaredValue found, err := readValue(r, n, &v) if err != nil { - return nil, nil, err + failed = err + break } if !found { continue } - from[n] = source - out[n] = map[string][]string{} + readFrom[n] = source + read[n] = map[string][]string{} for _, p := range v.Packages { if !p.Absent { - out[n][p.Package] = p.Modules + read[n][p.Package] = p.Modules } } } + if failed != nil { + if source == SourceEngine { + engineErr = SourceEngine + " cannot be read: " + failed.Error() + continue + } + return nil, nil, "", failed + } + for n, d := range read { + out[n], from[n] = d, readFrom[n] + } } - return out, from, nil + return out, from, engineErr, nil } // NodeView is one node in the view. @@ -198,7 +216,7 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) { for _, ni := range all { nodes = append(nodes, ni.Node) } - declared, sources, err := m.declaredOn(nodes) + declared, sources, engineErr, err := m.declaredOn(nodes) if err != nil { return nil, err } @@ -220,7 +238,11 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) { } on = append(on, p) } - return map[string]any{"package": name, "nodes": on}, nil + answer := map[string]any{"package": name, "nodes": on} + if engineErr != "" { + answer["declared_engine_error"] = engineErr + } + return answer, nil } views := []NodeView{} versions := map[string]map[string]string{} @@ -269,6 +291,9 @@ func (m *Machine) Packages(name string, limit int) (map[string]any, error) { } } out := map[string]any{"nodes": views} + if engineErr != "" { + out["declared_engine_error"] = engineErr + } for key, list := range map[string][]Spread{"differing": differing, "some_nodes": some} { b := bound(key, list, limit) out[key] = b[key] diff --git a/modules/pacman/cmd/pacman-tools/view_test.go b/modules/pacman/cmd/pacman-tools/view_test.go index 1c6873f3..6e9dbd51 100644 --- a/modules/pacman/cmd/pacman-tools/view_test.go +++ b/modules/pacman/cmd/pacman-tools/view_test.go @@ -6,10 +6,13 @@ package main import ( "context" "encoding/json" + "errors" "fmt" "strings" "testing" "time" + + stdio "git.novox.be/novox/mesh-sdk/go" ) // viewStates is two nodes' packages on the bus: node-a current and declared, node-b over the cap since a while, @@ -66,6 +69,31 @@ func TestOnePackageOnEveryNodeWithWhoDeclaresIt(t *testing.T) { on2[1].DeclaredFrom != SourceController || strings.Join(on2[1].DeclaredBy, ",") != "legacy" { t.Errorf("fallback: %+v", on2) } + // The engine's state cannot be read (its bucket is not on the bus yet): the mirror answers for every node, and the + // answer says why beside declared_from; a mirror that cannot be read either is an error. + read := m.States + m.States = func(name string) StateReader { + if name == SourceEngine { + return failingReader{errors.New("is issued and its bucket is not on the bus yet")} + } + return read(name) + } + r3, err3 := m.Packages("zsh", 500) + on3, _ := r3["nodes"].([]PackageOnNode) + if err3 != nil || len(on3) != 2 || on3[0].DeclaredFrom != SourceController || strings.Join(on3[0].DeclaredBy, ",") != "stale" || + on3[1].DeclaredFrom != SourceController || !strings.Contains(fmt.Sprint(r3["declared_engine_error"]), "not on the bus yet") { + t.Errorf("engine unreadable: %+v %v", r3, err3) + } + m.States = func(name string) StateReader { + if name == SourceEngine || name == SourceController { + return failingReader{errors.New("refused")} + } + return read(name) + } + if _, err := m.Packages("zsh", 500); err == nil { + t.Error("neither state could be read and no error was said") + } + m.States = read if a.Node != "node-a" || !a.Installed || a.Version != "5.9-2" || !a.Explicit || a.Repository != "extra" || strings.Join(a.DeclaredBy, ",") != "login,shell" || !a.Current { t.Errorf("node-a: %+v", a) @@ -174,3 +202,10 @@ func TestANodeArgumentIsOneKeyToken(t *testing.T) { t.Errorf("%q %v", n, err) } } + +// failingReader is a state that cannot be read. +type failingReader struct{ err error } + +func (f failingReader) Get(string) (*stdio.StateEntry, error) { return nil, f.err } +func (f failingReader) Keys() ([]string, error) { return nil, f.err } +func (f failingReader) History(string) ([]stdio.StateRevision, error) { return nil, f.err }