From d13c95a8b2b99dd9eb2cdb6031a6bae57a935ee9 Mon Sep 17 00:00:00 2001 From: Timothy Rule <34501912+trulede@users.noreply.github.com> Date: Sun, 8 Jun 2025 14:45:13 +0000 Subject: [PATCH] Execution graph to detect cyclic tasks. --- call.go | 1 + errors/errors.go | 3 +- errors/errors_task.go | 31 +-- executor.go | 5 +- executor_test.go | 196 +++++++++++++++++ setup.go | 2 - task.go | 92 ++++++-- task_test.go | 14 -- taskfile/ast/graph.go | 39 ++++ testdata/cyclic/.gitignore | 1 + testdata/cyclic/Taskfile.yml | 198 +++++++++++++++++- .../TestExecutionGraph-A#01-err-run.golden | 1 + .../testdata/TestExecutionGraph-A#01.golden | 4 + .../testdata/TestExecutionGraph-A.golden | 4 + .../TestExecutionGraph-call-bar.golden | 3 + ...nGraph-call-converge-cyclic-err-run.golden | 1 + ...ExecutionGraph-call-converge-cyclic.golden | 3 + .../TestExecutionGraph-call-converge.golden | 12 ++ ...utionGraph-call-deps-cyclic-err-run.golden | 1 + ...TestExecutionGraph-call-deps-cyclic.golden | 3 + .../TestExecutionGraph-call-deps.golden | 4 + .../TestExecutionGraph-call-foo.golden | 6 + .../TestExecutionGraph-deps-bar.golden | 1 + ...nGraph-deps-converge-cyclic-err-run.golden | 1 + ...ExecutionGraph-deps-converge-cyclic.golden | 3 + .../TestExecutionGraph-deps-converge.golden | 4 + .../TestExecutionGraph-deps-foo.golden | 2 + ...nGraph-for-duplicate-cyclic-err-run.golden | 1 + ...ExecutionGraph-for-duplicate-cyclic.golden | 3 + .../TestExecutionGraph-for-duplicate.golden | 12 ++ ...raph-for-staggered-cyclic-A-err-run.golden | 1 + ...ecutionGraph-for-staggered-cyclic-A.golden | 4 + ...nGraph-for-staggered-cyclic-err-run.golden | 1 + ...ExecutionGraph-for-staggered-cyclic.golden | 3 + .../TestExecutionGraph-for-staggered.golden | 9 + ...cutionGraph-long-loop-cycle-err-run.golden | 1 + .../TestExecutionGraph-long-loop-cycle.golden | 5 + ...stExecutionGraph-loop-cycle-err-run.golden | 1 + .../TestExecutionGraph-loop-cycle.golden | 2 + ...ecutionGraph-sources-cyclic-err-run.golden | 1 + .../TestExecutionGraph-sources-cyclic.golden | 2 + ...xecutionGraph-sources-modify-cyclic.golden | 10 + .../TestExecutionGraph-sources.golden | 2 + .../testdata/TestJsonListFormat.golden | 4 +- 44 files changed, 641 insertions(+), 56 deletions(-) create mode 100644 testdata/cyclic/.gitignore create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-A#01-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-A#01.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-A.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-bar.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-converge.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-deps.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-call-foo.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-deps-bar.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-deps-converge.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-deps-foo.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-duplicate.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-for-staggered.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-loop-cycle-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-loop-cycle.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic-err-run.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-sources-modify-cyclic.golden create mode 100644 testdata/cyclic/testdata/TestExecutionGraph-sources.golden diff --git a/call.go b/call.go index a0b357185c..ecc7571f40 100644 --- a/call.go +++ b/call.go @@ -8,4 +8,5 @@ type Call struct { Vars *ast.Vars Silent bool Indirect bool // True if the task was called by another task + Vertex *ast.TaskExecutionVertex } diff --git a/errors/errors.go b/errors/errors.go index 1ed50e8741..68654ec1aa 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -35,10 +35,11 @@ const ( CodeTaskRunError CodeTaskInternal CodeTaskNameConflict - CodeTaskCalledTooManyTimes + CodeTaskCalledTooManyTimes // Depreciated: replaced by CodeTaskCyclicExecutionDetected. CodeTaskCancelled CodeTaskMissingRequiredVars CodeTaskNotAllowedVars + CodeTaskCyclicExecutionDetected ) // TaskError extends the standard error interface with a Code method. This code will diff --git a/errors/errors_task.go b/errors/errors_task.go index 8e840c793d..9727417b1e 100644 --- a/errors/errors_task.go +++ b/errors/errors_task.go @@ -93,23 +93,30 @@ func (err *TaskNameFlattenConflictError) Code() int { return CodeTaskNameConflict } -// TaskCalledTooManyTimesError is returned when the maximum task call limit is -// exceeded. This is to prevent infinite loops and cyclic dependencies. -type TaskCalledTooManyTimesError struct { +// TaskCyclicExecutionDetectedError is returned when the Execution Graph detects +// a cyclic execution condition. +type TaskCyclicExecutionDetectedError struct { TaskName string - MaximumTaskCall int + CallingTaskName string } -func (err *TaskCalledTooManyTimesError) Error() string { - return fmt.Sprintf( - `task: Maximum task call exceeded (%d) for task %q: probably an cyclic dep or infinite loop`, - err.MaximumTaskCall, - err.TaskName, - ) +func (err *TaskCyclicExecutionDetectedError) Error() string { + if len(err.CallingTaskName) > 0 { + return fmt.Sprintf( + `task: Cyclic task call execution detected for task %q (calling task %q)`, + err.TaskName, + err.CallingTaskName, + ) + } else { + return fmt.Sprintf( + `task: Cyclic task call execution detected for task %q`, + err.TaskName, + ) + } } -func (err *TaskCalledTooManyTimesError) Code() int { - return CodeTaskCalledTooManyTimes +func (err *TaskCyclicExecutionDetectedError) Code() int { + return CodeTaskCyclicExecutionDetected } // TaskCancelledByUserError is returned when the user does not accept an optional prompt to continue. diff --git a/executor.go b/executor.go index 8f9233ef88..a9dd3c6dec 100644 --- a/executor.go +++ b/executor.go @@ -63,10 +63,11 @@ type ( UserWorkingDir string EnableVersionCheck bool + graph *ast.TaskExecutionGraph + fuzzyModel *fuzzy.Model concurrencySemaphore chan struct{} - taskCallCount map[string]*int32 mkdirMutexMap map[string]*sync.Mutex executionHashes map[string]context.Context executionHashesMutex sync.Mutex @@ -92,9 +93,9 @@ func NewExecutor(opts ...ExecutorOption) *Executor { OutputStyle: ast.Output{}, TaskSorter: sort.AlphaNumericWithRootTasksFirst, UserWorkingDir: "", + graph: ast.NewTaskExecutionGraph(), fuzzyModel: nil, concurrencySemaphore: nil, - taskCallCount: map[string]*int32{}, mkdirMutexMap: map[string]*sync.Mutex{}, executionHashes: map[string]context.Context{}, executionHashesMutex: sync.Mutex{}, diff --git a/executor_test.go b/executor_test.go index 4d1677db21..8a77fd7d6f 100644 --- a/executor_test.go +++ b/executor_test.go @@ -978,3 +978,199 @@ func TestIncludeChecksum(t *testing.T) { WithPostProcessFn(PPRemoveAbsolutePaths), ) } + +func TestExecutionGraph(t *testing.T) { + t.Parallel() + + NewExecutorTest(t, + WithName("loop-cycle"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("loop-cycle"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("long-loop-cycle"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("long-loop-cycle"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("A"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("A"), + ) + + NewExecutorTest(t, + WithName("A"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("A"), + WithVar("CYCLEBACK", "A"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("call-foo"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-foo"), + ) + NewExecutorTest(t, + WithName("call-bar"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-bar"), + ) + + NewExecutorTest(t, + WithName("call-converge"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-converge"), + ) + + NewExecutorTest(t, + WithName("call-converge-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-converge-cyclic"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("deps-foo"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("deps-foo"), + ) + NewExecutorTest(t, + WithName("deps-bar"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("deps-bar"), + ) + + NewExecutorTest(t, + WithName("deps-converge"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("deps-converge"), + ) + + NewExecutorTest(t, + WithName("deps-converge-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("deps-converge-cyclic"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("call-deps"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-deps"), + ) + + NewExecutorTest(t, + WithName("call-deps-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("call-deps-cyclic"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("for-staggered"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("for-staggered"), + ) + + NewExecutorTest(t, + WithName("for-staggered-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("for-staggered-cyclic"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("for-staggered-cyclic-A"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("for-staggered-cyclic-A"), + WithRunError(), + ) + + NewExecutorTest(t, + WithName("for-duplicate"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("for-duplicate"), + ) + + NewExecutorTest(t, + WithName("for-duplicate-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("for-duplicate-cyclic"), + WithRunError(), + ) + + newFoo := func() { + err := os.WriteFile(filepathext.SmartJoin("testdata/cyclic", "foo.txt"), []byte("foo"), 0o666) + require.NoError(t, err) + } + newFoo() + NewExecutorTest(t, + WithName("sources"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("sources"), + ) + + newFoo() + NewExecutorTest(t, + WithName("sources-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("sources-cyclic"), + WithRunError(), + ) + + newFoo() + NewExecutorTest(t, + WithName("sources-modify-cyclic"), + WithExecutorOptions( + task.WithDir("testdata/cyclic"), + ), + WithTask("sources-modify-cyclic"), + ) +} diff --git a/setup.go b/setup.go index 0e94f3e67a..7b434caaf5 100644 --- a/setup.go +++ b/setup.go @@ -251,10 +251,8 @@ func (e *Executor) setupDefaults() { func (e *Executor) setupConcurrencyState() { e.executionHashes = make(map[string]context.Context) - e.taskCallCount = make(map[string]*int32, e.Taskfile.Tasks.Len()) e.mkdirMutexMap = make(map[string]*sync.Mutex, e.Taskfile.Tasks.Len()) for k := range e.Taskfile.Tasks.Keys(nil) { - e.taskCallCount[k] = new(int32) e.mkdirMutexMap[k] = &sync.Mutex{} } diff --git a/task.go b/task.go index 0b762c6140..dc69b7c2de 100644 --- a/task.go +++ b/task.go @@ -6,8 +6,8 @@ import ( "os" "runtime" "slices" - "sync/atomic" + "github.com/dominikbraun/graph" "golang.org/x/sync/errgroup" "mvdan.cc/sh/v3/interp" @@ -24,12 +24,6 @@ import ( "github.com/go-task/task/v3/taskfile/ast" ) -const ( - // MaximumTaskCall is the max number of times a task can be called. - // This exists to prevent infinite loops on cyclic dependencies - MaximumTaskCall = 1000 -) - // MatchingTask represents a task that matches a given call. It includes the // task itself and a list of wildcards that were matched. type MatchingTask struct { @@ -116,6 +110,61 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca return } +func (e *Executor) updateExecutionGraph(t *ast.Task, call *Call) error { + e.graph.Lock() + defer e.graph.Unlock() + if call.Vertex == nil { + call.Vertex = &ast.TaskExecutionVertex{} + } + call.Vertex.Task = t + // Calculate the call token. + var callToken string + if v, ok := t.Vars.Get("CHECKSUM"); ok { + callToken = fmt.Sprintf("%v", v.Live) + } else if v, ok := t.Vars.Get("TIMESTAMP"); ok { + callToken = fmt.Sprintf("%v", v.Live) + } + if len(callToken) > 0 { + call.Vertex.CallToken = callToken + } else { + // Propagate the parent call token. + if call.Vertex.Parent != nil { + call.Vertex.CallToken = call.Vertex.Parent.CallToken + } + } + // Add vertex and edge. + if err := e.graph.AddVertex(call.Vertex); err != nil { + switch { + case errors.Is(err, graph.ErrVertexAlreadyExists): + // consume this error + default: + return err + } + } + if call.Vertex.Parent != nil { + if err := e.graph.AddEdge(call.Vertex.Parent.Hash, call.Vertex.Hash); err != nil { + switch { + case errors.Is(err, graph.ErrEdgeAlreadyExists): + // consume this error + case errors.Is(err, graph.ErrEdgeCreatesCycle): + if call.Vertex.Parent == nil { + return &errors.TaskCyclicExecutionDetectedError{ + TaskName: call.Task, + } + } else { + return &errors.TaskCyclicExecutionDetectedError{ + TaskName: call.Task, + CallingTaskName: call.Vertex.Parent.Task.Task, + } + } + default: + return err + } + } + } + return nil +} + // RunTask runs a task by its name func (e *Executor) RunTask(ctx context.Context, call *Call) error { t, err := e.FastCompiledTask(call) @@ -140,19 +189,17 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { return err } - if !e.Watch && atomic.AddInt32(e.taskCallCount[t.Task], 1) >= MaximumTaskCall { - return &errors.TaskCalledTooManyTimesError{ - TaskName: t.Task, - MaximumTaskCall: MaximumTaskCall, - } - } - release := e.acquireConcurrencyLimit() defer release() return e.startExecution(ctx, t, func(ctx context.Context) error { e.Logger.VerboseErrf(logger.Magenta, "task: %q started\n", call.Task) - if err := e.runDeps(ctx, t); err != nil { + + if err := e.updateExecutionGraph(t, call); err != nil { + return err + } + + if err := e.runDeps(ctx, t, call.Vertex); err != nil { return err } @@ -233,6 +280,11 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error { return err } + taskCyclicExecutionDetectedErr := &errors.TaskCyclicExecutionDetectedError{} + if errors.As(err, &taskCyclicExecutionDetectedErr) { + return err + } + return &errors.TaskRunError{TaskName: t.Task, Err: err} } } @@ -258,16 +310,17 @@ func (e *Executor) mkdir(t *ast.Task) error { return nil } -func (e *Executor) runDeps(ctx context.Context, t *ast.Task) error { +func (e *Executor) runDeps(ctx context.Context, t *ast.Task, v *ast.TaskExecutionVertex) error { g, ctx := errgroup.WithContext(ctx) reacquire := e.releaseConcurrencyLimit() defer reacquire() - for _, d := range t.Deps { + for i, d := range t.Deps { d := d g.Go(func() error { - err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true}) + vDep := &ast.TaskExecutionVertex{CallIndex: i, Parent: v} + err := e.RunTask(ctx, &Call{Task: d.Task, Vars: d.Vars, Silent: d.Silent, Indirect: true, Vertex: vDep}) if err != nil { return err } @@ -313,7 +366,8 @@ func (e *Executor) runCommand(ctx context.Context, t *ast.Task, call *Call, i in reacquire := e.releaseConcurrencyLimit() defer reacquire() - err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true}) + vCmd := &ast.TaskExecutionVertex{CallIndex: i, Parent: call.Vertex} + err := e.RunTask(ctx, &Call{Task: cmd.Task, Vars: cmd.Vars, Silent: cmd.Silent, Indirect: true, Vertex: vCmd}) if err != nil { return err } diff --git a/task_test.go b/task_test.go index 7b986662cd..d239ecc750 100644 --- a/task_test.go +++ b/task_test.go @@ -536,20 +536,6 @@ func TestCmdsVariables(t *testing.T) { assert.Contains(t, buff.String(), tf) } -func TestCyclicDep(t *testing.T) { - t.Parallel() - - const dir = "testdata/cyclic" - - e := task.NewExecutor( - task.WithDir(dir), - task.WithStdout(io.Discard), - task.WithStderr(io.Discard), - ) - require.NoError(t, e.Setup()) - assert.IsType(t, &errors.TaskCalledTooManyTimesError{}, e.Run(context.Background(), &task.Call{Task: "task-1"})) -} - func TestTaskVersion(t *testing.T) { t.Parallel() diff --git a/taskfile/ast/graph.go b/taskfile/ast/graph.go index cb30093d88..159e3ed642 100644 --- a/taskfile/ast/graph.go +++ b/taskfile/ast/graph.go @@ -2,11 +2,13 @@ package ast import ( "fmt" + "hash/fnv" "os" "sync" "github.com/dominikbraun/graph" "github.com/dominikbraun/graph/draw" + "github.com/mitchellh/hashstructure/v2" "golang.org/x/sync/errgroup" ) @@ -118,3 +120,40 @@ func (tfg *TaskfileGraph) Merge() (*Taskfile, error) { return rootVertex.Taskfile, nil } + +type TaskExecutionGraph struct { + sync.Mutex + graph.Graph[string, *TaskExecutionVertex] +} + +type TaskExecutionVertex struct { + Task *Task + CallIndex int + CallToken string + Parent *TaskExecutionVertex + Hash string +} + +func taskExecutionHash(v *TaskExecutionVertex) string { + if len(v.Hash) == 0 { + h := fnv.New64a() + if v.Task != nil { + taskHash, _ := hashstructure.Hash(v.Task, hashstructure.FormatV2, nil) + hashString := fmt.Sprintf("%s:%d:%v:%v", v.Task.Task, taskHash, v.CallIndex, v.CallToken) + h.Write([]byte(hashString)) + } + v.Hash = fmt.Sprintf("%v", h.Sum64()) + } + return v.Hash +} + +func NewTaskExecutionGraph() *TaskExecutionGraph { + return &TaskExecutionGraph{ + sync.Mutex{}, + graph.New(taskExecutionHash, + graph.Directed(), + graph.PreventCycles(), + graph.Rooted(), + ), + } +} diff --git a/testdata/cyclic/.gitignore b/testdata/cyclic/.gitignore new file mode 100644 index 0000000000..7c6ded14ec --- /dev/null +++ b/testdata/cyclic/.gitignore @@ -0,0 +1 @@ +foo.txt diff --git a/testdata/cyclic/Taskfile.yml b/testdata/cyclic/Taskfile.yml index 83cf39db48..4d04f3699b 100644 --- a/testdata/cyclic/Taskfile.yml +++ b/testdata/cyclic/Taskfile.yml @@ -1,10 +1,200 @@ version: '3' +silent: true + tasks: - task-1: + + foo: + cmds: + - echo "foo" + - task: bar + bar: + cmds: + - echo "bar" + + loop-cycle: + cmds: + - echo "foo" + - task: loop-cycle + + long-loop-cycle-A: + aliases: [long-loop-cycle] + cmds: + - echo "foo" + - task: long-loop-cycle-B + long-loop-cycle-B: + cmds: + - echo "bar" + - task: long-loop-cycle-C + long-loop-cycle-C: + cmds: + - echo "fubar" + - task: long-loop-cycle-D + long-loop-cycle-D: + cmds: + - echo "foobar" + - task: long-loop-cycle-A + + # Convergent call path, optional cyclic callback. + A: + cmds: + - echo "A" + - task: B + vars: + CYCLEBACK: '{{.CYCLEBACK}}' + B: + cmds: + - echo "B" + - task: C + vars: + CYCLEBACK: '{{.CYCLEBACK}}' + C: + cmds: + - echo "C" + - task: '{{if .CYCLEBACK}}{{.CYCLEBACK}}{{else}}D{{end}}' + D: + cmds: + - echo "D" + + call-foo: + cmds: + - task: foo + - task: foo + - task: foo + + call-bar: + cmds: + - task: bar + - task: bar + - task: bar + + call-converge: + cmds: + - task: A + - task: A + - task: A + + call-converge-cyclic: + cmds: + - task: A + vars: + CYCLEBACK: call-converge-cyclic + - task: A + vars: + CYCLEBACK: call-converge-cyclic + - task: A + vars: + CYCLEBACK: call-converge-cyclic + + deps-foo: + deps: + - task: foo + + deps-bar: deps: - - task: task-2 + - task: bar - task-2: + deps-converge: deps: - - task: task-1 + - task: A + + deps-converge-cyclic: + deps: + - task: A + vars: + CYCLEBACK: deps-converge-cyclic + + dep-call: + deps: + - task: A + vars: + CYCLEBACK: '{{.CYCLEBACK}}' + + call-deps: + cmds: + - task: dep-call + + call-deps-cyclic: + cmds: + - task: dep-call + vars: + CYCLEBACK: deps-call-cyclic + + for-staggered: + cmds: + - for: ['A','B','C'] + task: '{{.ITEM}}' + + for-staggered-cyclic: + cmds: + - for: ['A','B','C'] + task: '{{.ITEM}}' + vars: + CYCLEBACK: for-staggered-cyclic + + for-staggered-cyclic-A: + cmds: + - for: ['A','B','C'] + task: '{{.ITEM}}' + vars: + CYCLEBACK: A + + for-duplicate: + cmds: + - for: ['A','A','A'] + task: '{{.ITEM}}' + + for-duplicate-cyclic: + cmds: + - for: ['A','A','A'] + task: '{{.ITEM}}' + vars: + CYCLEBACK: for-duplicate-cyclic + + cat-file: + cmds: + - echo {{.ITEM}} + - cat {{.ITEM}} + - task: '{{if .CYCLEBACK}}{{.CYCLEBACK}}{{else}}nop{{end}}' + + nop: {} + + sources: + method: none + sources: + - foo.txt + cmds: + - for: sources + task: cat-file + vars: + ITEM: '{{.ITEM}}' + + sources-cyclic: + method: none + sources: + - foo.txt + cmds: + - for: sources + task: cat-file + vars: + ITEM: '{{.ITEM}}' + CYCLEBACK: sources-cyclic + + modify-file: + status: + # Break the cycle. + - if [[ $(wc -l > {{.ITEM}} + - cat {{.ITEM}} + - task: '{{if .CYCLEBACK}}{{.CYCLEBACK}}{{else}}nop{{end}}' + + sources-modify-cyclic: + sources: + - foo.txt + cmds: + - for: sources + task: modify-file + vars: + ITEM: '{{.ITEM}}' + CYCLEBACK: sources-modify-cyclic diff --git a/testdata/cyclic/testdata/TestExecutionGraph-A#01-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-A#01-err-run.golden new file mode 100644 index 0000000000..05df490797 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-A#01-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "B" (calling task "A") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-A#01.golden b/testdata/cyclic/testdata/TestExecutionGraph-A#01.golden new file mode 100644 index 0000000000..ec17ce3d10 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-A#01.golden @@ -0,0 +1,4 @@ +A +B +C +A diff --git a/testdata/cyclic/testdata/TestExecutionGraph-A.golden b/testdata/cyclic/testdata/TestExecutionGraph-A.golden new file mode 100644 index 0000000000..8422d40f12 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-A.golden @@ -0,0 +1,4 @@ +A +B +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-bar.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-bar.golden new file mode 100644 index 0000000000..706cfd8a34 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-bar.golden @@ -0,0 +1,3 @@ +bar +bar +bar diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic-err-run.golden new file mode 100644 index 0000000000..a2975723ba --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "A" (calling task "call-converge-cyclic") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic.golden new file mode 100644 index 0000000000..b1e67221af --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-converge-cyclic.golden @@ -0,0 +1,3 @@ +A +B +C diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-converge.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-converge.golden new file mode 100644 index 0000000000..1967dea532 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-converge.golden @@ -0,0 +1,12 @@ +A +B +C +D +A +B +C +D +A +B +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic-err-run.golden new file mode 100644 index 0000000000..c90b90daca --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Failed to run task "call-deps-cyclic": task: Task "deps-call-cyclic" does not exist \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic.golden new file mode 100644 index 0000000000..b1e67221af --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-deps-cyclic.golden @@ -0,0 +1,3 @@ +A +B +C diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-deps.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-deps.golden new file mode 100644 index 0000000000..8422d40f12 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-deps.golden @@ -0,0 +1,4 @@ +A +B +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-call-foo.golden b/testdata/cyclic/testdata/TestExecutionGraph-call-foo.golden new file mode 100644 index 0000000000..a5727b5e59 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-call-foo.golden @@ -0,0 +1,6 @@ +foo +bar +foo +bar +foo +bar diff --git a/testdata/cyclic/testdata/TestExecutionGraph-deps-bar.golden b/testdata/cyclic/testdata/TestExecutionGraph-deps-bar.golden new file mode 100644 index 0000000000..5716ca5987 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-deps-bar.golden @@ -0,0 +1 @@ +bar diff --git a/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic-err-run.golden new file mode 100644 index 0000000000..b4877d09a6 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "A" (calling task "deps-converge-cyclic") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic.golden new file mode 100644 index 0000000000..b1e67221af --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge-cyclic.golden @@ -0,0 +1,3 @@ +A +B +C diff --git a/testdata/cyclic/testdata/TestExecutionGraph-deps-converge.golden b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge.golden new file mode 100644 index 0000000000..8422d40f12 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-deps-converge.golden @@ -0,0 +1,4 @@ +A +B +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-deps-foo.golden b/testdata/cyclic/testdata/TestExecutionGraph-deps-foo.golden new file mode 100644 index 0000000000..3bd1f0e297 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-deps-foo.golden @@ -0,0 +1,2 @@ +foo +bar diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic-err-run.golden new file mode 100644 index 0000000000..9793cee59c --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "A" (calling task "for-duplicate-cyclic") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic.golden new file mode 100644 index 0000000000..b1e67221af --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate-cyclic.golden @@ -0,0 +1,3 @@ +A +B +C diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate.golden new file mode 100644 index 0000000000..1967dea532 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-duplicate.golden @@ -0,0 +1,12 @@ +A +B +C +D +A +B +C +D +A +B +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A-err-run.golden new file mode 100644 index 0000000000..05df490797 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "B" (calling task "A") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A.golden new file mode 100644 index 0000000000..ec17ce3d10 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-A.golden @@ -0,0 +1,4 @@ +A +B +C +A diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-err-run.golden new file mode 100644 index 0000000000..6e0f04a7e8 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "A" (calling task "for-staggered-cyclic") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic.golden new file mode 100644 index 0000000000..b1e67221af --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered-cyclic.golden @@ -0,0 +1,3 @@ +A +B +C diff --git a/testdata/cyclic/testdata/TestExecutionGraph-for-staggered.golden b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered.golden new file mode 100644 index 0000000000..b4779040ba --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-for-staggered.golden @@ -0,0 +1,9 @@ +A +B +C +D +B +C +D +C +D diff --git a/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle-err-run.golden new file mode 100644 index 0000000000..d0f74871e3 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "long-loop-cycle-B" (calling task "long-loop-cycle-A") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle.golden b/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle.golden new file mode 100644 index 0000000000..2e64bc1f3d --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-long-loop-cycle.golden @@ -0,0 +1,5 @@ +foo +bar +fubar +foobar +foo diff --git a/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle-err-run.golden new file mode 100644 index 0000000000..91b0d6eccd --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "loop-cycle" (calling task "loop-cycle") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle.golden b/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle.golden new file mode 100644 index 0000000000..0d55bed3a3 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-loop-cycle.golden @@ -0,0 +1,2 @@ +foo +foo diff --git a/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic-err-run.golden b/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic-err-run.golden new file mode 100644 index 0000000000..622674f396 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic-err-run.golden @@ -0,0 +1 @@ +task: Cyclic task call execution detected for task "cat-file" (calling task "sources-cyclic") \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic.golden new file mode 100644 index 0000000000..9606120333 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-sources-cyclic.golden @@ -0,0 +1,2 @@ +foo.txt +foo \ No newline at end of file diff --git a/testdata/cyclic/testdata/TestExecutionGraph-sources-modify-cyclic.golden b/testdata/cyclic/testdata/TestExecutionGraph-sources-modify-cyclic.golden new file mode 100644 index 0000000000..60a7b7376f --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-sources-modify-cyclic.golden @@ -0,0 +1,10 @@ +foofoo.txt +foofoo.txt +foo.txt +foofoo.txt +foo.txt +foo.txt +foofoo.txt +foo.txt +foo.txt +foo.txt diff --git a/testdata/cyclic/testdata/TestExecutionGraph-sources.golden b/testdata/cyclic/testdata/TestExecutionGraph-sources.golden new file mode 100644 index 0000000000..9606120333 --- /dev/null +++ b/testdata/cyclic/testdata/TestExecutionGraph-sources.golden @@ -0,0 +1,2 @@ +foo.txt +foo \ No newline at end of file diff --git a/testdata/json_list_format/testdata/TestJsonListFormat.golden b/testdata/json_list_format/testdata/TestJsonListFormat.golden index 44f0528452..8af3062d4b 100644 --- a/testdata/json_list_format/testdata/TestJsonListFormat.golden +++ b/testdata/json_list_format/testdata/TestJsonListFormat.golden @@ -10,9 +10,9 @@ "location": { "line": 4, "column": 3, - "taskfile": "{{ .TaskfileLocation }}" + "taskfile": "/workspaces/task/testdata/json_list_format/Taskfile.yml" } } ], - "location": "{{ .TaskfileLocation }}" + "location": "/workspaces/task/testdata/json_list_format/Taskfile.yml" }