github.com/windmilleng/tilt@v0.13.6/integration/fixture_test.go (about) 1 // +build integration 2 3 package integration 4 5 import ( 6 "context" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "runtime" 14 "strings" 15 "testing" 16 "time" 17 18 "github.com/windmilleng/tilt/internal/testutils/bufsync" 19 ) 20 21 var packageDir string 22 var installed bool 23 24 const namespaceFlag = "-n=tilt-integration" 25 26 func init() { 27 _, file, _, ok := runtime.Caller(0) 28 if !ok { 29 panic(fmt.Errorf("Could not locate path to Tilt integration tests")) 30 } 31 32 packageDir = filepath.Dir(file) 33 } 34 35 type fixture struct { 36 t *testing.T 37 ctx context.Context 38 cancel func() 39 dir string 40 logs *bufsync.ThreadSafeBuffer 41 originalFiles map[string]string 42 tilt *TiltDriver 43 activeTiltUp *TiltUpResponse 44 tearingDown bool 45 tiltArgs []string 46 } 47 48 func newFixture(t *testing.T, dir string) *fixture { 49 dir = filepath.Join(packageDir, dir) 50 err := os.Chdir(dir) 51 if err != nil { 52 t.Fatal(err) 53 } 54 55 client := NewTiltDriver() 56 client.Environ["TILT_DISABLE_ANALYTICS"] = "true" 57 58 ctx, cancel := context.WithCancel(context.Background()) 59 f := &fixture{ 60 t: t, 61 ctx: ctx, 62 cancel: cancel, 63 dir: dir, 64 logs: bufsync.NewThreadSafeBuffer(), 65 originalFiles: make(map[string]string), 66 tilt: client, 67 } 68 69 if !installed { 70 // Install tilt on the first test run. 71 f.installTilt() 72 installed = true 73 } 74 75 return f 76 } 77 78 func (f *fixture) testDirPath(s string) string { 79 return filepath.Join(f.dir, s) 80 } 81 82 func (f *fixture) installTilt() { 83 cmd := exec.CommandContext(f.ctx, "go", "install", "-mod", "vendor", "github.com/windmilleng/tilt/cmd/tilt") 84 f.runOrFail(cmd, "Building tilt") 85 } 86 87 func (f *fixture) runOrFail(cmd *exec.Cmd, msg string) { 88 // Use Output() instead of Run() because that captures Stderr in the ExitError. 89 _, err := cmd.Output() 90 if err == nil { 91 return 92 } 93 94 exitErr, isExitErr := err.(*exec.ExitError) 95 if isExitErr { 96 f.t.Fatalf("%s\nError: %v\nStderr:\n%s\n", msg, err, string(exitErr.Stderr)) 97 return 98 } 99 f.t.Fatalf("%s. Error: %v", msg, err) 100 } 101 102 func (f *fixture) DumpLogs() { 103 _, _ = os.Stdout.Write([]byte(f.logs.String())) 104 } 105 106 func (f *fixture) WaitUntil(ctx context.Context, msg string, fun func() (string, error), expectedContents string) { 107 for { 108 actualContents, err := fun() 109 if err == nil && strings.Contains(actualContents, expectedContents) { 110 return 111 } 112 113 select { 114 case <-f.activeTiltDone(): 115 f.t.Fatalf("Tilt died while waiting: %v", f.activeTiltErr()) 116 case <-ctx.Done(): 117 f.t.Fatalf("Timed out waiting for expected result (%s)\n"+ 118 "Expected: %s\n"+ 119 "Actual: %s\n"+ 120 "Current error: %v\n", 121 msg, expectedContents, actualContents, err) 122 case <-time.After(200 * time.Millisecond): 123 } 124 } 125 } 126 127 func (f *fixture) activeTiltDone() <-chan struct{} { 128 if f.activeTiltUp != nil { 129 return f.activeTiltUp.Done() 130 } 131 neverDone := make(chan struct{}) 132 return neverDone 133 } 134 135 func (f *fixture) activeTiltErr() error { 136 if f.activeTiltUp != nil { 137 return f.activeTiltUp.Err() 138 } 139 return nil 140 } 141 142 func (f *fixture) LogWriter() io.Writer { 143 return io.MultiWriter(f.logs, os.Stdout) 144 } 145 146 func (f *fixture) TiltUp(name string) { 147 args := []string{"--watch=false"} 148 if name != "" { 149 args = append(args, name) 150 } 151 response, err := f.tilt.Up(args, f.LogWriter()) 152 if err != nil { 153 f.t.Fatalf("TiltUp %s: %v", name, err) 154 } 155 select { 156 case <-response.Done(): 157 err := response.Err() 158 if err != nil { 159 f.t.Fatalf("TiltUp %s: %v", name, err) 160 } 161 case <-f.ctx.Done(): 162 err := f.ctx.Err() 163 if err != nil { 164 f.t.Fatalf("TiltUp %s: %v", name, err) 165 } 166 } 167 } 168 169 func (f *fixture) TiltWatch() { 170 response, err := f.tilt.Up(f.tiltArgs, f.LogWriter()) 171 if err != nil { 172 f.t.Fatalf("TiltWatch: %v", err) 173 } 174 f.activeTiltUp = response 175 } 176 177 func (f *fixture) TiltWatchExec() { 178 response, err := f.tilt.Up(append([]string{"--update-mode=exec"}, f.tiltArgs...), f.LogWriter()) 179 if err != nil { 180 f.t.Fatalf("TiltWatchExec: %v", err) 181 } 182 f.activeTiltUp = response 183 } 184 185 func (f *fixture) ReplaceContents(fileBaseName, original, replacement string) { 186 file := f.testDirPath(fileBaseName) 187 contentsBytes, err := ioutil.ReadFile(file) 188 if err != nil { 189 f.t.Fatal(err) 190 } 191 192 contents := string(contentsBytes) 193 _, hasStoredContents := f.originalFiles[file] 194 if !hasStoredContents { 195 f.originalFiles[file] = contents 196 } 197 198 newContents := strings.Replace(contents, original, replacement, -1) 199 if newContents == contents { 200 f.t.Fatalf("Could not find contents %q to replace in file %s: %s", original, fileBaseName, contents) 201 } 202 203 err = ioutil.WriteFile(file, []byte(newContents), os.FileMode(0777)) 204 if err != nil { 205 f.t.Fatal(err) 206 } 207 } 208 209 func (f *fixture) StartTearDown() { 210 if f.tearingDown { 211 return 212 } 213 214 isTiltStillUp := f.activeTiltUp != nil && f.activeTiltUp.Err() == nil 215 if f.t.Failed() && isTiltStillUp { 216 fmt.Printf("Test failed, dumping engine state\n----\n") 217 err := f.tilt.DumpEngine(os.Stdout) 218 if err != nil { 219 fmt.Printf("Error: %v", err) 220 } 221 fmt.Printf("\n----\n") 222 223 err = f.activeTiltUp.KillAndDumpThreads() 224 if err != nil { 225 fmt.Printf("error killing tilt: %v\n", err) 226 } 227 } 228 229 f.cancel() 230 f.ctx = context.Background() 231 f.tearingDown = true 232 } 233 234 func (f *fixture) KillProcs() { 235 if f.activeTiltUp != nil { 236 err := f.activeTiltUp.Kill() 237 if err != nil { 238 fmt.Printf("error killing tilt: %v\n", err) 239 } 240 } 241 } 242 243 func (f *fixture) TearDown() { 244 f.StartTearDown() 245 246 f.KillProcs() 247 248 // This is a hack. 249 // 250 // Deleting a namespace is slow. Doing it on every test case makes 251 // the tests more accurate. We believe that in this particular case, 252 // the trade-off of speed over accuracy is worthwhile, so 253 // we add this hack so that we can `tilt down` without deleting 254 // the namespace. 255 // 256 // Each Tiltfile reads this environment variable, and skips loading the namespace 257 // into Tilt, so that Tilt doesn't delete it. 258 // 259 // If users want to do the same thing in practice, it might be worth 260 // adding better in-product hooks (e.g., `tilt down --preserve-namespace`), 261 // or more scriptability in the Tiltfile. 262 f.tilt.Environ["SKIP_NAMESPACE"] = "true" 263 264 err := f.tilt.Down(os.Stdout) 265 if err != nil { 266 f.t.Errorf("Running tilt down: %v", err) 267 } 268 269 for k, v := range f.originalFiles { 270 _ = ioutil.WriteFile(k, []byte(v), os.FileMode(0777)) 271 } 272 }