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  }