mirror of
https://github.com/go-task/task.git
synced 2026-09-01 19:50:16 +02:00
feat: command timeouts (#2898)
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
@@ -21,6 +23,7 @@ type Cmd struct {
|
||||
IgnoreError bool
|
||||
Defer bool
|
||||
Platforms []*Platform
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (c *Cmd) DeepCopy() *Cmd {
|
||||
@@ -40,6 +43,7 @@ func (c *Cmd) DeepCopy() *Cmd {
|
||||
IgnoreError: c.IgnoreError,
|
||||
Defer: c.Defer,
|
||||
Platforms: deepcopy.Slice(c.Platforms),
|
||||
Timeout: c.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,11 +71,26 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
IgnoreError bool `yaml:"ignore_error"`
|
||||
Defer *Defer
|
||||
Platforms []*Platform
|
||||
Timeout string
|
||||
}
|
||||
if err := node.Decode(&cmdStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
|
||||
if cmdStruct.Timeout != "" {
|
||||
timeout, err := parseTimeout(cmdStruct.Timeout, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Timeout = timeout
|
||||
}
|
||||
|
||||
if cmdStruct.Defer != nil {
|
||||
// Rejected rather than dropped: without the field, yaml would
|
||||
// swallow the key without a word.
|
||||
if cmdStruct.Defer.Timeout != "" {
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithMessage("timeout must be set next to defer, not inside it")
|
||||
}
|
||||
|
||||
// A deferred command
|
||||
if cmdStruct.Defer.Cmd != "" {
|
||||
@@ -121,3 +140,16 @@ func (c *Cmd) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
return errors.NewTaskfileDecodeError(nil, node).WithTypeMessage("command")
|
||||
}
|
||||
|
||||
// parseTimeout rejects non-positive durations, which would otherwise read as no
|
||||
// timeout at all - the unbounded run the key exists to prevent.
|
||||
func parseTimeout(s string, node *yaml.Node) (time.Duration, error) {
|
||||
timeout, err := time.ParseDuration(s)
|
||||
if err != nil {
|
||||
return 0, errors.NewTaskfileDecodeError(err, node).WithMessage("invalid timeout format")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
return 0, errors.NewTaskfileDecodeError(nil, node).WithMessage("timeout must be greater than zero")
|
||||
}
|
||||
return timeout, nil
|
||||
}
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
)
|
||||
|
||||
type Defer struct {
|
||||
Cmd string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Cmd string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Timeout string
|
||||
}
|
||||
|
||||
func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
|
||||
@@ -26,10 +27,11 @@ func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
case yaml.MappingNode:
|
||||
var deferStruct struct {
|
||||
Defer string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Defer string
|
||||
Task string
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Timeout string
|
||||
}
|
||||
if err := node.Decode(&deferStruct); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
@@ -38,6 +40,7 @@ func (d *Defer) UnmarshalYAML(node *yaml.Node) error {
|
||||
d.Task = deferStruct.Task
|
||||
d.Vars = deferStruct.Vars
|
||||
d.Silent = deferStruct.Silent
|
||||
d.Timeout = deferStruct.Timeout
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package ast
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
@@ -8,10 +10,11 @@ import (
|
||||
|
||||
// Dep is a task dependency
|
||||
type Dep struct {
|
||||
Task string
|
||||
For *For
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Task string
|
||||
For *For
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
func (d *Dep) DeepCopy() *Dep {
|
||||
@@ -19,10 +22,11 @@ func (d *Dep) DeepCopy() *Dep {
|
||||
return nil
|
||||
}
|
||||
return &Dep{
|
||||
Task: d.Task,
|
||||
For: d.For.DeepCopy(),
|
||||
Vars: d.Vars.DeepCopy(),
|
||||
Silent: d.Silent,
|
||||
Task: d.Task,
|
||||
For: d.For.DeepCopy(),
|
||||
Vars: d.Vars.DeepCopy(),
|
||||
Silent: d.Silent,
|
||||
Timeout: d.Timeout,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,14 +43,22 @@ func (d *Dep) UnmarshalYAML(node *yaml.Node) error {
|
||||
|
||||
case yaml.MappingNode:
|
||||
var taskCall struct {
|
||||
Task string
|
||||
For *For
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Task string
|
||||
For *For
|
||||
Vars *Vars
|
||||
Silent bool
|
||||
Timeout string
|
||||
}
|
||||
if err := node.Decode(&taskCall); err != nil {
|
||||
return errors.NewTaskfileDecodeError(err, node)
|
||||
}
|
||||
if taskCall.Timeout != "" {
|
||||
timeout, err := parseTimeout(taskCall.Timeout, node)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.Timeout = timeout
|
||||
}
|
||||
d.Task = taskCall.Task
|
||||
d.For = taskCall.For
|
||||
d.Vars = taskCall.Vars
|
||||
|
||||
@@ -2,6 +2,7 @@ package ast_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -22,8 +23,10 @@ vars:
|
||||
PARAM1: VALUE1
|
||||
PARAM2: VALUE2
|
||||
`
|
||||
yamlDeferredCall = `defer: { task: some_task, vars: { PARAM1: "var" } }`
|
||||
yamlDeferredCmd = `defer: echo 'test'`
|
||||
yamlDeferredCall = `defer: { task: some_task, vars: { PARAM1: "var" } }`
|
||||
yamlDeferredCallWithTimeout = `{ defer: { task: some_task }, timeout: 1s }`
|
||||
yamlDeferredCmd = `defer: echo 'test'`
|
||||
yamlDeferredCmdWithTimeout = `{ defer: echo 'test', timeout: 1s }`
|
||||
)
|
||||
tests := []struct {
|
||||
content string
|
||||
@@ -77,6 +80,16 @@ vars:
|
||||
Defer: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
yamlDeferredCallWithTimeout,
|
||||
&ast.Cmd{},
|
||||
&ast.Cmd{Task: "some_task", Defer: true, Timeout: time.Second},
|
||||
},
|
||||
{
|
||||
yamlDeferredCmdWithTimeout,
|
||||
&ast.Cmd{},
|
||||
&ast.Cmd{Cmd: `echo 'test'`, Defer: true, Timeout: time.Second},
|
||||
},
|
||||
{
|
||||
yamlDep,
|
||||
&ast.Dep{},
|
||||
@@ -110,3 +123,55 @@ vars:
|
||||
assert.Equal(t, test.expected, test.v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTimeoutParseError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
message string
|
||||
}{
|
||||
{
|
||||
name: "unparsable duration",
|
||||
content: `{cmd: echo, timeout: invalid}`,
|
||||
message: "invalid timeout format",
|
||||
},
|
||||
{
|
||||
name: "zero duration",
|
||||
content: `{cmd: echo, timeout: 0s}`,
|
||||
message: "timeout must be greater than zero",
|
||||
},
|
||||
{
|
||||
name: "negative duration",
|
||||
content: `{cmd: echo, timeout: -1s}`,
|
||||
message: "timeout must be greater than zero",
|
||||
},
|
||||
{
|
||||
name: "negative duration on a deferred task",
|
||||
content: `{defer: {task: some_task}, timeout: -5m}`,
|
||||
message: "timeout must be greater than zero",
|
||||
},
|
||||
{
|
||||
name: "unparsable duration on a deferred task",
|
||||
content: `{defer: {task: some_task}, timeout: invalid}`,
|
||||
message: "invalid timeout format",
|
||||
},
|
||||
{
|
||||
name: "timeout nested inside defer",
|
||||
content: `{defer: {task: some_task, timeout: 1s}}`,
|
||||
message: "timeout must be set next to defer, not inside it",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var cmd ast.Cmd
|
||||
err := yaml.Unmarshal([]byte(test.content), &cmd)
|
||||
require.Error(t, err)
|
||||
assert.ErrorContains(t, err, test.message)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user