golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/workflow/workflow_test.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package workflow_test 6 7 import ( 8 "context" 9 "errors" 10 "fmt" 11 "reflect" 12 "strings" 13 "sync/atomic" 14 "testing" 15 "time" 16 17 "github.com/google/go-cmp/cmp" 18 "github.com/google/go-cmp/cmp/cmpopts" 19 "github.com/google/uuid" 20 wf "golang.org/x/build/internal/workflow" 21 ) 22 23 func TestTrivial(t *testing.T) { 24 echo := func(ctx context.Context, arg string) (string, error) { 25 return arg, nil 26 } 27 28 wd := wf.New() 29 wf.Task1(wd, "echo", echo, wf.Const("hello world")) 30 greeting := wf.Task1(wd, "echo", echo, wf.Const("hello world")) 31 wf.Output(wd, "greeting", greeting) 32 33 w := startWorkflow(t, wd, nil) 34 outputs := runWorkflow(t, w, nil) 35 if got, want := outputs["greeting"], "hello world"; got != want { 36 t.Errorf("greeting = %q, want %q", got, want) 37 } 38 } 39 40 func TestDependency(t *testing.T) { 41 var actionRan, checkRan bool 42 action := func(ctx context.Context) error { 43 actionRan = true 44 return nil 45 } 46 checkAction := func(ctx context.Context) (string, error) { 47 if !actionRan { 48 return "", fmt.Errorf("prior action didn't run") 49 } 50 checkRan = true 51 return "", nil 52 } 53 hi := func(ctx context.Context) (string, error) { 54 if !actionRan || !checkRan { 55 return "", fmt.Errorf("either action (%v) or checkAction (%v) didn't run", actionRan, checkRan) 56 } 57 return "hello world", nil 58 } 59 60 wd := wf.New() 61 firstDep := wf.Action0(wd, "first action", action) 62 secondDep := wf.Task0(wd, "check action", checkAction, wf.After(firstDep)) 63 wf.Output(wd, "greeting", wf.Task0(wd, "say hi", hi, wf.After(secondDep))) 64 65 w := startWorkflow(t, wd, nil) 66 outputs := runWorkflow(t, w, nil) 67 if got, want := outputs["greeting"], "hello world"; got != want { 68 t.Errorf("greeting = %q, want %q", got, want) 69 } 70 } 71 72 func TestDependencyError(t *testing.T) { 73 action := func(ctx context.Context) error { 74 return fmt.Errorf("hardcoded error") 75 } 76 task := func(ctx context.Context) (string, error) { 77 return "", fmt.Errorf("unexpected error") 78 } 79 80 wd := wf.New() 81 dep := wf.Action0(wd, "failing action", action) 82 wf.Output(wd, "output", wf.Task0(wd, "task", task, wf.After(dep))) 83 w := startWorkflow(t, wd, nil) 84 if got, want := runToFailure(t, w, nil, "failing action"), "hardcoded error"; got != want { 85 t.Errorf("got error %q, want %q", got, want) 86 } 87 } 88 89 func TestSub(t *testing.T) { 90 hi := func(ctx context.Context) (string, error) { 91 return "hi", nil 92 } 93 concat := func(ctx context.Context, s1, s2 string) (string, error) { 94 return s1 + " " + s2, nil 95 } 96 97 wd := wf.New() 98 sub1 := wd.Sub("sub1") 99 g1 := wf.Task0(sub1, "Greeting", hi) 100 sub2 := wd.Sub("sub2") 101 g2 := wf.Task0(sub2, "Greeting", hi) 102 wf.Output(wd, "result", wf.Task2(wd, "Concatenate", concat, g1, g2)) 103 104 w := startWorkflow(t, wd, nil) 105 outputs := runWorkflow(t, w, nil) 106 if got, want := outputs["result"], "hi hi"; got != want { 107 t.Errorf("result = %q, want %q", got, want) 108 } 109 } 110 111 func TestSplitJoin(t *testing.T) { 112 echo := func(ctx context.Context, arg string) (string, error) { 113 return arg, nil 114 } 115 appendInt := func(ctx context.Context, s string, i int) (string, error) { 116 return fmt.Sprintf("%v%v", s, i), nil 117 } 118 join := func(ctx context.Context, s []string) (string, error) { 119 return strings.Join(s, ","), nil 120 } 121 122 wd := wf.New() 123 in := wf.Task1(wd, "echo", echo, wf.Const("string #")) 124 add1 := wf.Task2(wd, "add 1", appendInt, in, wf.Const(1)) 125 add2 := wf.Task2(wd, "add 2", appendInt, in, wf.Const(2)) 126 both := wf.Slice(add1, add2) 127 out := wf.Task1(wd, "join", join, both) 128 wf.Output(wd, "strings", out) 129 130 w := startWorkflow(t, wd, nil) 131 outputs := runWorkflow(t, w, nil) 132 if got, want := outputs["strings"], "string #1,string #2"; got != want { 133 t.Errorf("joined output = %q, want %q", got, want) 134 } 135 } 136 137 func TestParallelism(t *testing.T) { 138 // block1 and block2 block until they're both running. 139 chan1, chan2 := make(chan bool, 1), make(chan bool, 1) 140 block1 := func(ctx context.Context) (string, error) { 141 chan1 <- true 142 select { 143 case <-chan2: 144 case <-ctx.Done(): 145 } 146 return "", ctx.Err() 147 } 148 block2 := func(ctx context.Context) (string, error) { 149 chan2 <- true 150 select { 151 case <-chan1: 152 case <-ctx.Done(): 153 } 154 return "", ctx.Err() 155 } 156 wd := wf.New() 157 out1 := wf.Task0(wd, "block #1", block1) 158 out2 := wf.Task0(wd, "block #2", block2) 159 wf.Output(wd, "out1", out1) 160 wf.Output(wd, "out2", out2) 161 162 w := startWorkflow(t, wd, nil) 163 runWorkflow(t, w, nil) 164 } 165 166 func TestParameters(t *testing.T) { 167 echo := func(ctx context.Context, arg string) (string, error) { 168 return arg, nil 169 } 170 171 wd := wf.New() 172 param1 := wf.Param(wd, wf.ParamDef[string]{Name: "param1"}) 173 param2 := wf.Param(wd, wf.ParamDef[string]{Name: "param2"}) 174 out1 := wf.Task1(wd, "echo 1", echo, param1) 175 out2 := wf.Task1(wd, "echo 2", echo, param2) 176 wf.Output(wd, "out1", out1) 177 wf.Output(wd, "out2", out2) 178 179 w := startWorkflow(t, wd, map[string]interface{}{"param1": "#1", "param2": "#2"}) 180 outputs := runWorkflow(t, w, nil) 181 if want := map[string]interface{}{"out1": "#1", "out2": "#2"}; !reflect.DeepEqual(outputs, want) { 182 t.Errorf("outputs = %#v, want %#v", outputs, want) 183 } 184 185 t.Run("CountMismatch", func(t *testing.T) { 186 _, err := wf.Start(wd, map[string]interface{}{"param1": "#1"}) 187 if err == nil { 188 t.Errorf("wf.Start didn't return an error despite a parameter count mismatch") 189 } 190 }) 191 t.Run("NameMismatch", func(t *testing.T) { 192 _, err := wf.Start(wd, map[string]interface{}{"paramA": "#1", "paramB": "#2"}) 193 if err == nil { 194 t.Errorf("wf.Start didn't return an error despite a parameter name mismatch") 195 } 196 }) 197 t.Run("TypeMismatch", func(t *testing.T) { 198 _, err := wf.Start(wd, map[string]interface{}{"param1": "#1", "param2": 42}) 199 if err == nil { 200 t.Errorf("wf.Start didn't return an error despite a parameter type mismatch") 201 } 202 }) 203 } 204 205 // Test that passing wf.Parameter{...} directly to Definition.Task would be a build-time error. 206 // Parameters need to be registered via the Definition.Parameter method. 207 func TestParameterValue(t *testing.T) { 208 var p interface{} = wf.ParamDef[int]{} 209 if _, ok := p.(wf.Value[int]); ok { 210 t.Errorf("Parameter unexpectedly implements Value; it intentionally tries not to reduce possible API misuse") 211 } 212 } 213 214 func TestExpansion(t *testing.T) { 215 first := func(_ context.Context) (string, error) { 216 return "hey", nil 217 } 218 second := func(_ context.Context) (string, error) { 219 return "there", nil 220 } 221 third := func(_ context.Context) (string, error) { 222 return "friend", nil 223 } 224 join := func(_ context.Context, args []string) (string, error) { 225 return strings.Join(args, " "), nil 226 } 227 228 wd := wf.New() 229 v1 := wf.Task0(wd, "first", first) 230 v2 := wf.Task0(wd, "second", second) 231 wf.Output(wd, "second", v2) 232 joined := wf.Expand1(wd, "add a task", func(wd *wf.Definition, arg string) (wf.Value[string], error) { 233 v3 := wf.Task0(wd, "third", third) 234 // v1 is resolved before the expansion runs, v2 and v3 are dependencies 235 // created outside and inside the epansion. 236 return wf.Task1(wd, "join", join, wf.Slice(wf.Const(arg), v2, v3)), nil 237 }, v1) 238 wf.Output(wd, "final value", joined) 239 240 w := startWorkflow(t, wd, nil) 241 outputs := runWorkflow(t, w, nil) 242 if got, want := outputs["final value"], "hey there friend"; got != want { 243 t.Errorf("joined output = %q, want %q", got, want) 244 } 245 } 246 247 func TestResumeExpansion(t *testing.T) { 248 counter := 0 249 succeeds := func(ctx *wf.TaskContext) (string, error) { 250 counter++ 251 return "", nil 252 } 253 wd := wf.New() 254 result := wf.Expand0(wd, "expand", func(wd *wf.Definition) (wf.Value[string], error) { 255 return wf.Task0(wd, "succeeds", succeeds), nil 256 }) 257 wf.Output(wd, "result", result) 258 259 storage := &mapListener{Listener: &verboseListener{t}} 260 w := startWorkflow(t, wd, nil) 261 runWorkflow(t, w, storage) 262 resumed, err := wf.Resume(wd, &wf.WorkflowState{ID: w.ID}, storage.states[w.ID]) 263 if err != nil { 264 t.Fatal(err) 265 } 266 runWorkflow(t, resumed, nil) 267 if counter != 1 { 268 t.Errorf("task ran %v times, wanted 1", counter) 269 } 270 } 271 272 func TestRetryExpansion(t *testing.T) { 273 counter := 0 274 wd := wf.New() 275 out := wf.Expand0(wd, "expand", func(wd *wf.Definition) (wf.Value[string], error) { 276 counter++ 277 if counter == 1 { 278 return nil, fmt.Errorf("first try fail") 279 } 280 return wf.Task0(wd, "hi", func(_ context.Context) (string, error) { 281 return "", nil 282 }), nil 283 }) 284 wf.Output(wd, "out", out) 285 286 w := startWorkflow(t, wd, nil) 287 retry := func(string) { 288 go func() { 289 w.RetryTask(context.Background(), "expand") 290 }() 291 } 292 listener := &errorListener{ 293 taskName: "expand", 294 callback: retry, 295 Listener: &verboseListener{t}, 296 } 297 runWorkflow(t, w, listener) 298 if counter != 2 { 299 t.Errorf("task ran %v times, wanted 2", counter) 300 } 301 } 302 303 func TestManualRetry(t *testing.T) { 304 counter := 0 305 needsRetry := func(ctx *wf.TaskContext) (string, error) { 306 ctx.DisableRetries() 307 counter++ 308 if counter == 1 { 309 return "", fmt.Errorf("counter %v too low", counter) 310 } 311 return "hi", nil 312 } 313 314 wd := wf.New() 315 wf.Output(wd, "result", wf.Task0(wd, "needs retry", needsRetry)) 316 317 w := startWorkflow(t, wd, nil) 318 319 retry := func(string) { 320 go func() { 321 w.RetryTask(context.Background(), "needs retry") 322 }() 323 } 324 listener := &errorListener{ 325 taskName: "needs retry", 326 callback: retry, 327 Listener: &verboseListener{t}, 328 } 329 runWorkflow(t, w, listener) 330 if counter != 2 { 331 t.Errorf("task ran %v times, wanted 2", counter) 332 } 333 } 334 335 func TestAutomaticRetry(t *testing.T) { 336 counter := 0 337 needsRetry := func(ctx *wf.TaskContext) (string, error) { 338 if counter < 2 { 339 counter++ 340 return "", fmt.Errorf("counter %v too low", counter) 341 } 342 return "hi", nil 343 } 344 345 wd := wf.New() 346 wf.Output(wd, "result", wf.Task0(wd, "needs retry", needsRetry)) 347 348 w := startWorkflow(t, wd, nil) 349 outputs := runWorkflow(t, w, nil) 350 if got, want := outputs["result"], "hi"; got != want { 351 t.Errorf("result = %q, want %q", got, want) 352 } 353 if counter != 2 { 354 t.Errorf("counter = %v, want 2", counter) 355 } 356 } 357 358 func TestAutomaticRetryDisabled(t *testing.T) { 359 counter := 0 360 noRetry := func(ctx *wf.TaskContext) (string, error) { 361 ctx.DisableRetries() 362 counter++ 363 return "", fmt.Errorf("do not pass go") 364 } 365 366 wd := wf.New() 367 wf.Output(wd, "result", wf.Task0(wd, "no retry", noRetry)) 368 369 w := startWorkflow(t, wd, nil) 370 if got, want := runToFailure(t, w, nil, "no retry"), "do not pass go"; got != want { 371 t.Errorf("got error %q, want %q", got, want) 372 } 373 if counter != 1 { 374 t.Errorf("task with retries disabled ran %v times, wanted 1", counter) 375 } 376 } 377 378 func TestWatchdog(t *testing.T) { 379 t.Run("success", func(t *testing.T) { 380 testWatchdog(t, true) 381 }) 382 t.Run("failure", func(t *testing.T) { 383 testWatchdog(t, false) 384 }) 385 } 386 387 func testWatchdog(t *testing.T, success bool) { 388 defer func(r int, d time.Duration) { 389 wf.MaxRetries = r 390 wf.WatchdogDelay = d 391 }(wf.MaxRetries, wf.WatchdogDelay) 392 wf.MaxRetries = 1 393 wf.WatchdogDelay = 750 * time.Millisecond 394 395 maybeLog := func(ctx *wf.TaskContext) (string, error) { 396 select { 397 case <-ctx.Done(): 398 return "", ctx.Err() 399 case <-time.After(500 * time.Millisecond): 400 } 401 if success { 402 ctx.Printf("*snore*") 403 } 404 select { 405 case <-ctx.Done(): 406 return "", ctx.Err() 407 case <-time.After(500 * time.Millisecond): 408 } 409 return "huh? what?", nil 410 } 411 412 wd := wf.New() 413 wf.Output(wd, "result", wf.Task0(wd, "sleepy", maybeLog)) 414 415 w := startWorkflow(t, wd, nil) 416 if success { 417 runWorkflow(t, w, nil) 418 } else { 419 if got, want := runToFailure(t, w, nil, "sleepy"), "assumed hung"; !strings.Contains(got, want) { 420 t.Errorf("got error %q, want %q", got, want) 421 } 422 } 423 } 424 425 func TestLogging(t *testing.T) { 426 log := func(ctx *wf.TaskContext, arg string) (string, error) { 427 ctx.Printf("logging argument: %v", arg) 428 return arg, nil 429 } 430 431 wd := wf.New() 432 out := wf.Task1(wd, "log", log, wf.Const("hey there")) 433 wf.Output(wd, "out", out) 434 435 logger := &capturingLogger{} 436 listener := &logTestListener{ 437 Listener: &verboseListener{t}, 438 logger: logger, 439 } 440 w := startWorkflow(t, wd, nil) 441 runWorkflow(t, w, listener) 442 if want := []string{"logging argument: hey there"}; !reflect.DeepEqual(logger.lines, want) { 443 t.Errorf("unexpected logging result: got %v, want %v", logger.lines, want) 444 } 445 } 446 447 type logTestListener struct { 448 wf.Listener 449 logger wf.Logger 450 } 451 452 func (l *logTestListener) Logger(_ uuid.UUID, _ string) wf.Logger { 453 return l.logger 454 } 455 456 type capturingLogger struct { 457 lines []string 458 } 459 460 func (l *capturingLogger) Printf(format string, v ...interface{}) { 461 l.lines = append(l.lines, fmt.Sprintf(format, v...)) 462 } 463 464 func TestResume(t *testing.T) { 465 // We expect runOnlyOnce to only run once. 466 var runs int64 467 runOnlyOnce := func(ctx context.Context) (string, error) { 468 atomic.AddInt64(&runs, 1) 469 return "ran", nil 470 } 471 // blockOnce blocks the first time it's called, so that the workflow can be 472 // canceled at its step. 473 block := true 474 blocked := make(chan bool, 1) 475 maybeBlock := func(ctx *wf.TaskContext, _ string) (string, error) { 476 ctx.DisableRetries() 477 if block { 478 blocked <- true 479 <-ctx.Done() 480 return "blocked", ctx.Err() 481 } 482 return "not blocked", nil 483 } 484 wd := wf.New() 485 v1 := wf.Task0(wd, "run once", runOnlyOnce) 486 v2 := wf.Task1(wd, "block", maybeBlock, v1) 487 wf.Output(wd, "output", v2) 488 489 // Cancel the workflow once we've entered maybeBlock. 490 ctx, cancel := context.WithCancel(context.Background()) 491 go func() { 492 <-blocked 493 cancel() 494 }() 495 w, err := wf.Start(wd, nil) 496 if err != nil { 497 t.Fatal(err) 498 } 499 storage := &mapListener{Listener: &verboseListener{t}} 500 _, err = w.Run(ctx, storage) 501 if !errors.Is(err, context.Canceled) { 502 t.Fatalf("canceled workflow returned error %v, wanted Canceled", err) 503 } 504 storage.assertState(t, w, map[string]*wf.TaskState{ 505 "run once": {Name: "run once", Started: true, Finished: true, Result: "ran"}, 506 "block": {Name: "block", Started: true, Finished: true, Error: "context canceled"}, // We cancelled the workflow before it could save its state. 507 }) 508 509 block = false 510 wfState := &wf.WorkflowState{ID: w.ID, Params: nil} 511 taskStates := storage.states[w.ID] 512 taskStates["block"] = &wf.TaskState{Name: "block"} 513 w2, err := wf.Resume(wd, wfState, taskStates) 514 if err != nil { 515 t.Fatal(err) 516 } 517 out := runWorkflow(t, w2, storage) 518 if got, want := out["output"], "not blocked"; got != want { 519 t.Errorf("output from maybeBlock was %q, wanted %q", got, want) 520 } 521 if runs != 1 { 522 t.Errorf("runOnlyOnce ran %v times, wanted 1", runs) 523 } 524 storage.assertState(t, w, map[string]*wf.TaskState{ 525 "run once": {Name: "run once", Started: true, Finished: true, Result: "ran"}, 526 "block": {Name: "block", Started: true, Finished: true, Result: "not blocked"}, 527 }) 528 } 529 530 type badResult struct { 531 unexported string 532 } 533 534 func TestBadMarshaling(t *testing.T) { 535 greet := func(_ context.Context) (badResult, error) { 536 return badResult{"hi"}, nil 537 } 538 539 wd := wf.New() 540 wf.Output(wd, "greeting", wf.Task0(wd, "greet", greet)) 541 w := startWorkflow(t, wd, nil) 542 if got, want := runToFailure(t, w, nil, "greet"), "JSON marshaling"; !strings.Contains(got, want) { 543 t.Errorf("got error %q, want %q", got, want) 544 } 545 } 546 547 type mapListener struct { 548 wf.Listener 549 states map[uuid.UUID]map[string]*wf.TaskState 550 } 551 552 func (l *mapListener) TaskStateChanged(workflowID uuid.UUID, taskID string, state *wf.TaskState) error { 553 if l.states == nil { 554 l.states = map[uuid.UUID]map[string]*wf.TaskState{} 555 } 556 if l.states[workflowID] == nil { 557 l.states[workflowID] = map[string]*wf.TaskState{} 558 } 559 l.states[workflowID][taskID] = state 560 return l.Listener.TaskStateChanged(workflowID, taskID, state) 561 } 562 563 func (l *mapListener) assertState(t *testing.T, w *wf.Workflow, want map[string]*wf.TaskState) { 564 t.Helper() 565 if diff := cmp.Diff(l.states[w.ID], want, cmpopts.IgnoreFields(wf.TaskState{}, "SerializedResult")); diff != "" { 566 t.Errorf("task state didn't match expectations: %v", diff) 567 } 568 } 569 570 func startWorkflow(t *testing.T, wd *wf.Definition, params map[string]interface{}) *wf.Workflow { 571 t.Helper() 572 w, err := wf.Start(wd, params) 573 if err != nil { 574 t.Fatal(err) 575 } 576 return w 577 } 578 579 func runWorkflow(t *testing.T, w *wf.Workflow, listener wf.Listener) map[string]interface{} { 580 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 581 defer cancel() 582 t.Helper() 583 if listener == nil { 584 listener = &verboseListener{t} 585 } 586 outputs, err := w.Run(ctx, listener) 587 if err != nil { 588 t.Fatalf("w.Run() = _, %v, wanted no error", err) 589 } 590 return outputs 591 } 592 593 type verboseListener struct{ t *testing.T } 594 595 func (l *verboseListener) WorkflowStalled(workflowID uuid.UUID) error { 596 l.t.Logf("workflow %q: stalled", workflowID.String()) 597 return nil 598 } 599 600 func (l *verboseListener) TaskStateChanged(_ uuid.UUID, _ string, st *wf.TaskState) error { 601 switch { 602 case !st.Started: 603 // Task creation is uninteresting. 604 case !st.Finished: 605 l.t.Logf("task %-10v: started", st.Name) 606 case st.Error != "": 607 l.t.Logf("task %-10v: error: %v", st.Name, st.Error) 608 default: 609 l.t.Logf("task %-10v: done: %v", st.Name, st.Result) 610 } 611 return nil 612 } 613 614 func (l *verboseListener) Logger(_ uuid.UUID, task string) wf.Logger { 615 return &testLogger{t: l.t, task: task} 616 } 617 618 type testLogger struct { 619 t *testing.T 620 task string 621 } 622 623 func (l *testLogger) Printf(format string, v ...interface{}) { 624 l.t.Logf("task %-10v: LOG: %s", l.task, fmt.Sprintf(format, v...)) 625 } 626 627 func runToFailure(t *testing.T, w *wf.Workflow, listener wf.Listener, task string) string { 628 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) 629 defer cancel() 630 t.Helper() 631 if listener == nil { 632 listener = &verboseListener{t} 633 } 634 var message string 635 listener = &errorListener{ 636 taskName: task, 637 callback: func(m string) { 638 message = m 639 // Allow other tasks to run before shutting down the workflow. 640 time.AfterFunc(50*time.Millisecond, cancel) 641 }, 642 Listener: listener, 643 } 644 _, err := w.Run(ctx, listener) 645 if err == nil { 646 t.Fatalf("workflow unexpectedly succeeded") 647 } 648 return message 649 } 650 651 type errorListener struct { 652 taskName string 653 callback func(string) 654 wf.Listener 655 } 656 657 func (l *errorListener) TaskStateChanged(id uuid.UUID, taskID string, st *wf.TaskState) error { 658 if st.Name == l.taskName && st.Finished && st.Error != "" { 659 l.callback(st.Error) 660 } 661 l.Listener.TaskStateChanged(id, taskID, st) 662 return nil 663 }