background_test.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. package shell
  2. import (
  3. "context"
  4. "runtime"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/stretchr/testify/require"
  9. )
  10. func TestBackgroundShellManager_Start(t *testing.T) {
  11. t.Skip("Skipping this until I figure out why its flaky")
  12. t.Parallel()
  13. ctx := t.Context()
  14. workingDir := t.TempDir()
  15. manager := newBackgroundShellManager()
  16. bgShell, err := manager.Start(ctx, workingDir, nil, "echo 'hello world'", "")
  17. if err != nil {
  18. t.Fatalf("failed to start background shell: %v", err)
  19. }
  20. if bgShell.ID == "" {
  21. t.Error("expected shell ID to be non-empty")
  22. }
  23. // Wait for the command to complete
  24. bgShell.Wait()
  25. stdout, stderr, done, err := bgShell.GetOutput()
  26. if !done {
  27. t.Error("expected shell to be done")
  28. }
  29. if err != nil {
  30. t.Errorf("expected no error, got: %v", err)
  31. }
  32. if !strings.Contains(stdout, "hello world") {
  33. t.Errorf("expected stdout to contain 'hello world', got: %s", stdout)
  34. }
  35. if stderr != "" {
  36. t.Errorf("expected empty stderr, got: %s", stderr)
  37. }
  38. }
  39. func TestBackgroundShellManager_Get(t *testing.T) {
  40. t.Parallel()
  41. ctx := t.Context()
  42. workingDir := t.TempDir()
  43. manager := newBackgroundShellManager()
  44. bgShell, err := manager.Start(ctx, workingDir, nil, "echo 'test'", "")
  45. if err != nil {
  46. t.Fatalf("failed to start background shell: %v", err)
  47. }
  48. // Retrieve the shell
  49. retrieved, ok := manager.Get(bgShell.ID)
  50. if !ok {
  51. t.Error("expected to find the background shell")
  52. }
  53. if retrieved.ID != bgShell.ID {
  54. t.Errorf("expected shell ID %s, got %s", bgShell.ID, retrieved.ID)
  55. }
  56. // Clean up
  57. manager.Kill(bgShell.ID)
  58. }
  59. func TestBackgroundShellManager_Kill(t *testing.T) {
  60. t.Parallel()
  61. ctx := t.Context()
  62. workingDir := t.TempDir()
  63. manager := newBackgroundShellManager()
  64. // Start a long-running command
  65. bgShell, err := manager.Start(ctx, workingDir, nil, "sleep 10", "")
  66. if err != nil {
  67. t.Fatalf("failed to start background shell: %v", err)
  68. }
  69. // Kill it
  70. err = manager.Kill(bgShell.ID)
  71. if err != nil {
  72. t.Errorf("failed to kill background shell: %v", err)
  73. }
  74. // Verify it's no longer in the manager
  75. _, ok := manager.Get(bgShell.ID)
  76. if ok {
  77. t.Error("expected shell to be removed after kill")
  78. }
  79. // Verify the shell is done
  80. if !bgShell.IsDone() {
  81. t.Error("expected shell to be done after kill")
  82. }
  83. }
  84. func TestBackgroundShellManager_KillNonExistent(t *testing.T) {
  85. t.Parallel()
  86. manager := newBackgroundShellManager()
  87. err := manager.Kill("non-existent-id")
  88. if err == nil {
  89. t.Error("expected error when killing non-existent shell")
  90. }
  91. }
  92. func TestBackgroundShell_IsDone(t *testing.T) {
  93. t.Parallel()
  94. ctx := t.Context()
  95. workingDir := t.TempDir()
  96. manager := newBackgroundShellManager()
  97. bgShell, err := manager.Start(ctx, workingDir, nil, "echo 'quick'", "")
  98. if err != nil {
  99. t.Fatalf("failed to start background shell: %v", err)
  100. }
  101. // Wait a bit for the command to complete
  102. time.Sleep(100 * time.Millisecond)
  103. if !bgShell.IsDone() {
  104. t.Error("expected shell to be done")
  105. }
  106. // Clean up
  107. manager.Kill(bgShell.ID)
  108. }
  109. func TestBackgroundShell_WithBlockFuncs(t *testing.T) {
  110. t.Parallel()
  111. ctx := t.Context()
  112. workingDir := t.TempDir()
  113. manager := newBackgroundShellManager()
  114. blockFuncs := []BlockFunc{
  115. CommandsBlocker([]string{"curl", "wget"}),
  116. }
  117. bgShell, err := manager.Start(ctx, workingDir, blockFuncs, "curl example.com", "")
  118. if err != nil {
  119. t.Fatalf("failed to start background shell: %v", err)
  120. }
  121. // Wait for the command to complete
  122. bgShell.Wait()
  123. stdout, stderr, done, execErr := bgShell.GetOutput()
  124. if !done {
  125. t.Error("expected shell to be done")
  126. }
  127. // The command should have been blocked
  128. output := stdout + stderr
  129. if !strings.Contains(output, "not allowed") && execErr == nil {
  130. t.Errorf("expected command to be blocked, got stdout: %s, stderr: %s, err: %v", stdout, stderr, execErr)
  131. }
  132. // Clean up
  133. manager.Kill(bgShell.ID)
  134. }
  135. func TestBackgroundShellManager_List(t *testing.T) {
  136. if runtime.GOOS == "windows" {
  137. t.Skip("skipping flacky test on windows")
  138. }
  139. t.Parallel()
  140. ctx := t.Context()
  141. workingDir := t.TempDir()
  142. manager := newBackgroundShellManager()
  143. // Start two shells
  144. bgShell1, err := manager.Start(ctx, workingDir, nil, "sleep 1", "")
  145. if err != nil {
  146. t.Fatalf("failed to start first background shell: %v", err)
  147. }
  148. bgShell2, err := manager.Start(ctx, workingDir, nil, "sleep 1", "")
  149. if err != nil {
  150. t.Fatalf("failed to start second background shell: %v", err)
  151. }
  152. ids := manager.List()
  153. // Check that both shells are in the list
  154. found1 := false
  155. found2 := false
  156. for _, id := range ids {
  157. if id == bgShell1.ID {
  158. found1 = true
  159. }
  160. if id == bgShell2.ID {
  161. found2 = true
  162. }
  163. }
  164. if !found1 {
  165. t.Errorf("expected to find shell %s in list", bgShell1.ID)
  166. }
  167. if !found2 {
  168. t.Errorf("expected to find shell %s in list", bgShell2.ID)
  169. }
  170. // Clean up
  171. manager.Kill(bgShell1.ID)
  172. manager.Kill(bgShell2.ID)
  173. }
  174. func TestBackgroundShellManager_KillAll(t *testing.T) {
  175. t.Parallel()
  176. ctx := t.Context()
  177. workingDir := t.TempDir()
  178. manager := newBackgroundShellManager()
  179. // Start multiple long-running shells
  180. shell1, err := manager.Start(ctx, workingDir, nil, "sleep 10", "")
  181. if err != nil {
  182. t.Fatalf("failed to start shell 1: %v", err)
  183. }
  184. shell2, err := manager.Start(ctx, workingDir, nil, "sleep 10", "")
  185. if err != nil {
  186. t.Fatalf("failed to start shell 2: %v", err)
  187. }
  188. shell3, err := manager.Start(ctx, workingDir, nil, "sleep 10", "")
  189. if err != nil {
  190. t.Fatalf("failed to start shell 3: %v", err)
  191. }
  192. // Verify shells are running
  193. if shell1.IsDone() || shell2.IsDone() || shell3.IsDone() {
  194. t.Error("shells should not be done yet")
  195. }
  196. // Kill all shells
  197. manager.KillAll(t.Context())
  198. // Verify all shells are done
  199. if !shell1.IsDone() {
  200. t.Error("shell1 should be done after KillAll")
  201. }
  202. if !shell2.IsDone() {
  203. t.Error("shell2 should be done after KillAll")
  204. }
  205. if !shell3.IsDone() {
  206. t.Error("shell3 should be done after KillAll")
  207. }
  208. // Verify they're removed from the manager
  209. if _, ok := manager.Get(shell1.ID); ok {
  210. t.Error("shell1 should be removed from manager")
  211. }
  212. if _, ok := manager.Get(shell2.ID); ok {
  213. t.Error("shell2 should be removed from manager")
  214. }
  215. if _, ok := manager.Get(shell3.ID); ok {
  216. t.Error("shell3 should be removed from manager")
  217. }
  218. // Verify list is empty (or doesn't contain our shells)
  219. ids := manager.List()
  220. for _, id := range ids {
  221. if id == shell1.ID || id == shell2.ID || id == shell3.ID {
  222. t.Errorf("shell %s should not be in list after KillAll", id)
  223. }
  224. }
  225. }
  226. func TestBackgroundShellManager_KillAll_Timeout(t *testing.T) {
  227. t.Parallel()
  228. // XXX: can't use synctest here - causes --race to trip.
  229. workingDir := t.TempDir()
  230. manager := newBackgroundShellManager()
  231. // Start a shell that traps signals and ignores cancellation.
  232. _, err := manager.Start(t.Context(), workingDir, nil, "trap '' TERM INT; sleep 60", "")
  233. require.NoError(t, err)
  234. // Short timeout to test the timeout path.
  235. ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
  236. t.Cleanup(cancel)
  237. start := time.Now()
  238. manager.KillAll(ctx)
  239. elapsed := time.Since(start)
  240. // Must return promptly after timeout, not hang for 60 seconds.
  241. require.Less(t, elapsed, 2*time.Second)
  242. }
  243. func TestBackgroundShell_WaitContext_Completed(t *testing.T) {
  244. t.Parallel()
  245. done := make(chan struct{})
  246. close(done)
  247. bgShell := &BackgroundShell{done: done}
  248. ctx, cancel := context.WithTimeout(t.Context(), time.Second)
  249. t.Cleanup(cancel)
  250. require.True(t, bgShell.WaitContext(ctx))
  251. }
  252. func TestBackgroundShell_WaitContext_Canceled(t *testing.T) {
  253. t.Parallel()
  254. bgShell := &BackgroundShell{done: make(chan struct{})}
  255. ctx, cancel := context.WithCancel(t.Context())
  256. cancel()
  257. require.False(t, bgShell.WaitContext(ctx))
  258. }