Merge pull request 'Promise reset-failed and wanted-by on the service manager, optional (hq issue 332)' (#158) from feat/service-manager-reset-failed-why-started into main
This commit was merged in pull request #158.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -42,7 +43,7 @@ func TestTheServiceManagerSeatServesTheUnitVerbs(t *testing.T) {
|
||||
if seat.Scope != ScopeNode {
|
||||
t.Fatalf("the service manager is a role each machine has once, and the seat is %s-scoped", seat.Scope)
|
||||
}
|
||||
want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed"}
|
||||
want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed", "reset-failed", "wanted-by"}
|
||||
var got []string
|
||||
for _, v := range seat.Serves {
|
||||
got = append(got, v.Name)
|
||||
@@ -80,13 +81,68 @@ func TestFailedIsAnOptionalVerbOfTheServiceManager(t *testing.T) {
|
||||
if err := CanHold(holder(eight[1:]...), seat); err == nil || !strings.Contains(err.Error(), "does not serve units") {
|
||||
t.Fatalf("a holder missing a required verb was accepted: %v", err)
|
||||
}
|
||||
optional := map[string]bool{"failed": true, "reset-failed": true, "wanted-by": true}
|
||||
for _, v := range seat.Serves {
|
||||
if v.Optional != (v.Name == "failed") {
|
||||
if v.Optional != optional[v.Name] {
|
||||
t.Errorf("%s optional: %v", v.Name, v.Optional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// **`reset-failed` and `wanted-by` join the seat optional** (novox/hq issue 332, ADR 0246 step 1): the
|
||||
// holder running today, serving the nine verbs, still holds the seat; a holder serving all eleven is not
|
||||
// refused; each replaces the shell command it names; and read back from the store's row they stay optional.
|
||||
func TestResetFailedAndWantedByAreOptionalVerbsOfTheServiceManager(t *testing.T) {
|
||||
defer UseSeats(DefaultSeats())
|
||||
nine := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed"}
|
||||
holder := func(serves ...string) Manifest {
|
||||
return Manifest{Module: "systemd", Version: "1", Claims: []Claim{{Name: ServiceManagerSeat, Scope: ScopeNode, Serves: serves}}}
|
||||
}
|
||||
seat, _ := SeatNamed(ServiceManagerSeat)
|
||||
if err := CanHold(holder(nine...), seat); err != nil {
|
||||
t.Fatalf("today's holder, without the two verbs, is refused: %v", err)
|
||||
}
|
||||
if err := CanHold(holder(append(nine, "reset-failed", "wanted-by")...), seat); err != nil {
|
||||
t.Fatalf("a holder serving both is refused: %v", err)
|
||||
}
|
||||
if err := CanHold(holder(append(nine, "why-started")...), seat); err == nil || !strings.Contains(err.Error(), "does not promise") {
|
||||
t.Fatalf("a verb the seat does not promise was accepted: %v", err)
|
||||
}
|
||||
replaces := map[string]string{"reset-failed": "systemctl reset-failed", "wanted-by": "systemctl list-dependencies --reverse"}
|
||||
for _, v := range seat.Serves {
|
||||
want, ok := replaces[v.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(v.Replaces, want) {
|
||||
t.Errorf("%s does not say it replaces %q: %v", v.Name, want, v.Replaces)
|
||||
}
|
||||
props, _ := v.Input["properties"].(map[string]any)
|
||||
if _, has := props["unit"]; !has {
|
||||
t.Errorf("%s takes no unit", v.Name)
|
||||
}
|
||||
delete(replaces, v.Name)
|
||||
}
|
||||
if len(replaces) > 0 {
|
||||
t.Fatalf("the seat does not promise %v", replaces)
|
||||
}
|
||||
// The row as seeding stores it: every verb, the mark not a column.
|
||||
var rows []Seat
|
||||
for _, s := range DefaultSeats() {
|
||||
row := s
|
||||
row.Serves = nil
|
||||
for _, v := range s.Serves {
|
||||
row.Serves = append(row.Serves, Verb{Name: v.Name, Description: v.Description, Input: v.Input})
|
||||
}
|
||||
rows = append(rows, row)
|
||||
}
|
||||
UseSeats(rows)
|
||||
seat, _ = SeatNamed(ServiceManagerSeat)
|
||||
if err := CanHold(holder(nine...), seat); err != nil {
|
||||
t.Fatalf("read back from the row, the two verbs are required: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The optional mark is not stored, so a seat set read back from the store's rows takes it from the
|
||||
// compiled seat: otherwise `failed`, seeded into the row, would come back required.
|
||||
func TestAnOptionalVerbStaysOptionalInASetReadFromTheStore(t *testing.T) {
|
||||
|
||||
@@ -620,7 +620,8 @@ func SeatsWithAProtocol() []Seat {
|
||||
// 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an
|
||||
// optional scope — "system" when absent, "user" for the operator account's own manager — so a
|
||||
// caller asks for a user unit the way it asks for a system one; `failed` alone reads both managers
|
||||
// when none is named, and is optional (Verb.Optional).
|
||||
// when none is named. `failed`, `reset-failed` and `wanted-by` are optional (Verb.Optional) until every
|
||||
// holder serves them (ADR 0246).
|
||||
func serviceManagerVerbs() []Verb {
|
||||
scoped := func(more map[string]string, required []string) map[string]any {
|
||||
props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"}
|
||||
@@ -669,6 +670,21 @@ func serviceManagerVerbs() []Verb {
|
||||
"account's; a manager that does not answer is reported with its error, never as nothing failed.",
|
||||
Input: schema(map[string]string{"scope": "\"system\" or \"user\": only that manager (both when absent)"}, nil),
|
||||
Replaces: []string{"systemctl --failed"}},
|
||||
// **Clearing a failed record, and finding what starts a unit** (novox/hq issue 332): a unit whose
|
||||
// file is gone stays failed in its manager until the record is reset, and what keeps asking for it
|
||||
// is a dependency or an enable link no verb could show. Both optional while their holders catch up
|
||||
// (ADR 0246 step 1); a later change requires them.
|
||||
{Name: "reset-failed", Optional: true, Description: "Clear one unit's failed record in its manager — " +
|
||||
"its failed state and its start-limit count — and answer its state after. It removes no file and " +
|
||||
"starts nothing.",
|
||||
Input: scoped(unit, []string{"unit"}), Replaces: []string{"systemctl reset-failed"}},
|
||||
{Name: "wanted-by", Optional: true, Description: "What wants, requires or triggers one unit, read-only: the " +
|
||||
"manager's reverse dependencies (WantedBy, RequiredBy, UpheldBy, BoundBy, TriggeredBy and the rest) and " +
|
||||
"every enable link naming it in the scope's configuration directories (*.wants, *.requires, *.upholds), " +
|
||||
"each with where it points and whether it dangles — the account's own directory included in user scope.",
|
||||
Input: scoped(unit, []string{"unit"}),
|
||||
Replaces: []string{"systemctl list-dependencies --reverse", "systemctl show -p WantedBy",
|
||||
"ls ~/.config/systemd/user/*.wants"}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user