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 }