1130 lines
27 KiB
Go
1130 lines
27 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"os/exec"
|
|
"strings"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
framework "dappco.re/go/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func newTestService(t *testing.T) (*Service, *framework.Core) {
|
|
t.Helper()
|
|
|
|
c := framework.New()
|
|
factory := NewService(Options{BufferSize: 1024})
|
|
raw, err := factory(c)
|
|
require.NoError(t, err)
|
|
|
|
svc := raw.(*Service)
|
|
return svc, c
|
|
}
|
|
|
|
func TestService_Start(t *testing.T) {
|
|
t.Run("echo command", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "hello")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, proc)
|
|
|
|
assert.NotEmpty(t, proc.ID)
|
|
assert.Equal(t, "echo", proc.Command)
|
|
assert.Equal(t, []string{"hello"}, proc.Args)
|
|
|
|
// Wait for completion
|
|
<-proc.Done()
|
|
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
assert.Equal(t, 0, proc.ExitCode)
|
|
assert.Contains(t, proc.Output(), "hello")
|
|
})
|
|
|
|
t.Run("works without core runtime", func(t *testing.T) {
|
|
svc := &Service{
|
|
processes: make(map[string]*Process),
|
|
bufSize: 1024,
|
|
}
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "standalone")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, proc)
|
|
|
|
<-proc.Done()
|
|
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
assert.Equal(t, 0, proc.ExitCode)
|
|
assert.Contains(t, proc.Output(), "standalone")
|
|
})
|
|
|
|
t.Run("failing command", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 42")
|
|
require.NoError(t, err)
|
|
|
|
<-proc.Done()
|
|
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
assert.Equal(t, 42, proc.ExitCode)
|
|
})
|
|
|
|
t.Run("non-existent command", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "nonexistent_command_xyz")
|
|
assert.Error(t, err)
|
|
require.NotNil(t, proc)
|
|
assert.Equal(t, StatusFailed, proc.Status)
|
|
assert.Equal(t, -1, proc.ExitCode)
|
|
assert.NotNil(t, proc.Done())
|
|
<-proc.Done()
|
|
|
|
got, getErr := svc.Get(proc.ID)
|
|
require.NoError(t, getErr)
|
|
assert.Equal(t, proc.ID, got.ID)
|
|
assert.Equal(t, StatusFailed, got.Status)
|
|
})
|
|
|
|
t.Run("empty command is rejected", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.StartWithOptions(context.Background(), RunOptions{})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "command is required")
|
|
})
|
|
|
|
t.Run("nil context is rejected", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.StartWithOptions(nil, RunOptions{
|
|
Command: "echo",
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrContextRequired)
|
|
})
|
|
|
|
t.Run("with working directory", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.StartWithOptions(context.Background(), RunOptions{
|
|
Command: "pwd",
|
|
Dir: "/tmp",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
<-proc.Done()
|
|
|
|
// On macOS /tmp is a symlink to /private/tmp
|
|
output := strings.TrimSpace(proc.Output())
|
|
assert.True(t, output == "/tmp" || output == "/private/tmp", "got: %s", output)
|
|
})
|
|
|
|
t.Run("context cancellation", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
proc, err := svc.Start(ctx, "sleep", "10")
|
|
require.NoError(t, err)
|
|
|
|
// Cancel immediately
|
|
cancel()
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
// Good - process was killed
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been killed")
|
|
}
|
|
})
|
|
|
|
t.Run("disable capture", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.StartWithOptions(context.Background(), RunOptions{
|
|
Command: "echo",
|
|
Args: []string{"no-capture"},
|
|
DisableCapture: true,
|
|
})
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
assert.Equal(t, "", proc.Output(), "output should be empty when capture is disabled")
|
|
})
|
|
|
|
t.Run("with environment variables", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.StartWithOptions(context.Background(), RunOptions{
|
|
Command: "sh",
|
|
Args: []string{"-c", "echo $MY_TEST_VAR"},
|
|
Env: []string{"MY_TEST_VAR=hello_env"},
|
|
})
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
assert.Contains(t, proc.Output(), "hello_env")
|
|
})
|
|
|
|
t.Run("detach survives parent context", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
proc, err := svc.StartWithOptions(ctx, RunOptions{
|
|
Command: "echo",
|
|
Args: []string{"detached"},
|
|
Detach: true,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// Cancel the parent context
|
|
cancel()
|
|
|
|
// Detached process should still complete normally
|
|
select {
|
|
case <-proc.Done():
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
assert.Equal(t, 0, proc.ExitCode)
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("detached process should have completed")
|
|
}
|
|
})
|
|
|
|
t.Run("kill group requires detach", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.StartWithOptions(context.Background(), RunOptions{
|
|
Command: "sleep",
|
|
Args: []string{"1"},
|
|
KillGroup: true,
|
|
})
|
|
require.Error(t, err)
|
|
assert.Contains(t, err.Error(), "KillGroup requires Detach")
|
|
})
|
|
}
|
|
|
|
func TestService_Run(t *testing.T) {
|
|
t.Run("returns output", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
output, err := svc.Run(context.Background(), "echo", "hello world")
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output, "hello world")
|
|
})
|
|
|
|
t.Run("returns error on failure", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.Run(context.Background(), "sh", "-c", "exit 1")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "exited with code 1")
|
|
})
|
|
}
|
|
|
|
func TestService_Actions(t *testing.T) {
|
|
t.Run("broadcasts events", func(t *testing.T) {
|
|
c := framework.New()
|
|
|
|
// Register process service on Core
|
|
factory := NewService(Options{})
|
|
raw, err := factory(c)
|
|
require.NoError(t, err)
|
|
svc := raw.(*Service)
|
|
|
|
var started []ActionProcessStarted
|
|
var outputs []ActionProcessOutput
|
|
var exited []ActionProcessExited
|
|
var mu sync.Mutex
|
|
|
|
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
switch m := msg.(type) {
|
|
case ActionProcessStarted:
|
|
started = append(started, m)
|
|
case ActionProcessOutput:
|
|
outputs = append(outputs, m)
|
|
case ActionProcessExited:
|
|
exited = append(exited, m)
|
|
}
|
|
return framework.Result{OK: true}
|
|
})
|
|
proc, err := svc.Start(context.Background(), "echo", "test")
|
|
require.NoError(t, err)
|
|
|
|
<-proc.Done()
|
|
|
|
// Give time for events to propagate
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
assert.Len(t, started, 1)
|
|
assert.Equal(t, "echo", started[0].Command)
|
|
assert.Equal(t, []string{"test"}, started[0].Args)
|
|
|
|
assert.NotEmpty(t, outputs)
|
|
foundTest := false
|
|
for _, o := range outputs {
|
|
if strings.Contains(o.Line, "test") {
|
|
foundTest = true
|
|
break
|
|
}
|
|
}
|
|
assert.True(t, foundTest, "should have output containing 'test'")
|
|
|
|
assert.Len(t, exited, 1)
|
|
assert.Equal(t, 0, exited[0].ExitCode)
|
|
assert.Nil(t, exited[0].Error)
|
|
})
|
|
|
|
t.Run("broadcasts killed events", func(t *testing.T) {
|
|
c := framework.New()
|
|
|
|
factory := NewService(Options{})
|
|
raw, err := factory(c)
|
|
require.NoError(t, err)
|
|
svc := raw.(*Service)
|
|
|
|
var killed []ActionProcessKilled
|
|
var exited []ActionProcessExited
|
|
var mu sync.Mutex
|
|
|
|
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if m, ok := msg.(ActionProcessKilled); ok {
|
|
killed = append(killed, m)
|
|
}
|
|
if m, ok := msg.(ActionProcessExited); ok {
|
|
exited = append(exited, m)
|
|
}
|
|
return framework.Result{OK: true}
|
|
})
|
|
|
|
proc, err := svc.Start(context.Background(), "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.Kill(proc.ID)
|
|
require.NoError(t, err)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
require.Len(t, killed, 1)
|
|
assert.Equal(t, proc.ID, killed[0].ID)
|
|
assert.NotEmpty(t, killed[0].Signal)
|
|
mu.Unlock()
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been killed")
|
|
}
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
assert.Len(t, exited, 1)
|
|
assert.Equal(t, proc.ID, exited[0].ID)
|
|
assert.Nil(t, exited[0].Error)
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
})
|
|
|
|
t.Run("broadcasts exited event on start failure", func(t *testing.T) {
|
|
c := framework.New()
|
|
|
|
factory := NewService(Options{})
|
|
raw, err := factory(c)
|
|
require.NoError(t, err)
|
|
svc := raw.(*Service)
|
|
|
|
var exited []ActionProcessExited
|
|
var mu sync.Mutex
|
|
|
|
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if m, ok := msg.(ActionProcessExited); ok {
|
|
exited = append(exited, m)
|
|
}
|
|
return framework.Result{OK: true}
|
|
})
|
|
|
|
_, err = svc.Start(context.Background(), "definitely-not-a-real-binary-xyz")
|
|
require.Error(t, err)
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
require.Len(t, exited, 1)
|
|
assert.Equal(t, -1, exited[0].ExitCode)
|
|
assert.Nil(t, exited[0].Error)
|
|
})
|
|
}
|
|
|
|
func TestService_List(t *testing.T) {
|
|
t.Run("tracks processes", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc1, _ := svc.Start(context.Background(), "echo", "1")
|
|
proc2, _ := svc.Start(context.Background(), "echo", "2")
|
|
|
|
<-proc1.Done()
|
|
<-proc2.Done()
|
|
|
|
list := svc.List()
|
|
assert.Len(t, list, 2)
|
|
assert.Equal(t, proc1.ID, list[0].ID)
|
|
assert.Equal(t, proc2.ID, list[1].ID)
|
|
})
|
|
|
|
t.Run("get by id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, _ := svc.Start(context.Background(), "echo", "test")
|
|
<-proc.Done()
|
|
|
|
got, err := svc.Get(proc.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, proc.ID, got.ID)
|
|
})
|
|
|
|
t.Run("get not found", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.Get("nonexistent")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
}
|
|
|
|
func TestService_Remove(t *testing.T) {
|
|
t.Run("removes completed process", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, _ := svc.Start(context.Background(), "echo", "test")
|
|
<-proc.Done()
|
|
|
|
err := svc.Remove(proc.ID)
|
|
require.NoError(t, err)
|
|
|
|
_, err = svc.Get(proc.ID)
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
|
|
t.Run("cannot remove running process", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc, _ := svc.Start(ctx, "sleep", "10")
|
|
|
|
err := svc.Remove(proc.ID)
|
|
assert.Error(t, err)
|
|
|
|
cancel()
|
|
<-proc.Done()
|
|
})
|
|
}
|
|
|
|
func TestService_Clear(t *testing.T) {
|
|
t.Run("clears completed processes", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc1, _ := svc.Start(context.Background(), "echo", "1")
|
|
proc2, _ := svc.Start(context.Background(), "echo", "2")
|
|
|
|
<-proc1.Done()
|
|
<-proc2.Done()
|
|
|
|
assert.Len(t, svc.List(), 2)
|
|
|
|
svc.Clear()
|
|
|
|
assert.Len(t, svc.List(), 0)
|
|
})
|
|
}
|
|
|
|
func TestService_Kill(t *testing.T) {
|
|
t.Run("kills running process", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.Kill(proc.ID)
|
|
assert.NoError(t, err)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
// Process killed successfully
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been killed")
|
|
}
|
|
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
})
|
|
|
|
t.Run("error on unknown id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
err := svc.Kill("nonexistent")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
}
|
|
|
|
func TestService_KillPID(t *testing.T) {
|
|
t.Run("terminates unmanaged process with SIGTERM", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
cmd := exec.Command("sleep", "60")
|
|
require.NoError(t, cmd.Start())
|
|
|
|
waitCh := make(chan error, 1)
|
|
go func() {
|
|
waitCh <- cmd.Wait()
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
if cmd.ProcessState == nil && cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
select {
|
|
case <-waitCh:
|
|
case <-time.After(2 * time.Second):
|
|
}
|
|
})
|
|
|
|
err := svc.KillPID(cmd.Process.Pid)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case err := <-waitCh:
|
|
require.Error(t, err)
|
|
var exitErr *exec.ExitError
|
|
require.ErrorAs(t, err, &exitErr)
|
|
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
|
require.True(t, ok)
|
|
assert.True(t, ws.Signaled())
|
|
assert.Equal(t, syscall.SIGTERM, ws.Signal())
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("unmanaged process should have been killed")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestService_Signal(t *testing.T) {
|
|
t.Run("signals running process by id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.Signal(proc.ID, syscall.SIGTERM)
|
|
assert.NoError(t, err)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been signalled")
|
|
}
|
|
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
})
|
|
|
|
t.Run("signals unmanaged process by pid", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
cmd := exec.Command("sleep", "60")
|
|
require.NoError(t, cmd.Start())
|
|
|
|
waitCh := make(chan error, 1)
|
|
go func() {
|
|
waitCh <- cmd.Wait()
|
|
}()
|
|
|
|
t.Cleanup(func() {
|
|
if cmd.ProcessState == nil && cmd.Process != nil {
|
|
_ = cmd.Process.Kill()
|
|
}
|
|
select {
|
|
case <-waitCh:
|
|
case <-time.After(2 * time.Second):
|
|
}
|
|
})
|
|
|
|
err := svc.SignalPID(cmd.Process.Pid, syscall.SIGTERM)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case err := <-waitCh:
|
|
require.Error(t, err)
|
|
var exitErr *exec.ExitError
|
|
require.ErrorAs(t, err, &exitErr)
|
|
ws, ok := exitErr.Sys().(syscall.WaitStatus)
|
|
require.True(t, ok)
|
|
assert.True(t, ws.Signaled())
|
|
assert.Equal(t, syscall.SIGTERM, ws.Signal())
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("unmanaged process should have been signalled")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestService_Output(t *testing.T) {
|
|
t.Run("returns captured output", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "captured")
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
output, err := svc.Output(proc.ID)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output, "captured")
|
|
})
|
|
|
|
t.Run("error on unknown id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.Output("nonexistent")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
}
|
|
|
|
func TestService_Input(t *testing.T) {
|
|
t.Run("writes to stdin", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "cat")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.Input(proc.ID, "service-input\n")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.CloseStdin(proc.ID)
|
|
require.NoError(t, err)
|
|
|
|
<-proc.Done()
|
|
|
|
assert.Contains(t, proc.Output(), "service-input")
|
|
})
|
|
|
|
t.Run("error on unknown id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
err := svc.Input("nonexistent", "test")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
}
|
|
|
|
func TestService_CloseStdin(t *testing.T) {
|
|
t.Run("closes stdin pipe", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "cat")
|
|
require.NoError(t, err)
|
|
|
|
err = svc.CloseStdin(proc.ID)
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("cat should exit when stdin is closed")
|
|
}
|
|
})
|
|
|
|
t.Run("error on unknown id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
err := svc.CloseStdin("nonexistent")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
}
|
|
|
|
func TestService_Wait(t *testing.T) {
|
|
t.Run("returns final info on success", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "waited")
|
|
require.NoError(t, err)
|
|
|
|
info, err := svc.Wait(proc.ID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, proc.ID, info.ID)
|
|
assert.Equal(t, StatusExited, info.Status)
|
|
assert.Equal(t, 0, info.ExitCode)
|
|
})
|
|
|
|
t.Run("returns error on unknown id", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.Wait("nonexistent")
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
|
|
t.Run("returns info alongside failure", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 7")
|
|
require.NoError(t, err)
|
|
|
|
info, err := svc.Wait(proc.ID)
|
|
require.Error(t, err)
|
|
assert.Equal(t, proc.ID, info.ID)
|
|
assert.Equal(t, StatusExited, info.Status)
|
|
assert.Equal(t, 7, info.ExitCode)
|
|
})
|
|
}
|
|
|
|
func TestService_OnShutdown(t *testing.T) {
|
|
t.Run("kills all running processes", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc1, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
proc2, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
assert.True(t, proc1.IsRunning())
|
|
assert.True(t, proc2.IsRunning())
|
|
|
|
err = svc.OnShutdown(context.Background())
|
|
assert.NoError(t, err)
|
|
|
|
select {
|
|
case <-proc1.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("proc1 should have been killed")
|
|
}
|
|
select {
|
|
case <-proc2.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("proc2 should have been killed")
|
|
}
|
|
})
|
|
|
|
t.Run("does not wait for process grace period", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
proc, err := svc.StartWithOptions(context.Background(), RunOptions{
|
|
Command: "sh",
|
|
Args: []string{"-c", "trap '' TERM; sleep 60"},
|
|
GracePeriod: 5 * time.Second,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, proc.IsRunning())
|
|
|
|
start := time.Now()
|
|
err = svc.OnShutdown(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been killed immediately on shutdown")
|
|
}
|
|
|
|
assert.Less(t, time.Since(start), 2*time.Second)
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
})
|
|
}
|
|
|
|
func TestService_OnStartup(t *testing.T) {
|
|
t.Run("registers process.start task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessStart{
|
|
Command: "sleep",
|
|
Args: []string{"1"},
|
|
})
|
|
|
|
require.True(t, result.OK)
|
|
|
|
info, ok := result.Value.(Info)
|
|
require.True(t, ok)
|
|
assert.NotEmpty(t, info.ID)
|
|
assert.Equal(t, StatusRunning, info.Status)
|
|
assert.True(t, info.Running)
|
|
|
|
proc, err := svc.Get(info.ID)
|
|
require.NoError(t, err)
|
|
assert.True(t, proc.IsRunning())
|
|
|
|
<-proc.Done()
|
|
assert.Equal(t, StatusExited, proc.Status)
|
|
})
|
|
|
|
t.Run("registers process.run task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessRun{
|
|
Command: "echo",
|
|
Args: []string{"action-run"},
|
|
})
|
|
|
|
require.True(t, result.OK)
|
|
assert.Contains(t, result.Value.(string), "action-run")
|
|
})
|
|
|
|
t.Run("forwards task execution options", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessRun{
|
|
Command: "sleep",
|
|
Args: []string{"60"},
|
|
Timeout: 100 * time.Millisecond,
|
|
GracePeriod: 50 * time.Millisecond,
|
|
})
|
|
|
|
require.False(t, result.OK)
|
|
assert.Nil(t, result.Value)
|
|
})
|
|
|
|
t.Run("registers process.kill task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "sleep", "60")
|
|
require.NoError(t, err)
|
|
require.True(t, proc.IsRunning())
|
|
|
|
var killed []ActionProcessKilled
|
|
c.RegisterAction(func(cc *framework.Core, msg framework.Message) framework.Result {
|
|
if m, ok := msg.(ActionProcessKilled); ok {
|
|
killed = append(killed, m)
|
|
}
|
|
return framework.Result{OK: true}
|
|
})
|
|
|
|
result := c.PERFORM(TaskProcessKill{PID: proc.Info().PID})
|
|
require.True(t, result.OK)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been killed by pid")
|
|
}
|
|
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
require.Len(t, killed, 1)
|
|
assert.Equal(t, proc.ID, killed[0].ID)
|
|
assert.NotEmpty(t, killed[0].Signal)
|
|
})
|
|
|
|
t.Run("registers process.signal task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessSignal{
|
|
ID: proc.ID,
|
|
Signal: syscall.SIGTERM,
|
|
})
|
|
require.True(t, result.OK)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have been signalled through core")
|
|
}
|
|
|
|
assert.Equal(t, StatusKilled, proc.Status)
|
|
})
|
|
|
|
t.Run("allows signal zero liveness checks", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessSignal{
|
|
ID: proc.ID,
|
|
Signal: syscall.Signal(0),
|
|
})
|
|
require.True(t, result.OK)
|
|
|
|
assert.True(t, proc.IsRunning())
|
|
|
|
cancel()
|
|
<-proc.Done()
|
|
})
|
|
|
|
t.Run("registers process.wait task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "action-wait")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessWait{ID: proc.ID})
|
|
require.True(t, result.OK)
|
|
|
|
info, ok := result.Value.(Info)
|
|
require.True(t, ok)
|
|
assert.Equal(t, proc.ID, info.ID)
|
|
assert.Equal(t, StatusExited, info.Status)
|
|
assert.Equal(t, 0, info.ExitCode)
|
|
})
|
|
|
|
t.Run("registers process.list task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessList{RunningOnly: true})
|
|
require.True(t, result.OK)
|
|
|
|
infos, ok := result.Value.([]Info)
|
|
require.True(t, ok)
|
|
require.Len(t, infos, 1)
|
|
assert.Equal(t, proc.ID, infos[0].ID)
|
|
assert.True(t, infos[0].Running)
|
|
|
|
cancel()
|
|
<-proc.Done()
|
|
})
|
|
|
|
t.Run("registers process.get task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "snapshot")
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
result := c.PERFORM(TaskProcessGet{ID: proc.ID})
|
|
require.True(t, result.OK)
|
|
|
|
info, ok := result.Value.(Info)
|
|
require.True(t, ok)
|
|
assert.Equal(t, proc.ID, info.ID)
|
|
assert.Equal(t, proc.Command, info.Command)
|
|
assert.Equal(t, proc.Args, info.Args)
|
|
assert.Equal(t, proc.Status, info.Status)
|
|
assert.Equal(t, proc.ExitCode, info.ExitCode)
|
|
assert.Equal(t, proc.Info().PID, info.PID)
|
|
})
|
|
|
|
t.Run("registers process.remove task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "remove-through-core")
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
result := c.PERFORM(TaskProcessRemove{ID: proc.ID})
|
|
require.True(t, result.OK)
|
|
|
|
_, err = svc.Get(proc.ID)
|
|
assert.ErrorIs(t, err, ErrProcessNotFound)
|
|
})
|
|
|
|
t.Run("registers process.clear task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
first, err := svc.Start(context.Background(), "echo", "clear-through-core-1")
|
|
require.NoError(t, err)
|
|
second, err := svc.Start(context.Background(), "echo", "clear-through-core-2")
|
|
require.NoError(t, err)
|
|
<-first.Done()
|
|
<-second.Done()
|
|
|
|
require.Len(t, svc.List(), 2)
|
|
|
|
result := c.PERFORM(TaskProcessClear{})
|
|
require.True(t, result.OK)
|
|
assert.Len(t, svc.List(), 0)
|
|
})
|
|
|
|
t.Run("registers process.output task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "echo", "snapshot-output")
|
|
require.NoError(t, err)
|
|
<-proc.Done()
|
|
|
|
result := c.PERFORM(TaskProcessOutput{ID: proc.ID})
|
|
require.True(t, result.OK)
|
|
|
|
output, ok := result.Value.(string)
|
|
require.True(t, ok)
|
|
assert.Contains(t, output, "snapshot-output")
|
|
})
|
|
|
|
t.Run("registers process.input task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "cat")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessInput{
|
|
ID: proc.ID,
|
|
Input: "typed-through-core\n",
|
|
})
|
|
require.True(t, result.OK)
|
|
|
|
err = proc.CloseStdin()
|
|
require.NoError(t, err)
|
|
|
|
<-proc.Done()
|
|
|
|
assert.Contains(t, proc.Output(), "typed-through-core")
|
|
})
|
|
|
|
t.Run("registers process.close_stdin task", func(t *testing.T) {
|
|
svc, c := newTestService(t)
|
|
|
|
err := svc.OnStartup(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
proc, err := svc.Start(context.Background(), "cat")
|
|
require.NoError(t, err)
|
|
|
|
result := c.PERFORM(TaskProcessInput{
|
|
ID: proc.ID,
|
|
Input: "close-through-core\n",
|
|
})
|
|
require.True(t, result.OK)
|
|
|
|
result = c.PERFORM(TaskProcessCloseStdin{ID: proc.ID})
|
|
require.True(t, result.OK)
|
|
|
|
select {
|
|
case <-proc.Done():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("process should have exited after stdin was closed")
|
|
}
|
|
|
|
assert.Contains(t, proc.Output(), "close-through-core")
|
|
})
|
|
}
|
|
|
|
func TestService_RunWithOptions(t *testing.T) {
|
|
t.Run("returns output on success", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
output, err := svc.RunWithOptions(context.Background(), RunOptions{
|
|
Command: "echo",
|
|
Args: []string{"opts-test"},
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Contains(t, output, "opts-test")
|
|
})
|
|
|
|
t.Run("returns error on failure", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.RunWithOptions(context.Background(), RunOptions{
|
|
Command: "sh",
|
|
Args: []string{"-c", "exit 2"},
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "exited with code 2")
|
|
})
|
|
|
|
t.Run("rejects nil context", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
_, err := svc.RunWithOptions(nil, RunOptions{
|
|
Command: "echo",
|
|
})
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, ErrContextRequired)
|
|
})
|
|
}
|
|
|
|
func TestService_Running(t *testing.T) {
|
|
t.Run("returns only running processes", func(t *testing.T) {
|
|
svc, _ := newTestService(t)
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
proc1, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
doneProc, err := svc.Start(context.Background(), "echo", "done")
|
|
require.NoError(t, err)
|
|
<-doneProc.Done()
|
|
|
|
running := svc.Running()
|
|
assert.Len(t, running, 1)
|
|
assert.Equal(t, proc1.ID, running[0].ID)
|
|
|
|
proc2, err := svc.Start(ctx, "sleep", "60")
|
|
require.NoError(t, err)
|
|
|
|
running = svc.Running()
|
|
assert.Len(t, running, 2)
|
|
assert.Equal(t, proc1.ID, running[0].ID)
|
|
assert.Equal(t, proc2.ID, running[1].ID)
|
|
|
|
cancel()
|
|
<-proc1.Done()
|
|
<-proc2.Done()
|
|
})
|
|
}
|