github.com/ungtb10d/cli/v2@v2.0.0-20221110210412-98537dd9d6a1/pkg/cmd/run/view/view_test.go (about) 1 package view 2 3 import ( 4 "archive/zip" 5 "bytes" 6 "fmt" 7 "io" 8 "net/http" 9 "testing" 10 "time" 11 12 "github.com/MakeNowJust/heredoc" 13 "github.com/ungtb10d/cli/v2/internal/browser" 14 "github.com/ungtb10d/cli/v2/internal/ghrepo" 15 "github.com/ungtb10d/cli/v2/pkg/cmd/run/shared" 16 workflowShared "github.com/ungtb10d/cli/v2/pkg/cmd/workflow/shared" 17 "github.com/ungtb10d/cli/v2/pkg/cmdutil" 18 "github.com/ungtb10d/cli/v2/pkg/httpmock" 19 "github.com/ungtb10d/cli/v2/pkg/iostreams" 20 "github.com/ungtb10d/cli/v2/pkg/prompt" 21 "github.com/google/shlex" 22 "github.com/stretchr/testify/assert" 23 ) 24 25 func TestNewCmdView(t *testing.T) { 26 tests := []struct { 27 name string 28 cli string 29 tty bool 30 wants ViewOptions 31 wantsErr bool 32 }{ 33 { 34 name: "blank nontty", 35 wantsErr: true, 36 }, 37 { 38 name: "blank tty", 39 tty: true, 40 wants: ViewOptions{ 41 Prompt: true, 42 }, 43 }, 44 { 45 name: "web tty", 46 tty: true, 47 cli: "--web", 48 wants: ViewOptions{ 49 Prompt: true, 50 Web: true, 51 }, 52 }, 53 { 54 name: "web nontty", 55 cli: "1234 --web", 56 wants: ViewOptions{ 57 Web: true, 58 RunID: "1234", 59 }, 60 }, 61 { 62 name: "disallow web and log", 63 tty: true, 64 cli: "-w --log", 65 wantsErr: true, 66 }, 67 { 68 name: "disallow log and log-failed", 69 tty: true, 70 cli: "--log --log-failed", 71 wantsErr: true, 72 }, 73 { 74 name: "exit status", 75 cli: "--exit-status 1234", 76 wants: ViewOptions{ 77 RunID: "1234", 78 ExitStatus: true, 79 }, 80 }, 81 { 82 name: "verbosity", 83 cli: "-v", 84 tty: true, 85 wants: ViewOptions{ 86 Verbose: true, 87 Prompt: true, 88 }, 89 }, 90 { 91 name: "with arg nontty", 92 cli: "1234", 93 wants: ViewOptions{ 94 RunID: "1234", 95 }, 96 }, 97 { 98 name: "job id passed", 99 cli: "--job 1234", 100 wants: ViewOptions{ 101 JobID: "1234", 102 }, 103 }, 104 { 105 name: "log passed", 106 tty: true, 107 cli: "--log", 108 wants: ViewOptions{ 109 Prompt: true, 110 Log: true, 111 }, 112 }, 113 { 114 name: "tolerates both run and job id", 115 cli: "1234 --job 4567", 116 wants: ViewOptions{ 117 JobID: "4567", 118 }, 119 }, 120 } 121 122 for _, tt := range tests { 123 t.Run(tt.name, func(t *testing.T) { 124 ios, _, _, _ := iostreams.Test() 125 ios.SetStdinTTY(tt.tty) 126 ios.SetStdoutTTY(tt.tty) 127 128 f := &cmdutil.Factory{ 129 IOStreams: ios, 130 } 131 132 argv, err := shlex.Split(tt.cli) 133 assert.NoError(t, err) 134 135 var gotOpts *ViewOptions 136 cmd := NewCmdView(f, func(opts *ViewOptions) error { 137 gotOpts = opts 138 return nil 139 }) 140 cmd.SetArgs(argv) 141 cmd.SetIn(&bytes.Buffer{}) 142 cmd.SetOut(io.Discard) 143 cmd.SetErr(io.Discard) 144 145 _, err = cmd.ExecuteC() 146 if tt.wantsErr { 147 assert.Error(t, err) 148 return 149 } 150 151 assert.NoError(t, err) 152 153 assert.Equal(t, tt.wants.RunID, gotOpts.RunID) 154 assert.Equal(t, tt.wants.Prompt, gotOpts.Prompt) 155 assert.Equal(t, tt.wants.ExitStatus, gotOpts.ExitStatus) 156 assert.Equal(t, tt.wants.Verbose, gotOpts.Verbose) 157 }) 158 } 159 } 160 161 func TestViewRun(t *testing.T) { 162 tests := []struct { 163 name string 164 httpStubs func(*httpmock.Registry) 165 askStubs func(*prompt.AskStubber) 166 opts *ViewOptions 167 tty bool 168 wantErr bool 169 wantOut string 170 browsedURL string 171 errMsg string 172 }{ 173 { 174 name: "associate with PR", 175 tty: true, 176 opts: &ViewOptions{ 177 RunID: "3", 178 Prompt: false, 179 }, 180 httpStubs: func(reg *httpmock.Registry) { 181 reg.Register( 182 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 183 httpmock.JSONResponse(shared.SuccessfulRun)) 184 reg.Register( 185 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 186 httpmock.JSONResponse(shared.TestWorkflow)) 187 reg.Register( 188 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), 189 httpmock.StringResponse(`{}`)) 190 reg.Register( 191 httpmock.GraphQL(`query PullRequestForRun`), 192 httpmock.StringResponse(`{"data": { 193 "repository": { 194 "pullRequests": { 195 "nodes": [ 196 {"number": 2898, 197 "headRepository": { 198 "owner": { 199 "login": "OWNER" 200 }, 201 "name": "REPO"}} 202 ]}}}}`)) 203 reg.Register( 204 httpmock.REST("GET", "runs/3/jobs"), 205 httpmock.JSONResponse(shared.JobsPayload{ 206 Jobs: []shared.Job{ 207 shared.SuccessfulJob, 208 }, 209 })) 210 reg.Register( 211 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 212 httpmock.JSONResponse([]shared.Annotation{})) 213 }, 214 wantOut: "\n✓ trunk CI #2898 · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", 215 }, 216 { 217 name: "exit status, failed run", 218 opts: &ViewOptions{ 219 RunID: "1234", 220 ExitStatus: true, 221 }, 222 httpStubs: func(reg *httpmock.Registry) { 223 reg.Register( 224 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 225 httpmock.JSONResponse(shared.FailedRun)) 226 reg.Register( 227 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 228 httpmock.JSONResponse(shared.TestWorkflow)) 229 reg.Register( 230 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/artifacts"), 231 httpmock.StringResponse(`{}`)) 232 reg.Register( 233 httpmock.GraphQL(`query PullRequestForRun`), 234 httpmock.StringResponse(``)) 235 reg.Register( 236 httpmock.REST("GET", "runs/1234/jobs"), 237 httpmock.JSONResponse(shared.JobsPayload{ 238 Jobs: []shared.Job{ 239 shared.FailedJob, 240 }, 241 })) 242 reg.Register( 243 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), 244 httpmock.JSONResponse(shared.FailedJobAnnotations)) 245 }, 246 wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", 247 wantErr: true, 248 }, 249 { 250 name: "with artifacts", 251 opts: &ViewOptions{ 252 RunID: "3", 253 }, 254 httpStubs: func(reg *httpmock.Registry) { 255 reg.Register( 256 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 257 httpmock.JSONResponse(shared.SuccessfulRun)) 258 reg.Register( 259 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), 260 httpmock.JSONResponse(map[string][]shared.Artifact{ 261 "artifacts": { 262 shared.Artifact{Name: "artifact-1", Expired: false}, 263 shared.Artifact{Name: "artifact-2", Expired: true}, 264 shared.Artifact{Name: "artifact-3", Expired: false}, 265 }, 266 })) 267 reg.Register( 268 httpmock.GraphQL(`query PullRequestForRun`), 269 httpmock.StringResponse(``)) 270 reg.Register( 271 httpmock.REST("GET", "runs/3/jobs"), 272 httpmock.JSONResponse(shared.JobsPayload{})) 273 reg.Register( 274 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 275 httpmock.JSONResponse(shared.TestWorkflow)) 276 }, 277 wantOut: heredoc.Doc(` 278 279 ✓ trunk CI · 3 280 Triggered via push about 59 minutes ago 281 282 JOBS 283 284 285 ARTIFACTS 286 artifact-1 287 artifact-2 (expired) 288 artifact-3 289 290 For more information about a job, try: gh run view --job=<job-id> 291 View this run on GitHub: https://github.com/runs/3 292 `), 293 }, 294 { 295 name: "exit status, successful run", 296 opts: &ViewOptions{ 297 RunID: "3", 298 ExitStatus: true, 299 }, 300 httpStubs: func(reg *httpmock.Registry) { 301 reg.Register( 302 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 303 httpmock.JSONResponse(shared.SuccessfulRun)) 304 reg.Register( 305 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), 306 httpmock.StringResponse(`{}`)) 307 reg.Register( 308 httpmock.GraphQL(`query PullRequestForRun`), 309 httpmock.StringResponse(``)) 310 reg.Register( 311 httpmock.REST("GET", "runs/3/jobs"), 312 httpmock.JSONResponse(shared.JobsPayload{ 313 Jobs: []shared.Job{ 314 shared.SuccessfulJob, 315 }, 316 })) 317 reg.Register( 318 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 319 httpmock.JSONResponse([]shared.Annotation{})) 320 reg.Register( 321 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 322 httpmock.JSONResponse(shared.TestWorkflow)) 323 }, 324 wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", 325 }, 326 { 327 name: "verbose", 328 tty: true, 329 opts: &ViewOptions{ 330 RunID: "1234", 331 Prompt: false, 332 Verbose: true, 333 }, 334 httpStubs: func(reg *httpmock.Registry) { 335 reg.Register( 336 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 337 httpmock.JSONResponse(shared.FailedRun)) 338 reg.Register( 339 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/artifacts"), 340 httpmock.StringResponse(`{}`)) 341 reg.Register( 342 httpmock.GraphQL(`query PullRequestForRun`), 343 httpmock.StringResponse(``)) 344 reg.Register( 345 httpmock.REST("GET", "runs/1234/jobs"), 346 httpmock.JSONResponse(shared.JobsPayload{ 347 Jobs: []shared.Job{ 348 shared.SuccessfulJob, 349 shared.FailedJob, 350 }, 351 })) 352 reg.Register( 353 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 354 httpmock.JSONResponse([]shared.Annotation{})) 355 reg.Register( 356 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), 357 httpmock.JSONResponse(shared.FailedJobAnnotations)) 358 reg.Register( 359 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 360 httpmock.JSONResponse(shared.TestWorkflow)) 361 }, 362 wantOut: "\nX trunk CI · 1234\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nTo see what failed, try: gh run view 1234 --log-failed\nView this run on GitHub: https://github.com/runs/1234\n", 363 }, 364 { 365 name: "prompts for choice, one job", 366 tty: true, 367 httpStubs: func(reg *httpmock.Registry) { 368 reg.Register( 369 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 370 httpmock.JSONResponse(shared.RunsPayload{ 371 WorkflowRuns: shared.TestRuns, 372 })) 373 reg.Register( 374 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 375 httpmock.JSONResponse(shared.SuccessfulRun)) 376 reg.Register( 377 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), 378 httpmock.StringResponse(`{}`)) 379 reg.Register( 380 httpmock.GraphQL(`query PullRequestForRun`), 381 httpmock.StringResponse(``)) 382 reg.Register( 383 httpmock.REST("GET", "runs/3/jobs"), 384 httpmock.JSONResponse(shared.JobsPayload{ 385 Jobs: []shared.Job{ 386 shared.SuccessfulJob, 387 }, 388 })) 389 reg.Register( 390 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 391 httpmock.JSONResponse([]shared.Annotation{})) 392 reg.Register( 393 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 394 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 395 Workflows: []workflowShared.Workflow{ 396 shared.TestWorkflow, 397 }, 398 })) 399 reg.Register( 400 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 401 httpmock.JSONResponse(shared.TestWorkflow)) 402 }, 403 askStubs: func(as *prompt.AskStubber) { 404 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 405 as.StubOne(2) 406 }, 407 opts: &ViewOptions{ 408 Prompt: true, 409 }, 410 wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\n\nFor more information about the job, try: gh run view --job=10\nView this run on GitHub: https://github.com/runs/3\n", 411 }, 412 { 413 name: "interactive with log", 414 tty: true, 415 opts: &ViewOptions{ 416 Prompt: true, 417 Log: true, 418 }, 419 httpStubs: func(reg *httpmock.Registry) { 420 reg.Register( 421 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 422 httpmock.JSONResponse(shared.RunsPayload{ 423 WorkflowRuns: shared.TestRuns, 424 })) 425 reg.Register( 426 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 427 httpmock.JSONResponse(shared.SuccessfulRun)) 428 reg.Register( 429 httpmock.REST("GET", "runs/3/jobs"), 430 httpmock.JSONResponse(shared.JobsPayload{ 431 Jobs: []shared.Job{ 432 shared.SuccessfulJob, 433 shared.FailedJob, 434 }, 435 })) 436 reg.Register( 437 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), 438 httpmock.FileResponse("./fixtures/run_log.zip")) 439 reg.Register( 440 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 441 httpmock.JSONResponse(shared.TestWorkflow)) 442 reg.Register( 443 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 444 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 445 Workflows: []workflowShared.Workflow{ 446 shared.TestWorkflow, 447 }, 448 })) 449 }, 450 askStubs: func(as *prompt.AskStubber) { 451 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 452 as.StubOne(2) 453 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 454 as.StubOne(1) 455 }, 456 wantOut: coolJobRunLogOutput, 457 }, 458 { 459 name: "noninteractive with log", 460 opts: &ViewOptions{ 461 JobID: "10", 462 Log: true, 463 }, 464 httpStubs: func(reg *httpmock.Registry) { 465 reg.Register( 466 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), 467 httpmock.JSONResponse(shared.SuccessfulJob)) 468 reg.Register( 469 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 470 httpmock.JSONResponse(shared.SuccessfulRun)) 471 reg.Register( 472 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), 473 httpmock.FileResponse("./fixtures/run_log.zip")) 474 reg.Register( 475 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 476 httpmock.JSONResponse(shared.TestWorkflow)) 477 }, 478 wantOut: coolJobRunLogOutput, 479 }, 480 { 481 name: "interactive with run log", 482 tty: true, 483 opts: &ViewOptions{ 484 Prompt: true, 485 Log: true, 486 }, 487 httpStubs: func(reg *httpmock.Registry) { 488 reg.Register( 489 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 490 httpmock.JSONResponse(shared.RunsPayload{ 491 WorkflowRuns: shared.TestRuns, 492 })) 493 reg.Register( 494 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 495 httpmock.JSONResponse(shared.SuccessfulRun)) 496 reg.Register( 497 httpmock.REST("GET", "runs/3/jobs"), 498 httpmock.JSONResponse(shared.JobsPayload{ 499 Jobs: []shared.Job{ 500 shared.SuccessfulJob, 501 shared.FailedJob, 502 }, 503 })) 504 reg.Register( 505 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), 506 httpmock.FileResponse("./fixtures/run_log.zip")) 507 reg.Register( 508 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 509 httpmock.JSONResponse(shared.TestWorkflow)) 510 reg.Register( 511 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 512 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 513 Workflows: []workflowShared.Workflow{ 514 shared.TestWorkflow, 515 }, 516 })) 517 }, 518 askStubs: func(as *prompt.AskStubber) { 519 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 520 as.StubOne(2) 521 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 522 as.StubOne(0) 523 }, 524 wantOut: expectedRunLogOutput, 525 }, 526 { 527 name: "noninteractive with run log", 528 tty: true, 529 opts: &ViewOptions{ 530 RunID: "3", 531 Log: true, 532 }, 533 httpStubs: func(reg *httpmock.Registry) { 534 reg.Register( 535 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 536 httpmock.JSONResponse(shared.SuccessfulRun)) 537 reg.Register( 538 httpmock.REST("GET", "runs/3/jobs"), 539 httpmock.JSONResponse(shared.JobsPayload{ 540 Jobs: []shared.Job{ 541 shared.SuccessfulJob, 542 shared.FailedJob, 543 }, 544 })) 545 reg.Register( 546 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/logs"), 547 httpmock.FileResponse("./fixtures/run_log.zip")) 548 reg.Register( 549 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 550 httpmock.JSONResponse(shared.TestWorkflow)) 551 }, 552 wantOut: expectedRunLogOutput, 553 }, 554 { 555 name: "interactive with log-failed", 556 tty: true, 557 opts: &ViewOptions{ 558 Prompt: true, 559 LogFailed: true, 560 }, 561 httpStubs: func(reg *httpmock.Registry) { 562 reg.Register( 563 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 564 httpmock.JSONResponse(shared.RunsPayload{ 565 WorkflowRuns: shared.TestRuns, 566 })) 567 reg.Register( 568 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 569 httpmock.JSONResponse(shared.FailedRun)) 570 reg.Register( 571 httpmock.REST("GET", "runs/1234/jobs"), 572 httpmock.JSONResponse(shared.JobsPayload{ 573 Jobs: []shared.Job{ 574 shared.SuccessfulJob, 575 shared.FailedJob, 576 }, 577 })) 578 reg.Register( 579 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), 580 httpmock.FileResponse("./fixtures/run_log.zip")) 581 reg.Register( 582 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 583 httpmock.JSONResponse(shared.TestWorkflow)) 584 reg.Register( 585 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 586 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 587 Workflows: []workflowShared.Workflow{ 588 shared.TestWorkflow, 589 }, 590 })) 591 }, 592 askStubs: func(as *prompt.AskStubber) { 593 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 594 as.StubOne(4) 595 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 596 as.StubOne(2) 597 }, 598 wantOut: quuxTheBarfLogOutput, 599 }, 600 { 601 name: "noninteractive with log-failed", 602 opts: &ViewOptions{ 603 JobID: "20", 604 LogFailed: true, 605 }, 606 httpStubs: func(reg *httpmock.Registry) { 607 reg.Register( 608 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), 609 httpmock.JSONResponse(shared.FailedJob)) 610 reg.Register( 611 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 612 httpmock.JSONResponse(shared.FailedRun)) 613 reg.Register( 614 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), 615 httpmock.FileResponse("./fixtures/run_log.zip")) 616 reg.Register( 617 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 618 httpmock.JSONResponse(shared.TestWorkflow)) 619 }, 620 wantOut: quuxTheBarfLogOutput, 621 }, 622 { 623 name: "interactive with run log-failed", 624 tty: true, 625 opts: &ViewOptions{ 626 Prompt: true, 627 LogFailed: true, 628 }, 629 httpStubs: func(reg *httpmock.Registry) { 630 reg.Register( 631 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 632 httpmock.JSONResponse(shared.RunsPayload{ 633 WorkflowRuns: shared.TestRuns, 634 })) 635 reg.Register( 636 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 637 httpmock.JSONResponse(shared.FailedRun)) 638 reg.Register( 639 httpmock.REST("GET", "runs/1234/jobs"), 640 httpmock.JSONResponse(shared.JobsPayload{ 641 Jobs: []shared.Job{ 642 shared.SuccessfulJob, 643 shared.FailedJob, 644 }, 645 })) 646 reg.Register( 647 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), 648 httpmock.FileResponse("./fixtures/run_log.zip")) 649 reg.Register( 650 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 651 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 652 Workflows: []workflowShared.Workflow{ 653 shared.TestWorkflow, 654 }, 655 })) 656 reg.Register( 657 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 658 httpmock.JSONResponse(shared.TestWorkflow)) 659 }, 660 askStubs: func(as *prompt.AskStubber) { 661 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 662 as.StubOne(4) 663 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 664 as.StubOne(0) 665 }, 666 wantOut: quuxTheBarfLogOutput, 667 }, 668 { 669 name: "noninteractive with run log-failed", 670 tty: true, 671 opts: &ViewOptions{ 672 RunID: "1234", 673 LogFailed: true, 674 }, 675 httpStubs: func(reg *httpmock.Registry) { 676 reg.Register( 677 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234"), 678 httpmock.JSONResponse(shared.FailedRun)) 679 reg.Register( 680 httpmock.REST("GET", "runs/1234/jobs"), 681 httpmock.JSONResponse(shared.JobsPayload{ 682 Jobs: []shared.Job{ 683 shared.SuccessfulJob, 684 shared.FailedJob, 685 }, 686 })) 687 reg.Register( 688 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/1234/logs"), 689 httpmock.FileResponse("./fixtures/run_log.zip")) 690 reg.Register( 691 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 692 httpmock.JSONResponse(shared.TestWorkflow)) 693 }, 694 wantOut: quuxTheBarfLogOutput, 695 }, 696 { 697 name: "run log but run is not done", 698 tty: true, 699 opts: &ViewOptions{ 700 RunID: "2", 701 Log: true, 702 }, 703 httpStubs: func(reg *httpmock.Registry) { 704 reg.Register( 705 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"), 706 httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, ""))) 707 reg.Register( 708 httpmock.REST("GET", "runs/2/jobs"), 709 httpmock.JSONResponse(shared.JobsPayload{ 710 Jobs: []shared.Job{}, 711 })) 712 reg.Register( 713 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 714 httpmock.JSONResponse(shared.TestWorkflow)) 715 }, 716 wantErr: true, 717 errMsg: "run 2 is still in progress; logs will be available when it is complete", 718 }, 719 { 720 name: "job log but job is not done", 721 tty: true, 722 opts: &ViewOptions{ 723 JobID: "20", 724 Log: true, 725 }, 726 httpStubs: func(reg *httpmock.Registry) { 727 reg.Register( 728 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/20"), 729 httpmock.JSONResponse(shared.Job{ 730 ID: 20, 731 Status: shared.InProgress, 732 RunID: 2, 733 })) 734 reg.Register( 735 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/2"), 736 httpmock.JSONResponse(shared.TestRun(2, shared.InProgress, ""))) 737 reg.Register( 738 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 739 httpmock.JSONResponse(shared.TestWorkflow)) 740 }, 741 wantErr: true, 742 errMsg: "job 20 is still in progress; logs will be available when it is complete", 743 }, 744 { 745 name: "noninteractive with job", 746 opts: &ViewOptions{ 747 JobID: "10", 748 }, 749 httpStubs: func(reg *httpmock.Registry) { 750 reg.Register( 751 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), 752 httpmock.JSONResponse(shared.SuccessfulJob)) 753 reg.Register( 754 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 755 httpmock.JSONResponse(shared.SuccessfulRun)) 756 reg.Register( 757 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 758 httpmock.JSONResponse([]shared.Annotation{})) 759 reg.Register( 760 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 761 httpmock.JSONResponse(shared.TestWorkflow)) 762 }, 763 wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", 764 }, 765 { 766 name: "interactive, multiple jobs, choose all jobs", 767 tty: true, 768 opts: &ViewOptions{ 769 Prompt: true, 770 }, 771 httpStubs: func(reg *httpmock.Registry) { 772 reg.Register( 773 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 774 httpmock.JSONResponse(shared.RunsPayload{ 775 WorkflowRuns: shared.TestRuns, 776 })) 777 reg.Register( 778 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 779 httpmock.JSONResponse(shared.SuccessfulRun)) 780 reg.Register( 781 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3/artifacts"), 782 httpmock.StringResponse(`{}`)) 783 reg.Register( 784 httpmock.REST("GET", "runs/3/jobs"), 785 httpmock.JSONResponse(shared.JobsPayload{ 786 Jobs: []shared.Job{ 787 shared.SuccessfulJob, 788 shared.FailedJob, 789 }, 790 })) 791 reg.Register( 792 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 793 httpmock.JSONResponse([]shared.Annotation{})) 794 reg.Register( 795 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/20/annotations"), 796 httpmock.JSONResponse(shared.FailedJobAnnotations)) 797 reg.Register( 798 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 799 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 800 Workflows: []workflowShared.Workflow{ 801 shared.TestWorkflow, 802 }, 803 })) 804 reg.Register( 805 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 806 httpmock.JSONResponse(shared.TestWorkflow)) 807 }, 808 askStubs: func(as *prompt.AskStubber) { 809 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 810 as.StubOne(2) 811 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 812 as.StubOne(0) 813 }, 814 wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\nJOBS\n✓ cool job in 4m34s (ID 10)\nX sad job in 4m34s (ID 20)\n ✓ barf the quux\n X quux the barf\n\nANNOTATIONS\nX the job is sad\nsad job: blaze.py#420\n\n\nFor more information about a job, try: gh run view --job=<job-id>\nView this run on GitHub: https://github.com/runs/3\n", 815 }, 816 { 817 name: "interactive, multiple jobs, choose specific jobs", 818 tty: true, 819 opts: &ViewOptions{ 820 Prompt: true, 821 }, 822 httpStubs: func(reg *httpmock.Registry) { 823 reg.Register( 824 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs"), 825 httpmock.JSONResponse(shared.RunsPayload{ 826 WorkflowRuns: shared.TestRuns, 827 })) 828 reg.Register( 829 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 830 httpmock.JSONResponse(shared.SuccessfulRun)) 831 reg.Register( 832 httpmock.REST("GET", "runs/3/jobs"), 833 httpmock.JSONResponse(shared.JobsPayload{ 834 Jobs: []shared.Job{ 835 shared.SuccessfulJob, 836 shared.FailedJob, 837 }, 838 })) 839 reg.Register( 840 httpmock.REST("GET", "repos/OWNER/REPO/check-runs/10/annotations"), 841 httpmock.JSONResponse([]shared.Annotation{})) 842 reg.Register( 843 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows"), 844 httpmock.JSONResponse(workflowShared.WorkflowsPayload{ 845 Workflows: []workflowShared.Workflow{ 846 shared.TestWorkflow, 847 }, 848 })) 849 reg.Register( 850 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 851 httpmock.JSONResponse(shared.TestWorkflow)) 852 }, 853 askStubs: func(as *prompt.AskStubber) { 854 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 855 as.StubOne(2) 856 //nolint:staticcheck // SA1019: as.StubOne is deprecated: use StubPrompt 857 as.StubOne(1) 858 }, 859 wantOut: "\n✓ trunk CI · 3\nTriggered via push about 59 minutes ago\n\n✓ cool job in 4m34s (ID 10)\n ✓ fob the barz\n ✓ barz the fob\n\nTo see the full job log, try: gh run view --log --job=10\nView this run on GitHub: https://github.com/runs/3\n", 860 }, 861 { 862 name: "web run", 863 tty: true, 864 opts: &ViewOptions{ 865 RunID: "3", 866 Web: true, 867 }, 868 httpStubs: func(reg *httpmock.Registry) { 869 reg.Register( 870 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 871 httpmock.JSONResponse(shared.SuccessfulRun)) 872 reg.Register( 873 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 874 httpmock.JSONResponse(shared.TestWorkflow)) 875 }, 876 browsedURL: "https://github.com/runs/3", 877 wantOut: "Opening github.com/runs/3 in your browser.\n", 878 }, 879 { 880 name: "web job", 881 tty: true, 882 opts: &ViewOptions{ 883 JobID: "10", 884 Web: true, 885 }, 886 httpStubs: func(reg *httpmock.Registry) { 887 reg.Register( 888 httpmock.REST("GET", "repos/OWNER/REPO/actions/jobs/10"), 889 httpmock.JSONResponse(shared.SuccessfulJob)) 890 reg.Register( 891 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/3"), 892 httpmock.JSONResponse(shared.SuccessfulRun)) 893 reg.Register( 894 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 895 httpmock.JSONResponse(shared.TestWorkflow)) 896 }, 897 browsedURL: "https://github.com/jobs/10?check_suite_focus=true", 898 wantOut: "Opening github.com/jobs/10 in your browser.\n", 899 }, 900 { 901 name: "hide job header, failure", 902 tty: true, 903 opts: &ViewOptions{ 904 RunID: "123", 905 }, 906 httpStubs: func(reg *httpmock.Registry) { 907 reg.Register( 908 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"), 909 httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.Failure))) 910 reg.Register( 911 httpmock.REST("GET", "runs/123/jobs"), 912 httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}})) 913 reg.Register( 914 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"), 915 httpmock.StringResponse(`{}`)) 916 reg.Register( 917 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 918 httpmock.JSONResponse(shared.TestWorkflow)) 919 }, 920 wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", 921 }, 922 { 923 name: "hide job header, startup_failure", 924 tty: true, 925 opts: &ViewOptions{ 926 RunID: "123", 927 }, 928 httpStubs: func(reg *httpmock.Registry) { 929 reg.Register( 930 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123"), 931 httpmock.JSONResponse(shared.TestRun(123, shared.Completed, shared.StartupFailure))) 932 reg.Register( 933 httpmock.REST("GET", "runs/123/jobs"), 934 httpmock.JSONResponse(shared.JobsPayload{Jobs: []shared.Job{}})) 935 reg.Register( 936 httpmock.REST("GET", "repos/OWNER/REPO/actions/runs/123/artifacts"), 937 httpmock.StringResponse(`{}`)) 938 reg.Register( 939 httpmock.REST("GET", "repos/OWNER/REPO/actions/workflows/123"), 940 httpmock.JSONResponse(shared.TestWorkflow)) 941 }, 942 wantOut: "\nX trunk CI · 123\nTriggered via push about 59 minutes ago\n\nX This run likely failed because of a workflow file issue.\n\nFor more information, see: https://github.com/runs/123\n", 943 }, 944 } 945 946 for _, tt := range tests { 947 reg := &httpmock.Registry{} 948 tt.httpStubs(reg) 949 tt.opts.HttpClient = func() (*http.Client, error) { 950 return &http.Client{Transport: reg}, nil 951 } 952 953 tt.opts.Now = func() time.Time { 954 notnow, _ := time.Parse("2006-01-02 15:04:05", "2021-02-23 05:50:00") 955 return notnow 956 } 957 958 ios, _, stdout, _ := iostreams.Test() 959 ios.SetStdoutTTY(tt.tty) 960 tt.opts.IO = ios 961 tt.opts.BaseRepo = func() (ghrepo.Interface, error) { 962 return ghrepo.FromFullName("OWNER/REPO") 963 } 964 965 //nolint:staticcheck // SA1019: prompt.InitAskStubber is deprecated: use NewAskStubber 966 as, teardown := prompt.InitAskStubber() 967 defer teardown() 968 if tt.askStubs != nil { 969 tt.askStubs(as) 970 } 971 972 browser := &browser.Stub{} 973 tt.opts.Browser = browser 974 rlc := testRunLogCache{} 975 tt.opts.RunLogCache = rlc 976 977 t.Run(tt.name, func(t *testing.T) { 978 err := runView(tt.opts) 979 if tt.wantErr { 980 assert.Error(t, err) 981 if tt.errMsg != "" { 982 assert.Equal(t, tt.errMsg, err.Error()) 983 } 984 if !tt.opts.ExitStatus { 985 return 986 } 987 } 988 if !tt.opts.ExitStatus { 989 assert.NoError(t, err) 990 } 991 assert.Equal(t, tt.wantOut, stdout.String()) 992 if tt.browsedURL != "" { 993 assert.Equal(t, tt.browsedURL, browser.BrowsedURL()) 994 } 995 reg.Verify(t) 996 }) 997 } 998 } 999 1000 // Structure of fixture zip file 1001 // 1002 // run log/ 1003 // ├── cool job/ 1004 // │ ├── 1_fob the barz.txt 1005 // │ └── 2_barz the fob.txt 1006 // └── sad job/ 1007 // ├── 1_barf the quux.txt 1008 // └── 2_quux the barf.txt 1009 func Test_attachRunLog(t *testing.T) { 1010 tests := []struct { 1011 name string 1012 job shared.Job 1013 wantMatch bool 1014 wantFilename string 1015 }{ 1016 { 1017 name: "matching job name and step number 1", 1018 job: shared.Job{ 1019 Name: "cool job", 1020 Steps: []shared.Step{{ 1021 Name: "fob the barz", 1022 Number: 1, 1023 }}, 1024 }, 1025 wantMatch: true, 1026 wantFilename: "cool job/1_fob the barz.txt", 1027 }, 1028 { 1029 name: "matching job name and step number 2", 1030 job: shared.Job{ 1031 Name: "cool job", 1032 Steps: []shared.Step{{ 1033 Name: "barz the fob", 1034 Number: 2, 1035 }}, 1036 }, 1037 wantMatch: true, 1038 wantFilename: "cool job/2_barz the fob.txt", 1039 }, 1040 { 1041 name: "matching job name and step number and mismatch step name", 1042 job: shared.Job{ 1043 Name: "cool job", 1044 Steps: []shared.Step{{ 1045 Name: "mismatch", 1046 Number: 1, 1047 }}, 1048 }, 1049 wantMatch: true, 1050 wantFilename: "cool job/1_fob the barz.txt", 1051 }, 1052 { 1053 name: "matching job name and mismatch step number", 1054 job: shared.Job{ 1055 Name: "cool job", 1056 Steps: []shared.Step{{ 1057 Name: "fob the barz", 1058 Number: 3, 1059 }}, 1060 }, 1061 wantMatch: false, 1062 }, 1063 { 1064 name: "escape metacharacters in job name", 1065 job: shared.Job{ 1066 Name: "metacharacters .+*?()|[]{}^$ job", 1067 Steps: []shared.Step{{ 1068 Name: "fob the barz", 1069 Number: 0, 1070 }}, 1071 }, 1072 wantMatch: false, 1073 }, 1074 { 1075 name: "mismatching job name", 1076 job: shared.Job{ 1077 Name: "mismatch", 1078 Steps: []shared.Step{{ 1079 Name: "fob the barz", 1080 Number: 1, 1081 }}, 1082 }, 1083 wantMatch: false, 1084 }, 1085 } 1086 rlz, _ := zip.OpenReader("./fixtures/run_log.zip") 1087 defer rlz.Close() 1088 for _, tt := range tests { 1089 t.Run(tt.name, func(t *testing.T) { 1090 attachRunLog(rlz, []shared.Job{tt.job}) 1091 for _, step := range tt.job.Steps { 1092 log := step.Log 1093 logPresent := log != nil 1094 assert.Equal(t, tt.wantMatch, logPresent) 1095 if logPresent { 1096 assert.Equal(t, tt.wantFilename, log.Name) 1097 } 1098 } 1099 }) 1100 } 1101 } 1102 1103 type testRunLogCache struct{} 1104 1105 func (testRunLogCache) Exists(path string) bool { 1106 return false 1107 } 1108 func (testRunLogCache) Create(path string, content io.ReadCloser) error { 1109 return nil 1110 } 1111 func (testRunLogCache) Open(path string) (*zip.ReadCloser, error) { 1112 return zip.OpenReader("./fixtures/run_log.zip") 1113 } 1114 1115 var barfTheFobLogOutput = heredoc.Doc(` 1116 cool job barz the fob log line 1 1117 cool job barz the fob log line 2 1118 cool job barz the fob log line 3 1119 `) 1120 1121 var fobTheBarzLogOutput = heredoc.Doc(` 1122 cool job fob the barz log line 1 1123 cool job fob the barz log line 2 1124 cool job fob the barz log line 3 1125 `) 1126 1127 var barfTheQuuxLogOutput = heredoc.Doc(` 1128 sad job barf the quux log line 1 1129 sad job barf the quux log line 2 1130 sad job barf the quux log line 3 1131 `) 1132 1133 var quuxTheBarfLogOutput = heredoc.Doc(` 1134 sad job quux the barf log line 1 1135 sad job quux the barf log line 2 1136 sad job quux the barf log line 3 1137 `) 1138 1139 var coolJobRunLogOutput = fmt.Sprintf("%s%s", fobTheBarzLogOutput, barfTheFobLogOutput) 1140 var sadJobRunLogOutput = fmt.Sprintf("%s%s", barfTheQuuxLogOutput, quuxTheBarfLogOutput) 1141 var expectedRunLogOutput = fmt.Sprintf("%s%s", coolJobRunLogOutput, sadJobRunLogOutput)