Leave out a block its holder cannot render, and keep if-capability to known names on offered kinds (hq ADR 0255)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
A piece whose shows the holder's template does not know rendered as nothing and failed the whole machine's declaration; it is now left out and named, for push, plan and the merge gate. quote escapes DEL, which TOML refuses bare. An if-capability nothing detects, or on a kind a holder depends on, would drop a piece silently, so both are refused.
This commit is contained in:
@@ -135,6 +135,7 @@ type mergeComposed struct {
|
||||
Problems []string `json:"problems,omitempty"`
|
||||
Withheld []string `json:"withheld,omitempty"`
|
||||
Unbound []string `json:"unbound,omitempty"`
|
||||
Unplaced []string `json:"unplaced,omitempty"`
|
||||
LeftOut map[string]string `json:"left-out,omitempty"`
|
||||
Resources []string `json:"resources,omitempty"`
|
||||
}
|
||||
@@ -372,6 +373,12 @@ func judgeChange(ctx context.Context, in mergeCheckInput) (mergeVerdict, error)
|
||||
v.Failures = append(v.Failures, fmt.Sprintf("%s: the change leaves a credential bound elsewhere — %s",
|
||||
gm.Described, u))
|
||||
}
|
||||
// A contribution its holder's template renders nothing for (novox/hq ADR 0255): the machine
|
||||
// would be sent without it, so a change that adds one is refused, naming it.
|
||||
for _, u := range newOnly(gm.Change.Unplaced, gm.Base.Unplaced) {
|
||||
v.Failures = append(v.Failures, fmt.Sprintf("%s: the change leaves a contribution unplaced — %s",
|
||||
gm.Described, u))
|
||||
}
|
||||
for module, why := range gm.Change.LeftOut {
|
||||
if _, was := gm.Base.LeftOut[module]; !was {
|
||||
v.Failures = append(v.Failures, fmt.Sprintf("%s: the change leaves %s out of its declaration — %s",
|
||||
@@ -780,6 +787,7 @@ func composeEveryMachine(ctx context.Context, in mergeCheckInput, shelf map[stri
|
||||
for _, u := range declared.unbound {
|
||||
c.Unbound = append(c.Unbound, u.String())
|
||||
}
|
||||
c.Unplaced = declared.unplaced
|
||||
sort.Strings(c.Withheld)
|
||||
sort.Strings(c.Unbound)
|
||||
out[m.Name] = c
|
||||
|
||||
@@ -419,7 +419,7 @@ func declarationWith(ctx context.Context, open *stores, node string,
|
||||
out := sendable{Resources: composed.Resources, Adoption: adoption,
|
||||
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
|
||||
LeftOut: sortedKeysOf(composed.LeftOut), leftOutWhy: composed.LeftOut, withheld: with.Withheld,
|
||||
unbound: with.Unbound, foreseen: composed.Foreseen}
|
||||
unbound: with.Unbound, foreseen: composed.Foreseen, unplaced: composed.Unplaced}
|
||||
// And which build of each module it carries, for the send to record (novox/hq issue 259, ADR
|
||||
// 0221). Read only on the send path: a question about what would be sent records nothing.
|
||||
if choosing == Allocating {
|
||||
@@ -528,6 +528,11 @@ func reportLeftOut(node string, declared sendable) {
|
||||
for _, u := range declared.unbound {
|
||||
fmt.Printf("%s: %s\n", node, u)
|
||||
}
|
||||
// And every contribution its holder could not render, which the machine is sent without (novox/hq
|
||||
// ADR 0255).
|
||||
for _, u := range declared.unplaced {
|
||||
fmt.Printf("%s: %s\n", node, u)
|
||||
}
|
||||
}
|
||||
|
||||
// busCredentialIssued refuses an own secret called `broker` whose bus account nobody issued.
|
||||
|
||||
@@ -58,6 +58,9 @@ type sendable struct {
|
||||
// make (Foreseeing, novox/hq issue 275). Never on the wire, and a declaration that has any is never
|
||||
// sent.
|
||||
foreseen []string
|
||||
// unplaced is every contribution its holder's template could not render, naming its module and
|
||||
// why (novox/hq ADR 0255): left out of this declaration, for push and plan to say, never on the wire.
|
||||
unplaced []string
|
||||
// Builds is the build of each module this declaration carries — module to the commit its build
|
||||
// was made from — recorded with the send and never on the wire (novox/hq issue 259, ADR 0221).
|
||||
// Composed only on the send path; nil records that it is not known.
|
||||
|
||||
@@ -32,7 +32,7 @@ func TestHandWrittenBackupLinesOfAnOlderModuleArePlaced(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
placed, err := seatContributions([]Manifest{pg, mail}, Manifest{}, BackupSeat+":"+"backup", Rendering{}, nil, nil)
|
||||
placed, _, err := seatContributions([]Manifest{pg, mail}, Manifest{}, BackupSeat+":"+"backup", Rendering{}, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) {
|
||||
"resources":[{"id":"meta","type":"directory","mode":"0700"}]}`)
|
||||
facts := map[string]string{"account-home": "/home/op"}
|
||||
with := Rendering{Settings: SettingsBy{"media": {{From: "node", Values: map[string]any{AccessesSetting: map[string]any{"films": "/tank/films"}}}}}}
|
||||
backup, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindBackup, with, facts, nil)
|
||||
backup, _, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindBackup, with, facts, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) {
|
||||
if backup != want {
|
||||
t.Fatalf("backup lines:\n%s\nwant:\n%s", backup, want)
|
||||
}
|
||||
data, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindData, with, facts, nil)
|
||||
data, _, err := seatContributions([]Manifest{pg, agent, media}, Manifest{}, BackupSeat+":"+BackupKindData, with, facts, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) {
|
||||
if data != want {
|
||||
t.Fatalf("data lines:\n%s\nwant:\n%s", data, want)
|
||||
}
|
||||
if _, err := seatContributions([]Manifest{agent}, Manifest{}, BackupSeat+":"+BackupKindData, Rendering{}, nil, nil); err == nil {
|
||||
if _, _, err := seatContributions([]Manifest{agent}, Manifest{}, BackupSeat+":"+BackupKindData, Rendering{}, nil, nil); err == nil {
|
||||
t.Fatal("a home directory with no account on the machine reached the holder as a placeholder")
|
||||
}
|
||||
// What keeps something irreplaceable depends on the machine's backup holder — it backs it up or
|
||||
|
||||
@@ -291,6 +291,10 @@ type Composed struct {
|
||||
// as `module/name`, sorted: what the next send will make. Never set on a declaration that is
|
||||
// sent — a placeholder in a sealed file is a credential the process cannot read.
|
||||
Foreseen []string
|
||||
// Unplaced is every contribution the holder's template could not render into something, each
|
||||
// naming its module and why (novox/hq ADR 0255): that piece is left out and the machine is told
|
||||
// everything else. Push and plan say it; the merge gate refuses a change that adds one.
|
||||
Unplaced []string
|
||||
}
|
||||
|
||||
// ForeseenSealed is what stands for an own secret a send will make, in a composition asked ahead of
|
||||
@@ -326,8 +330,8 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
|
||||
owner := map[string]string{}
|
||||
received := map[string]map[string][]Contribution{}
|
||||
leftOut := map[string]string{}
|
||||
var foreseen []string
|
||||
resources, err := r.compose(with, owner, received, leftOut, &foreseen)
|
||||
var foreseen, unplaced []string
|
||||
resources, err := r.compose(with, owner, received, leftOut, &foreseen, &unplaced)
|
||||
if err != nil {
|
||||
return Composed{}, err
|
||||
}
|
||||
@@ -340,8 +344,9 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
|
||||
"sealed": with.BusMembership, "mode": "0600",
|
||||
})
|
||||
}
|
||||
sort.Strings(unplaced)
|
||||
return Composed{Resources: resources, Owner: owner, Received: received, LeftOut: leftOut,
|
||||
Foreseen: foreseen}, nil
|
||||
Foreseen: foreseen, Unplaced: unplaced}, nil
|
||||
}
|
||||
|
||||
// BusMembershipID names the resource carrying a machine's membership for the new bus, and
|
||||
@@ -352,7 +357,7 @@ const BusMembershipPath = "/var/lib/mesh/membership-next.json"
|
||||
|
||||
func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
received map[string]map[string][]Contribution, leftOut map[string]string,
|
||||
foreseen *[]string) ([]map[string]any, error) {
|
||||
foreseen, unplaced *[]string) ([]map[string]any, error) {
|
||||
// **A setting is judged where it is stored, and an impossible one costs a module, not a
|
||||
// machine** (novox/hq ADR 0163, rule 6). A definition that moved under a stored setting makes
|
||||
// this module uncomposable; it is left out of the declaration — its held things kept, its
|
||||
@@ -970,7 +975,7 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
// seat that places them (novox/hq ADR 0203, ADR 0204). Gathered from every module on
|
||||
// the node, as the jails are, and **last of every placeholder pass**: shell code is a
|
||||
// shell's own syntax, full of `${…}` no pass above should ever be shown.
|
||||
if err := contributionsInto(copied, m, r.Modules, thisMachine, with, r.Capabilities); err != nil {
|
||||
if err := contributionsInto(copied, m, r.Modules, thisMachine, with, r.Capabilities, unplaced); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
copied["id"] = m.Module + "." + fmt.Sprint(resource["id"])
|
||||
|
||||
@@ -547,7 +547,7 @@ func shellCode(modules []Manifest, shell, slot string) string {
|
||||
// `${machine:…}` some module wrote for its shell to see. So nothing runs after them, the environment
|
||||
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
|
||||
// wrote, so a contributed piece is never scanned again.
|
||||
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string, with Rendering, caps map[string]bool) error {
|
||||
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string, with Rendering, caps map[string]bool, unplaced *[]string) error {
|
||||
if problems := append(placeholderProblems(m, resource), seatPlaceholderProblems(m, resource)...); len(problems) > 0 {
|
||||
return fmt.Errorf("%s", problems[0])
|
||||
}
|
||||
@@ -576,10 +576,13 @@ func contributionsInto(resource map[string]any, m Manifest, modules []Manifest,
|
||||
found := ofContributed.FindStringSubmatch(placeholder)
|
||||
first, second, _ := strings.Cut(found[2], ":")
|
||||
if found[1] == "contribution" {
|
||||
placed, err := seatContributions(modules, m, found[2], with, facts, caps)
|
||||
placed, left, err := seatContributions(modules, m, found[2], with, facts, caps)
|
||||
if err != nil && failed == nil {
|
||||
failed = err
|
||||
}
|
||||
if unplaced != nil {
|
||||
*unplaced = append(*unplaced, left...)
|
||||
}
|
||||
return placed
|
||||
}
|
||||
return shellCode(modules, first, second)
|
||||
|
||||
@@ -99,9 +99,22 @@ func (m Manifest) seatContributionProblems() []string {
|
||||
"%s's contribution %d to %s is of the kind %q; %s (novox/hq ADR 0212)",
|
||||
m.Module, i+1, s.Name, c.Kind, kindsOf(s)))
|
||||
}
|
||||
if c.IfCapability != "" && !capabilityName.MatchString(c.IfCapability) {
|
||||
problems = append(problems, fmt.Sprintf("%s's contribution %d is if-capability %q, which is not a "+
|
||||
"capability's name", m.Module, i+1, c.IfCapability))
|
||||
// Only on a kind the seat offers (novox/hq ADR 0255): a piece shown where the hardware is. On a
|
||||
// kind a holder depends on — what a backup keeps, a key's trigger — a machine missing the
|
||||
// capability would lose the piece without a word.
|
||||
if c.IfCapability != "" {
|
||||
switch {
|
||||
case !capabilityName.MatchString(c.IfCapability):
|
||||
problems = append(problems, fmt.Sprintf("%s's contribution %d is if-capability %q, which is not a "+
|
||||
"capability's name", m.Module, i+1, c.IfCapability))
|
||||
case !oneOf(KnownCapabilities, c.IfCapability):
|
||||
problems = append(problems, fmt.Sprintf("%s's contribution %d is if-capability %q, which no machine "+
|
||||
"reports; the node-engine detects %s", m.Module, i+1, c.IfCapability, strings.Join(KnownCapabilities, ", ")))
|
||||
case ok && !r.Offered:
|
||||
problems = append(problems, fmt.Sprintf("%s's contribution %d to %s (%s) is if-capability %s; only a "+
|
||||
"kind the seat offers may be left out where a machine lacks something (novox/hq ADR 0255)",
|
||||
m.Module, i+1, s.Name, c.Kind, c.IfCapability))
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case ok && r.Shape != nil:
|
||||
@@ -180,17 +193,20 @@ func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
|
||||
// A kind received as data is rendered through the holder's template instead, narrowed by where
|
||||
// (novox/hq ADR 0255); and a contribution naming a capability this machine did not report is left
|
||||
// out, of either form.
|
||||
func seatContributions(modules []Manifest, holder Manifest, placeholder string, with Rendering, facts map[string]string, caps map[string]bool) (string, error) {
|
||||
//
|
||||
// What it could not render is answered as unplaced, each naming its module and why, and left out: one
|
||||
// piece the holder's template does not know is that piece's fault, not the machine's.
|
||||
func seatContributions(modules []Manifest, holder Manifest, placeholder string, with Rendering, facts map[string]string, caps map[string]bool) (string, []string, error) {
|
||||
seat, rest, _ := strings.Cut(placeholder, ":")
|
||||
kind, whereText, _ := strings.Cut(rest, ":")
|
||||
s, r, ok := receivable(seat, kind)
|
||||
if !ok {
|
||||
return "", nil
|
||||
return "", nil, nil
|
||||
}
|
||||
if r.Shape != nil {
|
||||
where, err := placeholderWhere(r, whereText)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
return shapedContributions(modules, holder, s, r, where, caps)
|
||||
}
|
||||
@@ -244,5 +260,5 @@ func seatContributions(modules []Manifest, holder Manifest, placeholder string,
|
||||
}
|
||||
}
|
||||
}
|
||||
return b.String(), failed
|
||||
return b.String(), nil, failed
|
||||
}
|
||||
|
||||
@@ -49,17 +49,27 @@ type PlaceField struct {
|
||||
// OrderDefault is a piece's order when it gives none: the middle of 0–99.
|
||||
const OrderDefault = 50
|
||||
|
||||
// capabilityName is what an `if-capability` may name: a capability as a node's profile reports it.
|
||||
// capabilityName is the shape of a capability's name, as a node's profile reports it.
|
||||
var capabilityName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
||||
|
||||
// KnownCapabilities is every capability the node-engine detects (mesh-host internal/profile), the
|
||||
// names an `if-capability` may give (novox/hq ADR 0255). A name nothing detects would leave its
|
||||
// contribution out on every machine, silently, so it is refused. A detector added there is added here.
|
||||
var KnownCapabilities = []string{
|
||||
"battery", "container-runtime", "firewall", "graphical-session", "overlay", "package-manager",
|
||||
"power-meter", "privileged", "seat", "service-manager", "uplink-dhcpcd", "uplink-networkmanager",
|
||||
"uplink-systemd-networkd", "virtualisation",
|
||||
}
|
||||
|
||||
// renderFuncs are the functions a holder's template may call. `quote` writes a value as a JSON
|
||||
// string — a valid basic string in TOML and in most tools' grammars — so a contributed command
|
||||
// with quotes in it cannot break out of the holder's line.
|
||||
var renderFuncs = template.FuncMap{
|
||||
"quote": func(v any) (string, error) {
|
||||
b, err := json.Marshal(fmt.Sprint(v))
|
||||
return string(b), err
|
||||
},
|
||||
// string with DEL escaped as well — JSON leaves it bare, and TOML refuses it — which makes it a valid
|
||||
// basic string in TOML and in most tools' grammars, so a contributed command with quotes or control
|
||||
// characters in it cannot break out of the holder's line.
|
||||
var renderFuncs = template.FuncMap{"quote": quote}
|
||||
|
||||
func quote(v any) (string, error) {
|
||||
b, err := json.Marshal(fmt.Sprint(v))
|
||||
return strings.ReplaceAll(string(b), "\x7f", `\u007f`), err
|
||||
}
|
||||
|
||||
// shapedProblems is what is wrong with one contribution of a kind received as data.
|
||||
@@ -197,7 +207,11 @@ type placedPiece struct {
|
||||
// shapedContributions is every module's data of one kind to one seat, narrowed by where and by the
|
||||
// machine's capabilities, ordered, and rendered through the holder's template — each piece under a
|
||||
// comment line naming its module.
|
||||
func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivable, where map[string]string, caps map[string]bool) (string, error) {
|
||||
//
|
||||
// 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
|
||||
// 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) {
|
||||
var pieces []placedPiece
|
||||
for _, m := range inModuleOrder(modules) {
|
||||
for _, c := range m.allContributions() {
|
||||
@@ -224,18 +238,24 @@ func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivab
|
||||
}
|
||||
}
|
||||
if len(pieces) == 0 {
|
||||
return "", nil
|
||||
return "", nil, nil
|
||||
}
|
||||
t, err := holderTemplate(holder, s, r.Kind)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
sort.SliceStable(pieces, func(a, b int) bool { return pieces[a].order < pieces[b].order })
|
||||
var b strings.Builder
|
||||
var unplaced []string
|
||||
for _, p := range pieces {
|
||||
out, err := renderPiece(t, p.module, p.data)
|
||||
if err == nil && strings.TrimSpace(out) == "" {
|
||||
err = fmt.Errorf("%s's template renders nothing for it", holder.Module)
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
unplaced = append(unplaced, fmt.Sprintf("%s's %s:%s %s is not placed by %s: %v",
|
||||
p.module, s.Name, r.Kind, describePiece(p.data), holder.Module, err))
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "%s %s\n", r.Comment, p.module)
|
||||
b.WriteString(out)
|
||||
@@ -243,7 +263,19 @@ func shapedContributions(modules []Manifest, holder Manifest, s Seat, r Receivab
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
return b.String(), unplaced, nil
|
||||
}
|
||||
|
||||
// describePiece is a piece in one line, its fields in order, for a person reading why it was left out.
|
||||
func describePiece(data map[string]any) string {
|
||||
var parts []string
|
||||
for _, k := range sortedKeys(data) {
|
||||
if _, nested := data[k].(map[string]any); nested {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s=%v", k, data[k]))
|
||||
}
|
||||
return "(" + strings.Join(parts, " ") + ")"
|
||||
}
|
||||
|
||||
// capable is whether a contribution applies on a machine with these capabilities: always, unless it
|
||||
|
||||
@@ -2,6 +2,8 @@ package catalogue
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -83,26 +85,175 @@ func TestABlockIsRenderedByTheHolderInItsPlaceAndOrderNamedByModule(t *testing.T
|
||||
}
|
||||
|
||||
func TestAContributionIfACapabilityIsPlacedOnlyWhereTheMachineReportsIt(t *testing.T) {
|
||||
power := Manifest{Module: "power", Contributions: []SeatContribution{
|
||||
func() SeatContribution {
|
||||
c := block(`{"bar":"bottom","place":"status","shows":"battery"}`)
|
||||
c.IfCapability = "battery"
|
||||
return c
|
||||
}(),
|
||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_BATTERY 1 x", IfCapability: "battery"},
|
||||
}}
|
||||
c := block(`{"bar":"bottom","place":"status","shows":"battery"}`)
|
||||
c.IfCapability = "battery"
|
||||
power := Manifest{Module: "power", Contributions: []SeatContribution{c}}
|
||||
for _, c := range []struct {
|
||||
caps map[string]bool
|
||||
has bool
|
||||
}{{map[string]bool{"battery": true}, true}, {map[string]bool{"seat": true}, false}, {nil, false}} {
|
||||
r := Resolution{Node: "n", Account: "op", Capabilities: c.caps, Modules: []Manifest{barHolder(), hotkeysHolder(), power}}
|
||||
bar, keys := composedFile(t, r, "a-bar.bottom"), composedFile(t, r, "triggerhappy.triggers")
|
||||
if strings.Contains(bar, `block = "battery"`) != c.has || strings.Contains(keys, "KEY_BATTERY") != c.has {
|
||||
t.Errorf("with %v: the battery placed is not %v:\n%s\n%s", c.caps, c.has, bar, keys)
|
||||
r := Resolution{Node: "n", Account: "op", Capabilities: c.caps, Modules: []Manifest{barHolder(), power}}
|
||||
if bar := composedFile(t, r, "a-bar.bottom"); strings.Contains(bar, `block = "battery"`) != c.has {
|
||||
t.Errorf("with %v: the battery placed is not %v:\n%s", c.caps, c.has, bar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
// A name nothing detects would leave the piece out on every machine.
|
||||
`{"seat":"node-bar","kind":"block","if-capability":"batery","data":{"bar":"top","place":"status","shows":"battery"}}`: "which no machine reports",
|
||||
// A kind the holder depends on is never left out for want of something.
|
||||
`{"seat":"node-hotkeys","kind":"trigger","if-capability":"battery","content":"KEY_BATTERY 1 x"}`: "only a kind the seat offers",
|
||||
`{"seat":"node-backup","kind":"backup","if-capability":"seat","content":"path /x"}`: "only a kind the seat offers",
|
||||
}
|
||||
for c, want := range cases {
|
||||
raw := `{"module":"power","contributions":[` + c + `]}`
|
||||
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"battery", "power-meter", "seat"} {
|
||||
if !oneOf(KnownCapabilities, name) {
|
||||
t.Errorf("%s is not a known capability", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveCarriesTheMachinesCapabilities(t *testing.T) {
|
||||
node := workstation()
|
||||
node.Capabilities["battery"] = true
|
||||
r, err := Resolve(shelf(Manifest{Module: "a"}), []string{"a"}, node, World{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !r.Capabilities["battery"] || !r.Capabilities["seat"] {
|
||||
t.Fatalf("the resolution carries %v, not the node's %v", r.Capabilities, node.Capabilities)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPieceTheHolderCannotRenderIsLeftOutAndNamedNotTheMachine(t *testing.T) {
|
||||
// A shows the shape gained after the holder's template was written: built here, past the check.
|
||||
power := Manifest{Module: "power", Contributions: []SeatContribution{
|
||||
block(`{"bar":"bottom","place":"status","shows":"gpu"}`),
|
||||
block(`{"bar":"bottom","place":"status","shows":"battery"}`),
|
||||
}}
|
||||
r := Resolution{Node: "n", Account: "op", Modules: []Manifest{barHolder(), power}}
|
||||
c, err := r.Compose(Rendering{})
|
||||
if err != nil {
|
||||
t.Fatalf("one piece failed the whole machine: %v", err)
|
||||
}
|
||||
if len(c.Unplaced) != 1 || !strings.Contains(c.Unplaced[0], "power's node-bar:block") ||
|
||||
!strings.Contains(c.Unplaced[0], "shows=gpu") || !strings.Contains(c.Unplaced[0], "renders nothing") {
|
||||
t.Fatalf("unplaced: %v", c.Unplaced)
|
||||
}
|
||||
for _, res := range c.Resources {
|
||||
if res["id"] == "a-bar.bottom" && !strings.Contains(res["content"].(string), `block = "battery"`) {
|
||||
t.Fatalf("the piece beside it was lost too: %s", res["content"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheCataloguesBarRendersEveryExampleOfTheShape(t *testing.T) {
|
||||
holder := catalogueManifest(t, "i3status-rust")
|
||||
s, _ := SeatNamed(BarSeat)
|
||||
if !placesKind(holder, s, BarKindBlock) {
|
||||
t.Skip("the catalogue beside this checkout has a bar that places no blocks yet")
|
||||
}
|
||||
tmpl, err := holderTemplate(holder, s, BarKindBlock)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, ex := range s.Receives[0].Shape.Examples {
|
||||
out, err := renderPiece(tmpl, "example", ex)
|
||||
if err != nil || !strings.Contains(out, "[[block]]") {
|
||||
t.Errorf("the catalogue's bar renders %v as %q: %v", ex, out, err)
|
||||
}
|
||||
}
|
||||
for shows := range barShows {
|
||||
covered := false
|
||||
for _, ex := range s.Receives[0].Shape.Examples {
|
||||
covered = covered || ex["shows"] == shows
|
||||
}
|
||||
if !covered {
|
||||
t.Errorf("the shape has no example that shows %s, so no holder is made to render it", shows)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuoteIsATomlBasicStringOfTheSameText(t *testing.T) {
|
||||
for _, in := range []string{`plain`, `say "hi" \ there`, "tab\tnew\nline", "del\x7fend", "nul\x00 esc\x1b", "<&> é ☃"} {
|
||||
q, err := quote(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := tomlBasicString(q)
|
||||
if err != nil || got != in {
|
||||
t.Errorf("quote(%q) = %s, which TOML reads as %q: %v", in, q, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tomlBasicString reads one TOML basic string, as the TOML specification (1.0, "String") defines it:
|
||||
// any character but a quote, a backslash and the control characters U+0000–U+0008, U+000A–U+001F
|
||||
// and U+007F, which are written as escapes.
|
||||
func tomlBasicString(s string) (string, error) {
|
||||
if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' {
|
||||
return "", fmt.Errorf("not quoted")
|
||||
}
|
||||
var b strings.Builder
|
||||
rs := []rune(s[1 : len(s)-1])
|
||||
for i := 0; i < len(rs); i++ {
|
||||
r := rs[i]
|
||||
switch {
|
||||
case r == '"':
|
||||
return "", fmt.Errorf("a bare quote at %d", i)
|
||||
case r == 0x7f || (r < 0x20 && r != '\t'):
|
||||
return "", fmt.Errorf("a bare control character %U at %d", r, i)
|
||||
case r != '\\':
|
||||
b.WriteRune(r)
|
||||
continue
|
||||
}
|
||||
i++
|
||||
if i >= len(rs) {
|
||||
return "", fmt.Errorf("an escape at the end")
|
||||
}
|
||||
switch rs[i] {
|
||||
case 'b':
|
||||
b.WriteRune('\b')
|
||||
case 't':
|
||||
b.WriteRune('\t')
|
||||
case 'n':
|
||||
b.WriteRune('\n')
|
||||
case 'f':
|
||||
b.WriteRune('\f')
|
||||
case 'r':
|
||||
b.WriteRune('\r')
|
||||
case '"':
|
||||
b.WriteRune('"')
|
||||
case '\\':
|
||||
b.WriteRune('\\')
|
||||
case 'u', 'U':
|
||||
n := 4
|
||||
if rs[i] == 'U' {
|
||||
n = 8
|
||||
}
|
||||
if i+n > len(rs)-1 {
|
||||
return "", fmt.Errorf("a short \\u escape")
|
||||
}
|
||||
v, err := strconv.ParseUint(string(rs[i+1:i+1+n]), 16, 32)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
b.WriteRune(rune(v))
|
||||
i += n
|
||||
default:
|
||||
return "", fmt.Errorf("the escape \\%c is not TOML's", rs[i])
|
||||
}
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func TestABlockOutsideTheShapeIsRefused(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
`{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data",
|
||||
|
||||
Reference in New Issue
Block a user