mesh/merge-gate head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: builds mesh-host → ace, g14, novox, shanks; no…
mesh/repo-check head 5a7ec3775b98 merged into trunk 53fd32618916 beside mesh-controller#346@c711e622 — pass: [release feat/57-config-and-provisioning, judg…
mesh/delivery stopped: its walk plan-1792114347281764422 was stopped by a person (mesh-controller): mesh-host#114 merged into the release branch feat/57-…
mesh/delivery-group group 57-config-and-provisioning stopped: a member was stopped
The node-engine renders the feature's templates, collections and moved providers' files, on the SDK the feature's modules build on (feature 57, config and provisioning).
403 lines
15 KiB
Go
403 lines
15 KiB
Go
package units
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// The reading of failed units (novox/hq issue 315, "how it is checked"): a failed unit a module states,
|
|
// writes or installs is that module's, in either manager; one no module places is the machine's; a unit
|
|
// liveness judges is left to it; the two-look rule holds both ways; a manager that cannot be read is
|
|
// unread, never healthy; and only reads are asked.
|
|
|
|
type fakeReader struct {
|
|
failed map[string][]Listed // by "system" or "user:<account>"
|
|
shown map[string]Shown // by unit
|
|
owners map[string]string // by path
|
|
unread map[string]error
|
|
ownerQs int
|
|
// away is each account whose own manager is not running; noAccount each the machine lacks.
|
|
away map[string]bool
|
|
noAccount map[string]bool
|
|
// read is every manager whose failed units were asked, in order.
|
|
read []string
|
|
}
|
|
|
|
func mgr(scope, user string) string {
|
|
if scope == ScopeUser {
|
|
return "user:" + user
|
|
}
|
|
return "system"
|
|
}
|
|
|
|
func (f *fakeReader) Failed(_ context.Context, scope, user string) ([]Listed, error) {
|
|
f.read = append(f.read, mgr(scope, user))
|
|
if err := f.unread[mgr(scope, user)]; err != nil {
|
|
return nil, err
|
|
}
|
|
return f.failed[mgr(scope, user)], nil
|
|
}
|
|
|
|
func (f *fakeReader) Show(_ context.Context, _, _ string, units []string) (map[string]Shown, error) {
|
|
out := map[string]Shown{}
|
|
for _, u := range units {
|
|
out[u] = f.shown[u]
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (f *fakeReader) UserManagerRunning(_ context.Context, account string) (bool, bool, error) {
|
|
if f.noAccount[account] {
|
|
return false, false, nil
|
|
}
|
|
return !f.away[account], true, nil
|
|
}
|
|
|
|
func (f *fakeReader) PackageOwning(_ context.Context, path string) (string, bool, error) {
|
|
f.ownerQs++
|
|
p, ok := f.owners[path]
|
|
return p, ok, nil
|
|
}
|
|
|
|
// shanks is the desktop as found on 2026-10-08: two mounts of the machine's own mount table, the
|
|
// Razer daemon's packaged user unit, and a unit a removed package left behind.
|
|
func shanks() *fakeReader {
|
|
return &fakeReader{
|
|
failed: map[string][]Listed{
|
|
"system": {{Unit: "mnt-recalbox.mount", Load: "loaded"}, {Unit: "storage-media.mount", Load: "loaded"}},
|
|
"user:operator": {{Unit: "greenclip.service", Load: "not-found"},
|
|
{Unit: "openrazer-daemon.service", Load: "loaded"}},
|
|
},
|
|
shown: map[string]Shown{
|
|
"mnt-recalbox.mount": {FragmentPath: "/run/systemd/generator/mnt-recalbox.mount", Result: "exit-code"},
|
|
"storage-media.mount": {FragmentPath: "/run/systemd/generator/storage-media.mount", Result: "timeout"},
|
|
"greenclip.service": {},
|
|
"openrazer-daemon.service": {FragmentPath: "/usr/lib/systemd/user/openrazer-daemon.service", Result: "exit-code"},
|
|
},
|
|
owners: map[string]string{"/usr/lib/systemd/user/openrazer-daemon.service": "openrazer-daemon"},
|
|
}
|
|
}
|
|
|
|
func declared() *declaration.Declaration {
|
|
return &declaration.Declaration{Resources: []declaration.Resource{
|
|
&declaration.Package{ID: "openrazer.daemon", Type: declaration.TypePackage, Package: "openrazer-daemon"},
|
|
&declaration.Package{ID: "clipmenu.greenclip", Type: declaration.TypePackage, Package: "rofi-greenclip", Absent: true},
|
|
&declaration.User{ID: "zsh.account", Type: declaration.TypeUser, Name: "operator"},
|
|
// The operator's own manager is read because a module places a unit in it (dunst, on a desktop).
|
|
&declaration.Service{ID: "dunst.reload", Type: declaration.TypeService, Unit: "dunst.service",
|
|
Scope: declaration.ScopeUser, User: "operator"},
|
|
// A module account the declaration only creates: its manager is never asked (issue 764).
|
|
&declaration.User{ID: "telegram.account", Type: declaration.TypeUser, Name: "telegram"},
|
|
&declaration.Service{ID: "sshd.run", Type: declaration.TypeService, Unit: "sshd.service", State: "running"},
|
|
&declaration.Service{ID: "power.after-boot", Type: declaration.TypeService, Unit: "mesh-power-after-boot.service"},
|
|
&declaration.File{ID: "watcher.unit", Type: declaration.TypeFile, Path: "/home/operator/.config/systemd/user/watcher.service"},
|
|
}}
|
|
}
|
|
|
|
var t0 = time.Date(2026, 10, 8, 12, 0, 0, 0, time.UTC)
|
|
|
|
func lookTwice(t *testing.T, j *Judge) Statement {
|
|
t.Helper()
|
|
now := t0
|
|
j.Now = func() time.Time { return now }
|
|
first, _ := j.Look(context.Background())
|
|
if len(first.Failed) != 0 {
|
|
t.Fatalf("a unit was said failed on its first look: %+v", first.Failed)
|
|
}
|
|
now = now.Add(15 * time.Second)
|
|
st, _ := j.Look(context.Background())
|
|
return st
|
|
}
|
|
|
|
func TestTheModulesAndTheMachinesFailures(t *testing.T) {
|
|
r := shanks()
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
st := lookTwice(t, j)
|
|
if st.State != Degraded {
|
|
t.Fatalf("state %q, want degraded", st.State)
|
|
}
|
|
owned := st.Owned()
|
|
if len(owned) != 1 || owned[0].Unit != "openrazer-daemon.service" || owned[0].Module != "openrazer" ||
|
|
owned[0].Resource != "openrazer.daemon" || owned[0].Via != ViaPackage || owned[0].Scope != ScopeUser {
|
|
t.Fatalf("the Razer daemon's packaged user unit is openrazer's: %+v", owned)
|
|
}
|
|
if owned[0].Since != t0 || owned[0].Streak != 2 || owned[0].Result != "exit-code" {
|
|
t.Fatalf("since the first look that found it, two in a row, how it failed: %+v", owned[0])
|
|
}
|
|
var machine []string
|
|
for _, f := range st.Unowned() {
|
|
machine = append(machine, f.Scope+"/"+f.Unit)
|
|
}
|
|
want := "system/mnt-recalbox.mount system/storage-media.mount user/greenclip.service"
|
|
if strings.Join(machine, " ") != want {
|
|
t.Fatalf("the machine's own: %v, want %s", machine, want)
|
|
}
|
|
}
|
|
|
|
func TestADeclaredUnitAndAWrittenFileAreTheirModules(t *testing.T) {
|
|
r := &fakeReader{
|
|
failed: map[string][]Listed{
|
|
"system": {{Unit: "mesh-power-after-boot.service", Load: "loaded"}, {Unit: "sshd.service", Load: "loaded"}},
|
|
"user:operator": {{Unit: "watcher.service", Load: "loaded"}},
|
|
},
|
|
shown: map[string]Shown{
|
|
"watcher.service": {FragmentPath: "/home/operator/.config/systemd/user/watcher.service"},
|
|
},
|
|
}
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
st := lookTwice(t, j)
|
|
got := map[string]string{}
|
|
for _, f := range st.Failed {
|
|
got[f.Unit] = f.Module + " " + f.Via
|
|
}
|
|
if got["mesh-power-after-boot.service"] != "power unit" {
|
|
t.Errorf("a stateless service the declaration names is its module's: %q", got["mesh-power-after-boot.service"])
|
|
}
|
|
if got["watcher.service"] != "watcher file" {
|
|
t.Errorf("a user unit the mesh wrote is its module's: %q", got["watcher.service"])
|
|
}
|
|
if _, said := got["sshd.service"]; said {
|
|
t.Errorf("a service stated running is liveness's to judge, and said once: %v", got)
|
|
}
|
|
if r.ownerQs != 0 {
|
|
t.Errorf("the package database was asked %d time(s) for units the declaration already names", r.ownerQs)
|
|
}
|
|
}
|
|
|
|
func TestTwoLooksEachWay(t *testing.T) {
|
|
r := shanks()
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
now := t0
|
|
j.Now = func() time.Time { return now }
|
|
st, changed := j.Look(context.Background())
|
|
if len(st.Failed) != 0 || !changed {
|
|
t.Fatalf("first look: nothing said failed, the state said: %+v %v", st.Failed, changed)
|
|
}
|
|
now = now.Add(15 * time.Second)
|
|
if st, changed = j.Look(context.Background()); len(st.Failed) != 4 || !changed {
|
|
t.Fatalf("second look: four said failed: %+v %v", st.Failed, changed)
|
|
}
|
|
now = now.Add(15 * time.Second)
|
|
if _, changed = j.Look(context.Background()); changed {
|
|
t.Fatal("a third look the same is no change")
|
|
}
|
|
// The operator mounts the media library again: no longer failed on the first look that says so.
|
|
r.failed["system"] = r.failed["system"][:1]
|
|
now = now.Add(15 * time.Second)
|
|
st, changed = j.Look(context.Background())
|
|
if len(st.Failed) != 3 || !changed {
|
|
t.Fatalf("a unit no longer failed is no longer said: %+v", st.Failed)
|
|
}
|
|
// Every manager clean: running.
|
|
r.failed = map[string][]Listed{}
|
|
now = now.Add(15 * time.Second)
|
|
if st, _ = j.Look(context.Background()); st.State != Running || len(st.Failed) != 0 {
|
|
t.Fatalf("no failed unit is running: %+v", st)
|
|
}
|
|
}
|
|
|
|
func TestAManagerThatCannotBeReadIsUnread(t *testing.T) {
|
|
r := shanks()
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
lookTwice(t, j)
|
|
r.unread = map[string]error{"user:operator": errors.New("Failed to connect to bus: No medium found")}
|
|
st, _ := j.Look(context.Background())
|
|
if len(st.Unread) != 1 || !strings.Contains(st.Unread[0], "operator") {
|
|
t.Fatalf("the account manager is said unread: %v", st.Unread)
|
|
}
|
|
// Its failures are not counted while unread, and not forgotten either: back on the next read, said
|
|
// at once, from when they were first found.
|
|
r.unread = nil
|
|
st, _ = j.Look(context.Background())
|
|
for _, f := range st.Failed {
|
|
if f.Unit == "openrazer-daemon.service" && f.Since == t0 {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("a manager read again says what it held, from when it was first found: %+v", st.Failed)
|
|
}
|
|
|
|
func TestNoManagerReadIsUnknown(t *testing.T) {
|
|
r := &fakeReader{unread: map[string]error{"system": errors.New("systemctl: not found"),
|
|
"user:operator": errors.New("systemctl: not found")}}
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
if st, _ := j.Look(context.Background()); st.State != Unknown {
|
|
t.Fatalf("no manager read is unknown, never running: %q", st.State)
|
|
}
|
|
}
|
|
|
|
func TestAnAdoptedMachinesHoldingIsTheMachines(t *testing.T) {
|
|
d := &declaration.Declaration{Resources: []declaration.Resource{
|
|
&declaration.Service{ID: "found.cups", Type: declaration.TypeService, Unit: "cups.service"},
|
|
}}
|
|
o := OwnedBy(d, map[string]bool{"found.cups": true})
|
|
if _, ok := o.Units["system/cups.service"]; ok {
|
|
t.Fatal("a unit an adopted machine holds as found is the machine's, not a module's")
|
|
}
|
|
}
|
|
|
|
func TestTheReaderOnlyReads(t *testing.T) {
|
|
var asked []string
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked = append(asked, name+" "+strings.Join(args, " "))
|
|
switch {
|
|
case name == "systemctl" && contains(args, "list-units"):
|
|
return "● greenclip.service not-found failed failed greenclip.service\n" +
|
|
"openrazer-daemon.service loaded failed failed Daemon to manage razer devices in userspace\n", nil
|
|
case name == "systemctl" && contains(args, "show"):
|
|
return "Id=greenclip.service\nFragmentPath=\nResult=start-limit-hit\n\n" +
|
|
"Id=openrazer-daemon.service\nFragmentPath=/usr/lib/systemd/user/openrazer-daemon.service\nResult=exit-code\n", nil
|
|
case name == "pacman" && contains(args, "-Qqo"):
|
|
return "openrazer-daemon\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
e := Exec{Run: run, System: "arch"}
|
|
listed, err := e.Failed(context.Background(), ScopeUser, "operator")
|
|
if err != nil || len(listed) != 2 || listed[0].Unit != "greenclip.service" || listed[0].Load != "not-found" {
|
|
t.Fatalf("the list, its mark skipped: %+v %v", listed, err)
|
|
}
|
|
shown, err := e.Show(context.Background(), ScopeUser, "operator", []string{"greenclip.service", "openrazer-daemon.service"})
|
|
if err != nil || shown["openrazer-daemon.service"].FragmentPath == "" || shown["greenclip.service"].Result != "start-limit-hit" {
|
|
t.Fatalf("the show: %+v %v", shown, err)
|
|
}
|
|
if pkg, ok, err := e.PackageOwning(context.Background(), "/usr/lib/systemd/user/openrazer-daemon.service"); !ok ||
|
|
pkg != "openrazer-daemon" || err != nil {
|
|
t.Fatalf("the owner: %q %v %v", pkg, ok, err)
|
|
}
|
|
for _, a := range asked {
|
|
if !strings.Contains(a, "list-units") && !strings.Contains(a, " show ") && !strings.HasPrefix(a, "pacman -Q") {
|
|
t.Errorf("asked something that is not a read: %q", a)
|
|
}
|
|
if !strings.HasPrefix(a, "pacman") && !strings.Contains(a, "--machine=operator@") {
|
|
t.Errorf("an account's manager is asked as that account's: %q", a)
|
|
}
|
|
}
|
|
}
|
|
|
|
func contains(args []string, s string) bool {
|
|
for _, a := range args {
|
|
if a == s {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestNothingIsReadBeforeADeclaration(t *testing.T) {
|
|
r := shanks()
|
|
j := New(r)
|
|
if st, changed := j.Look(context.Background()); st.State != "" || changed {
|
|
t.Fatalf("before a declaration every unit would read as the machine's: %+v", st)
|
|
}
|
|
}
|
|
|
|
// Issue 764: a look starts no account's manager. Only an account the declaration places a user-scoped
|
|
// unit for is read at all, and its manager only while the machine's says it runs.
|
|
|
|
func TestAnAccountWithNoUserUnitsIsNeverRead(t *testing.T) {
|
|
o := OwnedBy(declared(), nil)
|
|
if strings.Join(o.Accounts, " ") != "operator" {
|
|
t.Fatalf("only an account with a user-scoped unit is read: %v", o.Accounts)
|
|
}
|
|
r := shanks()
|
|
j := New(r)
|
|
j.Set(o)
|
|
lookTwice(t, j)
|
|
for _, m := range r.read {
|
|
if m == "user:telegram" {
|
|
t.Fatalf("a module account the declaration only creates had its manager read: %v", r.read)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAManagerNotRunningIsAwayAndNotRead(t *testing.T) {
|
|
r := shanks()
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
lookTwice(t, j)
|
|
if len(j.Last().Owned()) != 1 {
|
|
t.Fatalf("logged in, the Razer daemon is said: %+v", j.Last().Failed)
|
|
}
|
|
// The operator logs out: the manager is not asked, nothing in it is failed, it is said away.
|
|
r.away = map[string]bool{"operator": true}
|
|
r.read = nil
|
|
st, changed := j.Look(context.Background())
|
|
if strings.Join(r.read, " ") != "system" {
|
|
t.Fatalf("a manager not running is not read: %v", r.read)
|
|
}
|
|
if !changed || len(st.Away) != 1 || !strings.Contains(st.Away[0], "operator") || len(st.Unread) != 0 {
|
|
t.Fatalf("away, said, and not unread: %+v", st)
|
|
}
|
|
for _, f := range st.Failed {
|
|
if f.Scope == ScopeUser {
|
|
t.Fatalf("a manager not running has no failed units: %+v", f)
|
|
}
|
|
}
|
|
if st.State != Degraded {
|
|
t.Fatalf("the machine's own failed mounts still say degraded: %q", st.State)
|
|
}
|
|
// Back in: read as before, and a failure is new again (two looks).
|
|
r.away = nil
|
|
st, _ = j.Look(context.Background())
|
|
if len(st.Away) != 0 || len(st.Owned()) != 0 {
|
|
t.Fatalf("a manager running again is read, its failure on its first look not yet said: %+v", st)
|
|
}
|
|
st, _ = j.Look(context.Background())
|
|
if len(st.Owned()) != 1 {
|
|
t.Fatalf("and said on the second: %+v", st.Failed)
|
|
}
|
|
}
|
|
|
|
func TestAnAccountTheMachineLacksIsUnread(t *testing.T) {
|
|
r := shanks()
|
|
r.noAccount = map[string]bool{"operator": true}
|
|
j := New(r)
|
|
j.Set(OwnedBy(declared(), nil))
|
|
st, _ := j.Look(context.Background())
|
|
if strings.Join(r.read, " ") != "system" || len(st.Unread) != 1 {
|
|
t.Fatalf("an account the machine lacks is unread and its manager never asked: %v %+v", r.read, st)
|
|
}
|
|
}
|
|
|
|
func TestWhetherAManagerRunsIsAskedOfTheMachines(t *testing.T) {
|
|
var asked []string
|
|
state := "inactive"
|
|
run := func(_ context.Context, name string, args ...string) (string, error) {
|
|
asked = append(asked, name+" "+strings.Join(args, " "))
|
|
switch name {
|
|
case "getent":
|
|
return "telegram:x:10007:10007::/var/lib/telegram:/usr/bin/nologin\n", nil
|
|
case "systemctl":
|
|
if state != "active" {
|
|
return state + "\n", errors.New("systemctl exited 3")
|
|
}
|
|
return state + "\n", nil
|
|
}
|
|
return "", errors.New("unexpected")
|
|
}
|
|
e := Exec{Run: run, System: "arch"}
|
|
if running, exists, err := e.UserManagerRunning(context.Background(), "telegram"); running || !exists || err != nil {
|
|
t.Fatalf("an inactive manager is not running: %v %v %v", running, exists, err)
|
|
}
|
|
state = "active"
|
|
if running, _, err := e.UserManagerRunning(context.Background(), "telegram"); !running || err != nil {
|
|
t.Fatalf("an active manager runs: %v %v", running, err)
|
|
}
|
|
for _, a := range asked {
|
|
if a != "getent passwd telegram" && a != "systemctl is-active user@10007.service" {
|
|
t.Errorf("asked something but the user database and the machine's manager: %q", a)
|
|
}
|
|
}
|
|
}
|