Let a module-declared seat carry needs, so mail-notifier can need node-notifier (issue 802)
ADR 0220's dependency mechanism served compiled seats only and retired its one user with
node-resolver-config (commit f68521da). ADR 0378 S8 revives it for module-declared seats:
SeatDeclaration gains Needs, judged by the catalogue check (node scope only, a name that
resolves to a node seat somewhere in the catalogue, never itself), and DependsOn now takes
the catalogue so a claimed seat's needs are found wherever they are declared.
This commit is contained in:
@@ -34,7 +34,7 @@ func TestAContributionDependsOnTheSeatThatPlacesIt(t *testing.T) {
|
|||||||
"an empty environment": {empty, nil},
|
"an empty environment": {empty, nil},
|
||||||
}
|
}
|
||||||
for name, c := range cases {
|
for name, c := range cases {
|
||||||
got := DependsOn(c.m)
|
got := DependsOn(nil, c.m)
|
||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
got = nil
|
got = nil
|
||||||
}
|
}
|
||||||
@@ -118,7 +118,7 @@ func TestCodeForAPowerMomentDependsOnThePowerSeat(t *testing.T) {
|
|||||||
for _, moment := range powerMoments {
|
for _, moment := range powerMoments {
|
||||||
m := mod("laptop", nil, nil, nil)
|
m := mod("laptop", nil, nil, nil)
|
||||||
m.Shell = []ShellCode{{For: moment, Slot: "normal", Code: "true"}}
|
m.Shell = []ShellCode{{For: moment, Slot: "normal", Code: "true"}}
|
||||||
if got := DependsOn(m); !reflect.DeepEqual(got, []string{PowerSeat}) {
|
if got := DependsOn(nil, m); !reflect.DeepEqual(got, []string{PowerSeat}) {
|
||||||
t.Errorf("code for %s depends on %v, want %s", moment, got, PowerSeat)
|
t.Errorf("code for %s depends on %v, want %s", moment, got, PowerSeat)
|
||||||
}
|
}
|
||||||
if p := m.shellProblems(); len(p) != 0 {
|
if p := m.shellProblems(); len(p) != 0 {
|
||||||
|
|||||||
@@ -197,9 +197,9 @@ func TestTheBackupHoldersLinesAreDerivedFromTheData(t *testing.T) {
|
|||||||
}
|
}
|
||||||
// What keeps something irreplaceable depends on the machine's backup holder — it backs it up or
|
// What keeps something irreplaceable depends on the machine's backup holder — it backs it up or
|
||||||
// watches its array; valuable data alone is backed up where a holder is, and refused nowhere.
|
// watches its array; valuable data alone is backed up where a holder is, and refused nowhere.
|
||||||
if !slices.Contains(DependsOn(media), BackupSeat) || !slices.Contains(DependsOn(pg), BackupSeat) ||
|
if !slices.Contains(DependsOn(nil, media), BackupSeat) || !slices.Contains(DependsOn(nil, pg), BackupSeat) ||
|
||||||
slices.Contains(DependsOn(agent), BackupSeat) {
|
slices.Contains(DependsOn(nil, agent), BackupSeat) {
|
||||||
t.Fatalf("depends: media %v, pg %v, agent %v", DependsOn(media), DependsOn(pg), DependsOn(agent))
|
t.Fatalf("depends: media %v, pg %v, agent %v", DependsOn(nil, media), DependsOn(nil, pg), DependsOn(nil, agent))
|
||||||
}
|
}
|
||||||
if it, _ := media.DataItem("films"); it.OwnedByModule() || it.Protection() != "redundancy" {
|
if it, _ := media.DataItem("films"); it.OwnedByModule() || it.Protection() != "redundancy" {
|
||||||
t.Fatalf("an operator's path reads %+v", it)
|
t.Fatalf("an operator's path reads %+v", it)
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ func TestTheResolverConfigSeatIsGone(t *testing.T) {
|
|||||||
t.Error("node-resolver-config is still in the mesh's set; the uplink's holder writes the resolver file")
|
t.Error("node-resolver-config is still in the mesh's set; the uplink's holder writes the resolver file")
|
||||||
}
|
}
|
||||||
// An uplink's holder needs no seat beside it for the file: it is its own.
|
// An uplink's holder needs no seat beside it for the file: it is its own.
|
||||||
if got := DependsOn(mod("networkmanager", nil, nil, nil, Claim{Name: "node-uplink"})); len(got) != 0 {
|
if got := DependsOn(nil, mod("networkmanager", nil, nil, nil, Claim{Name: "node-uplink"})); len(got) != 0 {
|
||||||
t.Errorf("an uplink holder with nothing declared depends on %v", got)
|
t.Errorf("an uplink holder with nothing declared depends on %v", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func TestAContributionDependsOnItsSeat(t *testing.T) {
|
|||||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
||||||
{Seat: DisplaySessionSeat, Kind: "config", Content: "y"},
|
{Seat: DisplaySessionSeat, Kind: "config", Content: "y"},
|
||||||
}}
|
}}
|
||||||
if got, want := DependsOn(m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) {
|
if got, want := DependsOn(nil, m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) {
|
||||||
t.Errorf("depends on %v, want %v", got, want)
|
t.Errorf("depends on %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()}
|
catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()}
|
||||||
|
|||||||
@@ -333,9 +333,9 @@ func TestAnOfferedBlockMakesNoDependencyOnTheBar(t *testing.T) {
|
|||||||
block(`{"bar":"bottom","place":"status","shows":"battery"}`),
|
block(`{"bar":"bottom","place":"status","shows":"battery"}`),
|
||||||
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
||||||
}}
|
}}
|
||||||
for _, seat := range DependsOn(power) {
|
for _, seat := range DependsOn(nil, power) {
|
||||||
if seat == BarSeat {
|
if seat == BarSeat {
|
||||||
t.Fatalf("a block made the power module depend on the bar: %v", DependsOn(power))
|
t.Fatalf("a block made the power module depend on the bar: %v", DependsOn(nil, power))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// And on a machine with no bar it is placed nowhere, and nothing fails.
|
// And on a machine with no bar it is placed nowhere, and nothing fails.
|
||||||
|
|||||||
@@ -6,10 +6,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A module depends on the node seats that apply its resources (novox/hq ADR 0207) and on the
|
// A module depends on the node seats that apply its resources (novox/hq ADR 0207), on the
|
||||||
// seats it contributes to (novox/hq ADR 0210). A third source — what a seat it holds needs beside it
|
// seats it contributes to (novox/hq ADR 0210), and on the seats a seat it holds needs beside it
|
||||||
// (novox/hq ADR 0220) — had one user, node-resolver-config needing node-uplink, and went with that
|
// (novox/hq ADR 0220). The third source once served only compiled seats and had one user,
|
||||||
// seat when the resolver file became the uplink's own (novox/hq ADR 0223).
|
// node-resolver-config needing node-uplink, which went with that seat when the resolver file became
|
||||||
|
// the uplink's own (novox/hq ADR 0223) — and now serves module-declared seats instead: mail-notifier
|
||||||
|
// needing node-notifier, and the like (novox/hq ADR 0378 §8).
|
||||||
//
|
//
|
||||||
// Some of what a module declares is applied through software on the machine that is itself a
|
// Some of what a module declares is applied through software on the machine that is itself a
|
||||||
// module: a service through the service manager, a package through the package manager, a container
|
// module: a service through the service manager, a package through the package manager, a container
|
||||||
@@ -76,13 +78,16 @@ func isFoundation(m Manifest) bool {
|
|||||||
return foundationModules[m.Module] || m.Computed != ""
|
return foundationModules[m.Module] || m.Computed != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// DependsOn is every seat a module needs held on its node, derived from the resource types it
|
// DependsOn is every seat a module needs held on its node: derived from the resource types it
|
||||||
// declares itself (novox/hq ADR 0207 §2), sorted.
|
// declares itself (novox/hq ADR 0207 §2), from what it contributes to (ADR 0210), and from what a
|
||||||
|
// node seat it claims needs beside it — read from whichever manifest in `catalogue` declares that
|
||||||
|
// seat (novox/hq ADR 0220, ADR 0378 §8). Sorted.
|
||||||
//
|
//
|
||||||
// **The module's own `resources` only.** What the controller composes around a module — its
|
// **The module's own `resources` only**, for the first source. What the controller composes around
|
||||||
// filter, jails, certificates, kept files, bundles, the guard — is the mesh's, put there because the
|
// a module — its filter, jails, certificates, kept files, bundles, the guard — is the mesh's, put
|
||||||
// module is assigned, and depending on it would make the module answer for the mesh's choices.
|
// there because the module is assigned, and depending on it would make the module answer for the
|
||||||
func DependsOn(m Manifest) []string {
|
// mesh's choices.
|
||||||
|
func DependsOn(catalogue map[string]Manifest, m Manifest) []string {
|
||||||
if isFoundation(m) {
|
if isFoundation(m) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -95,6 +100,9 @@ func DependsOn(m Manifest) []string {
|
|||||||
for _, seat := range contributedTo(m) {
|
for _, seat := range contributedTo(m) {
|
||||||
seen[seat] = true
|
seen[seat] = true
|
||||||
}
|
}
|
||||||
|
for _, seat := range neededBesideClaims(catalogue, m) {
|
||||||
|
seen[seat] = true
|
||||||
|
}
|
||||||
out := make([]string, 0, len(seen))
|
out := make([]string, 0, len(seen))
|
||||||
for s := range seen {
|
for s := range seen {
|
||||||
out = append(out, s)
|
out = append(out, s)
|
||||||
@@ -135,6 +143,25 @@ func contributedTo(m Manifest) []string {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// neededBesideClaims is every seat a node seat the module claims needs held on the same node
|
||||||
|
// (novox/hq ADR 0220, ADR 0378 §8): a module-declared seat's needs are read from whichever manifest
|
||||||
|
// in the catalogue declares that seat, never written again in the claimant's own manifest. Derived
|
||||||
|
// from the claim, as a resource's seat is derived from its type, so a module that claims the seat
|
||||||
|
// cannot leave the dependency out. A compiled seat carries no needs (novox/hq ADR 0378 §8).
|
||||||
|
func neededBesideClaims(catalogue map[string]Manifest, m Manifest) []string {
|
||||||
|
var out []string
|
||||||
|
for _, seat := range nodeSeatsClaimed(m) {
|
||||||
|
for _, candidate := range catalogue {
|
||||||
|
for _, s := range candidate.DefinesSeats {
|
||||||
|
if s.Name == seat {
|
||||||
|
out = append(out, s.Needs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
// claimsSeat is whether a module claims a node seat, by its current name or one it used to have
|
// claimsSeat is whether a module claims a node seat, by its current name or one it used to have
|
||||||
// (ADR 0122), so a rename leaves the dependency met.
|
// (ADR 0122), so a rename leaves the dependency met.
|
||||||
func claimsSeat(m Manifest, seat string) bool {
|
func claimsSeat(m Manifest, seat string) bool {
|
||||||
@@ -205,7 +232,7 @@ func UnheldDependencies(catalogue map[string]Manifest, node string, set []Manife
|
|||||||
if judged != nil && !judged[m.Module] {
|
if judged != nil && !judged[m.Module] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, seat := range DependsOn(m) {
|
for _, seat := range DependsOn(catalogue, m) {
|
||||||
if held[seat] {
|
if held[seat] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestADependencyIsDerivedFromEachResourceTypeThatAppliesThroughASeat(t *test
|
|||||||
"action": nil, "network": nil, "access": nil,
|
"action": nil, "network": nil, "access": nil,
|
||||||
}
|
}
|
||||||
for kind, want := range cases {
|
for kind, want := range cases {
|
||||||
got := DependsOn(withResources(mod("m", nil, nil, nil), res(kind, "x")))
|
got := DependsOn(nil, withResources(mod("m", nil, nil, nil), res(kind, "x")))
|
||||||
if len(got) == 0 {
|
if len(got) == 0 {
|
||||||
got = nil
|
got = nil
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ func TestADependencyIsDerivedFromEachResourceTypeThatAppliesThroughASeat(t *test
|
|||||||
t.Errorf("a %s resource depends on %v, want %v", kind, got, want)
|
t.Errorf("a %s resource depends on %v, want %v", kind, got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
all := DependsOn(withResources(mod("m", nil, nil, nil),
|
all := DependsOn(nil, withResources(mod("m", nil, nil, nil),
|
||||||
res("container", "a"), res("service", "b"), res("package", "c"), res("package", "d")))
|
res("container", "a"), res("service", "b"), res("package", "c"), res("package", "d")))
|
||||||
if want := []string{ContainerRuntimeSeat, PackageManagerSeat, ServiceManagerSeat}; !reflect.DeepEqual(all, want) {
|
if want := []string{ContainerRuntimeSeat, PackageManagerSeat, ServiceManagerSeat}; !reflect.DeepEqual(all, want) {
|
||||||
t.Errorf("a module of every kind depends on %v, want each seat once: %v", all, want)
|
t.Errorf("a module of every kind depends on %v, want each seat once: %v", all, want)
|
||||||
@@ -207,7 +207,7 @@ func TestTheFoundationsDeclarationsAreNotJudged(t *testing.T) {
|
|||||||
cat := shelf(append(coreThree(), network, host)...)
|
cat := shelf(append(coreThree(), network, host)...)
|
||||||
|
|
||||||
for _, m := range []Manifest{network, host} {
|
for _, m := range []Manifest{network, host} {
|
||||||
if d := DependsOn(m); len(d) != 0 {
|
if d := DependsOn(cat, m); len(d) != 0 {
|
||||||
t.Errorf("%s, the foundation's, depends on %v", m.Module, d)
|
t.Errorf("%s, the foundation's, depends on %v", m.Module, d)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,6 +223,36 @@ func TestTheFoundationsDeclarationsAreNotJudged(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A module-declared seat's needs are judged exactly as a compiled seat's once were (novox/hq ADR
|
||||||
|
// 0220, ADR 0378 §8): "mail" declares the node seat "mail-notifier" needing node-notifier, the
|
||||||
|
// mesh's own (desktop notifications); "notifier" claims mail-notifier; "dunst" claims node-notifier,
|
||||||
|
// as it does in the real catalogue.
|
||||||
|
func mailNotifierAndDunst() map[string]Manifest {
|
||||||
|
return shelf(
|
||||||
|
Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
|
||||||
|
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{NotifierSeat},
|
||||||
|
}}},
|
||||||
|
mod("notifier", nil, nil, nil, Claim{Name: "mail-notifier"}),
|
||||||
|
mod("dunst", nil, nil, nil, Claim{Name: NotifierSeat}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAModuleDeclaredSeatsNeedsAreJudgedLikeACompiledSeatsOnceWere(t *testing.T) {
|
||||||
|
cat := mailNotifierAndDunst()
|
||||||
|
_, err := AssignRefusal(cat, "workstation", nil, []string{"notifier"})
|
||||||
|
var refusal *Refusal
|
||||||
|
if !errors.As(err, &refusal) || !strings.Contains(err.Error(), "node-notifier") {
|
||||||
|
t.Fatalf("notifier assigned without dunst was not refused naming node-notifier: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := AssignRefusal(cat, "workstation", []string{"dunst"}, []string{"notifier"}); err != nil {
|
||||||
|
t.Errorf("notifier beside dunst was refused: %v", err)
|
||||||
|
}
|
||||||
|
if err := UnassignRefusal(cat, "workstation", []string{"dunst", "notifier"}, []string{"dunst"}); err == nil ||
|
||||||
|
!strings.Contains(err.Error(), "node-notifier") {
|
||||||
|
t.Errorf("taking dunst from under notifier was not refused: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestUnassigningTheLastHolderWhileDependentsRemainIsRefused(t *testing.T) {
|
func TestUnassigningTheLastHolderWhileDependentsRemainIsRefused(t *testing.T) {
|
||||||
sshd := withResources(mod("sshd", nil, nil, nil), res("service", "sshd"))
|
sshd := withResources(mod("sshd", nil, nil, nil), res("service", "sshd"))
|
||||||
cat := shelf(append(coreThree(), sshd)...)
|
cat := shelf(append(coreThree(), sshd)...)
|
||||||
|
|||||||
@@ -71,6 +71,12 @@ type SeatDeclaration struct {
|
|||||||
// mesh's own seats name theirs by: a node seat only, each absolute and literal, and none another seat
|
// mesh's own seats name theirs by: a node seat only, each absolute and literal, and none another seat
|
||||||
// names. A module's seat never steps back.
|
// names. A module's seat never steps back.
|
||||||
Paths []string `json:"paths,omitempty"`
|
Paths []string `json:"paths,omitempty"`
|
||||||
|
// Needs is every node seat this seat's holder needs held on its own node (novox/hq ADR 0220, ADR
|
||||||
|
// 0378 §8): a role whose holder is only right while another role is filled beside it. A module
|
||||||
|
// claiming this seat depends on each, exactly as a module declaring a service depends on the
|
||||||
|
// service manager (ADR 0207) — derived from the claim, never written again in the claimant's own
|
||||||
|
// manifest. Only a node seat may carry needs, and a compiled seat carries none.
|
||||||
|
Needs []string `json:"needs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator,
|
// KindedBenches are the seats that may be kinded (novox/hq ADR 0234 §2): `channel` sends to the operator,
|
||||||
@@ -168,6 +174,7 @@ func declaredSeatProblems(m Manifest) []string {
|
|||||||
}
|
}
|
||||||
problems = append(problems, trafficProblems(m, s)...)
|
problems = append(problems, trafficProblems(m, s)...)
|
||||||
problems = append(problems, declaredSeatPathProblems(m, s)...)
|
problems = append(problems, declaredSeatPathProblems(m, s)...)
|
||||||
|
problems = append(problems, declaredSeatNeedsProblems(m, s)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range m.Uses {
|
for _, u := range m.Uses {
|
||||||
@@ -228,6 +235,30 @@ func trafficProblems(m Manifest, s SeatDeclaration) []string {
|
|||||||
return problems
|
return problems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// declaredSeatNeedsProblems is what one declaration's needs can be judged on alone (novox/hq ADR
|
||||||
|
// 0378 §8): only a node seat is ever held beside another on the same node, so only a node seat may
|
||||||
|
// carry needs; and a seat needing itself names nothing that was not already true of holding it.
|
||||||
|
// Whether each named need itself exists as a node seat somewhere in the catalogue is CatalogueProblems'
|
||||||
|
// to say, once every manifest is known.
|
||||||
|
func declaredSeatNeedsProblems(m Manifest, s SeatDeclaration) []string {
|
||||||
|
var problems []string
|
||||||
|
if len(s.Needs) == 0 {
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
if s.At() != ScopeNode {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s declares %s needing %s, and only a node seat needs another held beside it "+
|
||||||
|
"(novox/hq ADR 0378 §8)", m.Module, s.Name, strings.Join(s.Needs, ", ")))
|
||||||
|
}
|
||||||
|
for _, need := range s.Needs {
|
||||||
|
if need == s.Name {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s declares %s needing itself (novox/hq ADR 0378 §8)", m.Module, s.Name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return problems
|
||||||
|
}
|
||||||
|
|
||||||
// A Shelf is every manifest the mesh has registered, by module name.
|
// A Shelf is every manifest the mesh has registered, by module name.
|
||||||
type Shelf map[string]Manifest
|
type Shelf map[string]Manifest
|
||||||
|
|
||||||
@@ -280,6 +311,36 @@ func CatalogueProblems(shelf Shelf) []string {
|
|||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// atNode says whether a seat, compiled or declared, is a node seat — judged here because only
|
||||||
|
// the whole catalogue knows a declared one's scope (novox/hq ADR 0378 §8).
|
||||||
|
atNode := func(seat string) bool {
|
||||||
|
if s, isMesh := SeatNamed(seat); isMesh {
|
||||||
|
return s.Scope == ScopeNode
|
||||||
|
}
|
||||||
|
if s, ok := declared[seat]; ok {
|
||||||
|
return s.At() == ScopeNode
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// A module-declared seat's needs name node seats that hold beside it (novox/hq ADR 0378 §8):
|
||||||
|
// whether one exists at all, compiled or declared by any module, only the whole catalogue can
|
||||||
|
// say — a single manifest's own problems (declaredSeatProblems) judge its needs no further.
|
||||||
|
for _, module := range shelfOrder(shelf) {
|
||||||
|
for _, s := range shelf[module].DefinesSeats {
|
||||||
|
for _, need := range s.Needs {
|
||||||
|
if !exists(need) {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s declares %s needing %s, "+UndeclaredSeat, module, s.Name, need))
|
||||||
|
} else if !atNode(need) {
|
||||||
|
problems = append(problems, fmt.Sprintf(
|
||||||
|
"%s declares %s needing %s, which is not a node seat (novox/hq ADR 0378 §8)",
|
||||||
|
module, s.Name, need))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Which seats name each path (novox/hq ADR 0287): a module's seat naming one another seat names loses.
|
// Which seats name each path (novox/hq ADR 0287): a module's seat naming one another seat names loses.
|
||||||
named, pathProblems := pathSeats(shelfOrder(shelf), shelf, declaredBy)
|
named, pathProblems := pathSeats(shelfOrder(shelf), shelf, declaredBy)
|
||||||
problems = append(problems, pathProblems...)
|
problems = append(problems, pathProblems...)
|
||||||
|
|||||||
@@ -116,6 +116,53 @@ func TestTheMeshsOwnSeatsAreStillClaimable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only a node seat may carry needs (novox/hq ADR 0378 §8): a per-mesh seat's holder is not on any
|
||||||
|
// one node to depend on anything beside it there.
|
||||||
|
func TestOnlyANodeSeatMayCarryNeeds(t *testing.T) {
|
||||||
|
m := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
|
||||||
|
Name: "mail-notifier", Scope: ScopeMesh, Needs: []string{"node-notifier"}}}}
|
||||||
|
got := strings.Join(declaredSeatProblems(m), "; ")
|
||||||
|
if !strings.Contains(got, "needing") || !strings.Contains(got, "node seat") {
|
||||||
|
t.Fatalf("a mesh-scope seat's needs were accepted: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A seat needing itself names nothing (novox/hq ADR 0378 §8).
|
||||||
|
func TestASeatCannotNeedItself(t *testing.T) {
|
||||||
|
m := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
|
||||||
|
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{"mail-notifier"}}}}
|
||||||
|
got := strings.Join(declaredSeatProblems(m), "; ")
|
||||||
|
if !strings.Contains(got, "needing itself") {
|
||||||
|
t.Fatalf("a seat needing itself was accepted: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A need naming nothing node-scope — undeclared, or declared at another scope — is the whole
|
||||||
|
// catalogue's to say (novox/hq ADR 0378 §8).
|
||||||
|
func TestANeedMustNameANodeSeatSomewhereInTheCatalogue(t *testing.T) {
|
||||||
|
mail := Manifest{Module: "mail", DefinesSeats: []SeatDeclaration{{
|
||||||
|
Name: "mail-notifier", Scope: ScopeNode, Needs: []string{"desk-beeper"}}}}
|
||||||
|
|
||||||
|
// Nobody declares desk-beeper at all.
|
||||||
|
got := problemsFor(t, Shelf{"mail": mail})
|
||||||
|
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "no module declares") {
|
||||||
|
t.Fatalf("needing an undeclared seat was accepted: %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declared, but at mesh scope: still not a node seat to hold beside it.
|
||||||
|
notNode := Manifest{Module: "dunst", DefinesSeats: []SeatDeclaration{{Name: "desk-beeper", Scope: ScopeMesh}}}
|
||||||
|
got = problemsFor(t, Shelf{"mail": mail, "dunst": notNode})
|
||||||
|
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "not a node seat") {
|
||||||
|
t.Fatalf("needing a mesh-scope seat was accepted: %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declared at node scope: accepted.
|
||||||
|
dunst := Manifest{Module: "dunst", DefinesSeats: []SeatDeclaration{{Name: "desk-beeper", Scope: ScopeNode}}}
|
||||||
|
if got := problemsFor(t, Shelf{"mail": mail, "dunst": dunst}); got != "" {
|
||||||
|
t.Fatalf("needing a node seat another module declares was refused: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// A refusal that reorders itself between runs is a refusal nobody can diff.
|
// A refusal that reorders itself between runs is a refusal nobody can diff.
|
||||||
func TestTheProblemsAreStable(t *testing.T) {
|
func TestTheProblemsAreStable(t *testing.T) {
|
||||||
shelf := Shelf{"telegram": telegram(), "shop": {Module: "shop", Uses: []string{"nope"}},
|
shelf := Shelf{"telegram": telegram(), "shop": {Module: "shop", Uses: []string{"nope"}},
|
||||||
|
|||||||
Reference in New Issue
Block a user