package command import ( "bytes" "testing" "time" ) func TestNew(t *testing.T) { t.Parallel() tests := []struct { name string cmdName string args []string expected string }{ { name: "simple command", cmdName: "echo", args: []string{"hello"}, expected: "echo", }, { name: "command with multiple args", cmdName: "ls", args: []string{"-l", "-a"}, expected: "ls", }, { name: "command with no args", cmdName: "pwd", args: []string{}, expected: "pwd", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New(tt.cmdName, tt.args, Options{ Stdout: &stdout, Stderr: &stderr, }) if cmd == nil { t.Fatal("New() returned nil") } if cmd.name != tt.expected { t.Errorf("expected name %s, got %s", tt.expected, cmd.name) } if len(cmd.args) != len(tt.args) { t.Errorf("expected %d args, got %d", len(tt.args), len(cmd.args)) } for i, arg := range tt.args { if i < len(cmd.args) && cmd.args[i] != arg { t.Errorf("expected arg[%d] %s, got %s", i, arg, cmd.args[i]) } } }) } } func TestStart(t *testing.T) { t.Parallel() tests := []struct { name string cmdName string args []string shouldError bool }{ { name: "valid command", cmdName: "echo", args: []string{"test"}, shouldError: false, }, { name: "invalid command", cmdName: "nonexistentcommand12345", args: []string{}, shouldError: true, }, { name: "valid command with args", cmdName: "sleep", args: []string{"0.1"}, shouldError: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New(tt.cmdName, tt.args, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if tt.shouldError { if err == nil { t.Error("expected error but got none") } } else { if err != nil { t.Errorf("unexpected error: %v", err) } if cmd.Cmd == nil { t.Error("exec.Cmd should be initialized after Start()") } if cmd.Process == nil { t.Error("Process should be set after successful Start()") } } }) } } func TestWait(t *testing.T) { t.Parallel() t.Run("wait for successful command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } err = cmd.Wait() if err != nil { t.Errorf("unexpected error waiting for command: %v", err) } if !cmd.Exited() { t.Error("command should be marked as exited after Wait()") } }) t.Run("wait for failing command", func(t *testing.T) { t.Parallel() // Use a command that will fail var stdout, stderr bytes.Buffer cmd := New("ls", []string{"/nonexistent/directory/12345"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } err = cmd.Wait() if err != nil { t.Errorf("unexpected error from failing command: %v", err) } if !cmd.Exited() { t.Error("command should be marked as exited after Wait() even on failure") } }) } func TestStop(t *testing.T) { t.Parallel() t.Run("stop running command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("sleep", []string{"10"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } // Give the command a moment to actually start time.Sleep(100 * time.Millisecond) err = cmd.Stop() if err != nil { t.Errorf("unexpected error stopping command: %v", err) } // Wait a bit and check if process is actually killed time.Sleep(100 * time.Millisecond) // Try to wait for the process - it should return quickly since it was killed waitDone := make(chan error, 1) go func() { waitDone <- cmd.Wait() }() select { case <-waitDone: // Good, Wait() returned quickly case <-time.After(1 * time.Second): t.Error("process was not killed properly - Wait() is still blocking") } }) t.Run("stop non-running command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) // Try to stop before starting err := cmd.Stop() if err != nil { t.Errorf("stopping non-running command should not error: %v", err) } }) t.Run("stop already finished command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } err = cmd.Wait() if err != nil { t.Fatalf("failed to wait for command: %v", err) } err = cmd.Stop() if err != nil { t.Errorf("stopping finished command should not error: %v", err) } }) } func TestExited(t *testing.T) { t.Parallel() t.Run("not started command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) if cmd.Exited() { t.Error("command that hasn't started should not be marked as exited") } }) t.Run("started but not finished command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("sleep", []string{"1"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } if cmd.Exited() { t.Error("running command should not be marked as exited") } // Clean up cmd.Stop() }) t.Run("finished command", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } err = cmd.Wait() if err != nil { t.Fatalf("failed to wait for command: %v", err) } if !cmd.Exited() { t.Error("finished command should be marked as exited") } }) } func TestMultipleStarts(t *testing.T) { t.Parallel() t.Run("multiple start calls", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) // First start err := cmd.Start() if err != nil { t.Fatalf("first start failed: %v", err) } err = cmd.Wait() if err != nil { t.Fatalf("wait failed: %v", err) } // Second start should work (creates new exec.Cmd) err = cmd.Start() if err != nil { t.Errorf("second start failed: %v", err) } err = cmd.Wait() if err != nil { t.Errorf("second wait failed: %v", err) } }) } func TestMakeCommand(t *testing.T) { t.Parallel() t.Run("makeCommand sets up exec.Cmd correctly", func(t *testing.T) { t.Parallel() var stdout, stderr bytes.Buffer cmd := New("echo", []string{"hello", "world"}, Options{ Stdout: &stdout, Stderr: &stderr, }) // makeCommand is called internally by Start, so we test it through Start err := cmd.Start() if err != nil { t.Fatalf("failed to start command: %v", err) } if cmd.Cmd == nil { t.Error("exec.Cmd should be initialized") } if cmd.Stdout != &stdout { t.Error("Stdout should be set to the provided buffer") } if cmd.Stderr != &stderr { t.Error("Stderr should be set to the provided buffer") } cmd.Wait() }) } func TestCommandInterface(t *testing.T) { t.Parallel() t.Run("Cmd implements Command interface", func(t *testing.T) { t.Parallel() var _ Command = &Cmd{} var stdout, stderr bytes.Buffer cmd := New("echo", []string{"test"}, Options{ Stdout: &stdout, Stderr: &stderr, }) // Test that all Command interface methods work err := cmd.Start() if err != nil { t.Fatalf("Start() failed: %v", err) } err = cmd.Wait() if err != nil { t.Errorf("Wait() failed: %v", err) } err = cmd.Stop() if err != nil { t.Errorf("Stop() failed: %v", err) } }) }