test(ioscompanion): cover stopProcess, restart, and failed bring-up supervision

This commit is contained in:
pj committed 2026-06-07 21:44:25 +05:30
1 parent 338a232ba2
commit 8c38a727e0
2 files changed
+159 -2

No files matched your search

+2 -2
View File
@@ -39,8 +39,8 @@ const startupTimeout = 30 * time.Second
const runnerStartupTimeout = 120 * time.Second const runnerStartupTimeout = 120 * time.Second
// shutdownGrace bounds how long the companion child gets to exit after SIGTERM // shutdownGrace bounds how long the companion child gets to exit after SIGTERM
// before it is killed. // before it is killed. A variable so the kill-escalation test can shrink it.
const shutdownGrace = 15 * time.Second var shutdownGrace = 15 * time.Second
// longPressHoldMilliseconds is how long LongPress holds the finger down. // longPressHoldMilliseconds is how long LongPress holds the finger down.
const longPressHoldMilliseconds = 600 const longPressHoldMilliseconds = 600
@@ -0,0 +1,157 @@
package ioscompanion
import (
"context"
"errors"
"net"
"os/exec"
"syscall"
"testing"
"time"
"github.com/priyanshujain/sanderling/internal/driver/ioscompanion/transport"
)
// These tests cover the process-supervision logic (stopProcess, bringUp,
// respawnAndRedial) with real child processes and seamed transports, so the
// default suite exercises it without a simulator.
func TestStopProcessReapsChildOnSigterm(t *testing.T) {
child := exec.Command("sleep", "30")
if err := child.Start(); err != nil {
t.Fatalf("start child: %v", err)
}
start := time.Now()
stopProcess(child)
if elapsed := time.Since(start); elapsed > 5*time.Second {
t.Fatalf("stopProcess took %v; SIGTERM on sleep should reap promptly", elapsed)
}
if child.ProcessState == nil {
t.Fatal("child not reaped: ProcessState is nil")
}
if err := child.Process.Signal(syscall.Signal(0)); err == nil {
t.Fatal("child still signalable after stopProcess")
}
}
func TestStopProcessEscalatesToKillWhenSigtermIgnored(t *testing.T) {
previousGrace := shutdownGrace
shutdownGrace = 200 * time.Millisecond
defer func() { shutdownGrace = previousGrace }()
child := exec.Command("sh", "-c", `trap "" TERM; sleep 30`)
if err := child.Start(); err != nil {
t.Fatalf("start child: %v", err)
}
// Give the shell a beat to install its TERM trap.
time.Sleep(100 * time.Millisecond)
stopProcess(child)
if child.ProcessState == nil {
t.Fatal("child not reaped: ProcessState is nil")
}
if child.ProcessState.Success() {
t.Fatal("child should have died by signal, not exited cleanly")
}
}
func TestStopProcessHandlesNilChild(t *testing.T) {
stopProcess(nil)
stopProcess(&exec.Cmd{})
}
// TestRespawnAndRedialReplacesTransportAndChild drives the real restart path
// through the seams: the old transport must be closed, a fresh child spawned,
// and the fresh transport installed.
func TestRespawnAndRedialReplacesTransportAndChild(t *testing.T) {
t.Setenv("SANDERLING_SIMULATOR_COMPANION", "legacy")
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
go func() {
for {
connection, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
_ = connection.Close()
}
}()
spawns := 0
dials := []*fakeCompanion{}
options := Options{
UniqueDeviceIdentifier: "FAKE-UDID",
pickAddress: func() (string, error) { return listener.Addr().String(), nil },
spawnChild: func(context.Context, string) (*exec.Cmd, error) {
spawns++
return &exec.Cmd{}, nil
},
dialCompanion: func(string) (transport.Companion, error) {
companion := &fakeCompanion{accessibilityJSON: "[]"}
dials = append(dials, companion)
return companion, nil
},
}
d, err := New(context.Background(), options)
if err != nil {
t.Fatal(err)
}
defer d.Close()
first := dials[0]
if err := d.respawnAndRedial(context.Background()); err != nil {
t.Fatalf("respawnAndRedial: %v", err)
}
if spawns != 2 {
t.Fatalf("spawns = %d, want 2 (initial bring-up plus restart)", spawns)
}
if len(dials) != 2 || d.companion != transport.Companion(dials[1]) {
t.Fatalf("restart must install the freshly dialed transport")
}
if indexOf(first.recorded(), "close") < 0 {
t.Fatal("restart must close the dead transport")
}
}
// TestBringUpStopsChildWhenDialFails proves a failed bring-up does not leak
// the child it spawned.
func TestBringUpStopsChildWhenDialFails(t *testing.T) {
t.Setenv("SANDERLING_SIMULATOR_COMPANION", "legacy")
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
go func() {
for {
connection, acceptErr := listener.Accept()
if acceptErr != nil {
return
}
_ = connection.Close()
}
}()
child := exec.Command("sleep", "30")
if err := child.Start(); err != nil {
t.Fatalf("start child: %v", err)
}
options := Options{
UniqueDeviceIdentifier: "FAKE-UDID",
pickAddress: func() (string, error) { return listener.Addr().String(), nil },
spawnChild: func(context.Context, string) (*exec.Cmd, error) {
return child, nil
},
dialCompanion: func(string) (transport.Companion, error) {
return nil, errors.New("dial refused")
},
}
if _, err := New(context.Background(), options); err == nil {
t.Fatal("New should fail when dial fails")
}
if child.ProcessState == nil {
t.Fatal("failed bring-up must reap the spawned child")
}
}