Files
mesh-host/internal/system/arch_test.go
T
jschoubben 1a021052fe
mesh/delivery merged: source.main moved to novox/mesh-host@b4fd2cc210bf
mesh/check being carried to 1a021052fe07 on b4fd2cc210bf | from 7a2af2376405
Drop the never-ran reading: a start timestamp of 0 does not mean a one-shot never ran (issue 1126)
Review round 1 measured it on systemd 262.1: a finished one-shot nothing references is
unloaded, and loaded again it reads inactive, ExecMainStatus=0 and a start timestamp of
0, as one that never ran does. The previous commit's rule read such a unit stopped, so
a one-shot declared running would be started, read stopped and fail on every apply, and
the hold, the take-over and the giving back would judge it wrongly.

ServiceState keeps main's reading. ServiceAtRest and declaredState stay: they alone
satisfy a stopped declaration for a one-shot at rest, which is the kernel cleanup's
case. A one-shot declared running that must be seen to have run lingers
(RemainAfterExit) or is enabled at boot. Unit tests: the arch table has 5 rows, the
apply test 1; 1262 in all.
2026-10-28 14:29:16 +01:00

56 lines
2.2 KiB
Go

package system
import (
"context"
"testing"
)
// How a one-shot unit's state is read: what it did, not whether a process is up.
//
// **A one-shot that did its work and exited is satisfied, not stopped.** Its unit loads a rule set
// and exits, so it is inactive the instant it succeeds; reading that as stopped made the firewall
// permanently unsatisfiable, on every apply, with the rules correctly in place the whole time.
//
// **One that failed is still stopped**, or the host would report success for work that did not
// happen — the opposite mistake and the worse one.
//
// **At rest** (novox/hq issue 1126, the kernel's boot-time cleanup on ace) is whether a one-shot that does not linger has nothing running now: a declaration
// that it be stopped is then satisfied, whatever it last did, because there is nothing to stop.
func TestAOneShotIsReadByWhatItDid(t *testing.T) {
for _, c := range []struct {
name, show, want string
atRest bool
}{
{"finished", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=0\n",
"running", true},
{"failed", "ActiveState=inactive\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=1\n",
"stopped", true},
{"running now", "ActiveState=activating\nType=oneshot\nRemainAfterExit=no\nExecMainStatus=0\n",
"running", false},
{"lingers", "ActiveState=active\nType=oneshot\nRemainAfterExit=yes\nExecMainStatus=0\n",
"running", false},
{"a daemon, stopped", "ActiveState=inactive\nType=simple\nRemainAfterExit=no\nExecMainStatus=0\n",
"stopped", false},
} {
t.Run(c.name, func(t *testing.T) {
run := func(_ context.Context, _ string, _ ...string) (string, error) {
return "LoadState=loaded\n" + c.show, nil
}
state, err := arch{}.ServiceState(context.Background(), run, "thing.service")
if err != nil {
t.Fatalf("read as an error: %v", err)
}
if state != c.want {
t.Errorf("reads as %q, want %q", state, c.want)
}
rest, err := arch{}.ServiceAtRest(context.Background(), run, "thing.service")
if err != nil {
t.Fatalf("at rest read as an error: %v", err)
}
if rest != c.atRest {
t.Errorf("at rest is %v, want %v", rest, c.atRest)
}
})
}
}