mirror of
https://github.com/go-task/task.git
synced 2025-02-13 13:59:32 +02:00
changed cyclic dep detection
since interpolation can be used, detection should be a execution time, and not before now, to prevent infinite execution, there's a miximum of 100 calls per task closes #37
This commit is contained in:
parent
fb4b0a187e
commit
2dd3564da1
34
cyclic.go
34
cyclic.go
@ -1,34 +0,0 @@
|
|||||||
package task
|
|
||||||
|
|
||||||
// CheckCyclicDep checks if a task tree has any cyclic dependency
|
|
||||||
func (e *Executor) CheckCyclicDep() error {
|
|
||||||
visits := make(map[string]struct{}, len(e.Tasks))
|
|
||||||
|
|
||||||
var checkCyclicDep func(string, *Task) error
|
|
||||||
checkCyclicDep = func(name string, t *Task) error {
|
|
||||||
if _, ok := visits[name]; ok {
|
|
||||||
return ErrCyclicDepDetected
|
|
||||||
}
|
|
||||||
visits[name] = struct{}{}
|
|
||||||
defer delete(visits, name)
|
|
||||||
|
|
||||||
for _, d := range t.Deps {
|
|
||||||
// FIXME: ignoring by now. should return an error instead?
|
|
||||||
task, ok := e.Tasks[d.Task]
|
|
||||||
if !ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := checkCyclicDep(d.Task, task); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for k, v := range e.Tasks {
|
|
||||||
if err := checkCyclicDep(k, v); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
@ -1,56 +0,0 @@
|
|||||||
package task_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/go-task/task"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestCyclicDepCheck(t *testing.T) {
|
|
||||||
isCyclic := &task.Executor{
|
|
||||||
Tasks: task.Tasks{
|
|
||||||
"task-a": &task.Task{
|
|
||||||
Deps: []*task.Dep{&task.Dep{Task: "task-b"}},
|
|
||||||
},
|
|
||||||
"task-b": &task.Task{
|
|
||||||
Deps: []*task.Dep{&task.Dep{Task: "task-a"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, task.ErrCyclicDepDetected, isCyclic.CheckCyclicDep(), "task should be cyclic")
|
|
||||||
|
|
||||||
isNotCyclic := &task.Executor{
|
|
||||||
Tasks: task.Tasks{
|
|
||||||
"task-a": &task.Task{
|
|
||||||
Deps: []*task.Dep{&task.Dep{Task: "task-c"}},
|
|
||||||
},
|
|
||||||
"task-b": &task.Task{
|
|
||||||
Deps: []*task.Dep{&task.Dep{Task: "task-c"}},
|
|
||||||
},
|
|
||||||
"task-c": &task.Task{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.NoError(t, isNotCyclic.CheckCyclicDep())
|
|
||||||
|
|
||||||
inexixtentTask := &task.Executor{
|
|
||||||
Tasks: task.Tasks{
|
|
||||||
"task-a": &task.Task{
|
|
||||||
Deps: []*task.Dep{&task.Dep{Task: "invalid-task"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: by now Task should ignore non existent tasks
|
|
||||||
// in the future we should improve the detection of
|
|
||||||
// tasks called with interpolation?
|
|
||||||
// task:
|
|
||||||
// deps:
|
|
||||||
// - task: "task{{.VARIABLE}}"
|
|
||||||
// vars:
|
|
||||||
// VARIABLE: something
|
|
||||||
assert.NoError(t, inexixtentTask.CheckCyclicDep())
|
|
||||||
}
|
|
17
errors.go
17
errors.go
@ -6,8 +6,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrCyclicDepDetected is returned when a cyclic dependency was found in the Taskfile
|
|
||||||
ErrCyclicDepDetected = errors.New("task: cyclic dependency detected")
|
|
||||||
// ErrTaskfileAlreadyExists is returned on creating a Taskfile if one already exists
|
// ErrTaskfileAlreadyExists is returned on creating a Taskfile if one already exists
|
||||||
ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists")
|
ErrTaskfileAlreadyExists = errors.New("task: A Taskfile already exists")
|
||||||
)
|
)
|
||||||
@ -61,3 +59,18 @@ type dynamicVarError struct {
|
|||||||
func (err *dynamicVarError) Error() string {
|
func (err *dynamicVarError) Error() string {
|
||||||
return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause)
|
return fmt.Sprintf(`task: Command "%s" in taskvars file failed: %s`, err.cmd, err.cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MaximumTaskCallExceededError is returned when a task is called too
|
||||||
|
// many times. In this case you probably have a cyclic dependendy or
|
||||||
|
// infinite loop
|
||||||
|
type MaximumTaskCallExceededError struct {
|
||||||
|
task string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *MaximumTaskCallExceededError) Error() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`task: maximum task call exceeded (%d) for task "%s": probably an cyclic dep or infinite loop`,
|
||||||
|
MaximumTaskCall,
|
||||||
|
e.task,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
14
task.go
14
task.go
@ -9,6 +9,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/go-task/task/execext"
|
"github.com/go-task/task/execext"
|
||||||
|
|
||||||
@ -18,6 +19,9 @@ import (
|
|||||||
const (
|
const (
|
||||||
// TaskFilePath is the default Taskfile
|
// TaskFilePath is the default Taskfile
|
||||||
TaskFilePath = "Taskfile"
|
TaskFilePath = "Taskfile"
|
||||||
|
// MaximumTaskCall is the max number of times a task can be called.
|
||||||
|
// This exists to prevent infinite loops on cyclic dependencies
|
||||||
|
MaximumTaskCall = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
// Executor executes a Taskfile
|
// Executor executes a Taskfile
|
||||||
@ -57,14 +61,12 @@ type Task struct {
|
|||||||
Vars Vars
|
Vars Vars
|
||||||
Set string
|
Set string
|
||||||
Env Vars
|
Env Vars
|
||||||
|
|
||||||
|
callCount int32
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run runs Task
|
// Run runs Task
|
||||||
func (e *Executor) Run(args ...string) error {
|
func (e *Executor) Run(args ...string) error {
|
||||||
if err := e.CheckCyclicDep(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Stdin == nil {
|
if e.Stdin == nil {
|
||||||
e.Stdin = os.Stdin
|
e.Stdin = os.Stdin
|
||||||
}
|
}
|
||||||
@ -110,6 +112,10 @@ func (e *Executor) RunTask(ctx context.Context, name string, vars Vars) error {
|
|||||||
return &taskNotFoundError{name}
|
return &taskNotFoundError{name}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if atomic.AddInt32(&t.callCount, 1) >= MaximumTaskCall {
|
||||||
|
return &MaximumTaskCallExceededError{task: name}
|
||||||
|
}
|
||||||
|
|
||||||
if err := e.runDeps(ctx, name, vars); err != nil {
|
if err := e.runDeps(ctx, name, vars); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
12
task_test.go
12
task_test.go
@ -199,3 +199,15 @@ func TestParams(t *testing.T) {
|
|||||||
assert.Equal(t, f.content, string(content))
|
assert.Equal(t, f.content, string(content))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCyclicDep(t *testing.T) {
|
||||||
|
const dir = "testdata/cyclic"
|
||||||
|
|
||||||
|
e := task.Executor{
|
||||||
|
Dir: dir,
|
||||||
|
Stdout: ioutil.Discard,
|
||||||
|
Stderr: ioutil.Discard,
|
||||||
|
}
|
||||||
|
assert.NoError(t, e.ReadTaskfile())
|
||||||
|
assert.IsType(t, &task.MaximumTaskCallExceededError{}, e.Run("task-1"))
|
||||||
|
}
|
||||||
|
7
testdata/cyclic/Taskfile.yml
vendored
Normal file
7
testdata/cyclic/Taskfile.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
task-1:
|
||||||
|
deps:
|
||||||
|
- task: task-2
|
||||||
|
|
||||||
|
task-2:
|
||||||
|
deps:
|
||||||
|
- task: task-1
|
Loading…
x
Reference in New Issue
Block a user