github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/localexec/execer_test.go (about)

     1  package localexec
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"os"
     7  	"runtime"
     8  	"strconv"
     9  	"strings"
    10  	"syscall"
    11  	"testing"
    12  	"time"
    13  
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  
    17  	"github.com/tilt-dev/tilt/internal/testutils"
    18  	"github.com/tilt-dev/tilt/internal/testutils/bufsync"
    19  	"github.com/tilt-dev/tilt/pkg/model"
    20  )
    21  
    22  func TestProcessExecer_Run(t *testing.T) {
    23  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
    24  	ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    25  	defer cancel()
    26  
    27  	// this works across both cmd.exe + sh
    28  	script := `echo hello from stdout && echo hello from stderr 1>&2`
    29  
    30  	execer := NewProcessExecer(EmptyEnv())
    31  
    32  	r, err := OneShot(ctx, execer, model.ToHostCmd(script))
    33  
    34  	require.NoError(t, err)
    35  	assert.Equal(t, 0, r.ExitCode)
    36  	// trim space to not deal with line-ending/whitespace differences between cmd.exe/sh
    37  	assert.Equal(t, "hello from stdout", strings.TrimSpace(string(r.Stdout)))
    38  	assert.Equal(t, "hello from stderr", strings.TrimSpace(string(r.Stderr)))
    39  }
    40  
    41  func TestProcessExecer_Run_ProcessGroup(t *testing.T) {
    42  	if testing.Short() {
    43  		t.Skip("skipping test in short mode")
    44  	}
    45  	if runtime.GOOS == "windows" {
    46  		t.Skip("test not supported on Windows")
    47  	}
    48  
    49  	ctx, _, _ := testutils.CtxAndAnalyticsForTest()
    50  	ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
    51  	defer cancel()
    52  
    53  	script := `sleep 60 & echo $!`
    54  
    55  	// to speed up test execution, as soon as we see the PID written to stdout, cancel the context
    56  	// to trigger process termination
    57  	var childPid int
    58  	stdoutBuf := bufsync.NewThreadSafeBuffer()
    59  	go func() {
    60  		for {
    61  			if ctx.Err() != nil {
    62  				return
    63  			}
    64  			output := strings.TrimSpace(stdoutBuf.String())
    65  			if output != "" {
    66  				var err error
    67  				childPid, err = strconv.Atoi(output)
    68  				if err == nil {
    69  					cancel()
    70  					return
    71  				}
    72  			}
    73  			time.Sleep(5 * time.Millisecond)
    74  		}
    75  	}()
    76  
    77  	execer := NewProcessExecer(EmptyEnv())
    78  	exitCode, err := execer.Run(ctx, model.ToUnixCmd(script), RunIO{Stdout: stdoutBuf})
    79  
    80  	require.NoError(t, err)
    81  	assert.Equal(t, 137, exitCode)
    82  
    83  	if assert.NotZero(t, childPid, "Process did not write child PID to stdout") {
    84  		// os.FindProcess is a no-op on Unix-like systems and always succeeds; need to send signal 0 to probe it
    85  		proc, _ := os.FindProcess(childPid)
    86  		childProcStopped := assert.Eventually(t, func() bool {
    87  			err = proc.Signal(syscall.Signal(0))
    88  			return errors.Is(err, os.ErrProcessDone)
    89  		}, time.Second, 50*time.Millisecond, "Child process was still running")
    90  		if !childProcStopped {
    91  			_ = proc.Kill()
    92  		}
    93  	}
    94  }