github.com/tilt-dev/tilt@v0.36.0/integration/tilt.go (about)

     1  package integration
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"context"
     7  	"fmt"
     8  	"go/build"
     9  	"io"
    10  	"net"
    11  	"os"
    12  	"os/exec"
    13  	"path/filepath"
    14  	"strings"
    15  	"sync"
    16  	"syscall"
    17  	"testing"
    18  	"time"
    19  
    20  	"github.com/stretchr/testify/require"
    21  )
    22  
    23  type UpCommand string
    24  
    25  const (
    26  	UpCommandUp   UpCommand = "up"
    27  	UpCommandDemo UpCommand = "demo"
    28  )
    29  
    30  type TiltDriver struct {
    31  	Environ map[string]string
    32  
    33  	t    testing.TB
    34  	port int
    35  }
    36  
    37  type TiltDriverOption func(t testing.TB, td *TiltDriver)
    38  
    39  func TiltDriverUseRandomFreePort(t testing.TB, td *TiltDriver) {
    40  	l, err := net.Listen("tcp", "")
    41  	require.NoError(t, err, "Could not get a free port")
    42  	td.port = l.Addr().(*net.TCPAddr).Port
    43  	require.NoError(t, l.Close(), "Could not get a free port")
    44  }
    45  
    46  func NewTiltDriver(t testing.TB, options ...TiltDriverOption) *TiltDriver {
    47  	td := &TiltDriver{
    48  		t:       t,
    49  		Environ: make(map[string]string),
    50  	}
    51  	for _, opt := range options {
    52  		opt(t, td)
    53  	}
    54  	return td
    55  }
    56  
    57  func (d *TiltDriver) cmd(ctx context.Context, args []string, out io.Writer) *exec.Cmd {
    58  	// rely on the Tilt binary in GOPATH that should have been created by `go install` from the
    59  	// fixture to avoid accidentally picking up a system install of tilt with higher precedence
    60  	// on system PATH
    61  	tiltBin := filepath.Join(build.Default.GOPATH, "bin", "tilt")
    62  	cmd := exec.CommandContext(ctx, tiltBin, args...)
    63  	cmd.Stdout = out
    64  	cmd.Stderr = out
    65  	cmd.Env = os.Environ()
    66  	for k, v := range d.Environ {
    67  		cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
    68  	}
    69  	if d.port > 0 {
    70  		for _, arg := range args {
    71  			if strings.HasPrefix(arg, "--port=") {
    72  				d.t.Fatalf("Cannot specify port argument when using automatic port mode: %s", arg)
    73  			}
    74  		}
    75  		if _, ok := d.Environ["TILT_PORT"]; ok {
    76  			d.t.Fatal("Cannot specify TILT_PORT environment variable when using automatic port mode")
    77  		}
    78  		cmd.Env = append(cmd.Env, fmt.Sprintf("TILT_PORT=%d", d.port))
    79  	}
    80  	return cmd
    81  }
    82  
    83  func (d *TiltDriver) DumpEngine(ctx context.Context, out io.Writer) error {
    84  	cmd := d.cmd(ctx, []string{"dump", "engine"}, out)
    85  	return cmd.Run()
    86  }
    87  
    88  func (d *TiltDriver) Down(ctx context.Context, out io.Writer) error {
    89  	cmd := d.cmd(ctx, []string{"down"}, out)
    90  	return cmd.Run()
    91  }
    92  
    93  func (d *TiltDriver) CI(ctx context.Context, out io.Writer, args ...string) error {
    94  	cmd := d.cmd(ctx, append([]string{
    95  		"ci",
    96  
    97  		// Debug logging for integration tests
    98  		"--debug",
    99  		"--klog=1",
   100  
   101  		// Even if we're on a debug build, don't start a debug webserver
   102  		"--web-mode=prod",
   103  	}, args...), out)
   104  	return cmd.Run()
   105  }
   106  
   107  func (d *TiltDriver) Up(ctx context.Context, command UpCommand, out io.Writer, args ...string) (*TiltUpResponse, error) {
   108  	if command == "" {
   109  		command = UpCommandUp
   110  	}
   111  	mandatoryArgs := []string{string(command),
   112  		// Can't attach a HUD or install browsers in headless mode
   113  		"--legacy=false",
   114  
   115  		// Debug logging for integration tests
   116  		"--debug",
   117  		"--klog=1",
   118  
   119  		// Even if we're on a debug build, don't start a debug webserver
   120  		"--web-mode=prod",
   121  	}
   122  
   123  	cmd := d.cmd(ctx, append(mandatoryArgs, args...), out)
   124  	err := cmd.Start()
   125  	if err != nil {
   126  		return nil, err
   127  	}
   128  
   129  	ch := make(chan struct{})
   130  	response := &TiltUpResponse{
   131  		done:    ch,
   132  		process: cmd.Process,
   133  	}
   134  	go func() {
   135  		err := cmd.Wait()
   136  		if err != nil {
   137  			response.mu.Lock()
   138  			response.err = err
   139  			response.mu.Unlock()
   140  		}
   141  		close(ch)
   142  	}()
   143  	return response, nil
   144  }
   145  
   146  func (d *TiltDriver) Args(ctx context.Context, args []string, out io.Writer) error {
   147  	cmd := d.cmd(ctx, append([]string{"args"}, args...), out)
   148  	return cmd.Run()
   149  }
   150  
   151  func (d *TiltDriver) APIResources(ctx context.Context) ([]string, error) {
   152  	var out bytes.Buffer
   153  	cmd := d.cmd(ctx, []string{"api-resources", "-o=name"}, &out)
   154  	err := cmd.Run()
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	var resources []string
   159  	s := bufio.NewScanner(&out)
   160  	for s.Scan() {
   161  		resources = append(resources, s.Text())
   162  	}
   163  	return resources, nil
   164  }
   165  
   166  func (d *TiltDriver) Get(ctx context.Context, apiType string, names ...string) ([]byte, error) {
   167  	var out bytes.Buffer
   168  	args := append([]string{"get", "-o=json", apiType}, names...)
   169  	cmd := d.cmd(ctx, args, &out)
   170  	err := cmd.Run()
   171  	return out.Bytes(), err
   172  }
   173  
   174  func (d *TiltDriver) Patch(ctx context.Context, apiType string, patch string, name string) error {
   175  	args := []string{"patch", apiType, "-p", patch, "--", name}
   176  	var out bytes.Buffer
   177  	return d.cmd(ctx, args, &out).Run()
   178  }
   179  
   180  type TiltUpResponse struct {
   181  	done chan struct{}
   182  	err  error
   183  	mu   sync.Mutex
   184  
   185  	process *os.Process
   186  }
   187  
   188  func (r *TiltUpResponse) Done() <-chan struct{} {
   189  	return r.done
   190  }
   191  
   192  func (r *TiltUpResponse) Err() error {
   193  	r.mu.Lock()
   194  	defer r.mu.Unlock()
   195  	return r.err
   196  }
   197  
   198  // TriggerExit sends a SIGTERM to the `tilt up` process to give it a chance to exit normally.
   199  //
   200  // If the signal cannot be sent or 2 seconds have elapsed, it will be forcibly killed with SIGKILL.
   201  func (r *TiltUpResponse) TriggerExit() error {
   202  	if r.process == nil {
   203  		return nil
   204  	}
   205  
   206  	if err := r.process.Signal(syscall.SIGTERM); err != nil {
   207  		return r.process.Kill()
   208  	}
   209  
   210  	select {
   211  	case <-r.Done():
   212  	case <-time.After(2 * time.Second):
   213  		return r.process.Kill()
   214  	}
   215  
   216  	return nil
   217  }
   218  
   219  // Kill the tilt process and print the goroutine/register state.
   220  // Useful if you think Tilt is deadlocked but aren't sure why.
   221  func (r *TiltUpResponse) KillAndDumpThreads() error {
   222  	if r.process == nil {
   223  		return nil
   224  	}
   225  
   226  	err := r.process.Signal(syscall.SIGINT)
   227  	if err != nil {
   228  		return err
   229  	}
   230  
   231  	select {
   232  	case <-r.Done():
   233  	case <-time.After(2 * time.Second):
   234  	}
   235  	return nil
   236  }