shell_test.go 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. package shell
  2. import (
  3. "context"
  4. "runtime"
  5. "strings"
  6. "testing"
  7. "time"
  8. )
  9. // Benchmark to measure CPU efficiency
  10. func BenchmarkShellQuickCommands(b *testing.B) {
  11. shell := NewShell(&Options{WorkingDir: b.TempDir()})
  12. b.ReportAllocs()
  13. for b.Loop() {
  14. _, _, err := shell.Exec(context.Background(), "echo test")
  15. exitCode := ExitCode(err)
  16. if err != nil || exitCode != 0 {
  17. b.Fatalf("Command failed: %v, exit code: %d", err, exitCode)
  18. }
  19. }
  20. }
  21. func TestTestTimeout(t *testing.T) {
  22. ctx, cancel := context.WithTimeout(t.Context(), time.Millisecond)
  23. t.Cleanup(cancel)
  24. shell := NewShell(&Options{WorkingDir: t.TempDir()})
  25. _, _, err := shell.Exec(ctx, "sleep 10")
  26. if status := ExitCode(err); status == 0 {
  27. t.Fatalf("Expected non-zero exit status, got %d", status)
  28. }
  29. if !IsInterrupt(err) {
  30. t.Fatalf("Expected command to be interrupted, but it was not")
  31. }
  32. if err == nil {
  33. t.Fatalf("Expected an error due to timeout, but got none")
  34. }
  35. }
  36. func TestTestCancel(t *testing.T) {
  37. ctx, cancel := context.WithCancel(t.Context())
  38. cancel() // immediately cancel the context
  39. shell := NewShell(&Options{WorkingDir: t.TempDir()})
  40. _, _, err := shell.Exec(ctx, "sleep 10")
  41. if status := ExitCode(err); status == 0 {
  42. t.Fatalf("Expected non-zero exit status, got %d", status)
  43. }
  44. if !IsInterrupt(err) {
  45. t.Fatalf("Expected command to be interrupted, but it was not")
  46. }
  47. if err == nil {
  48. t.Fatalf("Expected an error due to cancel, but got none")
  49. }
  50. }
  51. func TestRunCommandError(t *testing.T) {
  52. shell := NewShell(&Options{WorkingDir: t.TempDir()})
  53. _, _, err := shell.Exec(t.Context(), "nopenopenope")
  54. if status := ExitCode(err); status == 0 {
  55. t.Fatalf("Expected non-zero exit status, got %d", status)
  56. }
  57. if IsInterrupt(err) {
  58. t.Fatalf("Expected command to not be interrupted, but it was")
  59. }
  60. if err == nil {
  61. t.Fatalf("Expected an error, got nil")
  62. }
  63. }
  64. func TestRunContinuity(t *testing.T) {
  65. shell := NewShell(&Options{WorkingDir: t.TempDir()})
  66. shell.Exec(t.Context(), "export FOO=bar")
  67. dst := t.TempDir()
  68. shell.Exec(t.Context(), "cd "+dst)
  69. out, _, _ := shell.Exec(t.Context(), "echo $FOO ; pwd")
  70. expect := "bar\n" + dst + "\n"
  71. if out != expect {
  72. t.Fatalf("Expected output %q, got %q", expect, out)
  73. }
  74. }
  75. // New tests for Windows shell support
  76. func TestShellTypeDetection(t *testing.T) {
  77. shell := &PersistentShell{}
  78. tests := []struct {
  79. command string
  80. expected ShellType
  81. windowsOnly bool
  82. }{
  83. // Windows-specific commands
  84. {"dir", ShellTypeCmd, true},
  85. {"type file.txt", ShellTypeCmd, true},
  86. {"copy file1.txt file2.txt", ShellTypeCmd, true},
  87. {"del file.txt", ShellTypeCmd, true},
  88. {"md newdir", ShellTypeCmd, true},
  89. {"tasklist", ShellTypeCmd, true},
  90. // PowerShell commands
  91. {"Get-Process", ShellTypePowerShell, true},
  92. {"Get-ChildItem", ShellTypePowerShell, true},
  93. {"Set-Location C:\\", ShellTypePowerShell, true},
  94. {"Get-Content file.txt | Where-Object {$_ -match 'pattern'}", ShellTypePowerShell, true},
  95. {"$files = Get-ChildItem", ShellTypePowerShell, true},
  96. // Unix/cross-platform commands
  97. {"ls -la", ShellTypePOSIX, false},
  98. {"cat file.txt", ShellTypePOSIX, false},
  99. {"grep pattern file.txt", ShellTypePOSIX, false},
  100. {"echo hello", ShellTypePOSIX, false},
  101. {"git status", ShellTypePOSIX, false},
  102. {"go build", ShellTypePOSIX, false},
  103. }
  104. for _, test := range tests {
  105. t.Run(test.command, func(t *testing.T) {
  106. result := shell.determineShellType(test.command)
  107. if test.windowsOnly && runtime.GOOS != "windows" {
  108. // On non-Windows systems, everything should use POSIX
  109. if result != ShellTypePOSIX {
  110. t.Errorf("On non-Windows, command %q should use POSIX shell, got %v", test.command, result)
  111. }
  112. } else if runtime.GOOS == "windows" {
  113. // On Windows, check the expected shell type
  114. if result != test.expected {
  115. t.Errorf("Command %q should use %v shell, got %v", test.command, test.expected, result)
  116. }
  117. }
  118. })
  119. }
  120. }
  121. func TestWindowsCDHandling(t *testing.T) {
  122. if runtime.GOOS != "windows" {
  123. t.Skip("Windows CD handling test only runs on Windows")
  124. }
  125. shell := NewShell(&Options{
  126. WorkingDir: "C:\\Users",
  127. })
  128. tests := []struct {
  129. command string
  130. expectedCwd string
  131. shouldError bool
  132. }{
  133. {"cd ..", "C:\\", false},
  134. {"cd Documents", "C:\\Users\\Documents", false},
  135. {"cd C:\\Windows", "C:\\Windows", false},
  136. {"cd", "", true}, // Missing argument
  137. }
  138. for _, test := range tests {
  139. t.Run(test.command, func(t *testing.T) {
  140. originalCwd := shell.GetWorkingDir()
  141. stdout, stderr, err := shell.handleWindowsCD(test.command)
  142. if test.shouldError {
  143. if err == nil {
  144. t.Errorf("Command %q should have failed", test.command)
  145. }
  146. } else {
  147. if err != nil {
  148. t.Errorf("Command %q failed: %v", test.command, err)
  149. }
  150. if shell.GetWorkingDir() != test.expectedCwd {
  151. t.Errorf("Command %q: expected cwd %q, got %q", test.command, test.expectedCwd, shell.GetWorkingDir())
  152. }
  153. }
  154. // Reset for next test
  155. shell.SetWorkingDir(originalCwd)
  156. _ = stdout
  157. _ = stderr
  158. })
  159. }
  160. }
  161. func TestCrossPlatformExecution(t *testing.T) {
  162. shell := NewShell(&Options{WorkingDir: "."})
  163. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  164. defer cancel()
  165. // Test a simple command that should work on all platforms
  166. stdout, stderr, err := shell.Exec(ctx, "echo hello")
  167. if err != nil {
  168. t.Fatalf("Echo command failed: %v, stderr: %s", err, stderr)
  169. }
  170. if stdout == "" {
  171. t.Error("Echo command produced no output")
  172. }
  173. // The output should contain "hello" regardless of platform
  174. if !strings.Contains(strings.ToLower(stdout), "hello") {
  175. t.Errorf("Echo output should contain 'hello', got: %q", stdout)
  176. }
  177. }
  178. func TestWindowsNativeCommands(t *testing.T) {
  179. if runtime.GOOS != "windows" {
  180. t.Skip("Windows native command test only runs on Windows")
  181. }
  182. shell := NewShell(&Options{WorkingDir: "."})
  183. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  184. defer cancel()
  185. // Test Windows dir command
  186. stdout, stderr, err := shell.Exec(ctx, "dir")
  187. if err != nil {
  188. t.Fatalf("Dir command failed: %v, stderr: %s", err, stderr)
  189. }
  190. if stdout == "" {
  191. t.Error("Dir command produced no output")
  192. }
  193. }