termtap/internal/process/runner_test.go
Hayden Hargreaves 002773e77f test: AI generated all of these tests
Just for the MVP of course. Need to validate the idea.
2026-04-23 19:47:04 -07:00

220 lines
5.1 KiB
Go

package process
import (
"os"
"os/exec"
"reflect"
"strings"
"testing"
"time"
"termtap.dev/internal/model"
)
func TestCommandString(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cmd model.Command
want string
}{
{
name: "empty args",
cmd: model.Command{Name: "go", Args: []string{}},
want: "go ",
},
{
name: "multiple args",
cmd: model.Command{Name: "curl", Args: []string{"-s", "https://example.com"}},
want: "curl -s https://example.com",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if got := CommandString(tt.cmd); got != tt.want {
t.Fatalf("CommandString() = %q, want %q", got, tt.want)
}
})
}
}
func TestInjectEnv(t *testing.T) {
t.Parallel()
cmd := exec.Command("sh", "-c", "true")
injectEnv(cmd, "127.0.0.1:8080")
mustContain := []string{
"HTTP_PROXY=http://127.0.0.1:8080",
"http_proxy=http://127.0.0.1:8080",
"HTTPS_PROXY=http://127.0.0.1:8080",
"https_proxy=http://127.0.0.1:8080",
"NO_PROXY=",
"no_proxy=",
}
for _, kv := range mustContain {
if !containsEnvEntry(cmd.Env, kv) {
t.Fatalf("injectEnv() missing env entry %q", kv)
}
}
}
func TestReadPipe(t *testing.T) {
t.Parallel()
t.Run("stdout lines emit stdout events", func(t *testing.T) {
t.Parallel()
ch := make(chan model.Event, 4)
input := strings.NewReader("line1\nline2\n")
readPipe(input, model.EventTypeProcessStdout, ch)
events := drainEvents(t, ch, 2, time.Second)
if events[0].Type != model.EventTypeProcessStdout || events[0].Body != "line1" {
t.Fatalf("event[0] = %#v, want stdout line1", events[0])
}
if events[1].Type != model.EventTypeProcessStdout || events[1].Body != "line2" {
t.Fatalf("event[1] = %#v, want stdout line2", events[1])
}
})
t.Run("stderr lines emit stderr events", func(t *testing.T) {
t.Parallel()
ch := make(chan model.Event, 4)
input := strings.NewReader("err1\nerr2\n")
readPipe(input, model.EventTypeProcessStderr, ch)
events := drainEvents(t, ch, 2, time.Second)
if events[0].Type != model.EventTypeProcessStderr || events[0].Body != "err1" {
t.Fatalf("event[0] = %#v, want stderr err1", events[0])
}
if events[1].Type != model.EventTypeProcessStderr || events[1].Body != "err2" {
t.Fatalf("event[1] = %#v, want stderr err2", events[1])
}
})
}
func TestUpdateStatus(t *testing.T) {
t.Parallel()
t.Run("nil process is no-op", func(t *testing.T) {
t.Parallel()
UpdateStatus(nil, true, make(chan model.Event, 1))
})
t.Run("state unchanged is no-op", func(t *testing.T) {
t.Parallel()
ch := make(chan model.Event, 1)
proc := &model.Process{Running: true}
UpdateStatus(proc, true, ch)
select {
case ev := <-ch:
t.Fatalf("unexpected event for unchanged state: %#v", ev)
default:
}
})
t.Run("emits started and stopped events with pid", func(t *testing.T) {
t.Parallel()
ch := make(chan model.Event, 2)
proc := &model.Process{
Exec: &exec.Cmd{Process: &os.Process{Pid: 4321}},
}
UpdateStatus(proc, true, ch)
events := drainEvents(t, ch, 1, time.Second)
started := events[0]
if started.Type != model.EventTypeProcessStarted {
t.Fatalf("started type = %s, want %s", started.Type, model.EventTypeProcessStarted)
}
if started.PID != 4321 {
t.Fatalf("started PID = %d, want %d", started.PID, 4321)
}
if !proc.Running {
t.Fatal("proc.Running = false, want true")
}
UpdateStatus(proc, false, ch)
events = drainEvents(t, ch, 1, time.Second)
stopped := events[0]
if stopped.Type != model.EventTypeProcessExited {
t.Fatalf("stopped type = %s, want %s", stopped.Type, model.EventTypeProcessExited)
}
if stopped.PID != 4321 {
t.Fatalf("stopped PID = %d, want %d", stopped.PID, 4321)
}
if proc.Running {
t.Fatal("proc.Running = true, want false")
}
})
}
func TestNewProcess(t *testing.T) {
t.Parallel()
ch := make(chan model.Event, 8)
cmd := model.Command{Name: "sh", Args: []string{"-c", "printf test"}}
proc := NewProcess(cmd, "127.0.0.1:8080", ch)
if proc == nil {
t.Fatal("NewProcess() returned nil")
}
if !reflect.DeepEqual(proc.Command, cmd) {
t.Fatalf("process command = %#v, want %#v", proc.Command, cmd)
}
if proc.Exec == nil {
t.Fatal("process Exec is nil")
}
if proc.Running {
t.Fatal("new process should not be running")
}
if proc.Done == nil {
t.Fatal("Done channel is nil")
}
if got, want := proc.Exec.Args[0], "sh"; got != want {
t.Fatalf("Exec.Args[0] = %q, want %q", got, want)
}
if !containsEnvEntry(proc.Exec.Env, "HTTP_PROXY=http://127.0.0.1:8080") {
t.Fatal("process env missing injected HTTP_PROXY")
}
}
func containsEnvEntry(env []string, want string) bool {
for _, entry := range env {
if entry == want {
return true
}
}
return false
}
func drainEvents(t *testing.T, ch <-chan model.Event, n int, timeout time.Duration) []model.Event {
t.Helper()
events := make([]model.Event, 0, n)
deadline := time.After(timeout)
for len(events) < n {
select {
case ev := <-ch:
events = append(events, ev)
case <-deadline:
t.Fatalf("timeout waiting for %d events; got %d", n, len(events))
}
}
return events
}