bash_test.go

  1package claudetool
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"os"
  7	"path/filepath"
  8	"strings"
  9	"syscall"
 10	"testing"
 11	"time"
 12)
 13
 14func TestBashSlowOk(t *testing.T) {
 15	// Test that slow_ok flag is properly handled
 16	t.Run("SlowOk Flag", func(t *testing.T) {
 17		input := json.RawMessage(`{"command":"echo 'slow test'","slow_ok":true}`)
 18
 19		bashTool := (&BashTool{WorkingDir: NewMutableWorkingDir("/")}).Tool()
 20		toolOut := bashTool.Run(context.Background(), input)
 21		if toolOut.Error != nil {
 22			t.Fatalf("Unexpected error: %v", toolOut.Error)
 23		}
 24		result := toolOut.LLMContent
 25
 26		expected := "slow test\n"
 27		if len(result) == 0 || result[0].Text != expected {
 28			t.Errorf("Expected %q, got %q", expected, result[0].Text)
 29		}
 30	})
 31
 32	// Test that slow_ok with background works
 33	t.Run("SlowOk with Background", func(t *testing.T) {
 34		input := json.RawMessage(`{"command":"echo 'slow background test'","slow_ok":true,"background":true}`)
 35
 36		bashTool := (&BashTool{WorkingDir: NewMutableWorkingDir("/")}).Tool()
 37		toolOut := bashTool.Run(context.Background(), input)
 38		if toolOut.Error != nil {
 39			t.Fatalf("Unexpected error: %v", toolOut.Error)
 40		}
 41		result := toolOut.LLMContent
 42
 43		// Should return background result XML-ish format
 44		resultStr := result[0].Text
 45		if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
 46			t.Errorf("Expected XML-ish background result format, got: %s", resultStr)
 47		}
 48
 49		// Extract PID and output file from XML-ish format for cleanup
 50		// This is a simple extraction for test cleanup - in real usage the agent would parse this
 51		lines := strings.Split(resultStr, "\n")
 52		var outFile string
 53		for _, line := range lines {
 54			if strings.Contains(line, "<output_file>") {
 55				start := strings.Index(line, "<output_file>") + len("<output_file>")
 56				end := strings.Index(line, "</output_file>")
 57				if end > start {
 58					outFile = line[start:end]
 59				}
 60				break
 61			}
 62		}
 63
 64		if outFile != "" {
 65			// Clean up
 66			os.Remove(outFile)
 67			os.Remove(filepath.Dir(outFile))
 68		}
 69	})
 70}
 71
 72func TestBashTool(t *testing.T) {
 73	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
 74	tool := bashTool.Tool()
 75
 76	// Test basic functionality
 77	t.Run("Basic Command", func(t *testing.T) {
 78		input := json.RawMessage(`{"command":"echo 'Hello, world!'"}`)
 79
 80		toolOut := tool.Run(context.Background(), input)
 81		if toolOut.Error != nil {
 82			t.Fatalf("Unexpected error: %v", toolOut.Error)
 83		}
 84		result := toolOut.LLMContent
 85
 86		expected := "Hello, world!\n"
 87		if len(result) == 0 || result[0].Text != expected {
 88			t.Errorf("Expected %q, got %q", expected, result[0].Text)
 89		}
 90
 91		// Verify Display data contains working directory
 92		display, ok := toolOut.Display.(BashDisplayData)
 93		if !ok {
 94			t.Fatalf("Expected Display to be BashDisplayData, got %T", toolOut.Display)
 95		}
 96		if display.WorkingDir != "/" {
 97			t.Errorf("Expected WorkingDir to be '/', got %q", display.WorkingDir)
 98		}
 99	})
100
101	// Test with arguments
102	t.Run("Command With Arguments", func(t *testing.T) {
103		input := json.RawMessage(`{"command":"echo -n foo && echo -n bar"}`)
104
105		toolOut := tool.Run(context.Background(), input)
106		if toolOut.Error != nil {
107			t.Fatalf("Unexpected error: %v", toolOut.Error)
108		}
109		result := toolOut.LLMContent
110
111		expected := "foobar"
112		if len(result) == 0 || result[0].Text != expected {
113			t.Errorf("Expected %q, got %q", expected, result[0].Text)
114		}
115	})
116
117	// Test with slow_ok parameter
118	t.Run("With SlowOK", func(t *testing.T) {
119		inputObj := struct {
120			Command string `json:"command"`
121			SlowOK  bool   `json:"slow_ok"`
122		}{
123			Command: "sleep 0.1 && echo 'Completed'",
124			SlowOK:  true,
125		}
126		inputJSON, err := json.Marshal(inputObj)
127		if err != nil {
128			t.Fatalf("Failed to marshal input: %v", err)
129		}
130
131		toolOut := tool.Run(context.Background(), inputJSON)
132		if toolOut.Error != nil {
133			t.Fatalf("Unexpected error: %v", toolOut.Error)
134		}
135		result := toolOut.LLMContent
136
137		expected := "Completed\n"
138		if len(result) == 0 || result[0].Text != expected {
139			t.Errorf("Expected %q, got %q", expected, result[0].Text)
140		}
141	})
142
143	// Test command timeout with custom timeout config
144	t.Run("Command Timeout", func(t *testing.T) {
145		// Use a custom BashTool with very short timeout
146		customTimeouts := &Timeouts{
147			Fast:       100 * time.Millisecond,
148			Slow:       100 * time.Millisecond,
149			Background: 100 * time.Millisecond,
150		}
151		customBash := &BashTool{
152			WorkingDir: NewMutableWorkingDir("/"),
153			Timeouts:   customTimeouts,
154		}
155		tool := customBash.Tool()
156
157		input := json.RawMessage(`{"command":"sleep 0.5 && echo 'Should not see this'"}`)
158
159		toolOut := tool.Run(context.Background(), input)
160		if toolOut.Error == nil {
161			t.Errorf("Expected timeout error, got none")
162		} else if !strings.Contains(toolOut.Error.Error(), "timed out") {
163			t.Errorf("Expected timeout error, got: %v", toolOut.Error)
164		}
165	})
166
167	// Test command that fails
168	t.Run("Failed Command", func(t *testing.T) {
169		input := json.RawMessage(`{"command":"exit 1"}`)
170
171		toolOut := tool.Run(context.Background(), input)
172		if toolOut.Error == nil {
173			t.Errorf("Expected error for failed command, got none")
174		}
175	})
176
177	// Test invalid input
178	t.Run("Invalid JSON Input", func(t *testing.T) {
179		input := json.RawMessage(`{"command":123}`) // Invalid JSON (command must be string)
180
181		toolOut := tool.Run(context.Background(), input)
182		if toolOut.Error == nil {
183			t.Errorf("Expected error for invalid input, got none")
184		}
185	})
186}
187
188func TestExecuteBash(t *testing.T) {
189	ctx := context.Background()
190	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
191
192	// Test successful command
193	t.Run("Successful Command", func(t *testing.T) {
194		req := bashInput{
195			Command: "echo 'Success'",
196		}
197
198		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
199		if err != nil {
200			t.Fatalf("Unexpected error: %v", err)
201		}
202
203		want := "Success\n"
204		if output != want {
205			t.Errorf("Expected %q, got %q", want, output)
206		}
207	})
208
209	// Test SKETCH=1 environment variable is set
210	t.Run("SKETCH Environment Variable", func(t *testing.T) {
211		req := bashInput{
212			Command: "echo $SKETCH",
213		}
214
215		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
216		if err != nil {
217			t.Fatalf("Unexpected error: %v", err)
218		}
219
220		want := "1\n"
221		if output != want {
222			t.Errorf("Expected SKETCH=1, got %q", output)
223		}
224	})
225
226	// Test SHELLEY_CONVERSATION_ID environment variable is set when configured
227	t.Run("SHELLEY_CONVERSATION_ID Environment Variable", func(t *testing.T) {
228		bashWithConvID := &BashTool{
229			WorkingDir:     NewMutableWorkingDir("/"),
230			ConversationID: "test-conv-123",
231		}
232		req := bashInput{
233			Command: "echo $SHELLEY_CONVERSATION_ID",
234		}
235
236		output, err := bashWithConvID.executeBash(ctx, req, 5*time.Second)
237		if err != nil {
238			t.Fatalf("Unexpected error: %v", err)
239		}
240
241		want := "test-conv-123\n"
242		if output != want {
243			t.Errorf("Expected SHELLEY_CONVERSATION_ID=test-conv-123, got %q", output)
244		}
245	})
246
247	// Test SHELLEY_CONVERSATION_ID is not set when not configured
248	t.Run("SHELLEY_CONVERSATION_ID Not Set When Empty", func(t *testing.T) {
249		req := bashInput{
250			Command: "echo \"conv_id:$SHELLEY_CONVERSATION_ID:\"",
251		}
252
253		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
254		if err != nil {
255			t.Fatalf("Unexpected error: %v", err)
256		}
257
258		// Should be empty since ConversationID is not set on bashTool
259		want := "conv_id::\n"
260		if output != want {
261			t.Errorf("Expected empty SHELLEY_CONVERSATION_ID, got %q", output)
262		}
263	})
264
265	// Test command with output to stderr
266	t.Run("Command with stderr", func(t *testing.T) {
267		req := bashInput{
268			Command: "echo 'Error message' >&2 && echo 'Success'",
269		}
270
271		output, err := bashTool.executeBash(ctx, req, 5*time.Second)
272		if err != nil {
273			t.Fatalf("Unexpected error: %v", err)
274		}
275
276		want := "Error message\nSuccess\n"
277		if output != want {
278			t.Errorf("Expected %q, got %q", want, output)
279		}
280	})
281
282	// Test command that fails with stderr
283	t.Run("Failed Command with stderr", func(t *testing.T) {
284		req := bashInput{
285			Command: "echo 'Error message' >&2 && exit 1",
286		}
287
288		_, err := bashTool.executeBash(ctx, req, 5*time.Second)
289		if err == nil {
290			t.Errorf("Expected error for failed command, got none")
291		} else if !strings.Contains(err.Error(), "Error message") {
292			t.Errorf("Expected stderr in error message, got: %v", err)
293		}
294	})
295
296	// Test timeout
297	t.Run("Command Timeout", func(t *testing.T) {
298		req := bashInput{
299			Command: "sleep 1 && echo 'Should not see this'",
300		}
301
302		start := time.Now()
303		_, err := bashTool.executeBash(ctx, req, 100*time.Millisecond)
304		elapsed := time.Since(start)
305
306		// Command should time out after ~100ms, not wait for full 1 second
307		if elapsed >= 1*time.Second {
308			t.Errorf("Command did not respect timeout, took %v", elapsed)
309		}
310
311		if err == nil {
312			t.Errorf("Expected timeout error, got none")
313		} else if !strings.Contains(err.Error(), "timed out") {
314			t.Errorf("Expected timeout error, got: %v", err)
315		}
316	})
317}
318
319func TestBackgroundBash(t *testing.T) {
320	bashTool := &BashTool{WorkingDir: NewMutableWorkingDir("/")}
321	tool := bashTool.Tool()
322
323	// Test basic background execution
324	t.Run("Basic Background Command", func(t *testing.T) {
325		inputObj := struct {
326			Command    string `json:"command"`
327			Background bool   `json:"background"`
328		}{
329			Command:    "echo 'Hello from background' $SKETCH",
330			Background: true,
331		}
332		inputJSON, err := json.Marshal(inputObj)
333		if err != nil {
334			t.Fatalf("Failed to marshal input: %v", err)
335		}
336
337		toolOut := tool.Run(context.Background(), inputJSON)
338		if toolOut.Error != nil {
339			t.Fatalf("Unexpected error: %v", toolOut.Error)
340		}
341		result := toolOut.LLMContent
342
343		// Parse the returned XML-ish format
344		resultStr := result[0].Text
345		if !strings.Contains(resultStr, "<pid>") || !strings.Contains(resultStr, "<output_file>") {
346			t.Fatalf("Expected XML-ish background result format, got: %s", resultStr)
347		}
348
349		// Extract PID and output file from XML-ish format
350		lines := strings.Split(resultStr, "\n")
351		var pidStr, outFile string
352		for _, line := range lines {
353			if strings.Contains(line, "<pid>") {
354				start := strings.Index(line, "<pid>") + len("<pid>")
355				end := strings.Index(line, "</pid>")
356				if end > start {
357					pidStr = line[start:end]
358				}
359			} else if strings.Contains(line, "<output_file>") {
360				start := strings.Index(line, "<output_file>") + len("<output_file>")
361				end := strings.Index(line, "</output_file>")
362				if end > start {
363					outFile = line[start:end]
364				}
365			}
366		}
367
368		// Verify we got valid values
369		if pidStr == "" || outFile == "" {
370			t.Errorf("Failed to extract PID or output file from result: %s", resultStr)
371			return
372		}
373
374		// Verify output file exists
375		if _, err := os.Stat(outFile); os.IsNotExist(err) {
376			t.Errorf("Output file doesn't exist: %s", outFile)
377		}
378
379		// Wait for the command output to be written to file
380		waitForFile(t, outFile)
381
382		// Check file contents
383		outputContent, err := os.ReadFile(outFile)
384		if err != nil {
385			t.Fatalf("Failed to read output file: %v", err)
386		}
387		// The implementation appends a completion message to the output
388		outputStr := string(outputContent)
389		if !strings.Contains(outputStr, "Hello from background 1") {
390			t.Errorf("Expected output to contain 'Hello from background 1', got %q", outputStr)
391		}
392		if !strings.Contains(outputStr, "[background process completed]") {
393			t.Errorf("Expected output to contain completion message, got %q", outputStr)
394		}
395
396		// Clean up
397		os.Remove(outFile)
398		os.Remove(filepath.Dir(outFile))
399	})
400
401	// Test background command with stderr output
402	t.Run("Background Command with stderr", func(t *testing.T) {
403		inputObj := struct {
404			Command    string `json:"command"`
405			Background bool   `json:"background"`
406		}{
407			Command:    "echo 'Output to stdout' && echo 'Output to stderr' >&2",
408			Background: true,
409		}
410		inputJSON, err := json.Marshal(inputObj)
411		if err != nil {
412			t.Fatalf("Failed to marshal input: %v", err)
413		}
414
415		toolOut := tool.Run(context.Background(), inputJSON)
416		if toolOut.Error != nil {
417			t.Fatalf("Unexpected error: %v", toolOut.Error)
418		}
419		result := toolOut.LLMContent
420
421		// Parse the returned XML-ish format
422		resultStr := result[0].Text
423		lines := strings.Split(resultStr, "\n")
424		var outFile string
425		for _, line := range lines {
426			if strings.Contains(line, "<output_file>") {
427				start := strings.Index(line, "<output_file>") + len("<output_file>")
428				end := strings.Index(line, "</output_file>")
429				if end > start {
430					outFile = line[start:end]
431				}
432				break
433			}
434		}
435
436		// Wait for the command output to be written to file
437		waitForFile(t, outFile)
438
439		// Check output content (stdout and stderr are combined in implementation)
440		outputContent, err := os.ReadFile(outFile)
441		if err != nil {
442			t.Fatalf("Failed to read output file: %v", err)
443		}
444		// Implementation combines stdout and stderr into one file
445		outputStr := string(outputContent)
446		if !strings.Contains(outputStr, "Output to stdout") || !strings.Contains(outputStr, "Output to stderr") {
447			t.Errorf("Expected both stdout and stderr content, got %q", outputStr)
448		}
449
450		// Clean up
451		os.Remove(outFile)
452		os.Remove(filepath.Dir(outFile))
453	})
454
455	// Test background command running without waiting
456	t.Run("Background Command Running", func(t *testing.T) {
457		// Create a script that will continue running after we check
458		inputObj := struct {
459			Command    string `json:"command"`
460			Background bool   `json:"background"`
461		}{
462			Command:    "echo 'Running in background' && sleep 5",
463			Background: true,
464		}
465		inputJSON, err := json.Marshal(inputObj)
466		if err != nil {
467			t.Fatalf("Failed to marshal input: %v", err)
468		}
469
470		// Start the command in the background
471		toolOut := tool.Run(context.Background(), inputJSON)
472		if toolOut.Error != nil {
473			t.Fatalf("Unexpected error: %v", toolOut.Error)
474		}
475		result := toolOut.LLMContent
476
477		// Parse the returned XML-ish format
478		resultStr := result[0].Text
479		lines := strings.Split(resultStr, "\n")
480		var pidStr, outFile string
481		for _, line := range lines {
482			if strings.Contains(line, "<pid>") {
483				start := strings.Index(line, "<pid>") + len("<pid>")
484				end := strings.Index(line, "</pid>")
485				if end > start {
486					pidStr = line[start:end]
487				}
488			} else if strings.Contains(line, "<output_file>") {
489				start := strings.Index(line, "<output_file>") + len("<output_file>")
490				end := strings.Index(line, "</output_file>")
491				if end > start {
492					outFile = line[start:end]
493				}
494			}
495		}
496
497		// Wait for the command output to be written to file
498		waitForFile(t, outFile)
499
500		// Check output content
501		outputContent, err := os.ReadFile(outFile)
502		if err != nil {
503			t.Fatalf("Failed to read output file: %v", err)
504		}
505		expectedOutput := "Running in background\n"
506		if string(outputContent) != expectedOutput {
507			t.Errorf("Expected output content %q, got %q", expectedOutput, string(outputContent))
508		}
509
510		// Verify the process is still running by parsing PID
511		if pidStr != "" {
512			// We can't easily test if the process is still running without importing strconv
513			// and the process might have finished by now anyway due to timing
514			t.Log("Process started in background with PID:", pidStr)
515		}
516
517		// Clean up
518		os.Remove(outFile)
519		os.Remove(filepath.Dir(outFile))
520	})
521}
522
523func TestBashTimeout(t *testing.T) {
524	// Test default timeout values
525	t.Run("Default Timeout Values", func(t *testing.T) {
526		// Test foreground default timeout
527		foreground := bashInput{
528			Command:    "echo 'test'",
529			Background: false,
530		}
531		fgTimeout := foreground.timeout(nil)
532		expectedFg := 30 * time.Second
533		if fgTimeout != expectedFg {
534			t.Errorf("Expected foreground default timeout to be %v, got %v", expectedFg, fgTimeout)
535		}
536
537		// Test background default timeout
538		background := bashInput{
539			Command:    "echo 'test'",
540			Background: true,
541		}
542		bgTimeout := background.timeout(nil)
543		expectedBg := 24 * time.Hour
544		if bgTimeout != expectedBg {
545			t.Errorf("Expected background default timeout to be %v, got %v", expectedBg, bgTimeout)
546		}
547
548		// Test slow_ok timeout
549		slowOk := bashInput{
550			Command:    "echo 'test'",
551			Background: false,
552			SlowOK:     true,
553		}
554		slowTimeout := slowOk.timeout(nil)
555		expectedSlow := 15 * time.Minute
556		if slowTimeout != expectedSlow {
557			t.Errorf("Expected slow_ok timeout to be %v, got %v", expectedSlow, slowTimeout)
558		}
559
560		// Test custom timeout config
561		customTimeouts := &Timeouts{
562			Fast:       5 * time.Second,
563			Slow:       2 * time.Minute,
564			Background: 1 * time.Hour,
565		}
566		customFast := bashInput{
567			Command:    "echo 'test'",
568			Background: false,
569		}
570		customTimeout := customFast.timeout(customTimeouts)
571		expectedCustom := 5 * time.Second
572		if customTimeout != expectedCustom {
573			t.Errorf("Expected custom timeout to be %v, got %v", expectedCustom, customTimeout)
574		}
575	})
576}
577
578func TestFormatForegroundBashOutput(t *testing.T) {
579	// Test small output (under threshold) - should pass through unchanged
580	t.Run("Small Output", func(t *testing.T) {
581		smallOutput := "line 1\nline 2\nline 3\n"
582		result, err := formatForegroundBashOutput(smallOutput)
583		if err != nil {
584			t.Fatalf("Unexpected error: %v", err)
585		}
586		if result != smallOutput {
587			t.Errorf("Expected small output to pass through unchanged, got %q", result)
588		}
589	})
590
591	// Test large output (over 50KB) - should save to file and return summary
592	t.Run("Large Output With Lines", func(t *testing.T) {
593		// Generate output > 50KB with many lines
594		var lines []string
595		for i := 1; i <= 1000; i++ {
596			lines = append(lines, strings.Repeat("x", 60)+" line "+string(rune('0'+i%10)))
597		}
598		largeOutput := strings.Join(lines, "\n")
599		if len(largeOutput) < largeOutputThreshold {
600			t.Fatalf("Test setup error: output is only %d bytes, need > %d", len(largeOutput), largeOutputThreshold)
601		}
602
603		result, err := formatForegroundBashOutput(largeOutput)
604		if err != nil {
605			t.Fatalf("Unexpected error: %v", err)
606		}
607
608		// Should mention the file
609		if !strings.Contains(result, "saved to:") {
610			t.Errorf("Expected result to mention saved file, got:\n%s", result)
611		}
612
613		// Should have first 2 lines numbered
614		if !strings.Contains(result, "    1:") || !strings.Contains(result, "    2:") {
615			t.Errorf("Expected first 2 numbered lines, got:\n%s", result)
616		}
617
618		// Should have last 5 lines numbered
619		if !strings.Contains(result, "  996:") || !strings.Contains(result, " 1000:") {
620			t.Errorf("Expected last 5 numbered lines, got:\n%s", result)
621		}
622
623		t.Logf("Large output result:\n%s", result)
624	})
625
626	// Test large output with few/no lines (binary-like)
627	t.Run("Large Output No Lines", func(t *testing.T) {
628		// Generate > 50KB of data with no newlines
629		largeOutput := strings.Repeat("x", largeOutputThreshold+1000)
630
631		result, err := formatForegroundBashOutput(largeOutput)
632		if err != nil {
633			t.Fatalf("Unexpected error: %v", err)
634		}
635
636		// Should mention the file
637		if !strings.Contains(result, "saved to:") {
638			t.Errorf("Expected result to mention saved file, got:\n%s", result)
639		}
640
641		// Should indicate line count
642		if !strings.Contains(result, "1 lines") {
643			t.Errorf("Expected result to indicate line count, got:\n%s", result)
644		}
645
646		t.Logf("Large binary-like output result:\n%s", result)
647	})
648
649	// Test large output with very long lines (e.g., minified JS)
650	t.Run("Large Output With Long Lines", func(t *testing.T) {
651		// Generate output > 50KB with few very long lines
652		longLine := strings.Repeat("abcdefghij", 1000) // 10KB per line
653		lines := []string{longLine, longLine, longLine, longLine, longLine, longLine}
654		largeOutput := strings.Join(lines, "\n")
655		if len(largeOutput) < largeOutputThreshold {
656			t.Fatalf("Test setup error: output is only %d bytes, need > %d", len(largeOutput), largeOutputThreshold)
657		}
658
659		result, err := formatForegroundBashOutput(largeOutput)
660		if err != nil {
661			t.Fatalf("Unexpected error: %v", err)
662		}
663
664		// Result should be reasonable size (not blow up context)
665		if len(result) > 4096 {
666			t.Errorf("Expected truncated result < 4KB, got %d bytes:\n%s", len(result), result)
667		}
668
669		// Should mention the file
670		if !strings.Contains(result, "saved to:") {
671			t.Errorf("Expected result to mention saved file, got:\n%s", result)
672		}
673
674		// Lines should be truncated
675		if !strings.Contains(result, "...") {
676			t.Errorf("Expected truncated lines with '...', got:\n%s", result)
677		}
678
679		t.Logf("Large output with long lines result:\n%s", result)
680	})
681}
682
683// waitForFile waits for a file to exist and be non-empty or times out
684func waitForFile(t *testing.T, filepath string) {
685	timeout := time.After(5 * time.Second)
686	tick := time.NewTicker(10 * time.Millisecond)
687	defer tick.Stop()
688
689	for {
690		select {
691		case <-timeout:
692			t.Fatalf("Timed out waiting for file to exist and have contents: %s", filepath)
693			return
694		case <-tick.C:
695			info, err := os.Stat(filepath)
696			if err == nil && info.Size() > 0 {
697				return // File exists and has content
698			}
699		}
700	}
701}
702
703// waitForProcessDeath waits for a process to no longer exist or times out
704func waitForProcessDeath(t *testing.T, pid int) {
705	timeout := time.After(5 * time.Second)
706	tick := time.NewTicker(50 * time.Millisecond)
707	defer tick.Stop()
708
709	for {
710		select {
711		case <-timeout:
712			t.Fatalf("Timed out waiting for process %d to exit", pid)
713			return
714		case <-tick.C:
715			process, _ := os.FindProcess(pid)
716			err := process.Signal(syscall.Signal(0))
717			if err != nil {
718				// Process doesn't exist
719				return
720			}
721		}
722	}
723}