mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-03 19:47:10 +00:00
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
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")
|
|
}
|
|
}
|