github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/internal/hud/renderer_test.go (about) 1 package hud 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "sync" 8 "testing" 9 "time" 10 11 "github.com/stretchr/testify/assert" 12 13 "github.com/tilt-dev/tilt/internal/container" 14 "github.com/tilt-dev/tilt/internal/hud/view" 15 "github.com/tilt-dev/tilt/internal/rty" 16 "github.com/tilt-dev/tilt/internal/store" 17 "github.com/tilt-dev/tilt/pkg/apis/core/v1alpha1" 18 "github.com/tilt-dev/tilt/pkg/logger" 19 "github.com/tilt-dev/tilt/pkg/model" 20 "github.com/tilt-dev/tilt/pkg/model/logstore" 21 22 "github.com/gdamore/tcell" 23 ) 24 25 const testCID = container.ID("beep-boop") 26 27 var clockForTest = func() time.Time { return time.Date(2017, 1, 1, 12, 0, 0, 0, time.UTC) } 28 29 func newView(resources ...view.Resource) view.View { 30 return view.View{ 31 LogReader: newLogReader(""), 32 Resources: resources, 33 } 34 } 35 36 func newSpanLogReader(mn model.ManifestName, spanID logstore.SpanID, msg string) logstore.Reader { 37 logStore := logstore.NewLogStore() 38 logStore.Append(testLogAction{mn: mn, spanID: spanID, time: time.Now(), msg: msg}, nil) 39 return logstore.NewReader(&sync.RWMutex{}, logStore) 40 } 41 42 func newWarningLogReader(mn model.ManifestName, spanID logstore.SpanID, warnings []string) logstore.Reader { 43 logStore := logstore.NewLogStore() 44 for _, warning := range warnings { 45 logStore.Append(testLogAction{ 46 mn: mn, 47 spanID: spanID, 48 time: time.Now(), 49 msg: warning, 50 level: logger.WarnLvl, 51 }, nil) 52 } 53 return logstore.NewReader(&sync.RWMutex{}, logStore) 54 } 55 56 func appendSpanLog(logStore *logstore.LogStore, mn model.ManifestName, spanID logstore.SpanID, msg string) { 57 logStore.Append(testLogAction{mn: mn, spanID: spanID, time: time.Now(), msg: msg}, nil) 58 } 59 60 func TestRender(t *testing.T) { 61 rtf := newRendererTestFixture(t) 62 63 v := newView(view.Resource{ 64 Name: "foo", 65 ResourceInfo: view.K8sResourceInfo{}, 66 }) 67 68 plainVs := fakeViewState(1, view.CollapseNo) 69 70 rtf.run("one undeployed resource", 70, 20, v, plainVs) 71 72 v = newView(view.Resource{ 73 Name: "a-a-a-aaaaabe vigoda", 74 BuildHistory: []model.BuildRecord{{ 75 FinishTime: time.Now(), 76 Error: fmt.Errorf("oh no the build failed"), 77 SpanID: "vigoda:1", 78 }}, 79 ResourceInfo: view.K8sResourceInfo{}, 80 }) 81 v.LogReader = newSpanLogReader("a-a-a-aaaaabe vigoda", "vigoda:1", 82 "1\n2\n3\nthe compiler did not understand!\n5\n6\n7\n8\n") 83 84 rtf.run("inline build log", 70, 20, v, plainVs) 85 86 v = newView(view.Resource{ 87 Name: "a-a-a-aaaaabe vigoda", 88 BuildHistory: []model.BuildRecord{{ 89 FinishTime: time.Now(), 90 Error: fmt.Errorf("oh no the build failed"), 91 SpanID: "vigoda:1", 92 }}, 93 ResourceInfo: view.K8sResourceInfo{}, 94 }) 95 v.LogReader = newSpanLogReader("a-a-a-aaaaabe vigoda", "vigoda:1", 96 `STEP 1/2 — Building Dockerfile: [gcr.io/windmill-public-containers/servantes/snack] 97 │ Tarring context… 98 │ Applying via kubectl 99 ╎ Created tarball (size: 11 kB) 100 │ Building image 101 ╎ RUNNING: go install github.com/tilt-dev/servantes/snack 102 103 ╎ ERROR IN: go install github.com/tilt-dev/servantes/snack 104 ╎ → # github.com/tilt-dev/servantes/snack 105 src/github.com/tilt-dev/servantes/snack/main.go:16:36: syntax error: unexpected newline, expecting comma or } 106 107 ERROR: ImageBuild: executor failed running [/bin/sh -c go install github.com/tilt-dev/servantes/snack]: exit code 2`) 108 rtf.run("inline build log with wrapping", 117, 20, v, plainVs) 109 110 v = newView(view.Resource{ 111 Name: "a-a-a-aaaaabe vigoda", 112 Endpoints: []string{"1.2.3.4:8080"}, 113 ResourceInfo: view.K8sResourceInfo{ 114 PodName: "vigoda-pod", 115 PodStatus: "Running", 116 PodRestarts: 1, 117 SpanID: "vigoda:pod", 118 RunStatus: v1alpha1.RuntimeStatusOK, 119 }, 120 }) 121 v.LogReader = newSpanLogReader("a-a-a-aaaaabe vigoda", "vigoda:pod", 122 "1\n2\n3\n4\nabe vigoda is now dead\n5\n6\n7\n8\n") 123 124 rtf.run("pod log displayed inline", 70, 20, v, plainVs) 125 126 v = newView(view.Resource{ 127 Name: "a-a-a-aaaaabe vigoda", 128 BuildHistory: []model.BuildRecord{{ 129 Error: fmt.Errorf("broken go code!"), 130 SpanID: "vigoda:1", 131 }}, 132 ResourceInfo: view.K8sResourceInfo{}, 133 }) 134 v.LogReader = newSpanLogReader("a-a-a-aaaaabe vigoda", "vigoda:1", 135 "mashing keys is not a good way to generate code") 136 rtf.run("manifest error and build error", 70, 20, v, plainVs) 137 138 ts := time.Now().Add(-5 * time.Minute) 139 v = newView(view.Resource{ 140 Name: "a-a-a-aaaaabe vigoda", 141 LastDeployTime: ts, 142 BuildHistory: []model.BuildRecord{{ 143 Edits: []string{"main.go", "cli.go"}, 144 Error: fmt.Errorf("the build failed!"), 145 FinishTime: ts, 146 StartTime: ts.Add(-1400 * time.Millisecond), 147 }}, 148 PendingBuildEdits: []string{"main.go", "cli.go", "vigoda.go"}, 149 PendingBuildSince: ts, 150 CurrentBuild: model.BuildRecord{ 151 Edits: []string{"main.go"}, 152 StartTime: ts, 153 }, 154 Endpoints: []string{"1.2.3.4:8080"}, 155 ResourceInfo: view.K8sResourceInfo{ 156 PodName: "vigoda-pod", 157 PodCreationTime: ts, 158 PodStatus: "Running", 159 RunStatus: v1alpha1.RuntimeStatusOK, 160 PodRestarts: 1, 161 SpanID: "vigoda:pod", 162 }, 163 }) 164 v.LogReader = newSpanLogReader("a-a-a-aaaaabe vigoda", "vigoda:pod", 165 "1\n2\n3\n4\nabe vigoda is now dead\n5\n6\n7\n8\n") 166 rtf.run("all the data at once", 70, 20, v, plainVs) 167 rtf.run("all the data at once 50w", 50, 20, v, plainVs) 168 rtf.run("all the data at once 10w", 10, 20, v, plainVs) 169 170 v = newView(view.Resource{ 171 Name: "vigoda", 172 LastDeployTime: ts, 173 BuildHistory: []model.BuildRecord{{ 174 Edits: []string{"main.go", "cli.go"}, 175 FinishTime: ts, 176 StartTime: ts.Add(-1400 * time.Millisecond), 177 }}, 178 ResourceInfo: view.K8sResourceInfo{ 179 PodName: "vigoda-pod", 180 PodCreationTime: ts, 181 PodStatus: "Running", 182 RunStatus: v1alpha1.RuntimeStatusOK, 183 PodRestarts: 1, 184 SpanID: "vigoda:pod", 185 }, 186 Endpoints: []string{"1.2.3.4:8080"}, 187 }) 188 v.LogReader = newSpanLogReader("vigoda", "vigoda:pod", 189 `abe vigoda is crashing 190 oh noooooooooooooooooo nooooooooooo noooooooooooo nooooooooooo 191 oh noooooooooooooooooo nooooooooooo noooooooooooo nooooooooooo nooooooooooo noooooooooooo nooooooooooo 192 oh noooooooooooooooooo nooooooooooo noooooooooooo nooooooooooo 193 oh noooooooooooooooooo nooooooooooo noooooooooooo nooooooooooo nooooooooooo noooooooooooo nooooooooooo nooooooooooo noooooooooooo nooooooooooo 194 oh noooooooooooooooooo nooooooooooo noooooooooooo nooooooooooo`) 195 rtf.run("pod log with inline wrapping", 70, 20, v, plainVs) 196 197 v = newView(view.Resource{ 198 Name: model.UnresourcedYAMLManifestName, 199 BuildHistory: []model.BuildRecord{{ 200 FinishTime: ts, 201 StartTime: ts.Add(-1400 * time.Millisecond), 202 }}, 203 LastDeployTime: ts, 204 ResourceInfo: view.YAMLResourceInfo{ 205 K8sDisplayNames: []string{"sancho:deployment"}, 206 }, 207 }) 208 rtf.run("no collapse unresourced yaml manifest", 70, 20, v, plainVs) 209 rtf.run("default collapse unresourced yaml manifest", 70, 20, v, fakeViewState(1, view.CollapseAuto)) 210 211 alertVs := plainVs 212 alertVs.AlertMessage = "this is only a test" 213 rtf.run("alert message", 70, 20, v, alertVs) 214 215 v = newView(view.Resource{ 216 Name: "vigoda", 217 CurrentBuild: model.BuildRecord{ 218 StartTime: ts.Add(-5 * time.Second), 219 Edits: []string{"main.go"}, 220 }, 221 ResourceInfo: view.K8sResourceInfo{}, 222 }) 223 rtf.run("build in progress", 70, 20, v, plainVs) 224 225 v = newView(view.Resource{ 226 Name: "vigoda", 227 PendingBuildSince: ts.Add(-5 * time.Second), 228 PendingBuildEdits: []string{"main.go"}, 229 ResourceInfo: view.K8sResourceInfo{ 230 RunStatus: v1alpha1.RuntimeStatusPending, 231 }, 232 }) 233 rtf.run("pending build", 70, 20, v, plainVs) 234 235 v = newView(view.Resource{ 236 Name: "vigoda", 237 LastDeployTime: ts.Add(-5 * time.Second), 238 BuildHistory: []model.BuildRecord{{ 239 Edits: []string{"abbot.go", "costello.go", "harold.go"}, 240 }}, 241 ResourceInfo: view.K8sResourceInfo{ 242 RunStatus: v1alpha1.RuntimeStatusPending, 243 }, 244 }) 245 rtf.run("edited files narrow term", 60, 20, v, plainVs) 246 rtf.run("edited files normal term", 80, 20, v, plainVs) 247 rtf.run("edited files wide term", 120, 20, v, plainVs) 248 } 249 250 func TestRenderTiltLog(t *testing.T) { 251 rtf := newRendererTestFixture(t) 252 253 v := newView() 254 v.LogReader = newLogReader(strings.Repeat("abcdefg", 30)) 255 256 vs := fakeViewState(0, view.CollapseNo) 257 258 rtf.run("tilt log", 70, 20, v, vs) 259 260 vs.TiltLogState = view.TiltLogHalfScreen 261 rtf.run("tilt log half screen", 70, 20, v, vs) 262 263 vs.TiltLogState = view.TiltLogFullScreen 264 rtf.run("tilt log full screen", 70, 20, v, vs) 265 } 266 267 func TestRenderNarrationMessage(t *testing.T) { 268 rtf := newRendererTestFixture(t) 269 270 v := newView() 271 vs := view.ViewState{ 272 ShowNarration: true, 273 NarrationMessage: "hi mom", 274 } 275 276 rtf.run("narration message", 60, 20, v, vs) 277 } 278 279 func TestAutoCollapseModes(t *testing.T) { 280 rtf := newRendererTestFixture(t) 281 282 goodView := newView(view.Resource{ 283 Name: "vigoda", 284 ResourceInfo: view.K8sResourceInfo{}, 285 }) 286 badView := newView(view.Resource{ 287 Name: "vigoda", 288 BuildHistory: []model.BuildRecord{{ 289 FinishTime: time.Now(), 290 Error: fmt.Errorf("oh no the build failed"), 291 SpanID: "vigoda:1", 292 }}, 293 ResourceInfo: view.K8sResourceInfo{}, 294 }) 295 badView.LogReader = newSpanLogReader("vigoda", "vigoda:1", 296 "1\n2\n3\nthe compiler did not understand!\n5\n6\n7\n8\n") 297 298 autoVS := fakeViewState(1, view.CollapseAuto) 299 collapseYesVS := fakeViewState(1, view.CollapseYes) 300 collapseNoVS := fakeViewState(1, view.CollapseNo) 301 rtf.run("collapse-auto-good", 70, 20, goodView, autoVS) 302 rtf.run("collapse-auto-bad", 70, 20, badView, autoVS) 303 rtf.run("collapse-no-good", 70, 20, goodView, collapseNoVS) 304 rtf.run("collapse-yes-bad", 70, 20, badView, collapseYesVS) 305 } 306 307 func TestPodPending(t *testing.T) { 308 rtf := newRendererTestFixture(t) 309 ts := time.Now().Add(-30 * time.Second) 310 311 v := newView(view.Resource{ 312 Name: "vigoda", 313 BuildHistory: []model.BuildRecord{{ 314 StartTime: ts, 315 FinishTime: ts, 316 SpanID: "vigoda:1", 317 }}, 318 ResourceInfo: view.K8sResourceInfo{ 319 PodName: "vigoda-pod", 320 SpanID: "vigoda:pod", 321 PodStatus: "", 322 }, 323 LastDeployTime: ts, 324 }) 325 logStore := logstore.NewLogStore() 326 appendSpanLog(logStore, "vigoda", "vigoda:1", `STEP 1/2 — Building Dockerfile: [gcr.io/windmill-public-containers/servantes/snack] 327 │ Tarring context… 328 │ Applying via kubectl 329 ╎ Created tarball (size: 11 kB) 330 │ Building image 331 `) 332 appendSpanLog(logStore, "vigoda", "vigoda:pod", "serving on 8080") 333 v.LogReader = logstore.NewReader(&sync.RWMutex{}, logStore) 334 vs := fakeViewState(1, view.CollapseAuto) 335 336 rtf.run("pending pod no status", 80, 20, v, vs) 337 assert.Equal(t, statusDisplay{color: cPending}, 338 combinedStatus(v.Resources[0])) 339 340 v.Resources[0].ResourceInfo = view.K8sResourceInfo{ 341 PodCreationTime: ts, 342 PodStatus: "Pending", 343 RunStatus: v1alpha1.RuntimeStatusPending, 344 } 345 rtf.run("pending pod pending status", 80, 20, v, vs) 346 assert.Equal(t, statusDisplay{color: cPending, spinner: true}, 347 combinedStatus(v.Resources[0])) 348 } 349 350 func TestNonCrashingPodNoInlineCrashLog(t *testing.T) { 351 rtf := newRendererTestFixture(t) 352 ts := time.Now().Add(-30 * time.Second) 353 354 v := newView(view.Resource{ 355 Name: "vigoda", 356 Endpoints: []string{"1.2.3.4:8080"}, 357 BuildHistory: []model.BuildRecord{{ 358 SpanID: "vigoda:1", 359 StartTime: ts, 360 FinishTime: ts, 361 }}, 362 ResourceInfo: view.K8sResourceInfo{ 363 PodName: "vigoda-pod", 364 PodStatus: "Running", 365 RunStatus: v1alpha1.RuntimeStatusOK, 366 SpanID: "vigoda:pod", 367 PodUpdateStartTime: ts, 368 PodCreationTime: ts.Add(-time.Minute), 369 }, 370 LastDeployTime: ts, 371 }) 372 373 logStore := logstore.NewLogStore() 374 appendSpanLog(logStore, "vigoda", "vigoda:1", 375 "Building (1/2)\nBuilding (2/2)\n") 376 appendSpanLog(logStore, "vigoda", "vigoda:pod", 377 "Something's maybe wrong idk") 378 v.LogReader = logstore.NewReader(&sync.RWMutex{}, logStore) 379 380 vs := fakeViewState(1, view.CollapseAuto) 381 rtf.run("non-crashing pod displays no logs inline even if crash log if present", 70, 20, v, vs) 382 } 383 384 func TestCompletedPod(t *testing.T) { 385 rtf := newRendererTestFixture(t) 386 ts := time.Now().Add(-30 * time.Second) 387 388 v := newView(view.Resource{ 389 Name: "vigoda", 390 Endpoints: []string{"1.2.3.4:8080"}, 391 BuildHistory: []model.BuildRecord{{ 392 SpanID: "vigoda:1", 393 StartTime: ts, 394 FinishTime: ts, 395 }}, 396 ResourceInfo: view.K8sResourceInfo{ 397 PodName: "vigoda-pod", 398 PodStatus: "Completed", 399 RunStatus: v1alpha1.RuntimeStatusOK, 400 PodUpdateStartTime: ts, 401 PodCreationTime: ts.Add(-time.Minute), 402 }, 403 LastDeployTime: ts, 404 }) 405 v.LogReader = newSpanLogReader("vigoda", "vigoda:1", 406 "Building (1/2)\nBuilding (2/2)\n") 407 vs := fakeViewState(1, view.CollapseAuto) 408 rtf.run("Completed is a good status", 70, 20, v, vs) 409 } 410 411 func TestBrackets(t *testing.T) { 412 rtf := newRendererTestFixture(t) 413 ts := time.Now().Add(-30 * time.Second) 414 415 v := newView(view.Resource{ 416 Name: "[vigoda]", 417 BuildHistory: []model.BuildRecord{{ 418 StartTime: ts, 419 FinishTime: ts, 420 }}, 421 ResourceInfo: view.K8sResourceInfo{ 422 PodName: "vigoda-pod", 423 PodStatus: "Running", 424 RunStatus: v1alpha1.RuntimeStatusOK, 425 PodCreationTime: ts, 426 }, 427 LastDeployTime: ts, 428 }) 429 v.LogReader = newLogReader(`[build] This line should be prefixed with 'build' 430 [hello world] This line should be prefixed with [hello world] 431 [hello world] this line too 432 `) 433 434 vs := fakeViewState(1, view.CollapseNo) 435 436 rtf.run("text in brackets", 80, 20, v, vs) 437 } 438 439 func TestPendingBuildInManualTriggerMode(t *testing.T) { 440 rtf := newRendererTestFixture(t) 441 ts := time.Now().Add(-30 * time.Second) 442 v := newView(view.Resource{ 443 Name: "vigoda", 444 PendingBuildSince: ts.Add(-5 * time.Second), 445 PendingBuildEdits: []string{"main.go"}, 446 TriggerMode: model.TriggerModeManualWithAutoInit, 447 ResourceInfo: view.K8sResourceInfo{}, 448 }) 449 vs := fakeViewState(1, view.CollapseNo) 450 rtf.run("pending build with manual trigger", 80, 20, v, vs) 451 } 452 453 func TestBuildHistory(t *testing.T) { 454 rtf := newRendererTestFixture(t) 455 ts := time.Now().Add(-30 * time.Second) 456 457 v := newView(view.Resource{ 458 Name: "vigoda", 459 BuildHistory: []model.BuildRecord{ 460 { 461 Edits: []string{"main.go"}, 462 StartTime: ts.Add(-10 * time.Second), 463 FinishTime: ts, 464 }, 465 { 466 Reason: model.BuildReasonFlagInit, 467 StartTime: ts.Add(-2 * time.Minute), 468 FinishTime: ts.Add(-2 * time.Minute).Add(5 * time.Second), 469 }, 470 }, 471 ResourceInfo: view.K8sResourceInfo{ 472 PodName: "vigoda-pod", 473 PodStatus: "Running", 474 RunStatus: v1alpha1.RuntimeStatusOK, 475 PodUpdateStartTime: ts, 476 PodCreationTime: ts.Add(-time.Minute), 477 }, 478 LastDeployTime: ts, 479 }) 480 vs := fakeViewState(1, view.CollapseNo) 481 rtf.run("multiple build history entries", 80, 20, v, vs) 482 } 483 484 func TestDockerComposeUpExpanded(t *testing.T) { 485 rtf := newRendererTestFixture(t) 486 487 now := time.Now() 488 v := newView(view.Resource{ 489 Name: "snack", 490 ResourceInfo: view.NewDCResourceInfo("running", testCID, "snack:dc", now.Add(-5*time.Second), v1alpha1.RuntimeStatusOK), 491 Endpoints: []string{"http://localhost:3000"}, 492 CurrentBuild: model.BuildRecord{ 493 StartTime: now.Add(-5 * time.Second), 494 Reason: model.BuildReasonFlagChangedFiles, 495 }, 496 }) 497 v.LogReader = newSpanLogReader("snack", "snack:dc", "hellllo") 498 499 vs := fakeViewState(1, view.CollapseNo) 500 rtf.run("docker-compose up expanded", 80, 20, v, vs) 501 } 502 503 func TestStatusBarDCRebuild(t *testing.T) { 504 rtf := newRendererTestFixture(t) 505 506 now := time.Now() 507 v := newView(view.Resource{ 508 Name: "snack", 509 ResourceInfo: view.NewDCResourceInfo("exited", testCID, "snack:dc", now.Add(-5*time.Second), v1alpha1.RuntimeStatusError), 510 CurrentBuild: model.BuildRecord{ 511 StartTime: now.Add(-5 * time.Second), 512 Reason: model.BuildReasonFlagChangedFiles, 513 }, 514 }) 515 v.LogReader = newSpanLogReader("snack", "snack:dc", "hellllo") 516 517 vs := fakeViewState(1, view.CollapseYes) 518 rtf.run("status bar after intentional DC restart", 60, 20, v, vs) 519 } 520 521 func TestDetectDCCrashExpanded(t *testing.T) { 522 rtf := newRendererTestFixture(t) 523 524 now := time.Now() 525 v := newView(view.Resource{ 526 Name: "snack", 527 ResourceInfo: view.NewDCResourceInfo("exited", testCID, "snack:dc", now.Add(-5*time.Second), v1alpha1.RuntimeStatusError), 528 }) 529 v.LogReader = newSpanLogReader("snack", "snack:dc", "hi im a crash") 530 531 vs := fakeViewState(1, view.CollapseNo) 532 rtf.run("detected docker compose build crash expanded", 80, 20, v, vs) 533 } 534 535 func TestDetectDCCrashNotExpanded(t *testing.T) { 536 rtf := newRendererTestFixture(t) 537 538 now := time.Now() 539 v := newView(view.Resource{ 540 Name: "snack", 541 ResourceInfo: view.NewDCResourceInfo("exited", testCID, "snack:dc", now.Add(-5*time.Second), v1alpha1.RuntimeStatusError), 542 }) 543 v.LogReader = newSpanLogReader("snack", "snack:dc", "hi im a crash") 544 545 vs := fakeViewState(1, view.CollapseYes) 546 rtf.run("detected docker compose build crash not expanded", 80, 20, v, vs) 547 } 548 549 func TestDetectDCCrashAutoExpand(t *testing.T) { 550 rtf := newRendererTestFixture(t) 551 552 now := time.Now() 553 v := newView(view.Resource{ 554 Name: "snack", 555 ResourceInfo: view.NewDCResourceInfo("exited", testCID, "snack:dc", now.Add(-5*time.Second), v1alpha1.RuntimeStatusError), 556 }) 557 v.LogReader = newSpanLogReader("snack", "snack:dc", "hi im a crash") 558 559 vs := fakeViewState(1, view.CollapseAuto) 560 rtf.run("detected docker compose build crash auto expand", 80, 20, v, vs) 561 } 562 563 func TestTiltfileResource(t *testing.T) { 564 rtf := newRendererTestFixture(t) 565 566 v := newView(view.Resource{ 567 Name: store.MainTiltfileManifestName, 568 IsTiltfile: true, 569 ResourceInfo: view.TiltfileResourceInfo{}, 570 }) 571 572 vs := fakeViewState(1, view.CollapseNo) 573 rtf.run("Tiltfile resource no run", 80, 20, v, vs) 574 575 now := time.Now() 576 v = newView(view.Resource{ 577 Name: store.MainTiltfileManifestName, 578 IsTiltfile: true, 579 ResourceInfo: view.TiltfileResourceInfo{}, 580 BuildHistory: []model.BuildRecord{ 581 { 582 StartTime: now.Add(-5 * time.Second), 583 FinishTime: now.Add(-4 * time.Second), 584 Reason: model.BuildReasonFlagInit, 585 SpanID: "tiltfile:1", 586 }, 587 }, 588 }) 589 rtf.run("Tiltfile resource first run", 80, 20, v, vs) 590 } 591 592 func TestTiltfileResourceWithWarning(t *testing.T) { 593 rtf := newRendererTestFixture(t) 594 now := time.Now() 595 v := newView(view.Resource{ 596 Name: store.MainTiltfileManifestName, 597 IsTiltfile: true, 598 ResourceInfo: view.TiltfileResourceInfo{}, 599 BuildHistory: []model.BuildRecord{ 600 { 601 Edits: []string{"Tiltfile"}, 602 StartTime: now.Add(-5 * time.Second), 603 FinishTime: now.Add(-4 * time.Second), 604 Reason: model.BuildReasonFlagConfig, 605 WarningCount: 2, 606 SpanID: "tiltfile:1", 607 }, 608 }, 609 }) 610 v.LogReader = newWarningLogReader( 611 store.MainTiltfileManifestName, 612 "tiltfile:1", 613 []string{"I am warning you\n", "Something is alarming here\n"}) 614 615 vs := fakeViewState(1, view.CollapseNo) 616 rtf.run("Tiltfile resource with warning", 80, 20, v, vs) 617 } 618 619 func TestTiltfileResourcePending(t *testing.T) { 620 rtf := newRendererTestFixture(t) 621 622 now := time.Now() 623 v := newView(view.Resource{ 624 Name: store.MainTiltfileManifestName, 625 IsTiltfile: true, 626 ResourceInfo: view.TiltfileResourceInfo{}, 627 CurrentBuild: model.BuildRecord{ 628 Edits: []string{"Tiltfile"}, 629 StartTime: now.Add(-5 * time.Second), 630 Reason: model.BuildReasonFlagConfig, 631 SpanID: "tiltfile:1", 632 }, 633 }) 634 v.LogReader = newSpanLogReader(store.MainTiltfileManifestName, "tiltfile:1", "Building...") 635 636 vs := fakeViewState(1, view.CollapseNo) 637 rtf.run("Tiltfile resource pending", 80, 20, v, vs) 638 } 639 640 func TestRenderEscapedNbsp(t *testing.T) { 641 rtf := newRendererTestFixture(t) 642 plainVs := fakeViewState(1, view.CollapseNo) 643 v := newView(view.Resource{ 644 Name: "vigoda", 645 BuildHistory: []model.BuildRecord{{ 646 FinishTime: time.Now(), 647 Error: fmt.Errorf("oh no the build failed"), 648 SpanID: "vigoda:1", 649 }}, 650 ResourceInfo: view.K8sResourceInfo{}, 651 }) 652 v.LogReader = newSpanLogReader("vigoda", "vigoda:1", "\xa0 NBSP!") 653 rtf.run("escaped nbsp", 70, 20, v, plainVs) 654 } 655 656 func TestLineWrappingInInlineError(t *testing.T) { 657 rtf := newRendererTestFixture(t) 658 vs := fakeViewState(1, view.CollapseNo) 659 lines := []string{} 660 for i := 0; i < 10; i++ { 661 lines = append(lines, fmt.Sprintf("line %d: %s", i, strings.Repeat("xxx ", 20))) 662 } 663 v := newView(view.Resource{ 664 Name: "vigoda", 665 BuildHistory: []model.BuildRecord{{ 666 FinishTime: time.Now(), 667 Error: fmt.Errorf("failure"), 668 SpanID: "vigoda:1", 669 }}, 670 ResourceInfo: view.K8sResourceInfo{}, 671 }) 672 v.LogReader = newSpanLogReader("vigoda", "vigoda:1", strings.Join(lines, "\n")) 673 rtf.run("line wrapping in inline error", 80, 40, v, vs) 674 } 675 676 func TestRenderTabView(t *testing.T) { 677 rtf := newRendererTestFixture(t) 678 679 vs := fakeViewState(1, view.CollapseAuto) 680 now := time.Now() 681 v := newView(view.Resource{ 682 Name: "vigoda", 683 BuildHistory: []model.BuildRecord{{ 684 StartTime: now.Add(-time.Minute), 685 FinishTime: now, 686 SpanID: "vigoda:1", 687 }}, 688 ResourceInfo: view.K8sResourceInfo{ 689 PodName: "vigoda-pod", 690 PodCreationTime: now, 691 PodStatus: "Running", 692 RunStatus: v1alpha1.RuntimeStatusOK, 693 SpanID: "vigoda:pod", 694 }, 695 LastDeployTime: now, 696 }) 697 logStore := logstore.NewLogStore() 698 appendSpanLog(logStore, "vigoda", "vigoda:1", 699 `STEP 1/2 — Building Dockerfile: [gcr.io/windmill-public-containers/servantes/snack] 700 │ Tarring context… 701 │ Applying via kubectl 702 ╎ Created tarball (size: 11 kB) 703 │ Building image 704 `) 705 appendSpanLog(logStore, "vigoda", "vigoda:pod", "serving on 8080") 706 v.LogReader = logstore.NewReader(&sync.RWMutex{}, logStore) 707 708 rtf.run("log tab default", 117, 20, v, vs) 709 710 vs.TabState = view.TabBuildLog 711 rtf.run("log tab build", 117, 20, v, vs) 712 713 vs.TabState = view.TabRuntimeLog 714 rtf.run("log tab pod", 117, 20, v, vs) 715 } 716 717 func TestPendingLocalResource(t *testing.T) { 718 rtf := newRendererTestFixture(t) 719 720 ts := time.Now().Add(-5 * time.Minute) 721 722 v := newView(view.Resource{ 723 Name: "yarn-add", 724 CurrentBuild: model.BuildRecord{ 725 StartTime: ts.Add(-5 * time.Second), 726 Edits: []string{"node.json"}, 727 }, 728 ResourceInfo: view.NewLocalResourceInfo(v1alpha1.RuntimeStatusPending, 0, model.LogSpanID("rt1")), 729 }) 730 731 vs := fakeViewState(1, view.CollapseAuto) 732 rtf.run("unfinished local resource", 80, 20, v, vs) 733 } 734 735 func TestFinishedLocalResource(t *testing.T) { 736 rtf := newRendererTestFixture(t) 737 738 v := newView(view.Resource{ 739 Name: "yarn-add", 740 BuildHistory: []model.BuildRecord{ 741 model.BuildRecord{FinishTime: time.Now()}, 742 }, 743 ResourceInfo: view.NewLocalResourceInfo(v1alpha1.RuntimeStatusNotApplicable, 0, model.LogSpanID("rt1")), 744 }) 745 746 vs := fakeViewState(1, view.CollapseAuto) 747 rtf.run("finished local resource", 80, 20, v, vs) 748 } 749 750 func TestFailedBuildLocalResource(t *testing.T) { 751 rtf := newRendererTestFixture(t) 752 753 v := newView(view.Resource{ 754 Name: "yarn-add", 755 BuildHistory: []model.BuildRecord{ 756 model.BuildRecord{ 757 FinishTime: time.Now(), 758 Error: fmt.Errorf("help i'm trapped in an error factory"), 759 SpanID: "build:1", 760 }, 761 }, 762 ResourceInfo: view.LocalResourceInfo{}, 763 }) 764 v.LogReader = newSpanLogReader("yarn-add", "build:1", 765 "1\n2\n3\nthe compiler did not understand!\n5\n6\n7\n8\n") 766 767 vs := fakeViewState(1, view.CollapseAuto) 768 rtf.run("failed build local resource", 80, 20, v, vs) 769 } 770 771 func TestLocalResourceErroredServe(t *testing.T) { 772 rtf := newRendererTestFixture(t) 773 774 v := newView(view.Resource{ 775 Name: "yarn-add", 776 BuildHistory: []model.BuildRecord{ 777 model.BuildRecord{FinishTime: time.Now()}, 778 }, 779 ResourceInfo: view.NewLocalResourceInfo(v1alpha1.RuntimeStatusError, 0, model.LogSpanID("rt1")), 780 }) 781 782 vs := fakeViewState(1, view.CollapseAuto) 783 rtf.run("local resource errored serve", 80, 20, v, vs) 784 } 785 786 type rendererTestFixture struct { 787 i rty.InteractiveTester 788 } 789 790 func newRendererTestFixture(t rty.ErrorReporter) rendererTestFixture { 791 return rendererTestFixture{ 792 i: rty.NewInteractiveTester(t, screen), 793 } 794 } 795 796 func (rtf rendererTestFixture) run(name string, w int, h int, v view.View, vs view.ViewState) { 797 rtf.i.T().Helper() 798 799 // Assert that the view is serializable 800 serialized, err := json.Marshal(v) 801 if err != nil { 802 rtf.i.T().Errorf("Malformed view: not serializable: %v\nView: %+q\n", err, v) 803 } 804 805 // Then, assert that the view can be marshaled back. 806 if !json.Valid(serialized) { 807 rtf.i.T().Errorf("Malformed view: bad serialization: %s", string(serialized)) 808 809 } 810 811 r := NewRenderer(clockForTest) 812 r.rty = rty.NewRTY(tcell.NewSimulationScreen(""), rtf.i.T()) 813 c := r.layout(v, vs) 814 rtf.i.Run(name, w, h, c) 815 } 816 817 var screen tcell.Screen 818 819 func TestMain(m *testing.M) { 820 rty.InitScreenAndRun(m, &screen) 821 } 822 823 func fakeViewState(count int, collapse view.CollapseState) view.ViewState { 824 vs := view.ViewState{} 825 for i := 0; i < count; i++ { 826 vs.Resources = append(vs.Resources, view.ResourceViewState{ 827 CollapseState: collapse, 828 }) 829 } 830 return vs 831 } 832 833 func newLogReader(msg string) logstore.Reader { 834 store := logstore.NewLogStoreForTesting(msg) 835 return logstore.NewReader(&sync.RWMutex{}, store) 836 } 837 838 type testLogAction struct { 839 mn model.ManifestName 840 spanID logstore.SpanID 841 time time.Time 842 msg string 843 level logger.Level 844 fields logger.Fields 845 } 846 847 func (e testLogAction) Fields() logger.Fields { 848 return e.fields 849 } 850 851 func (e testLogAction) Message() []byte { 852 return []byte(e.msg) 853 } 854 855 func (e testLogAction) Level() logger.Level { 856 if e.level == (logger.Level{}) { 857 return logger.InfoLvl 858 } 859 return e.level 860 } 861 862 func (e testLogAction) Time() time.Time { 863 return e.time 864 } 865 866 func (e testLogAction) ManifestName() model.ManifestName { 867 return e.mn 868 } 869 870 func (e testLogAction) SpanID() logstore.SpanID { 871 return e.spanID 872 }