Merge pull request 'Let a block show a value its module keeps on the bus, and grant the bar the read (hq ADR 0260)' (#149) from feat/the-bar-takes-blocks into main
This commit was merged in pull request #149.
This commit is contained in:
@@ -129,6 +129,23 @@ var barShows = map[string]map[string]barOption{
|
|||||||
"battery": {"device": {kind: "text"}},
|
"battery": {"device": {kind: "text"}},
|
||||||
// One line a command prints, run every interval seconds: what the bar cannot know itself.
|
// One line a command prints, run every interval seconds: what the bar cannot know itself.
|
||||||
"command": {"command": {kind: "text", required: true}, "interval": {kind: "seconds", required: true}},
|
"command": {"command": {kind: "text", required: true}, "interval": {kind: "seconds", required: true}},
|
||||||
|
// A value the contributor keeps on the bus (novox/hq ADR 0255): its state `<module>.<name>` and the
|
||||||
|
// key, this machine's name when absent. The value is an object whose `text` is what is shown. The
|
||||||
|
// holder is granted to read it and shows it however its bar shows a changing value; no file and
|
||||||
|
// no command passes between the two modules.
|
||||||
|
"state": {"state": {kind: "text", required: true}, "key": {kind: "text"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// barBlockReads is the state a block shows, if any.
|
||||||
|
func barBlockReads(data map[string]any) []string {
|
||||||
|
if data["shows"] != "state" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
options, _ := data["options"].(map[string]any)
|
||||||
|
if s, ok := options["state"].(string); ok && s != "" {
|
||||||
|
return []string{s}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type barOption struct {
|
type barOption struct {
|
||||||
@@ -150,11 +167,15 @@ func barBlockShape() *Shape {
|
|||||||
{Field: "place", Values: []string{"resources", "status"}}},
|
{Field: "place", Values: []string{"resources", "status"}}},
|
||||||
Order: "order",
|
Order: "order",
|
||||||
Check: barBlockProblems,
|
Check: barBlockProblems,
|
||||||
|
Reads: barBlockReads,
|
||||||
Examples: []map[string]any{
|
Examples: []map[string]any{
|
||||||
{"bar": "bottom", "place": "status", "shows": "battery"},
|
{"bar": "bottom", "place": "status", "shows": "battery"},
|
||||||
{"bar": "bottom", "place": "status", "shows": "battery", "options": map[string]any{"device": "BAT0"}},
|
{"bar": "bottom", "place": "status", "shows": "battery", "options": map[string]any{"device": "BAT0"}},
|
||||||
{"bar": "bottom", "place": "status", "shows": "command",
|
{"bar": "bottom", "place": "status", "shows": "command",
|
||||||
"options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}},
|
"options": map[string]any{"command": "echo \"a line\"", "interval": float64(5)}},
|
||||||
|
{"bar": "bottom", "place": "status", "shows": "state", "options": map[string]any{"state": "power.draw"}},
|
||||||
|
{"bar": "bottom", "place": "status", "shows": "state",
|
||||||
|
"options": map[string]any{"state": "power.draw", "key": "a-machine"}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ func (m Manifest) seatContributionProblems() []string {
|
|||||||
switch {
|
switch {
|
||||||
case ok && r.Shape != nil:
|
case ok && r.Shape != nil:
|
||||||
problems = append(problems, shapedProblems(m.Module, i, r, c)...)
|
problems = append(problems, shapedProblems(m.Module, i, r, c)...)
|
||||||
|
problems = append(problems, ownStateProblems(m, i, r, c)...)
|
||||||
case len(c.Data) > 0:
|
case len(c.Data) > 0:
|
||||||
problems = append(problems, fmt.Sprintf("%s's contribution %d has data; %s receives %s as text, "+
|
problems = append(problems, fmt.Sprintf("%s's contribution %d has data; %s receives %s as text, "+
|
||||||
"given as `content`", m.Module, i+1, s.Name, c.Kind))
|
"given as `content`", m.Module, i+1, s.Name, c.Kind))
|
||||||
@@ -208,7 +209,7 @@ func seatContributions(modules []Manifest, holder Manifest, placeholder string,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
return shapedContributions(modules, holder, s, r, where, caps)
|
return shapedContributions(modules, holder, facts["name"], s, r, where, caps)
|
||||||
}
|
}
|
||||||
var failed error
|
var failed error
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ type Shape struct {
|
|||||||
// Examples are pieces every holder's template must render, one for each variant the shape has,
|
// Examples are pieces every holder's template must render, one for each variant the shape has,
|
||||||
// so a holder that cannot render one is refused at registration and not found on a machine.
|
// so a holder that cannot render one is refused at registration and not found on a machine.
|
||||||
Examples []map[string]any
|
Examples []map[string]any
|
||||||
|
// Reads is the state on the bus a piece shows, as `<module>.<name>`, or nothing (novox/hq ADR
|
||||||
|
// 0255): a contributor offers only state it keeps itself, and the holder on the same machine is
|
||||||
|
// granted to read it, so the holder's manifest names no contributor.
|
||||||
|
Reads func(data map[string]any) []string `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PlaceField is one field a holder places by, and the values it takes.
|
// PlaceField is one field a holder places by, and the values it takes.
|
||||||
@@ -105,6 +109,62 @@ func shapedProblems(module string, i int, r Receivable, c SeatContribution) []st
|
|||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ownStateProblems is a data contribution showing state its module does not keep (novox/hq ADR 0255):
|
||||||
|
// a module offers the holder its own state and nobody else's, so what it publishes stays its own.
|
||||||
|
func ownStateProblems(m Manifest, i int, r Receivable, c SeatContribution) []string {
|
||||||
|
if r.Shape == nil || r.Shape.Reads == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
keeps := map[string]bool{}
|
||||||
|
for _, s := range m.State {
|
||||||
|
keeps[s.Name] = true
|
||||||
|
}
|
||||||
|
var problems []string
|
||||||
|
for _, read := range r.Shape.Reads(c.Data) {
|
||||||
|
module, local, err := ReadState(read)
|
||||||
|
switch {
|
||||||
|
case err != nil:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's contribution %d shows state %v", m.Module, i+1, err))
|
||||||
|
case module != m.Module || !keeps[local]:
|
||||||
|
problems = append(problems, fmt.Sprintf("%s's contribution %d shows the state %s, which %s does not keep; a "+
|
||||||
|
"module offers its own state only (novox/hq ADR 0255)", m.Module, i+1, read, m.Module))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadsGranted is the state a holder reads because modules on its machine offer it in pieces of a
|
||||||
|
// kind its seat receives as data (novox/hq ADR 0255), sorted and once each. The bus grants the holder
|
||||||
|
// these beside its own `reads`: the holder names no contributor, and reads only what is offered to it
|
||||||
|
// on the machine it runs on.
|
||||||
|
func ReadsGranted(holder Manifest, onMachine []Manifest) []string {
|
||||||
|
seen := map[string]bool{}
|
||||||
|
for _, claim := range holder.Claims {
|
||||||
|
s, known := SeatNamed(claim.Name)
|
||||||
|
if !known {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, r := range s.Receives {
|
||||||
|
if r.Shape == nil || r.Shape.Reads == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, m := range onMachine {
|
||||||
|
for _, c := range m.allContributions() {
|
||||||
|
if cs, ok := SeatNamed(c.Seat); !ok || cs.Name != s.Name || c.Kind != r.Kind {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, read := range r.Shape.Reads(c.Data) {
|
||||||
|
if module, _, err := ReadState(read); err == nil && module == m.Module && module != holder.Module {
|
||||||
|
seen[read] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sortedKeys(seen)
|
||||||
|
}
|
||||||
|
|
||||||
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
|
// wholeIn is v as a whole number within [lo, hi]; JSON gives every number as a float.
|
||||||
func wholeIn(v any, lo, hi int) (int, bool) {
|
func wholeIn(v any, lo, hi int) (int, bool) {
|
||||||
f, ok := v.(float64)
|
f, ok := v.(float64)
|
||||||
@@ -178,11 +238,11 @@ func holderTemplate(holder Manifest, seat Seat, kind string) (*template.Template
|
|||||||
"(novox/hq ADR 0255)", holder.Module, seat.Name, kind)
|
"(novox/hq ADR 0255)", holder.Module, seat.Name, kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
// renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields and
|
// renderPiece is one piece of data in the holder's grammar. The template sees the piece's fields,
|
||||||
// `module`, the contributor. A value the piece does not have reads as nothing in a `with` or an `if`;
|
// `module`, the contributor, and `machine`, the node it is composed for. A value the piece does not have reads as nothing in a `with` or an `if`;
|
||||||
// printed bare it would write `<no value>` into the tool's file, which is refused here instead.
|
// printed bare it would write `<no value>` into the tool's file, which is refused here instead.
|
||||||
func renderPiece(t *template.Template, module string, data map[string]any) (string, error) {
|
func renderPiece(t *template.Template, module, machine string, data map[string]any) (string, error) {
|
||||||
view := map[string]any{"module": module}
|
view := map[string]any{"module": module, "machine": machine}
|
||||||
for k, v := range data {
|
for k, v := range data {
|
||||||
view[k] = v
|
view[k] = v
|
||||||
}
|
}
|
||||||
@@ -211,7 +271,7 @@ type placedPiece struct {
|
|||||||
// A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is
|
// A piece the template renders to nothing — a `shows` the holder does not know yet — or fails on is
|
||||||
// left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's
|
// left out and answered as unplaced, naming its module: the other pieces and the rest of the machine's
|
||||||
// declaration go on (novox/hq ADR 0255).
|
// declaration go on (novox/hq ADR 0255).
|
||||||
func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, []string, error) {
|
func shapedContributions(modules []Manifest, holder Manifest, machine string, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, []string, error) {
|
||||||
var pieces []placedPiece
|
var pieces []placedPiece
|
||||||
for _, m := range inModuleOrder(modules) {
|
for _, m := range inModuleOrder(modules) {
|
||||||
for _, c := range m.allContributions() {
|
for _, c := range m.allContributions() {
|
||||||
@@ -248,7 +308,7 @@ func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivab
|
|||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
var unplaced []string
|
var unplaced []string
|
||||||
for _, p := range pieces {
|
for _, p := range pieces {
|
||||||
out, err := renderPiece(t, p.module, p.data)
|
out, err := renderPiece(t, p.module, machine, p.data)
|
||||||
if err == nil && strings.TrimSpace(out) == "" {
|
if err == nil && strings.TrimSpace(out) == "" {
|
||||||
err = fmt.Errorf("%s's template renders nothing for it", holder.Module)
|
err = fmt.Errorf("%s's template renders nothing for it", holder.Module)
|
||||||
}
|
}
|
||||||
@@ -309,7 +369,7 @@ func (m Manifest) placementProblems() []string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, ex := range r.Shape.Examples {
|
for _, ex := range r.Shape.Examples {
|
||||||
if out, err := renderPiece(t, "example", ex); err != nil || strings.TrimSpace(out) == "" {
|
if out, err := renderPiece(t, "example", "machine", ex); err != nil || strings.TrimSpace(out) == "" {
|
||||||
problems = append(problems, fmt.Sprintf("%s's template for %s:%s renders nothing for %v: %v",
|
problems = append(problems, fmt.Sprintf("%s's template for %s:%s renders nothing for %v: %v",
|
||||||
m.Module, s.Name, r.Kind, ex, err))
|
m.Module, s.Name, r.Kind, ex, err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ block = "battery"
|
|||||||
block = "custom"
|
block = "custom"
|
||||||
command = {{quote .options.command}}
|
command = {{quote .options.command}}
|
||||||
interval = {{.options.interval}}
|
interval = {{.options.interval}}
|
||||||
|
{{else if eq .shows "state"}}[[block]]
|
||||||
|
block = "custom"
|
||||||
|
command = {{quote (printf "show %s %s" .options.state (or .options.key .machine))}}
|
||||||
{{end}}`
|
{{end}}`
|
||||||
|
|
||||||
func barHolder() Manifest {
|
func barHolder() Manifest {
|
||||||
@@ -165,7 +168,7 @@ func TestTheCataloguesBarRendersEveryExampleOfTheShape(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
for _, ex := range s.Receives[0].Shape.Examples {
|
for _, ex := range s.Receives[0].Shape.Examples {
|
||||||
out, err := renderPiece(tmpl, "example", ex)
|
out, err := renderPiece(tmpl, "example", "a-machine", ex)
|
||||||
if err != nil || !strings.Contains(out, "[[block]]") {
|
if err != nil || !strings.Contains(out, "[[block]]") {
|
||||||
t.Errorf("the catalogue's bar renders %v as %q: %v", ex, out, err)
|
t.Errorf("the catalogue's bar renders %v as %q: %v", ex, out, err)
|
||||||
}
|
}
|
||||||
@@ -352,3 +355,41 @@ func TestTheBarReceivesBlocksAsData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAStateBlockIsRenderedForThisMachineAndTheHolderIsGrantedToReadIt(t *testing.T) {
|
||||||
|
power := Manifest{Module: "power", State: []StateDeclaration{{Name: "draw"}}, Contributions: []SeatContribution{
|
||||||
|
block(`{"bar":"bottom","place":"status","shows":"state","options":{"state":"power.draw"}}`),
|
||||||
|
}}
|
||||||
|
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{barHolder(), power}}
|
||||||
|
if bar := composedFile(t, r, "a-bar.bottom"); !strings.Contains(bar, `command = "show power.draw laptop"`) {
|
||||||
|
t.Fatalf("the block does not read this machine's key:\n%s", bar)
|
||||||
|
}
|
||||||
|
if got := ReadsGranted(barHolder(), []Manifest{barHolder(), power}); len(got) != 1 || got[0] != "power.draw" {
|
||||||
|
t.Fatalf("the holder is granted %v", got)
|
||||||
|
}
|
||||||
|
// Only the holder, and only for what the modules on its machine offer.
|
||||||
|
if got := ReadsGranted(power, []Manifest{barHolder(), power}); len(got) != 0 {
|
||||||
|
t.Fatalf("a contributor is granted %v", got)
|
||||||
|
}
|
||||||
|
if got := ReadsGranted(barHolder(), []Manifest{barHolder()}); len(got) != 0 {
|
||||||
|
t.Fatalf("a holder alone is granted %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAModuleOffersItsOwnStateOnly(t *testing.T) {
|
||||||
|
block := `{"seat":"node-bar","kind":"block","data":{"bar":"bottom","place":"status","shows":"state","options":{"state":"%s"}}}`
|
||||||
|
cases := map[string]string{
|
||||||
|
`{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "other.draw") + `]}`: "which power does not keep",
|
||||||
|
`{"module":"power","contributions":[` + fmt.Sprintf(block, "power.draw") + `]}`: "which power does not keep",
|
||||||
|
`{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "draw") + `]}`: "does not name a module",
|
||||||
|
}
|
||||||
|
for raw, want := range cases {
|
||||||
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
|
||||||
|
t.Errorf("%s: accepted, or refused without %q: %v", raw, want, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ok := `{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "power.draw") + `]}`
|
||||||
|
if _, err := ParseManifest([]byte(ok)); err != nil {
|
||||||
|
t.Errorf("its own state was refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -79,6 +79,10 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
|||||||
"be derived", module, n.Name)
|
"be derived", module, n.Name)
|
||||||
}
|
}
|
||||||
d := declaredFor(m, seats)
|
d := declaredFor(m, seats)
|
||||||
|
// And the state offered to it as a seat's holder by the modules beside it (novox/hq ADR 0255).
|
||||||
|
if granted := catalogue.ReadsGranted(m, onMachine(declared, modules)); len(granted) > 0 {
|
||||||
|
d.Reads = append(append([]string(nil), d.Reads...), granted...)
|
||||||
|
}
|
||||||
d.Holds = heldHere(d.Holds, holdings, n.Name, module)
|
d.Holds = heldHere(d.Holds, holdings, n.Name, module)
|
||||||
out.Assigned[n.Name] = append(out.Assigned[n.Name], d)
|
out.Assigned[n.Name] = append(out.Assigned[n.Name], d)
|
||||||
if m.Instances == catalogue.InstancesInterchangeable {
|
if m.Instances == catalogue.InstancesInterchangeable {
|
||||||
@@ -103,6 +107,17 @@ func (i *Inventory) BusRecords(ctx context.Context) (broker.Records, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// onMachine is the manifests of the modules assigned to one machine that the catalogue knows.
|
||||||
|
func onMachine(declared map[string]catalogue.Manifest, modules []string) []catalogue.Manifest {
|
||||||
|
var out []catalogue.Manifest
|
||||||
|
for _, module := range modules {
|
||||||
|
if m, known := declared[module]; known {
|
||||||
|
out = append(out, m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// declaredFor is one module's manifest as the composer needs it: what it says about itself, and the
|
// declaredFor is one module's manifest as the composer needs it: what it says about itself, and the
|
||||||
// protocol of every seat it holds or uses.
|
// protocol of every seat it holds or uses.
|
||||||
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration) broker.Declared {
|
func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaration) broker.Declared {
|
||||||
|
|||||||
@@ -296,3 +296,48 @@ func TestTheBussOwnModuleBecomesTheSnapshotUser(t *testing.T) {
|
|||||||
t.Fatalf("users derived: %v", seen)
|
t.Fatalf("users derived: %v", seen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The bar's holder reads the state the modules beside it offer in its blocks (novox/hq ADR 0255),
|
||||||
|
// without naming them: granted on the machine where both are, and nowhere else.
|
||||||
|
func TestABarIsGrantedTheStateItsMachinesModulesOfferIt(t *testing.T) {
|
||||||
|
bar := catalogue.Manifest{Module: "a-bar", Version: "1",
|
||||||
|
Claims: []catalogue.Claim{{Name: catalogue.BarSeat, Scope: catalogue.ScopeNode}}}
|
||||||
|
power := catalogue.Manifest{Module: "power", Version: "1",
|
||||||
|
State: []catalogue.StateDeclaration{{Name: "draw"}},
|
||||||
|
Contributions: []catalogue.SeatContribution{{Seat: catalogue.BarSeat, Kind: catalogue.BarKindBlock,
|
||||||
|
Data: map[string]any{"bar": "bottom", "place": "status", "shows": "state",
|
||||||
|
"options": map[string]any{"state": "power.draw"}}}}}
|
||||||
|
inv, ctx := aMeshWith(t, bar, power)
|
||||||
|
for node, modules := range map[string][]string{"laptop": {"a-bar", "power"}, "desk": {"a-bar"}} {
|
||||||
|
if _, err := inv.AddNode(ctx, node); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, module := range modules {
|
||||||
|
if _, err := inv.Assign(ctx, node, module); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
records, err := inv.BusRecords(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
reads := func(node, module string) []string {
|
||||||
|
for _, d := range records.Assigned[node] {
|
||||||
|
if d.Module == module {
|
||||||
|
return d.Reads
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("%s is not on %s", module, node)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if got := reads("laptop", "a-bar"); len(got) != 1 || got[0] != "power.draw" {
|
||||||
|
t.Fatalf("the laptop's bar reads %v", got)
|
||||||
|
}
|
||||||
|
if got := reads("desk", "a-bar"); len(got) != 0 {
|
||||||
|
t.Fatalf("a bar with no power beside it reads %v", got)
|
||||||
|
}
|
||||||
|
if got := reads("laptop", "power"); len(got) != 0 {
|
||||||
|
t.Fatalf("power reads %v", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user