Files

227 lines
6.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"context"
"encoding/json"
"errors"
"os"
"os/exec"
"path/filepath"
"strconv"
"syscall"
"testing"
"time"
)
func supervisor_program(t testing.TB) []string {
t.Helper()
executable, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return []string{executable, "-test.run=^TestSupervisorHelper$", "--", "supervisor"}
}
func supervised_manager(t testing.TB) *Manager {
t.Helper()
manager, err := NewSupervisedManager(supervisor_program(t), "auto")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
ctx, cancel := context.WithTimeout(context.Background(), 7*time.Second)
defer cancel()
if err := manager.Shutdown(ctx); err != nil {
t.Error(err)
}
if manager.Stats() != (Stats{}) {
t.Error(manager.Stats())
}
})
return manager
}
func supervised_descendant(t testing.TB, session *Session) int {
t.Helper()
result, err := session.Invoke(context.Background(), "echo", nil)
if err != nil {
t.Fatal(err)
}
value := wait_result(t, result)
var pid int
if value.Err != nil || json.Unmarshal(value.Value, &pid) != nil || pid <= 1 {
t.Fatalf("descendant: %+v", value)
}
return pid
}
func wait_supervised_reap(t *testing.T, pid int) {
t.Helper()
deadline := time.NewTimer(6 * time.Second)
defer deadline.Stop()
ticker := time.NewTicker(time.Millisecond)
defer ticker.Stop()
for {
if _, err := os.Stat("/proc/" + strconv.Itoa(pid)); os.IsNotExist(err) || errors.Is(err, syscall.ESRCH) {
return
}
select {
case <-deadline.C:
t.Fatalf("supervisor did not reap descendant %d", pid)
case <-ticker.C:
}
}
}
func TestSupervisorHelper(t *testing.T) {
if os.Args[len(os.Args)-1] == "supervisor" {
if err := RunSupervisor(context.Background()); err != nil {
os.Exit(1)
}
os.Exit(0)
}
if len(os.Args) < 3 || os.Args[len(os.Args)-2] != "supervisor-owner" {
return
}
marker := os.Args[len(os.Args)-1]
manager, err := NewSupervisedManager(supervisor_program(t), "auto")
if err != nil {
t.Fatal(err)
}
session := start_active(t, manager, "sample", "spawn-chain")
pid := supervised_descendant(t, session)
if err := os.WriteFile(marker, []byte(strconv.Itoa(pid)), 0600); err != nil {
t.Fatal(err)
}
// Abrupt reader death: no Disable, Shutdown, deferred cleanup or signals.
os.Exit(0)
}
func TestSupervisionWithoutInitToolsCleansDetachedDescendants(t *testing.T) {
t.Setenv("PATH", "")
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
for _, mode := range []string{"spawn", "spawn-detached", "spawn-chain"} {
t.Run(mode, func(t *testing.T) {
manager := supervised_manager(t)
first := start_active(t, manager, "first", mode)
second := start_active(t, manager, "second", "spawn-detached")
pid := supervised_descendant(t, first)
other := supervised_descendant(t, second)
first.Disable()
wait_signal(t, first.Done())
if state, err := first.Status(); state != Stopped || err != nil {
t.Fatalf("disable: %s %v", state, err)
}
wait_supervised_reap(t, pid)
if _, err := os.Stat("/proc/" + strconv.Itoa(other)); err != nil {
t.Fatal("another session was terminated", err)
}
second.Disable()
wait_signal(t, second.Done())
wait_supervised_reap(t, other)
})
}
}
func TestSupervisionCleansDescendantsAfterReaderDeath(t *testing.T) {
t.Setenv("PATH", "")
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
executable, _ := os.Executable()
marker := filepath.Join(t.TempDir(), "descendant")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
command := exec.CommandContext(ctx, executable, "-test.run=^TestSupervisorHelper$", "--", "supervisor-owner", marker)
if output, err := command.CombinedOutput(); err != nil {
t.Fatalf("owner: %v %s", err, output)
}
data, err := os.ReadFile(marker)
if err != nil {
t.Fatal(err)
}
pid, err := strconv.Atoi(string(data))
if err != nil || pid <= 1 {
t.Fatal("invalid descendant marker")
}
wait_supervised_reap(t, pid)
}
func TestSupervisionReapsExitedDescendantWhileSessionActive(t *testing.T) {
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
manager := supervised_manager(t)
session := start_active(t, manager, "sample", "spawn-chain")
pid := supervised_descendant(t, session)
if err := syscall.Kill(pid, syscall.SIGKILL); err != nil {
t.Fatal(err)
}
wait_supervised_reap(t, pid)
if state, err := session.Status(); state != Active || err != nil {
t.Fatalf("descendant exit retired the live session: %s %v", state, err)
}
// A status flag alone does not prove the main child's protocol still works.
_ = supervised_descendant(t, session)
}
func TestSupervisionMissingProgramReleasesCapacity(t *testing.T) {
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
manager := supervised_manager(t)
spec := Spec{ID: "sample", Program: []string{filepath.Join(t.TempDir(), "missing")}}
session, err := manager.Start(context.Background(), spec)
if err != nil {
t.Fatal(err)
}
wait_signal(t, session.Done())
if state, failure := session.Status(); state != Failed || failure == nil {
t.Fatal(state, failure)
}
healthy := start_active(t, manager, "sample", "echo")
healthy.Disable()
wait_signal(t, healthy.Done())
}
func TestSupervisionRejectsInvalidExplicitCgroupParent(t *testing.T) {
for _, parent := range []string{"relative", t.TempDir()} {
if _, err := NewSupervisedManager(supervisor_program(t), parent); !errors.Is(err, ErrContainment) {
t.Fatalf("silently ignored explicit parent %q: %v", parent, err)
}
}
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", t.TempDir())
if _, err := NewSupervisedManager(supervisor_program(t), "auto"); !errors.Is(err, ErrContainment) {
t.Fatalf("silently ignored configured parent: %v", err)
}
}
func TestSupervisionCancelledStartupReleasesCapacity(t *testing.T) {
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
manager := supervised_manager(t)
session, err := manager.Start(context.Background(), child_spec(t, "sample", "hang-start"))
if err != nil {
t.Fatal(err)
}
wait_control(t, func() bool {
session.mutex.Lock()
defer session.mutex.Unlock()
return len(session.pending) > 0
})
session.Disable()
wait_signal(t, session.Done())
if state, err := session.Status(); state != Stopped || err != nil {
t.Fatal(state, err)
}
healthy := start_active(t, manager, "sample", "echo")
healthy.Disable()
wait_signal(t, healthy.Done())
}
func TestSupervisionDetectsChildExitAfterActivation(t *testing.T) {
t.Setenv("CTHULHU_TENTACLE_CGROUP_PARENT", "")
manager := supervised_manager(t)
session := start_active(t, manager, "sample", "exit-active")
wait_signal(t, session.Done())
if state, err := session.Status(); state != Failed || !errors.Is(err, ErrExited) {
t.Fatal(state, err)
}
}