fix(go-process): replace testify with stdlib testing patterns (AX-6)

Removes testify + davecgh/go-spew + pmezard/go-difflib from go.mod;
rewrites assert/require calls across 13 _test.go files
(buffer/daemon/errors/exec/global/health/pidfile/process/program/
registry/runner/service + pkg/api/provider) to stdlib t.Fatalf
patterns. go vet + go test all clean (GOWORK=off).

Closes tasks.lthn.sh/view.php?id=719

Co-authored-by: Codex <noreply@openai.com>
Via-codex-lane: Cyclops-719 dispatch (haiku forwarder, ~17min lane)
This commit is contained in:
Codex 2026-04-24 18:03:24 +01:00
parent 55f7245017
commit d86f9abc29
15 changed files with 986 additions and 936 deletions

View file

@ -2,8 +2,6 @@ package process
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRingBuffer_Basics_Good(t *testing.T) {
@ -11,22 +9,22 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
rb := NewRingBuffer(10)
n, err := rb.Write([]byte("hello"))
assert.NoError(t, err)
assert.Equal(t, 5, n)
assert.Equal(t, "hello", rb.String())
assert.Equal(t, 5, rb.Len())
assertNoError(t, err)
assertEqual(t, 5, n)
assertEqual(t, "hello", rb.String())
assertEqual(t, 5, rb.Len())
})
t.Run("overflow wraps around", func(t *testing.T) {
rb := NewRingBuffer(5)
_, _ = rb.Write([]byte("hello"))
assert.Equal(t, "hello", rb.String())
assertEqual(t, "hello", rb.String())
_, _ = rb.Write([]byte("world"))
// Should contain "world" (overwrote "hello")
assert.Equal(t, 5, rb.Len())
assert.Equal(t, "world", rb.String())
assertEqual(t, 5, rb.Len())
assertEqual(t, "world", rb.String())
})
t.Run("partial overflow", func(t *testing.T) {
@ -35,27 +33,27 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
_, _ = rb.Write([]byte("hello"))
_, _ = rb.Write([]byte("worldx"))
// Should contain "lloworldx" (11 chars, buffer is 10)
assert.Equal(t, 10, rb.Len())
assertEqual(t, 10, rb.Len())
})
t.Run("empty buffer", func(t *testing.T) {
rb := NewRingBuffer(10)
assert.Equal(t, "", rb.String())
assert.Equal(t, 0, rb.Len())
assert.Nil(t, rb.Bytes())
assertEqual(t, "", rb.String())
assertEqual(t, 0, rb.Len())
assertNil(t, rb.Bytes())
})
t.Run("reset", func(t *testing.T) {
rb := NewRingBuffer(10)
_, _ = rb.Write([]byte("hello"))
rb.Reset()
assert.Equal(t, "", rb.String())
assert.Equal(t, 0, rb.Len())
assertEqual(t, "", rb.String())
assertEqual(t, 0, rb.Len())
})
t.Run("cap", func(t *testing.T) {
rb := NewRingBuffer(42)
assert.Equal(t, 42, rb.Cap())
assertEqual(t, 42, rb.Cap())
})
t.Run("bytes returns copy", func(t *testing.T) {
@ -63,11 +61,11 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
_, _ = rb.Write([]byte("hello"))
bytes := rb.Bytes()
assert.Equal(t, []byte("hello"), bytes)
assertEqual(t, []byte("hello"), bytes)
// Modifying returned bytes shouldn't affect buffer
bytes[0] = 'x'
assert.Equal(t, "hello", rb.String())
assertEqual(t, "hello", rb.String())
})
t.Run("zero or negative capacity is a no-op", func(t *testing.T) {
@ -75,12 +73,12 @@ func TestRingBuffer_Basics_Good(t *testing.T) {
rb := NewRingBuffer(size)
n, err := rb.Write([]byte("discarded"))
assert.NoError(t, err)
assert.Equal(t, len("discarded"), n)
assert.Equal(t, 0, rb.Cap())
assert.Equal(t, 0, rb.Len())
assert.Equal(t, "", rb.String())
assert.Nil(t, rb.Bytes())
assertNoError(t, err)
assertEqual(t, len("discarded"), n)
assertEqual(t, 0, rb.Cap())
assertEqual(t, 0, rb.Len())
assertEqual(t, "", rb.String())
assertNil(t, rb.Bytes())
}
})
}

View file

@ -8,9 +8,6 @@ import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDaemon_StartAndStop(t *testing.T) {
@ -23,18 +20,18 @@ func TestDaemon_StartAndStop(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
addr := d.HealthAddr()
require.NotEmpty(t, addr)
requireNotEmpty(t, addr)
resp, err := http.Get("http://" + addr + "/health")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
err = d.Stop()
require.NoError(t, err)
requireNoError(t, err)
}
func TestDaemon_StopMarksNotReadyBeforeShutdownCompletes(t *testing.T) {
@ -55,10 +52,10 @@ func TestDaemon_StopMarksNotReadyBeforeShutdownCompletes(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
addr := d.HealthAddr()
require.NotEmpty(t, addr)
requireNotEmpty(t, addr)
healthErr := make(chan error, 1)
go func() {
@ -82,7 +79,7 @@ func TestDaemon_StopMarksNotReadyBeforeShutdownCompletes(t *testing.T) {
stopDone <- d.Stop()
}()
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
return !d.Ready()
}, 500*time.Millisecond, 10*time.Millisecond, "daemon should become not ready before shutdown completes")
@ -96,14 +93,14 @@ func TestDaemon_StopMarksNotReadyBeforeShutdownCompletes(t *testing.T) {
select {
case err := <-stopDone:
require.NoError(t, err)
requireNoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("daemon stop did not finish after health check unblocked")
}
select {
case err := <-healthErr:
require.NoError(t, err)
requireNoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("/health request did not finish")
}
@ -134,10 +131,10 @@ func TestDaemon_StopUnregistersAfterHealthShutdownCompletes(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
addr := d.HealthAddr()
require.NotEmpty(t, addr)
requireNotEmpty(t, addr)
healthErr := make(chan error, 1)
go func() {
@ -161,12 +158,12 @@ func TestDaemon_StopUnregistersAfterHealthShutdownCompletes(t *testing.T) {
stopDone <- d.Stop()
}()
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
return !d.Ready()
}, 500*time.Millisecond, 10*time.Millisecond, "daemon should become not ready before shutdown completes")
_, ok := reg.Get("test-app", "serve")
assert.True(t, ok, "daemon should remain registered until health shutdown completes")
assertTrue(t, ok, "daemon should remain registered until health shutdown completes")
select {
case err := <-stopDone:
@ -178,19 +175,19 @@ func TestDaemon_StopUnregistersAfterHealthShutdownCompletes(t *testing.T) {
select {
case err := <-stopDone:
require.NoError(t, err)
requireNoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("daemon stop did not finish after health check unblocked")
}
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
_, ok := reg.Get("test-app", "serve")
return !ok
}, 500*time.Millisecond, 10*time.Millisecond, "daemon should unregister after health shutdown completes")
select {
case err := <-healthErr:
require.NoError(t, err)
requireNoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("/health request did not finish")
}
@ -202,12 +199,12 @@ func TestDaemon_DoubleStartFails(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
defer func() { _ = d.Stop() }()
err = d.Start()
assert.Error(t, err)
assert.Contains(t, err.Error(), "already running")
assertError(t, err)
assertContains(t, err.Error(), "already running")
}
func TestDaemon_RunWithoutStartFails(t *testing.T) {
@ -217,16 +214,16 @@ func TestDaemon_RunWithoutStartFails(t *testing.T) {
cancel()
err := d.Run(ctx)
assert.Error(t, err)
assert.Contains(t, err.Error(), "not started")
assertError(t, err)
assertContains(t, err.Error(), "not started")
}
func TestDaemon_RunNilContextFails(t *testing.T) {
d := NewDaemon(DaemonOptions{})
err := d.Run(nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrDaemonContextRequired)
requireError(t, err)
assertErrorIs(t, err, ErrDaemonContextRequired)
}
func TestDaemon_SetReady(t *testing.T) {
@ -235,37 +232,37 @@ func TestDaemon_SetReady(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
defer func() { _ = d.Stop() }()
addr := d.HealthAddr()
resp, _ := http.Get("http://" + addr + "/ready")
assert.Equal(t, http.StatusOK, resp.StatusCode)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
assert.True(t, d.Ready())
assertTrue(t, d.Ready())
d.SetReady(false)
assert.False(t, d.Ready())
assertFalse(t, d.Ready())
resp, _ = http.Get("http://" + addr + "/ready")
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
assertEqual(t, http.StatusServiceUnavailable, resp.StatusCode)
_ = resp.Body.Close()
}
func TestDaemon_ReadyWithoutHealthServer(t *testing.T) {
d := NewDaemon(DaemonOptions{})
assert.False(t, d.Ready())
assertFalse(t, d.Ready())
}
func TestDaemon_NoHealthAddrReturnsEmpty(t *testing.T) {
d := NewDaemon(DaemonOptions{})
assert.Empty(t, d.HealthAddr())
assertEqual(t, "", d.HealthAddr())
}
func TestDaemon_DefaultShutdownTimeout(t *testing.T) {
d := NewDaemon(DaemonOptions{})
assert.Equal(t, 30*time.Second, d.opts.ShutdownTimeout)
assertEqual(t, 30*time.Second, d.opts.ShutdownTimeout)
}
func TestDaemon_RunBlocksUntilCancelled(t *testing.T) {
@ -274,7 +271,7 @@ func TestDaemon_RunBlocksUntilCancelled(t *testing.T) {
})
err := d.Start()
require.NoError(t, err)
requireNoError(t, err)
ctx, cancel := context.WithCancel(context.Background())
@ -295,7 +292,7 @@ func TestDaemon_RunBlocksUntilCancelled(t *testing.T) {
select {
case err := <-done:
assert.NoError(t, err)
assertNoError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("Run should return after context cancellation")
}
@ -306,16 +303,16 @@ func TestDaemon_StopIdempotent(t *testing.T) {
// Stop without Start should be a no-op
err := d.Stop()
assert.NoError(t, err)
assertNoError(t, err)
}
func TestDaemon_AutoRegisters(t *testing.T) {
dir := t.TempDir()
reg := NewRegistry(filepath.Join(dir, "daemons"))
wd, err := os.Getwd()
require.NoError(t, err)
requireNoError(t, err)
exe, err := os.Executable()
require.NoError(t, err)
requireNoError(t, err)
d := NewDaemon(DaemonOptions{
HealthAddr: "127.0.0.1:0",
@ -327,22 +324,22 @@ func TestDaemon_AutoRegisters(t *testing.T) {
})
err = d.Start()
require.NoError(t, err)
requireNoError(t, err)
// Should be registered
entry, ok := reg.Get("test-app", "serve")
require.True(t, ok)
assert.Equal(t, os.Getpid(), entry.PID)
assert.NotEmpty(t, entry.Health)
assert.Equal(t, wd, entry.Project)
assert.Equal(t, exe, entry.Binary)
requireTrue(t, ok)
assertEqual(t, os.Getpid(), entry.PID)
assertNotEmpty(t, entry.Health)
assertEqual(t, wd, entry.Project)
assertEqual(t, exe, entry.Binary)
// Stop should unregister
err = d.Stop()
require.NoError(t, err)
requireNoError(t, err)
_, ok = reg.Get("test-app", "serve")
assert.False(t, ok)
assertFalse(t, ok)
}
func TestDaemon_StartRollsBackOnRegistryFailure(t *testing.T) {
@ -350,8 +347,8 @@ func TestDaemon_StartRollsBackOnRegistryFailure(t *testing.T) {
pidPath := filepath.Join(dir, "daemon.pid")
regDir := filepath.Join(dir, "registry")
require.NoError(t, os.MkdirAll(regDir, 0o755))
require.NoError(t, os.Chmod(regDir, 0o555))
requireNoError(t, os.MkdirAll(regDir, 0o755))
requireNoError(t, os.Chmod(regDir, 0o555))
d := NewDaemon(DaemonOptions{
PIDFile: pidPath,
@ -364,20 +361,20 @@ func TestDaemon_StartRollsBackOnRegistryFailure(t *testing.T) {
})
err := d.Start()
require.Error(t, err)
requireError(t, err)
_, statErr := os.Stat(pidPath)
assert.True(t, os.IsNotExist(statErr))
assertTrue(t, os.IsNotExist(statErr))
addr := d.HealthAddr()
require.NotEmpty(t, addr)
requireNotEmpty(t, addr)
client := &http.Client{Timeout: 250 * time.Millisecond}
resp, reqErr := client.Get("http://" + addr + "/health")
if resp != nil {
_ = resp.Body.Close()
}
assert.Error(t, reqErr)
assertError(t, reqErr)
assert.NoError(t, d.Stop())
assertNoError(t, d.Stop())
}

View file

@ -2,14 +2,11 @@ package process
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestServiceError(t *testing.T) {
err := ServiceError("service failed", ErrContextRequired)
require.Error(t, err)
assert.Contains(t, err.Error(), "service failed")
assert.ErrorIs(t, err, ErrContextRequired)
requireError(t, err)
assertContains(t, err.Error(), "service failed")
assertErrorIs(t, err, ErrContextRequired)
}

View file

@ -2,6 +2,7 @@ package exec_test
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
@ -11,8 +12,6 @@ import (
"time"
"dappco.re/go/core/process/exec"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// mockLogger captures log calls for testing
@ -159,7 +158,9 @@ func TestDefaultLogger_IsConcurrentSafe(t *testing.T) {
}
wg.Wait()
assert.NotNil(t, exec.DefaultLogger())
if exec.DefaultLogger() == nil {
t.Fatal("expected non-nil default logger")
}
}
func TestCommand_UsesDefaultLogger(t *testing.T) {
@ -260,26 +261,42 @@ func TestCommand_Run_Background(t *testing.T) {
func TestCommand_NilContextRejected(t *testing.T) {
t.Run("start", func(t *testing.T) {
err := exec.Command(nil, "echo", "test").Start()
require.Error(t, err)
assert.ErrorIs(t, err, exec.ErrCommandContextRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, exec.ErrCommandContextRequired) {
t.Fatalf("expected ErrCommandContextRequired, got %v", err)
}
})
t.Run("run", func(t *testing.T) {
err := exec.Command(nil, "echo", "test").Run()
require.Error(t, err)
assert.ErrorIs(t, err, exec.ErrCommandContextRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, exec.ErrCommandContextRequired) {
t.Fatalf("expected ErrCommandContextRequired, got %v", err)
}
})
t.Run("output", func(t *testing.T) {
_, err := exec.Command(nil, "echo", "test").Output()
require.Error(t, err)
assert.ErrorIs(t, err, exec.ErrCommandContextRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, exec.ErrCommandContextRequired) {
t.Fatalf("expected ErrCommandContextRequired, got %v", err)
}
})
t.Run("combined output", func(t *testing.T) {
_, err := exec.Command(nil, "echo", "test").CombinedOutput()
require.Error(t, err)
assert.ErrorIs(t, err, exec.ErrCommandContextRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, exec.ErrCommandContextRequired) {
t.Fatalf("expected ErrCommandContextRequired, got %v", err)
}
})
}

View file

@ -9,8 +9,6 @@ import (
"time"
framework "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGlobal_DefaultNotInitialized(t *testing.T) {
@ -22,46 +20,46 @@ func TestGlobal_DefaultNotInitialized(t *testing.T) {
}
}()
assert.Nil(t, Default())
assertNil(t, Default())
_, err := Start(context.Background(), "echo", "test")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
_, err = Run(context.Background(), "echo", "test")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
_, err = Get("proc-1")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
_, err = Output("proc-1")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
err = Input("proc-1", "test")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
err = CloseStdin("proc-1")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
assert.Nil(t, List())
assert.Nil(t, Running())
assertNil(t, List())
assertNil(t, Running())
err = Remove("proc-1")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
// Clear is a no-op without a default service.
Clear()
err = Kill("proc-1")
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
err = KillPID(1234)
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
_, err = StartWithOptions(context.Background(), RunOptions{Command: "echo"})
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
_, err = RunWithOptions(context.Background(), RunOptions{Command: "echo"})
assert.ErrorIs(t, err, ErrServiceNotInitialized)
assertErrorIs(t, err, ErrServiceNotInitialized)
}
func newGlobalTestService(t *testing.T) *Service {
@ -69,7 +67,7 @@ func newGlobalTestService(t *testing.T) *Service {
c := framework.New()
factory := NewService(Options{})
raw, err := factory(c)
require.NoError(t, err)
requireNoError(t, err)
return raw.(*Service)
}
@ -85,13 +83,13 @@ func TestGlobal_SetDefault(t *testing.T) {
svc := newGlobalTestService(t)
err := SetDefault(svc)
require.NoError(t, err)
assert.Equal(t, svc, Default())
requireNoError(t, err)
assertEqual(t, svc, Default())
})
t.Run("errors on nil", func(t *testing.T) {
err := SetDefault(nil)
assert.Error(t, err)
assertError(t, err)
})
}
@ -99,13 +97,13 @@ func TestGlobal_Register(t *testing.T) {
c := framework.New()
result := Register(c)
require.True(t, result.OK)
requireTrue(t, result.OK)
svc, ok := result.Value.(*Service)
require.True(t, ok)
require.NotNil(t, svc)
assert.NotNil(t, svc.ServiceRuntime)
assert.Equal(t, DefaultBufferSize, svc.bufSize)
requireTrue(t, ok)
requireNotNil(t, svc)
assertNotNil(t, svc.ServiceRuntime)
assertEqual(t, DefaultBufferSize, svc.bufSize)
}
func TestGlobal_ConcurrentDefault(t *testing.T) {
@ -119,7 +117,7 @@ func TestGlobal_ConcurrentDefault(t *testing.T) {
svc := newGlobalTestService(t)
err := SetDefault(svc)
require.NoError(t, err)
requireNoError(t, err)
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
@ -127,8 +125,8 @@ func TestGlobal_ConcurrentDefault(t *testing.T) {
go func() {
defer wg.Done()
s := Default()
assert.NotNil(t, s)
assert.Equal(t, svc, s)
assertNotNil(t, s)
assertEqual(t, svc, s)
}()
}
wg.Wait()
@ -159,7 +157,7 @@ func TestGlobal_ConcurrentSetDefault(t *testing.T) {
wg.Wait()
final := Default()
assert.NotNil(t, final)
assertNotNil(t, final)
found := false
for _, svc := range services {
@ -168,7 +166,7 @@ func TestGlobal_ConcurrentSetDefault(t *testing.T) {
break
}
}
assert.True(t, found, "Default should be one of the set services")
assertTrue(t, found, "Default should be one of the set services")
}
func TestGlobal_ConcurrentOperations(t *testing.T) {
@ -182,7 +180,7 @@ func TestGlobal_ConcurrentOperations(t *testing.T) {
svc := newGlobalTestService(t)
err := SetDefault(svc)
require.NoError(t, err)
requireNoError(t, err)
var wg sync.WaitGroup
var processes []*Process
@ -218,7 +216,7 @@ func TestGlobal_ConcurrentOperations(t *testing.T) {
}
procMu.Unlock()
assert.Len(t, processes, 20)
assertLen(t, processes, 20)
var wg2 sync.WaitGroup
for _, p := range processes {
@ -226,8 +224,8 @@ func TestGlobal_ConcurrentOperations(t *testing.T) {
go func(id string) {
defer wg2.Done()
got, err := Get(id)
assert.NoError(t, err)
assert.NotNil(t, got)
assertNoError(t, err)
assertNotNil(t, got)
}(p.ID)
}
wg2.Wait()
@ -247,12 +245,12 @@ func TestGlobal_StartWithOptions(t *testing.T) {
Command: "echo",
Args: []string{"with", "options"},
})
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
assert.Equal(t, 0, proc.ExitCode)
assert.Contains(t, proc.Output(), "with options")
assertEqual(t, 0, proc.ExitCode)
assertContains(t, proc.Output(), "with options")
}
func TestGlobal_RunWithOptions(t *testing.T) {
@ -269,8 +267,8 @@ func TestGlobal_RunWithOptions(t *testing.T) {
Command: "echo",
Args: []string{"run", "options"},
})
require.NoError(t, err)
assert.Contains(t, output, "run options")
requireNoError(t, err)
assertContains(t, output, "run options")
}
func TestGlobal_Output(t *testing.T) {
@ -284,12 +282,12 @@ func TestGlobal_Output(t *testing.T) {
}()
proc, err := Start(context.Background(), "echo", "global-output")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
output, err := Output(proc.ID)
require.NoError(t, err)
assert.Contains(t, output, "global-output")
requireNoError(t, err)
assertContains(t, output, "global-output")
}
func TestGlobal_InputAndCloseStdin(t *testing.T) {
@ -303,17 +301,17 @@ func TestGlobal_InputAndCloseStdin(t *testing.T) {
}()
proc, err := Start(context.Background(), "cat")
require.NoError(t, err)
requireNoError(t, err)
err = Input(proc.ID, "global-input\n")
require.NoError(t, err)
requireNoError(t, err)
err = CloseStdin(proc.ID)
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
assert.Contains(t, proc.Output(), "global-input")
assertContains(t, proc.Output(), "global-input")
}
func TestGlobal_Wait(t *testing.T) {
@ -327,13 +325,13 @@ func TestGlobal_Wait(t *testing.T) {
}()
proc, err := Start(context.Background(), "echo", "global-wait")
require.NoError(t, err)
requireNoError(t, err)
info, err := 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)
requireNoError(t, err)
assertEqual(t, proc.ID, info.ID)
assertEqual(t, StatusExited, info.Status)
assertEqual(t, 0, info.ExitCode)
}
func TestGlobal_Signal(t *testing.T) {
@ -347,10 +345,10 @@ func TestGlobal_Signal(t *testing.T) {
}()
proc, err := Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
err = Signal(proc.ID, syscall.SIGTERM)
require.NoError(t, err)
requireNoError(t, err)
select {
case <-proc.Done():
@ -370,7 +368,7 @@ func TestGlobal_SignalPID(t *testing.T) {
}()
cmd := exec.Command("sleep", "60")
require.NoError(t, cmd.Start())
requireNoError(t, cmd.Start())
waitCh := make(chan error, 1)
go func() {
@ -388,11 +386,11 @@ func TestGlobal_SignalPID(t *testing.T) {
})
err := SignalPID(cmd.Process.Pid, syscall.SIGTERM)
require.NoError(t, err)
requireNoError(t, err)
select {
case err := <-waitCh:
require.Error(t, err)
requireError(t, err)
case <-time.After(2 * time.Second):
t.Fatal("unmanaged process should have been signalled through the global helper")
}
@ -412,17 +410,17 @@ func TestGlobal_Running(t *testing.T) {
defer cancel()
proc, err := Start(ctx, "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
running := Running()
assert.Len(t, running, 1)
assert.Equal(t, proc.ID, running[0].ID)
assertLen(t, running, 1)
assertEqual(t, proc.ID, running[0].ID)
cancel()
<-proc.Done()
running = Running()
assert.Len(t, running, 0)
assertLen(t, running, 0)
}
func TestGlobal_RemoveAndClear(t *testing.T) {
@ -436,21 +434,21 @@ func TestGlobal_RemoveAndClear(t *testing.T) {
}()
proc, err := Start(context.Background(), "echo", "remove-me")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
err = Remove(proc.ID)
require.NoError(t, err)
requireNoError(t, err)
_, err = Get(proc.ID)
require.ErrorIs(t, err, ErrProcessNotFound)
requireErrorIs(t, err, ErrProcessNotFound)
proc2, err := Start(context.Background(), "echo", "clear-me")
require.NoError(t, err)
requireNoError(t, err)
<-proc2.Done()
Clear()
_, err = Get(proc2.ID)
require.ErrorIs(t, err, ErrProcessNotFound)
requireErrorIs(t, err, ErrProcessNotFound)
}

3
go.mod
View file

@ -10,7 +10,6 @@ require (
dappco.re/go/core/ws v0.4.0
github.com/gin-gonic/gin v1.12.0
github.com/gorilla/websocket v1.5.3
github.com/stretchr/testify v1.11.1
)
require (
@ -28,7 +27,6 @@ require (
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/authz v1.0.6 // indirect
@ -75,7 +73,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.0 // indirect
github.com/redis/go-redis/v9 v9.18.0 // indirect

5
go.sum
View file

@ -7,6 +7,7 @@ dappco.re/go/core/io v0.2.0/go.mod h1:1QnQV6X9LNgFKfm8SkOtR9LLaj3bDcsOIeJOOyjbL5
dappco.re/go/core/log v0.1.0 h1:pa71Vq2TD2aoEUQWFKwNcaJ3GBY8HbaNGqtE688Unyc=
dappco.re/go/core/log v0.1.0/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs=
dappco.re/go/core/ws v0.4.0 h1:yEDV9whXyo+GWzBSjuB3NiLiH2bmBPBWD6rydwHyBn8=
dappco.re/go/core/ws v0.4.0/go.mod h1:L1rrgW6zU+DztcVBJW2yO5Lm3rGXpyUMOA8OL9zsAok=
forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0=
forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw=
github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc=
@ -249,6 +250,7 @@ golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
@ -258,6 +260,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -272,6 +275,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@ -280,6 +284,7 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

View file

@ -4,47 +4,44 @@ import (
"context"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHealthServer_Endpoints(t *testing.T) {
hs := NewHealthServer("127.0.0.1:0")
assert.True(t, hs.Ready())
assertTrue(t, hs.Ready())
err := hs.Start()
require.NoError(t, err)
requireNoError(t, err)
defer func() { _ = hs.Stop(context.Background()) }()
addr := hs.Addr()
require.NotEmpty(t, addr)
requireNotEmpty(t, addr)
resp, err := http.Get("http://" + addr + "/health")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
resp, err = http.Get("http://" + addr + "/ready")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
hs.SetReady(false)
assert.False(t, hs.Ready())
assertFalse(t, hs.Ready())
resp, err = http.Get("http://" + addr + "/ready")
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusServiceUnavailable, resp.StatusCode)
_ = resp.Body.Close()
}
func TestHealthServer_Ready(t *testing.T) {
hs := NewHealthServer("127.0.0.1:0")
assert.True(t, hs.Ready())
assertTrue(t, hs.Ready())
hs.SetReady(false)
assert.False(t, hs.Ready())
assertFalse(t, hs.Ready())
}
func TestHealthServer_WithChecks(t *testing.T) {
@ -53,27 +50,27 @@ func TestHealthServer_WithChecks(t *testing.T) {
healthy := true
hs.AddCheck(func() error {
if !healthy {
return assert.AnError
return errSentinel
}
return nil
})
err := hs.Start()
require.NoError(t, err)
requireNoError(t, err)
defer func() { _ = hs.Stop(context.Background()) }()
addr := hs.Addr()
resp, err := http.Get("http://" + addr + "/health")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
healthy = false
resp, err = http.Get("http://" + addr + "/health")
require.NoError(t, err)
assert.Equal(t, http.StatusServiceUnavailable, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusServiceUnavailable, resp.StatusCode)
_ = resp.Body.Close()
}
@ -84,14 +81,14 @@ func TestHealthServer_NilCheckIgnored(t *testing.T) {
hs.AddCheck(check)
err := hs.Start()
require.NoError(t, err)
requireNoError(t, err)
defer func() { _ = hs.Stop(context.Background()) }()
addr := hs.Addr()
resp, err := http.Get("http://" + addr + "/health")
require.NoError(t, err)
assert.Equal(t, http.StatusOK, resp.StatusCode)
requireNoError(t, err)
assertEqual(t, http.StatusOK, resp.StatusCode)
_ = resp.Body.Close()
}
@ -100,49 +97,49 @@ func TestHealthServer_ChecksSnapshotIsStable(t *testing.T) {
hs.AddCheck(func() error { return nil })
snapshot := hs.checksSnapshot()
hs.AddCheck(func() error { return assert.AnError })
hs.AddCheck(func() error { return errSentinel })
require.Len(t, snapshot, 1)
require.NotNil(t, snapshot[0])
requireLen(t, snapshot, 1)
requireNotNil(t, snapshot[0])
}
func TestWaitForHealth_Reachable(t *testing.T) {
hs := NewHealthServer("127.0.0.1:0")
require.NoError(t, hs.Start())
requireNoError(t, hs.Start())
defer func() { _ = hs.Stop(context.Background()) }()
ok := WaitForHealth(hs.Addr(), 2_000)
assert.True(t, ok)
assertTrue(t, ok)
}
func TestWaitForHealth_Unreachable(t *testing.T) {
ok := WaitForHealth("127.0.0.1:19999", 500)
assert.False(t, ok)
assertFalse(t, ok)
}
func TestWaitForReady_Reachable(t *testing.T) {
hs := NewHealthServer("127.0.0.1:0")
require.NoError(t, hs.Start())
requireNoError(t, hs.Start())
defer func() { _ = hs.Stop(context.Background()) }()
ok := WaitForReady(hs.Addr(), 2_000)
assert.True(t, ok)
assertTrue(t, ok)
}
func TestWaitForReady_Unreachable(t *testing.T) {
ok := WaitForReady("127.0.0.1:19999", 500)
assert.False(t, ok)
assertFalse(t, ok)
}
func TestHealthServer_StopMarksNotReady(t *testing.T) {
hs := NewHealthServer("127.0.0.1:0")
require.NoError(t, hs.Start())
requireNoError(t, hs.Start())
require.NotEmpty(t, hs.Addr())
assert.True(t, hs.Ready())
requireNotEmpty(t, hs.Addr())
assertTrue(t, hs.Ready())
require.NoError(t, hs.Stop(context.Background()))
requireNoError(t, hs.Stop(context.Background()))
assert.False(t, hs.Ready())
assert.NotEmpty(t, hs.Addr())
assertFalse(t, hs.Ready())
assertNotEmpty(t, hs.Addr())
}

View file

@ -5,72 +5,70 @@ import (
"testing"
"dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPIDFile_Acquire_Good(t *testing.T) {
pidPath := core.JoinPath(t.TempDir(), "test.pid")
pid := NewPIDFile(pidPath)
err := pid.Acquire()
require.NoError(t, err)
requireNoError(t, err)
data, err := os.ReadFile(pidPath)
require.NoError(t, err)
assert.NotEmpty(t, data)
requireNoError(t, err)
assertNotEmpty(t, data)
err = pid.Release()
require.NoError(t, err)
requireNoError(t, err)
_, err = os.Stat(pidPath)
assert.True(t, os.IsNotExist(err))
assertTrue(t, os.IsNotExist(err))
}
func TestPIDFile_AcquireStale_Good(t *testing.T) {
pidPath := core.JoinPath(t.TempDir(), "stale.pid")
require.NoError(t, os.WriteFile(pidPath, []byte("999999999"), 0644))
requireNoError(t, os.WriteFile(pidPath, []byte("999999999"), 0644))
pid := NewPIDFile(pidPath)
err := pid.Acquire()
require.NoError(t, err)
requireNoError(t, err)
err = pid.Release()
require.NoError(t, err)
requireNoError(t, err)
}
func TestPIDFile_CreateDirectory_Good(t *testing.T) {
pidPath := core.JoinPath(t.TempDir(), "subdir", "nested", "test.pid")
pid := NewPIDFile(pidPath)
err := pid.Acquire()
require.NoError(t, err)
requireNoError(t, err)
err = pid.Release()
require.NoError(t, err)
requireNoError(t, err)
}
func TestPIDFile_Path_Good(t *testing.T) {
pid := NewPIDFile("/tmp/test.pid")
assert.Equal(t, "/tmp/test.pid", pid.Path())
assertEqual(t, "/tmp/test.pid", pid.Path())
}
func TestPIDFile_Release_MissingIsNoop(t *testing.T) {
pidPath := core.JoinPath(t.TempDir(), "absent.pid")
pid := NewPIDFile(pidPath)
require.NoError(t, pid.Release())
requireNoError(t, pid.Release())
}
func TestReadPID_Missing_Bad(t *testing.T) {
pid, running := ReadPID("/nonexistent/path.pid")
assert.Equal(t, 0, pid)
assert.False(t, running)
assertEqual(t, 0, pid)
assertFalse(t, running)
}
func TestReadPID_Invalid_Bad(t *testing.T) {
path := core.JoinPath(t.TempDir(), "bad.pid")
require.NoError(t, os.WriteFile(path, []byte("notanumber"), 0644))
requireNoError(t, os.WriteFile(path, []byte("notanumber"), 0644))
pid, running := ReadPID(path)
assert.Equal(t, 0, pid)
assert.False(t, running)
assertEqual(t, 0, pid)
assertFalse(t, running)
}
func TestReadPID_Stale_Bad(t *testing.T) {
path := core.JoinPath(t.TempDir(), "stale.pid")
require.NoError(t, os.WriteFile(path, []byte("999999999"), 0644))
requireNoError(t, os.WriteFile(path, []byte("999999999"), 0644))
pid, running := ReadPID(path)
assert.Equal(t, 999999999, pid)
assert.False(t, running)
assertEqual(t, 999999999, pid)
assertFalse(t, running)
}

View file

@ -21,8 +21,6 @@ import (
corews "dappco.re/go/core/ws"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func init() {
@ -31,33 +29,33 @@ func init() {
func TestProcessProvider_Name_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
assert.Equal(t, "process", p.Name())
assertEqual(t, "process", p.Name())
}
func TestProcessProvider_BasePath_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
assert.Equal(t, "/api/process", p.BasePath())
assertEqual(t, "/api/process", p.BasePath())
}
func TestProcessProvider_Channels_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
channels := p.Channels()
assert.Contains(t, channels, "process.daemon.started")
assert.Contains(t, channels, "process.daemon.stopped")
assert.Contains(t, channels, "process.daemon.health")
assertContains(t, channels, "process.daemon.started")
assertContains(t, channels, "process.daemon.stopped")
assertContains(t, channels, "process.daemon.health")
}
func TestProcessProvider_Describe_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
descs := p.Describe()
assert.GreaterOrEqual(t, len(descs), 5)
assertGreaterOrEqual(t, len(descs), 5)
// Verify all descriptions have required fields
for _, d := range descs {
assert.NotEmpty(t, d.Method)
assert.NotEmpty(t, d.Path)
assert.NotEmpty(t, d.Summary)
assert.NotEmpty(t, d.Tags)
assertNotEmpty(t, d.Method)
assertNotEmpty(t, d.Path)
assertNotEmpty(t, d.Summary)
assertNotEmpty(t, d.Tags)
}
foundPipelineRoute := false
@ -70,8 +68,8 @@ func TestProcessProvider_Describe_Good(t *testing.T) {
foundSignalRoute = true
}
}
assert.True(t, foundPipelineRoute, "pipeline route should be described")
assert.True(t, foundSignalRoute, "signal route should be described")
assertTrue(t, foundPipelineRoute, "pipeline route should be described")
assertTrue(t, foundSignalRoute, "signal route should be described")
}
func TestProcessProvider_ListDaemons_Good(t *testing.T) {
@ -85,18 +83,18 @@ func TestProcessProvider_ListDaemons_Good(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/process/daemons", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[[]any]
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Success)
requireNoError(t, err)
assertTrue(t, resp.Success)
}
func TestProcessProvider_ListDaemons_BroadcastsStarted_Good(t *testing.T) {
dir := t.TempDir()
registry := newTestRegistry(dir)
require.NoError(t, registry.Register(process.DaemonEntry{
requireNoError(t, registry.Register(process.DaemonEntry{
Code: "test",
Daemon: "serve",
PID: os.Getpid(),
@ -114,7 +112,7 @@ func TestProcessProvider_ListDaemons_BroadcastsStarted_Good(t *testing.T) {
conn := connectWS(t, server.URL)
defer conn.Close()
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
return hub.ClientCount() == 1
}, time.Second, 10*time.Millisecond)
@ -123,16 +121,16 @@ func TestProcessProvider_ListDaemons_BroadcastsStarted_Good(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/process/daemons", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
events := readWSEvents(t, conn, "process.daemon.started")
started := events["process.daemon.started"]
require.NotNil(t, started)
requireNotNil(t, started)
startedData := started.Data.(map[string]any)
assert.Equal(t, "test", startedData["code"])
assert.Equal(t, "serve", startedData["daemon"])
assert.Equal(t, float64(os.Getpid()), startedData["pid"])
assertEqual(t, "test", startedData["code"])
assertEqual(t, "serve", startedData["daemon"])
assertEqual(t, float64(os.Getpid()), startedData["pid"])
}
func TestProcessProvider_GetDaemon_Bad(t *testing.T) {
@ -145,7 +143,7 @@ func TestProcessProvider_GetDaemon_Bad(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/process/daemons/test/nonexistent", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
assertEqual(t, http.StatusNotFound, w.Code)
}
func TestProcessProvider_HealthCheck_Bad(t *testing.T) {
@ -159,7 +157,7 @@ func TestProcessProvider_HealthCheck_Bad(t *testing.T) {
defer healthSrv.Close()
hostPort := strings.TrimPrefix(healthSrv.URL, "http://")
require.NoError(t, registry.Register(process.DaemonEntry{
requireNoError(t, registry.Register(process.DaemonEntry{
Code: "test",
Daemon: "broken",
PID: os.Getpid(),
@ -173,40 +171,40 @@ func TestProcessProvider_HealthCheck_Bad(t *testing.T) {
req, _ := http.NewRequest("GET", "/api/process/daemons/test/broken/health", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
assertEqual(t, http.StatusServiceUnavailable, w.Code)
var resp goapi.Response[map[string]any]
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
requireNoError(t, err)
requireTrue(t, resp.Success)
assert.Equal(t, false, resp.Data["healthy"])
assert.Equal(t, hostPort, resp.Data["address"])
assert.Equal(t, "upstream health check failed", resp.Data["reason"])
assertEqual(t, false, resp.Data["healthy"])
assertEqual(t, hostPort, resp.Data["address"])
assertEqual(t, "upstream health check failed", resp.Data["reason"])
}
func TestProcessProvider_RegistersAsRouteGroup_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
engine, err := goapi.New()
require.NoError(t, err)
requireNoError(t, err)
engine.Register(p)
assert.Len(t, engine.Groups(), 1)
assert.Equal(t, "process", engine.Groups()[0].Name())
assertLen(t, engine.Groups(), 1)
assertEqual(t, "process", engine.Groups()[0].Name())
}
func TestProcessProvider_Channels_RegisterAsStreamGroup_Good(t *testing.T) {
p := processapi.NewProvider(nil, nil, nil)
engine, err := goapi.New()
require.NoError(t, err)
requireNoError(t, err)
engine.Register(p)
// Engine.Channels() discovers StreamGroups
channels := engine.Channels()
assert.Contains(t, channels, "process.daemon.started")
assertContains(t, channels, "process.daemon.started")
}
func TestProcessProvider_RunPipeline_Good(t *testing.T) {
@ -224,19 +222,19 @@ func TestProcessProvider_RunPipeline_Good(t *testing.T) {
]
}`)
req, err := http.NewRequest("POST", "/api/process/pipelines/run", body)
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[process.RunAllResult]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Success)
assert.Equal(t, 2, resp.Data.Passed)
assert.Len(t, resp.Data.Results, 2)
requireNoError(t, err)
assertTrue(t, resp.Success)
assertEqual(t, 2, resp.Data.Passed)
assertLen(t, resp.Data.Results, 2)
}
func TestProcessProvider_RunPipeline_Unavailable(t *testing.T) {
@ -246,18 +244,18 @@ func TestProcessProvider_RunPipeline_Unavailable(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/pipelines/run", strings.NewReader(`{"mode":"all","specs":[]}`))
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
assertEqual(t, http.StatusServiceUnavailable, w.Code)
}
func TestProcessProvider_ListProcesses_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "echo", "hello-api")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
p := processapi.NewProvider(nil, svc, nil)
@ -265,28 +263,28 @@ func TestProcessProvider_ListProcesses_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/process/processes", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[[]process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
require.Len(t, resp.Data, 1)
assert.Equal(t, proc.ID, resp.Data[0].ID)
assert.Equal(t, "echo", resp.Data[0].Command)
requireNoError(t, err)
requireTrue(t, resp.Success)
requireLen(t, resp.Data, 1)
assertEqual(t, proc.ID, resp.Data[0].ID)
assertEqual(t, "echo", resp.Data[0].Command)
}
func TestProcessProvider_ListProcesses_RunningOnly_Good(t *testing.T) {
svc := newTestProcessService(t)
runningProc, err := svc.Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
exitedProc, err := svc.Start(context.Background(), "echo", "done")
require.NoError(t, err)
requireNoError(t, err)
<-exitedProc.Done()
p := processapi.NewProvider(nil, svc, nil)
@ -294,20 +292,20 @@ func TestProcessProvider_ListProcesses_RunningOnly_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/process/processes?runningOnly=true", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[[]process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
require.Len(t, resp.Data, 1)
assert.Equal(t, runningProc.ID, resp.Data[0].ID)
assert.Equal(t, process.StatusRunning, resp.Data[0].Status)
requireNoError(t, err)
requireTrue(t, resp.Success)
requireLen(t, resp.Data, 1)
assertEqual(t, runningProc.ID, resp.Data[0].ID)
assertEqual(t, process.StatusRunning, resp.Data[0].Status)
require.NoError(t, svc.Kill(runningProc.ID))
requireNoError(t, svc.Kill(runningProc.ID))
<-runningProc.Done()
}
@ -323,26 +321,26 @@ func TestProcessProvider_StartProcess_Good(t *testing.T) {
"killGroup": true
}`)
req, err := http.NewRequest("POST", "/api/process/processes", body)
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, "sleep", resp.Data.Command)
assert.Equal(t, process.StatusRunning, resp.Data.Status)
assert.True(t, resp.Data.Running)
assert.NotEmpty(t, resp.Data.ID)
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, "sleep", resp.Data.Command)
assertEqual(t, process.StatusRunning, resp.Data.Status)
assertTrue(t, resp.Data.Running)
assertNotEmpty(t, resp.Data.ID)
managed, err := svc.Get(resp.Data.ID)
require.NoError(t, err)
require.NoError(t, svc.Kill(managed.ID))
requireNoError(t, err)
requireNoError(t, svc.Kill(managed.ID))
select {
case <-managed.Done():
case <-time.After(5 * time.Second):
@ -360,25 +358,25 @@ func TestProcessProvider_RunProcess_Good(t *testing.T) {
"args": ["run-check"]
}`)
req, err := http.NewRequest("POST", "/api/process/processes/run", body)
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[string]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Contains(t, resp.Data, "run-check")
requireNoError(t, err)
requireTrue(t, resp.Success)
assertContains(t, resp.Data, "run-check")
}
func TestProcessProvider_GetProcess_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "echo", "single")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
p := processapi.NewProvider(nil, svc, nil)
@ -386,23 +384,23 @@ func TestProcessProvider_GetProcess_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/process/processes/"+proc.ID, nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, proc.ID, resp.Data.ID)
assert.Equal(t, "echo", resp.Data.Command)
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, proc.ID, resp.Data.ID)
assertEqual(t, "echo", resp.Data.Command)
}
func TestProcessProvider_GetProcessOutput_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "echo", "output-check")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
p := processapi.NewProvider(nil, svc, nil)
@ -410,97 +408,97 @@ func TestProcessProvider_GetProcessOutput_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/process/processes/"+proc.ID+"/output", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[string]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Contains(t, resp.Data, "output-check")
requireNoError(t, err)
requireTrue(t, resp.Success)
assertContains(t, resp.Data, "output-check")
}
func TestProcessProvider_WaitProcess_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "echo", "wait-check")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/wait", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[process.Info]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, proc.ID, resp.Data.ID)
assert.Equal(t, process.StatusExited, resp.Data.Status)
assert.Equal(t, 0, resp.Data.ExitCode)
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, proc.ID, resp.Data.ID)
assertEqual(t, process.StatusExited, resp.Data.Status)
assertEqual(t, 0, resp.Data.ExitCode)
}
func TestProcessProvider_WaitProcess_NonZeroExit_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 7")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/wait", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusConflict, w.Code)
assertEqual(t, http.StatusConflict, w.Code)
var resp goapi.Response[any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.False(t, resp.Success)
require.NotNil(t, resp.Error)
assert.Equal(t, "wait_failed", resp.Error.Code)
assert.Contains(t, resp.Error.Message, "process exited with code 7")
requireNoError(t, err)
requireFalse(t, resp.Success)
requireNotNil(t, resp.Error)
assertEqual(t, "wait_failed", resp.Error.Code)
assertContains(t, resp.Error.Message, "process exited with code 7")
details, ok := resp.Error.Details.(map[string]any)
require.True(t, ok)
assert.Equal(t, "exited", details["status"])
assert.Equal(t, float64(7), details["exitCode"])
assert.Equal(t, proc.ID, details["id"])
requireTrue(t, ok)
assertEqual(t, "exited", details["status"])
assertEqual(t, float64(7), details["exitCode"])
assertEqual(t, proc.ID, details["id"])
}
func TestProcessProvider_InputAndCloseStdin_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "cat")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
inputReq := strings.NewReader("{\"input\":\"hello-api\\n\"}")
inputHTTPReq, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/input", inputReq)
require.NoError(t, err)
requireNoError(t, err)
inputHTTPReq.Header.Set("Content-Type", "application/json")
inputResp := httptest.NewRecorder()
r.ServeHTTP(inputResp, inputHTTPReq)
assert.Equal(t, http.StatusOK, inputResp.Code)
assertEqual(t, http.StatusOK, inputResp.Code)
closeReq, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/close-stdin", nil)
require.NoError(t, err)
requireNoError(t, err)
closeResp := httptest.NewRecorder()
r.ServeHTTP(closeResp, closeReq)
assert.Equal(t, http.StatusOK, closeResp.Code)
assertEqual(t, http.StatusOK, closeResp.Code)
select {
case <-proc.Done():
@ -508,36 +506,36 @@ func TestProcessProvider_InputAndCloseStdin_Good(t *testing.T) {
t.Fatal("process should have exited after stdin was closed")
}
assert.Contains(t, proc.Output(), "hello-api")
assertContains(t, proc.Output(), "hello-api")
}
func TestProcessProvider_KillProcess_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/kill", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[map[string]any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, true, resp.Data["killed"])
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, true, resp.Data["killed"])
select {
case <-proc.Done():
case <-time.After(5 * time.Second):
t.Fatal("process should have been killed")
}
assert.Equal(t, process.StatusKilled, proc.Status)
assertEqual(t, process.StatusKilled, proc.Status)
}
func TestProcessProvider_KillProcess_ByPID_Good(t *testing.T) {
@ -546,7 +544,7 @@ func TestProcessProvider_KillProcess_ByPID_Good(t *testing.T) {
r := setupRouter(p)
cmd := exec.Command("sleep", "60")
require.NoError(t, cmd.Start())
requireNoError(t, cmd.Start())
waitCh := make(chan error, 1)
go func() {
@ -565,20 +563,20 @@ func TestProcessProvider_KillProcess_ByPID_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+strconv.Itoa(cmd.Process.Pid)+"/kill", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[map[string]any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, true, resp.Data["killed"])
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, true, resp.Data["killed"])
select {
case err := <-waitCh:
require.Error(t, err)
requireError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("unmanaged process should have been killed by PID")
}
@ -587,31 +585,31 @@ func TestProcessProvider_KillProcess_ByPID_Good(t *testing.T) {
func TestProcessProvider_SignalProcess_Good(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/signal", strings.NewReader(`{"signal":"SIGTERM"}`))
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[map[string]any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, true, resp.Data["signalled"])
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, true, resp.Data["signalled"])
select {
case <-proc.Done():
case <-time.After(5 * time.Second):
t.Fatal("process should have been signalled")
}
assert.Equal(t, process.StatusKilled, proc.Status)
assertEqual(t, process.StatusKilled, proc.Status)
}
func TestProcessProvider_SignalProcess_ByPID_Good(t *testing.T) {
@ -620,7 +618,7 @@ func TestProcessProvider_SignalProcess_ByPID_Good(t *testing.T) {
r := setupRouter(p)
cmd := exec.Command("sleep", "60")
require.NoError(t, cmd.Start())
requireNoError(t, cmd.Start())
waitCh := make(chan error, 1)
go func() {
@ -639,21 +637,21 @@ func TestProcessProvider_SignalProcess_ByPID_Good(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+strconv.Itoa(cmd.Process.Pid)+"/signal", strings.NewReader(`{"signal":"SIGTERM"}`))
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[map[string]any]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
require.True(t, resp.Success)
assert.Equal(t, true, resp.Data["signalled"])
requireNoError(t, err)
requireTrue(t, resp.Success)
assertEqual(t, true, resp.Data["signalled"])
select {
case err := <-waitCh:
require.Error(t, err)
requireError(t, err)
case <-time.After(5 * time.Second):
t.Fatal("unmanaged process should have been signalled by PID")
}
@ -662,21 +660,21 @@ func TestProcessProvider_SignalProcess_ByPID_Good(t *testing.T) {
func TestProcessProvider_SignalProcess_InvalidSignal_Bad(t *testing.T) {
svc := newTestProcessService(t)
proc, err := svc.Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
p := processapi.NewProvider(nil, svc, nil)
r := setupRouter(p)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/processes/"+proc.ID+"/signal", strings.NewReader(`{"signal":"NOPE"}`))
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.True(t, proc.IsRunning())
assertEqual(t, http.StatusBadRequest, w.Code)
assertTrue(t, proc.IsRunning())
require.NoError(t, svc.Kill(proc.ID))
requireNoError(t, svc.Kill(proc.ID))
<-proc.Done()
}
@ -695,35 +693,35 @@ func TestProcessProvider_BroadcastsProcessEvents_Good(t *testing.T) {
conn := connectWS(t, server.URL)
defer conn.Close()
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
return hub.ClientCount() == 1
}, time.Second, 10*time.Millisecond)
proc, err := svc.Start(context.Background(), "sh", "-c", "echo live-event")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
events := readWSEvents(t, conn, "process.started", "process.output", "process.exited")
started := events["process.started"]
require.NotNil(t, started)
requireNotNil(t, started)
startedData := started.Data.(map[string]any)
assert.Equal(t, proc.ID, startedData["id"])
assert.Equal(t, "sh", startedData["command"])
assert.Equal(t, float64(proc.Info().PID), startedData["pid"])
assertEqual(t, proc.ID, startedData["id"])
assertEqual(t, "sh", startedData["command"])
assertEqual(t, float64(proc.Info().PID), startedData["pid"])
output := events["process.output"]
require.NotNil(t, output)
requireNotNil(t, output)
outputData := output.Data.(map[string]any)
assert.Equal(t, proc.ID, outputData["id"])
assert.Equal(t, "stdout", outputData["stream"])
assert.Contains(t, outputData["line"], "live-event")
assertEqual(t, proc.ID, outputData["id"])
assertEqual(t, "stdout", outputData["stream"])
assertContains(t, outputData["line"], "live-event")
exited := events["process.exited"]
require.NotNil(t, exited)
requireNotNil(t, exited)
exitedData := exited.Data.(map[string]any)
assert.Equal(t, proc.ID, exitedData["id"])
assert.Equal(t, float64(0), exitedData["exitCode"])
assertEqual(t, proc.ID, exitedData["id"])
assertEqual(t, float64(0), exitedData["exitCode"])
}
func TestProcessProvider_BroadcastsKilledEvents_Good(t *testing.T) {
@ -741,14 +739,14 @@ func TestProcessProvider_BroadcastsKilledEvents_Good(t *testing.T) {
conn := connectWS(t, server.URL)
defer conn.Close()
require.Eventually(t, func() bool {
requireEventually(t, func() bool {
return hub.ClientCount() == 1
}, time.Second, 10*time.Millisecond)
proc, err := svc.Start(context.Background(), "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
require.NoError(t, svc.Kill(proc.ID))
requireNoError(t, svc.Kill(proc.ID))
select {
case <-proc.Done():
@ -759,17 +757,17 @@ func TestProcessProvider_BroadcastsKilledEvents_Good(t *testing.T) {
events := readWSEvents(t, conn, "process.killed", "process.exited")
killed := events["process.killed"]
require.NotNil(t, killed)
requireNotNil(t, killed)
killedData := killed.Data.(map[string]any)
assert.Equal(t, proc.ID, killedData["id"])
assert.Equal(t, "SIGKILL", killedData["signal"])
assert.Equal(t, float64(-1), killedData["exitCode"])
assertEqual(t, proc.ID, killedData["id"])
assertEqual(t, "SIGKILL", killedData["signal"])
assertEqual(t, float64(-1), killedData["exitCode"])
exited := events["process.exited"]
require.NotNil(t, exited)
requireNotNil(t, exited)
exitedData := exited.Data.(map[string]any)
assert.Equal(t, proc.ID, exitedData["id"])
assert.Equal(t, float64(-1), exitedData["exitCode"])
assertEqual(t, proc.ID, exitedData["id"])
assertEqual(t, float64(-1), exitedData["exitCode"])
}
func TestProcessProvider_ProcessRoutes_Unavailable(t *testing.T) {
@ -797,9 +795,9 @@ func TestProcessProvider_ProcessRoutes_Unavailable(t *testing.T) {
method = "POST"
}
req, err := http.NewRequest(method, path, nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusServiceUnavailable, w.Code)
assertEqual(t, http.StatusServiceUnavailable, w.Code)
}
}
@ -809,23 +807,23 @@ func TestProcessProvider_RFCListAlias_Good(t *testing.T) {
r := setupRouter(p)
proc, err := svc.Start(context.Background(), "sleep", "0.1")
require.NoError(t, err)
requireNoError(t, err)
t.Cleanup(func() {
_ = svc.Kill(proc.ID)
})
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", "/api/process/process/list?runningOnly=true", nil)
require.NoError(t, err)
requireNoError(t, err)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[[]string]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Success)
assert.Contains(t, resp.Data, proc.ID)
requireNoError(t, err)
assertTrue(t, resp.Success)
assertContains(t, resp.Data, proc.ID)
}
func TestProcessProvider_RFCStartAlias_Good(t *testing.T) {
@ -836,20 +834,20 @@ func TestProcessProvider_RFCStartAlias_Good(t *testing.T) {
body := strings.NewReader(`{"command":"sleep","args":["0.1"]}`)
w := httptest.NewRecorder()
req, err := http.NewRequest("POST", "/api/process/process/start", body)
require.NoError(t, err)
requireNoError(t, err)
req.Header.Set("Content-Type", "application/json")
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
assertEqual(t, http.StatusOK, w.Code)
var resp goapi.Response[string]
err = json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.True(t, resp.Success)
assert.NotEmpty(t, resp.Data)
requireNoError(t, err)
assertTrue(t, resp.Success)
assertNotEmpty(t, resp.Data)
proc, err := svc.Get(resp.Data)
require.NoError(t, err)
requireNoError(t, err)
select {
case <-proc.Done():
@ -857,8 +855,8 @@ func TestProcessProvider_RFCStartAlias_Good(t *testing.T) {
t.Fatal("RFC alias start should detach from the HTTP request context")
}
assert.Equal(t, process.StatusExited, proc.Status)
assert.Equal(t, 0, proc.ExitCode)
assertEqual(t, process.StatusExited, proc.Status)
assertEqual(t, 0, proc.ExitCode)
}
// -- Test helpers -------------------------------------------------------------
@ -880,7 +878,7 @@ func newTestProcessService(t *testing.T) *process.Service {
c := core.New()
factory := process.NewService(process.Options{})
raw, err := factory(c)
require.NoError(t, err)
requireNoError(t, err)
return raw.(*process.Service)
}
@ -890,7 +888,7 @@ func connectWS(t *testing.T, serverURL string) *websocket.Conn {
wsURL := "ws" + strings.TrimPrefix(serverURL, "http")
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
require.NoError(t, err)
requireNoError(t, err)
return conn
}
@ -906,10 +904,10 @@ func readWSEvents(t *testing.T, conn *websocket.Conn, channels ...string) map[st
deadline := time.Now().Add(3 * time.Second)
for len(events) < len(channels) && time.Now().Before(deadline) {
require.NoError(t, conn.SetReadDeadline(time.Now().Add(500*time.Millisecond)))
requireNoError(t, conn.SetReadDeadline(time.Now().Add(500*time.Millisecond)))
_, payload, err := conn.ReadMessage()
require.NoError(t, err)
requireNoError(t, err)
for _, line := range strings.Split(strings.TrimSpace(string(payload)), "\n") {
if strings.TrimSpace(line) == "" {
@ -917,7 +915,7 @@ func readWSEvents(t *testing.T, conn *websocket.Conn, channels ...string) map[st
}
var msg corews.Message
require.NoError(t, json.Unmarshal([]byte(line), &msg))
requireNoError(t, json.Unmarshal([]byte(line), &msg))
if _, ok := want[msg.Channel]; ok {
events[msg.Channel] = msg
@ -925,6 +923,6 @@ func readWSEvents(t *testing.T, conn *websocket.Conn, channels ...string) map[st
}
}
require.Len(t, events, len(channels))
requireLen(t, events, len(channels))
return events
}

View file

@ -6,9 +6,6 @@ import (
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var _ *Process = (*ManagedProcess)(nil)
@ -17,18 +14,18 @@ func TestProcess_Info(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "hello")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
info := proc.Info()
assert.Equal(t, proc.ID, info.ID)
assert.Equal(t, "echo", info.Command)
assert.Equal(t, []string{"hello"}, info.Args)
assert.False(t, info.Running)
assert.Equal(t, StatusExited, info.Status)
assert.Equal(t, 0, info.ExitCode)
assert.Greater(t, info.Duration, time.Duration(0))
assertEqual(t, proc.ID, info.ID)
assertEqual(t, "echo", info.Command)
assertEqual(t, []string{"hello"}, info.Args)
assertFalse(t, info.Running)
assertEqual(t, StatusExited, info.Status)
assertEqual(t, 0, info.ExitCode)
assertGreater(t, info.Duration, time.Duration(0))
}
func TestProcess_Info_Pending(t *testing.T) {
@ -39,8 +36,8 @@ func TestProcess_Info_Pending(t *testing.T) {
}
info := proc.Info()
assert.Equal(t, StatusPending, info.Status)
assert.False(t, info.Running)
assertEqual(t, StatusPending, info.Status)
assertFalse(t, info.Running)
}
func TestProcess_Info_RunningDuration(t *testing.T) {
@ -50,13 +47,13 @@ func TestProcess_Info_RunningDuration(t *testing.T) {
defer cancel()
proc, err := svc.Start(ctx, "sleep", "10")
require.NoError(t, err)
requireNoError(t, err)
time.Sleep(10 * time.Millisecond)
info := proc.Info()
assert.True(t, info.Running)
assert.Equal(t, StatusRunning, info.Status)
assert.Greater(t, info.Duration, time.Duration(0))
assertTrue(t, info.Running)
assertEqual(t, StatusRunning, info.Status)
assertGreater(t, info.Duration, time.Duration(0))
cancel()
<-proc.Done()
@ -66,17 +63,17 @@ func TestProcess_InfoSnapshot(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "snapshot")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
info := proc.Info()
require.NotEmpty(t, info.Args)
requireNotEmpty(t, info.Args)
info.Args[0] = "mutated"
assert.Equal(t, "snapshot", proc.Args[0])
assert.Equal(t, "mutated", info.Args[0])
assertEqual(t, "snapshot", proc.Args[0])
assertEqual(t, "mutated", info.Args[0])
}
func TestProcess_Output(t *testing.T) {
@ -84,25 +81,25 @@ func TestProcess_Output(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "hello world")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
output := proc.Output()
assert.Contains(t, output, "hello world")
assertContains(t, output, "hello world")
})
t.Run("OutputBytes returns copy", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "test")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
bytes := proc.OutputBytes()
assert.NotNil(t, bytes)
assert.Contains(t, string(bytes), "test")
assertNotNil(t, bytes)
assertContains(t, string(bytes), "test")
})
}
@ -114,27 +111,27 @@ func TestProcess_IsRunning(t *testing.T) {
defer cancel()
proc, err := svc.Start(ctx, "sleep", "10")
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, proc.IsRunning())
assert.True(t, proc.Info().Running)
assertTrue(t, proc.IsRunning())
assertTrue(t, proc.Info().Running)
cancel()
<-proc.Done()
assert.False(t, proc.IsRunning())
assert.False(t, proc.Info().Running)
assertFalse(t, proc.IsRunning())
assertFalse(t, proc.Info().Running)
})
t.Run("false after completion", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "done")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
assert.False(t, proc.IsRunning())
assertFalse(t, proc.IsRunning())
})
}
@ -143,20 +140,20 @@ func TestProcess_Wait(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "ok")
require.NoError(t, err)
requireNoError(t, err)
err = proc.Wait()
assert.NoError(t, err)
assertNoError(t, err)
})
t.Run("returns error on failure", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "sh", "-c", "exit 1")
require.NoError(t, err)
requireNoError(t, err)
err = proc.Wait()
assert.Error(t, err)
assertError(t, err)
})
}
@ -165,7 +162,7 @@ func TestProcess_Done(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "test")
require.NoError(t, err)
requireNoError(t, err)
select {
case <-proc.Done():
@ -184,12 +181,12 @@ func TestProcess_Kill(t *testing.T) {
defer cancel()
proc, err := svc.Start(ctx, "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, proc.IsRunning())
assertTrue(t, proc.IsRunning())
err = proc.Kill()
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -198,19 +195,19 @@ func TestProcess_Kill(t *testing.T) {
t.Fatal("process should have been killed")
}
assert.Equal(t, StatusKilled, proc.Status)
assertEqual(t, StatusKilled, proc.Status)
})
t.Run("noop on completed process", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "done")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
err = proc.Kill()
assert.NoError(t, err)
assertNoError(t, err)
})
}
@ -220,29 +217,29 @@ func TestProcess_SendInput(t *testing.T) {
// Use cat to echo back stdin
proc, err := svc.Start(context.Background(), "cat")
require.NoError(t, err)
requireNoError(t, err)
err = proc.SendInput("hello\n")
assert.NoError(t, err)
assertNoError(t, err)
err = proc.CloseStdin()
assert.NoError(t, err)
assertNoError(t, err)
<-proc.Done()
assert.Contains(t, proc.Output(), "hello")
assertContains(t, proc.Output(), "hello")
})
t.Run("error on completed process", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "done")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
err = proc.SendInput("test")
assert.ErrorIs(t, err, ErrProcessNotRunning)
assertErrorIs(t, err, ErrProcessNotRunning)
})
}
@ -254,10 +251,10 @@ func TestProcess_Signal(t *testing.T) {
defer cancel()
proc, err := svc.Start(ctx, "sleep", "60")
require.NoError(t, err)
requireNoError(t, err)
err = proc.Signal(os.Interrupt)
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -266,18 +263,18 @@ func TestProcess_Signal(t *testing.T) {
t.Fatal("process should have been terminated by signal")
}
assert.Equal(t, StatusKilled, proc.Status)
assertEqual(t, StatusKilled, proc.Status)
})
t.Run("error on completed process", func(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "echo", "done")
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
err = proc.Signal(os.Interrupt)
assert.ErrorIs(t, err, ErrProcessNotRunning)
assertErrorIs(t, err, ErrProcessNotRunning)
})
t.Run("signals process group when kill group is enabled", func(t *testing.T) {
@ -289,10 +286,10 @@ func TestProcess_Signal(t *testing.T) {
Detach: true,
KillGroup: true,
})
require.NoError(t, err)
requireNoError(t, err)
err = proc.Signal(os.Interrupt)
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -311,16 +308,16 @@ func TestProcess_Signal(t *testing.T) {
Detach: true,
KillGroup: true,
})
require.NoError(t, err)
requireNoError(t, err)
err = proc.Signal(syscall.Signal(0))
assert.NoError(t, err)
assertNoError(t, err)
time.Sleep(300 * time.Millisecond)
assert.True(t, proc.IsRunning())
assertTrue(t, proc.IsRunning())
err = proc.Kill()
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -335,10 +332,10 @@ func TestProcess_CloseStdin(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "cat")
require.NoError(t, err)
requireNoError(t, err)
err = proc.CloseStdin()
assert.NoError(t, err)
assertNoError(t, err)
// Process should exit now that stdin is closed
select {
@ -353,17 +350,17 @@ func TestProcess_CloseStdin(t *testing.T) {
svc, _ := newTestService(t)
proc, err := svc.Start(context.Background(), "cat")
require.NoError(t, err)
requireNoError(t, err)
// First close
err = proc.CloseStdin()
assert.NoError(t, err)
assertNoError(t, err)
<-proc.Done()
// Second close should be safe (stdin already nil)
err = proc.CloseStdin()
assert.NoError(t, err)
assertNoError(t, err)
})
}
@ -376,7 +373,7 @@ func TestProcess_Timeout(t *testing.T) {
Args: []string{"60"},
Timeout: 200 * time.Millisecond,
})
require.NoError(t, err)
requireNoError(t, err)
select {
case <-proc.Done():
@ -385,8 +382,8 @@ func TestProcess_Timeout(t *testing.T) {
t.Fatal("process should have been killed by timeout")
}
assert.False(t, proc.IsRunning())
assert.Equal(t, StatusKilled, proc.Status)
assertFalse(t, proc.IsRunning())
assertEqual(t, StatusKilled, proc.Status)
})
t.Run("no timeout when zero", func(t *testing.T) {
@ -397,10 +394,10 @@ func TestProcess_Timeout(t *testing.T) {
Args: []string{"fast"},
Timeout: 0,
})
require.NoError(t, err)
requireNoError(t, err)
<-proc.Done()
assert.Equal(t, 0, proc.ExitCode)
assertEqual(t, 0, proc.ExitCode)
})
}
@ -414,12 +411,12 @@ func TestProcess_Shutdown(t *testing.T) {
Args: []string{"60"},
GracePeriod: 100 * time.Millisecond,
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, proc.IsRunning())
assertTrue(t, proc.IsRunning())
err = proc.Shutdown()
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -428,7 +425,7 @@ func TestProcess_Shutdown(t *testing.T) {
t.Fatal("shutdown should have completed")
}
assert.Equal(t, StatusKilled, proc.Status)
assertEqual(t, StatusKilled, proc.Status)
})
t.Run("immediate kill without grace period", func(t *testing.T) {
@ -438,10 +435,10 @@ func TestProcess_Shutdown(t *testing.T) {
Command: "sleep",
Args: []string{"60"},
})
require.NoError(t, err)
requireNoError(t, err)
err = proc.Shutdown()
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -463,13 +460,13 @@ func TestProcess_KillGroup(t *testing.T) {
Detach: true,
KillGroup: true,
})
require.NoError(t, err)
requireNoError(t, err)
// Give child time to spawn
time.Sleep(100 * time.Millisecond)
err = proc.Kill()
assert.NoError(t, err)
assertNoError(t, err)
select {
case <-proc.Done():
@ -478,7 +475,7 @@ func TestProcess_KillGroup(t *testing.T) {
t.Fatal("process group should have been killed")
}
assert.Equal(t, StatusKilled, proc.Status)
assertEqual(t, StatusKilled, proc.Status)
})
}
@ -492,7 +489,7 @@ func TestProcess_TimeoutWithGrace(t *testing.T) {
Timeout: 200 * time.Millisecond,
GracePeriod: 100 * time.Millisecond,
})
require.NoError(t, err)
requireNoError(t, err)
select {
case <-proc.Done():
@ -501,6 +498,6 @@ func TestProcess_TimeoutWithGrace(t *testing.T) {
t.Fatal("process should have been killed by timeout")
}
assert.Equal(t, StatusKilled, proc.Status)
assertEqual(t, StatusKilled, proc.Status)
})
}

View file

@ -2,14 +2,12 @@ package process_test
import (
"context"
"errors"
"os/exec"
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
process "dappco.re/go/core/process"
)
@ -22,60 +20,94 @@ func testCtx(t *testing.T) context.Context {
func TestProgram_Find_KnownBinary(t *testing.T) {
p := &process.Program{Name: "echo"}
require.NoError(t, p.Find())
assert.NotEmpty(t, p.Path)
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p.Path == "" {
t.Fatal("expected non-empty path")
}
}
func TestProgram_Find_UnknownBinary(t *testing.T) {
p := &process.Program{Name: "no-such-binary-xyzzy-42"}
err := p.Find()
require.Error(t, err)
assert.ErrorIs(t, err, process.ErrProgramNotFound)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, process.ErrProgramNotFound) {
t.Fatalf("expected ErrProgramNotFound, got %v", err)
}
}
func TestProgram_Find_UsesExistingPath(t *testing.T) {
path, err := exec.LookPath("echo")
require.NoError(t, err)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
p := &process.Program{Path: path}
require.NoError(t, p.Find())
assert.Equal(t, path, p.Path)
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p.Path != path {
t.Fatalf("want %v, got %v", path, p.Path)
}
}
func TestProgram_Find_PrefersExistingPathOverName(t *testing.T) {
path, err := exec.LookPath("echo")
require.NoError(t, err)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
p := &process.Program{
Name: "no-such-binary-xyzzy-42",
Path: path,
}
require.NoError(t, p.Find())
assert.Equal(t, path, p.Path)
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if p.Path != path {
t.Fatalf("want %v, got %v", path, p.Path)
}
}
func TestProgram_Find_EmptyName(t *testing.T) {
p := &process.Program{}
require.Error(t, p.Find())
if err := p.Find(); err == nil {
t.Fatal("expected error, got nil")
}
}
func TestProgram_Run_ReturnsOutput(t *testing.T) {
p := &process.Program{Name: "echo"}
require.NoError(t, p.Find())
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out, err := p.Run(testCtx(t), "hello")
require.NoError(t, err)
assert.Equal(t, "hello", out)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != "hello" {
t.Fatalf("want %q, got %q", "hello", out)
}
}
func TestProgram_Run_PreservesLeadingWhitespace(t *testing.T) {
p := &process.Program{Name: "sh"}
require.NoError(t, p.Find())
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
out, err := p.Run(testCtx(t), "-c", "printf ' hello \n'")
require.NoError(t, err)
assert.Equal(t, " hello", out)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != " hello" {
t.Fatalf("want %q, got %q", " hello", out)
}
}
func TestProgram_Run_WithoutFind_FallsBackToName(t *testing.T) {
@ -83,46 +115,72 @@ func TestProgram_Run_WithoutFind_FallsBackToName(t *testing.T) {
p := &process.Program{Name: "echo"}
out, err := p.Run(testCtx(t), "fallback")
require.NoError(t, err)
assert.Equal(t, "fallback", out)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if out != "fallback" {
t.Fatalf("want %q, got %q", "fallback", out)
}
}
func TestProgram_RunDir_UsesDirectory(t *testing.T) {
p := &process.Program{Name: "pwd"}
require.NoError(t, p.Find())
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
dir := t.TempDir()
out, err := p.RunDir(testCtx(t), dir)
require.NoError(t, err)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Resolve symlinks on both sides for portability (macOS uses /private/ prefix).
canonicalDir, err := filepath.EvalSymlinks(dir)
require.NoError(t, err)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
canonicalOut, err := filepath.EvalSymlinks(out)
require.NoError(t, err)
assert.Equal(t, canonicalDir, canonicalOut)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if canonicalDir != canonicalOut {
t.Fatalf("want %q, got %q", canonicalDir, canonicalOut)
}
}
func TestProgram_Run_FailingCommand(t *testing.T) {
p := &process.Program{Name: "false"}
require.NoError(t, p.Find())
if err := p.Find(); err != nil {
t.Fatalf("unexpected error: %v", err)
}
_, err := p.Run(testCtx(t))
require.Error(t, err)
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestProgram_Run_NilContextRejected(t *testing.T) {
p := &process.Program{Name: "echo"}
_, err := p.Run(nil, "test")
require.Error(t, err)
assert.ErrorIs(t, err, process.ErrProgramContextRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, process.ErrProgramContextRequired) {
t.Fatalf("expected ErrProgramContextRequired, got %v", err)
}
}
func TestProgram_RunDir_EmptyNameRejected(t *testing.T) {
p := &process.Program{}
_, err := p.RunDir(testCtx(t), "", "test")
require.Error(t, err)
assert.ErrorIs(t, err, process.ErrProgramNameRequired)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, process.ErrProgramNameRequired) {
t.Fatalf("expected ErrProgramNameRequired, got %v", err)
}
}

View file

@ -5,9 +5,6 @@ import (
"path/filepath"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRegistry_RegisterAndGet(t *testing.T) {
@ -26,17 +23,17 @@ func TestRegistry_RegisterAndGet(t *testing.T) {
}
err := reg.Register(entry)
require.NoError(t, err)
requireNoError(t, err)
got, ok := reg.Get("myapp", "worker")
require.True(t, ok)
assert.Equal(t, "myapp", got.Code)
assert.Equal(t, "worker", got.Daemon)
assert.Equal(t, os.Getpid(), got.PID)
assert.Equal(t, "healthy", got.Health)
assert.Equal(t, "test-project", got.Project)
assert.Equal(t, "/usr/bin/worker", got.Binary)
assert.Equal(t, started, got.Started)
requireTrue(t, ok)
assertEqual(t, "myapp", got.Code)
assertEqual(t, "worker", got.Daemon)
assertEqual(t, os.Getpid(), got.PID)
assertEqual(t, "healthy", got.Health)
assertEqual(t, "test-project", got.Project)
assertEqual(t, "/usr/bin/worker", got.Binary)
assertEqual(t, started, got.Started)
}
func TestRegistry_Unregister(t *testing.T) {
@ -50,19 +47,19 @@ func TestRegistry_Unregister(t *testing.T) {
}
err := reg.Register(entry)
require.NoError(t, err)
requireNoError(t, err)
// File should exist
path := filepath.Join(dir, "myapp-server.json")
_, err = os.Stat(path)
require.NoError(t, err)
requireNoError(t, err)
err = reg.Unregister("myapp", "server")
require.NoError(t, err)
requireNoError(t, err)
// File should be gone
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
assertTrue(t, os.IsNotExist(err))
}
func TestRegistry_UnregisterMissingIsNoop(t *testing.T) {
@ -70,7 +67,7 @@ func TestRegistry_UnregisterMissingIsNoop(t *testing.T) {
reg := NewRegistry(dir)
err := reg.Unregister("missing", "entry")
require.NoError(t, err)
requireNoError(t, err)
}
func TestRegistry_List(t *testing.T) {
@ -78,15 +75,15 @@ func TestRegistry_List(t *testing.T) {
reg := NewRegistry(dir)
err := reg.Register(DaemonEntry{Code: "app1", Daemon: "web", PID: os.Getpid()})
require.NoError(t, err)
requireNoError(t, err)
err = reg.Register(DaemonEntry{Code: "app2", Daemon: "api", PID: os.Getpid()})
require.NoError(t, err)
requireNoError(t, err)
entries, err := reg.List()
require.NoError(t, err)
require.Len(t, entries, 2)
assert.Equal(t, "app1", entries[0].Code)
assert.Equal(t, "app2", entries[1].Code)
requireNoError(t, err)
requireLen(t, entries, 2)
assertEqual(t, "app1", entries[0].Code)
assertEqual(t, "app2", entries[1].Code)
}
func TestRegistry_List_PrunesStale(t *testing.T) {
@ -94,20 +91,20 @@ func TestRegistry_List_PrunesStale(t *testing.T) {
reg := NewRegistry(dir)
err := reg.Register(DaemonEntry{Code: "dead", Daemon: "proc", PID: 999999999})
require.NoError(t, err)
requireNoError(t, err)
// File should exist before listing
path := filepath.Join(dir, "dead-proc.json")
_, err = os.Stat(path)
require.NoError(t, err)
requireNoError(t, err)
entries, err := reg.List()
require.NoError(t, err)
assert.Empty(t, entries)
requireNoError(t, err)
assertEmpty(t, entries)
// Stale file should be removed
_, err = os.Stat(path)
assert.True(t, os.IsNotExist(err))
assertTrue(t, os.IsNotExist(err))
}
func TestRegistry_Get_NotFound(t *testing.T) {
@ -115,8 +112,8 @@ func TestRegistry_Get_NotFound(t *testing.T) {
reg := NewRegistry(dir)
got, ok := reg.Get("nope", "missing")
assert.Nil(t, got)
assert.False(t, ok)
assertNil(t, got)
assertFalse(t, ok)
}
func TestRegistry_CreatesDirectory(t *testing.T) {
@ -124,14 +121,14 @@ func TestRegistry_CreatesDirectory(t *testing.T) {
reg := NewRegistry(dir)
err := reg.Register(DaemonEntry{Code: "app", Daemon: "srv", PID: os.Getpid()})
require.NoError(t, err)
requireNoError(t, err)
info, err := os.Stat(dir)
require.NoError(t, err)
assert.True(t, info.IsDir())
requireNoError(t, err)
assertTrue(t, info.IsDir())
}
func TestDefaultRegistry(t *testing.T) {
reg := DefaultRegistry()
assert.NotNil(t, reg)
assertNotNil(t, reg)
}

View file

@ -5,8 +5,6 @@ import (
"testing"
framework "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestRunner(t *testing.T) *Runner {
@ -15,7 +13,7 @@ func newTestRunner(t *testing.T) *Runner {
c := framework.New()
factory := NewService(Options{})
raw, err := factory(c)
require.NoError(t, err)
requireNoError(t, err)
return NewRunner(raw.(*Service))
}
@ -29,12 +27,12 @@ func TestRunner_RunSequential(t *testing.T) {
{Name: "second", Command: "echo", Args: []string{"2"}},
{Name: "third", Command: "echo", Args: []string{"3"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 3, result.Passed)
assert.Equal(t, 0, result.Failed)
assert.Equal(t, 0, result.Skipped)
assertTrue(t, result.Success())
assertEqual(t, 3, result.Passed)
assertEqual(t, 0, result.Failed)
assertEqual(t, 0, result.Skipped)
})
t.Run("stops on failure", func(t *testing.T) {
@ -45,18 +43,18 @@ func TestRunner_RunSequential(t *testing.T) {
{Name: "fails", Command: "sh", Args: []string{"-c", "exit 1"}},
{Name: "third", Command: "echo", Args: []string{"3"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.False(t, result.Success())
assert.Equal(t, 1, result.Passed)
assert.Equal(t, 1, result.Failed)
assert.Equal(t, 1, result.Skipped)
require.Len(t, result.Results, 3)
assert.Equal(t, 0, result.Results[0].ExitCode)
assert.NoError(t, result.Results[0].Error)
assert.Equal(t, 1, result.Results[1].ExitCode)
assert.NoError(t, result.Results[1].Error)
assert.True(t, result.Results[2].Skipped)
assertFalse(t, result.Success())
assertEqual(t, 1, result.Passed)
assertEqual(t, 1, result.Failed)
assertEqual(t, 1, result.Skipped)
requireLen(t, result.Results, 3)
assertEqual(t, 0, result.Results[0].ExitCode)
assertNoError(t, result.Results[0].Error)
assertEqual(t, 1, result.Results[1].ExitCode)
assertNoError(t, result.Results[1].Error)
assertTrue(t, result.Results[2].Skipped)
})
t.Run("allow failure continues", func(t *testing.T) {
@ -67,12 +65,12 @@ func TestRunner_RunSequential(t *testing.T) {
{Name: "fails", Command: "sh", Args: []string{"-c", "exit 1"}, AllowFailure: true},
{Name: "third", Command: "echo", Args: []string{"3"}},
})
require.NoError(t, err)
requireNoError(t, err)
// Still counts as failed but pipeline continues
assert.Equal(t, 2, result.Passed)
assert.Equal(t, 1, result.Failed)
assert.Equal(t, 0, result.Skipped)
assertEqual(t, 2, result.Passed)
assertEqual(t, 1, result.Failed)
assertEqual(t, 0, result.Skipped)
})
}
@ -85,11 +83,11 @@ func TestRunner_RunParallel(t *testing.T) {
{Name: "second", Command: "echo", Args: []string{"2"}},
{Name: "third", Command: "echo", Args: []string{"3"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 3, result.Passed)
assert.Len(t, result.Results, 3)
assertTrue(t, result.Success())
assertEqual(t, 3, result.Passed)
assertLen(t, result.Results, 3)
})
t.Run("failure doesnt stop others", func(t *testing.T) {
@ -100,11 +98,11 @@ func TestRunner_RunParallel(t *testing.T) {
{Name: "fails", Command: "sh", Args: []string{"-c", "exit 1"}},
{Name: "third", Command: "echo", Args: []string{"3"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.False(t, result.Success())
assert.Equal(t, 2, result.Passed)
assert.Equal(t, 1, result.Failed)
assertFalse(t, result.Success())
assertEqual(t, 2, result.Passed)
assertEqual(t, 1, result.Failed)
})
}
@ -117,10 +115,10 @@ func TestRunner_RunAll(t *testing.T) {
{Name: "first", Command: "echo", Args: []string{"1"}},
{Name: "second", Command: "echo", Args: []string{"2"}, After: []string{"first"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 3, result.Passed)
assertTrue(t, result.Success())
assertEqual(t, 3, result.Passed)
})
t.Run("skips dependents on failure", func(t *testing.T) {
@ -131,12 +129,12 @@ func TestRunner_RunAll(t *testing.T) {
{Name: "second", Command: "echo", Args: []string{"2"}, After: []string{"first"}},
{Name: "third", Command: "echo", Args: []string{"3"}, After: []string{"second"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.False(t, result.Success())
assert.Equal(t, 0, result.Passed)
assert.Equal(t, 1, result.Failed)
assert.Equal(t, 2, result.Skipped)
assertFalse(t, result.Success())
assertEqual(t, 0, result.Passed)
assertEqual(t, 1, result.Failed)
assertEqual(t, 2, result.Skipped)
})
t.Run("parallel independent specs", func(t *testing.T) {
@ -149,10 +147,10 @@ func TestRunner_RunAll(t *testing.T) {
{Name: "c", Command: "echo", Args: []string{"c"}},
{Name: "final", Command: "echo", Args: []string{"done"}, After: []string{"a", "b", "c"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 4, result.Passed)
assertTrue(t, result.Success())
assertEqual(t, 4, result.Passed)
})
t.Run("preserves input order", func(t *testing.T) {
@ -165,11 +163,11 @@ func TestRunner_RunAll(t *testing.T) {
}
result, err := runner.RunAll(context.Background(), specs)
require.NoError(t, err)
requireNoError(t, err)
require.Len(t, result.Results, len(specs))
requireLen(t, result.Results, len(specs))
for i, res := range result.Results {
assert.Equal(t, specs[i].Name, res.Name)
assertEqual(t, specs[i].Name, res.Name)
}
})
}
@ -182,15 +180,15 @@ func TestRunner_RunAll_CircularDeps(t *testing.T) {
{Name: "a", Command: "echo", Args: []string{"a"}, After: []string{"b"}},
{Name: "b", Command: "echo", Args: []string{"b"}, After: []string{"a"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 0, result.Failed)
assert.Equal(t, 2, result.Skipped)
assertTrue(t, result.Success())
assertEqual(t, 0, result.Failed)
assertEqual(t, 2, result.Skipped)
for _, res := range result.Results {
assert.True(t, res.Skipped)
assert.Equal(t, 0, res.ExitCode)
assert.Error(t, res.Error)
assertTrue(t, res.Skipped)
assertEqual(t, 0, res.ExitCode)
assertError(t, res.Error)
}
})
@ -200,15 +198,15 @@ func TestRunner_RunAll_CircularDeps(t *testing.T) {
result, err := runner.RunAll(context.Background(), []RunSpec{
{Name: "a", Command: "echo", Args: []string{"a"}, After: []string{"missing"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.True(t, result.Success())
assert.Equal(t, 0, result.Failed)
assert.Equal(t, 1, result.Skipped)
require.Len(t, result.Results, 1)
assert.True(t, result.Results[0].Skipped)
assert.Equal(t, 0, result.Results[0].ExitCode)
assert.Error(t, result.Results[0].Error)
assertTrue(t, result.Success())
assertEqual(t, 0, result.Failed)
assertEqual(t, 1, result.Skipped)
requireLen(t, result.Results, 1)
assertTrue(t, result.Results[0].Skipped)
assertEqual(t, 0, result.Results[0].ExitCode)
assertError(t, result.Results[0].Error)
})
}
@ -223,17 +221,17 @@ func TestRunner_ContextCancellation(t *testing.T) {
{Name: "first", Command: "echo", Args: []string{"1"}},
{Name: "second", Command: "echo", Args: []string{"2"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.Equal(t, 0, result.Passed)
assert.Equal(t, 0, result.Failed)
assert.Equal(t, 2, result.Skipped)
require.Len(t, result.Results, 2)
assertEqual(t, 0, result.Passed)
assertEqual(t, 0, result.Failed)
assertEqual(t, 2, result.Skipped)
requireLen(t, result.Results, 2)
for _, res := range result.Results {
assert.True(t, res.Skipped)
assert.Equal(t, 1, res.ExitCode)
assert.Error(t, res.Error)
assert.Contains(t, res.Error.Error(), "context canceled")
assertTrue(t, res.Skipped)
assertEqual(t, 1, res.ExitCode)
assertError(t, res.Error)
assertContains(t, res.Error.Error(), "context canceled")
}
})
@ -247,17 +245,17 @@ func TestRunner_ContextCancellation(t *testing.T) {
{Name: "first", Command: "echo", Args: []string{"1"}},
{Name: "second", Command: "echo", Args: []string{"2"}, After: []string{"first"}},
})
require.NoError(t, err)
requireNoError(t, err)
assert.Equal(t, 0, result.Passed)
assert.Equal(t, 0, result.Failed)
assert.Equal(t, 2, result.Skipped)
require.Len(t, result.Results, 2)
assertEqual(t, 0, result.Passed)
assertEqual(t, 0, result.Failed)
assertEqual(t, 2, result.Skipped)
requireLen(t, result.Results, 2)
for _, res := range result.Results {
assert.True(t, res.Skipped)
assert.Equal(t, 1, res.ExitCode)
assert.Error(t, res.Error)
assert.Contains(t, res.Error.Error(), "context canceled")
assertTrue(t, res.Skipped)
assertEqual(t, 1, res.ExitCode)
assertError(t, res.Error)
assertContains(t, res.Error.Error(), "context canceled")
}
})
}
@ -265,22 +263,22 @@ func TestRunner_ContextCancellation(t *testing.T) {
func TestRunResult_Passed(t *testing.T) {
t.Run("success", func(t *testing.T) {
r := RunResult{ExitCode: 0}
assert.True(t, r.Passed())
assertTrue(t, r.Passed())
})
t.Run("non-zero exit", func(t *testing.T) {
r := RunResult{ExitCode: 1}
assert.False(t, r.Passed())
assertFalse(t, r.Passed())
})
t.Run("skipped", func(t *testing.T) {
r := RunResult{ExitCode: 0, Skipped: true}
assert.False(t, r.Passed())
assertFalse(t, r.Passed())
})
t.Run("error", func(t *testing.T) {
r := RunResult{ExitCode: 0, Error: assert.AnError}
assert.False(t, r.Passed())
r := RunResult{ExitCode: 0, Error: errSentinel}
assertFalse(t, r.Passed())
})
}
@ -288,32 +286,32 @@ func TestRunner_NilService(t *testing.T) {
runner := NewRunner(nil)
_, err := runner.RunAll(context.Background(), nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerNoService)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerNoService)
_, err = runner.RunSequential(context.Background(), nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerNoService)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerNoService)
_, err = runner.RunParallel(context.Background(), nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerNoService)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerNoService)
}
func TestRunner_NilContext(t *testing.T) {
runner := newTestRunner(t)
_, err := runner.RunAll(nil, nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerContextRequired)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerContextRequired)
_, err = runner.RunSequential(nil, nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerContextRequired)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerContextRequired)
_, err = runner.RunParallel(nil, nil)
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerContextRequired)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerContextRequired)
}
func TestRunner_InvalidSpecNames(t *testing.T) {
@ -323,32 +321,32 @@ func TestRunner_InvalidSpecNames(t *testing.T) {
_, err := runner.RunSequential(context.Background(), []RunSpec{
{Name: "", Command: "echo", Args: []string{"a"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidSpecName)
})
t.Run("rejects empty dependency names", func(t *testing.T) {
_, err := runner.RunAll(context.Background(), []RunSpec{
{Name: "one", Command: "echo", Args: []string{"a"}, After: []string{""}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidDependencyName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidDependencyName)
})
t.Run("rejects duplicated dependency names", func(t *testing.T) {
_, err := runner.RunAll(context.Background(), []RunSpec{
{Name: "one", Command: "echo", Args: []string{"a"}, After: []string{"two", "two"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidDependencyName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidDependencyName)
})
t.Run("rejects self dependency", func(t *testing.T) {
_, err := runner.RunAll(context.Background(), []RunSpec{
{Name: "one", Command: "echo", Args: []string{"a"}, After: []string{"one"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidDependencyName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidDependencyName)
})
t.Run("rejects duplicate names", func(t *testing.T) {
@ -356,8 +354,8 @@ func TestRunner_InvalidSpecNames(t *testing.T) {
{Name: "same", Command: "echo", Args: []string{"a"}},
{Name: "same", Command: "echo", Args: []string{"b"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidSpecName)
})
t.Run("rejects duplicate names in parallel mode", func(t *testing.T) {
@ -365,7 +363,7 @@ func TestRunner_InvalidSpecNames(t *testing.T) {
{Name: "one", Command: "echo", Args: []string{"a"}},
{Name: "one", Command: "echo", Args: []string{"b"}},
})
require.Error(t, err)
assert.ErrorIs(t, err, ErrRunnerInvalidSpecName)
requireError(t, err)
assertErrorIs(t, err, ErrRunnerInvalidSpecName)
})
}

File diff suppressed because it is too large Load diff