Skip to content

Commit d6234af

Browse files
authored
feat: allow variable references in a matrix (#2069)
1 parent a31f2cf commit d6234af

File tree

5 files changed

+185
-39
lines changed

5 files changed

+185
-39
lines changed

task_test.go

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2975,6 +2975,7 @@ func TestForCmds(t *testing.T) {
29752975
tests := []struct {
29762976
name string
29772977
expectedOutput string
2978+
wantErr bool
29782979
}{
29792980
{
29802981
name: "loop-explicit",
@@ -2984,6 +2985,14 @@ func TestForCmds(t *testing.T) {
29842985
name: "loop-matrix",
29852986
expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n",
29862987
},
2988+
{
2989+
name: "loop-matrix-ref",
2990+
expectedOutput: "windows/amd64\nwindows/arm64\nlinux/amd64\nlinux/arm64\ndarwin/amd64\ndarwin/arm64\n",
2991+
},
2992+
{
2993+
name: "loop-matrix-ref-error",
2994+
wantErr: true,
2995+
},
29872996
{
29882997
name: "loop-sources",
29892998
expectedOutput: "bar\nfoo\n",
@@ -3018,18 +3027,22 @@ func TestForCmds(t *testing.T) {
30183027
t.Run(test.name, func(t *testing.T) {
30193028
t.Parallel()
30203029

3021-
var stdOut bytes.Buffer
3022-
var stdErr bytes.Buffer
3023-
e := task.Executor{
3030+
buf := &bytes.Buffer{}
3031+
e := &task.Executor{
30243032
Dir: "testdata/for/cmds",
3025-
Stdout: &stdOut,
3026-
Stderr: &stdErr,
3033+
Stdout: buf,
3034+
Stderr: buf,
30273035
Silent: true,
30283036
Force: true,
30293037
}
30303038
require.NoError(t, e.Setup())
3031-
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.name}))
3032-
assert.Equal(t, test.expectedOutput, stdOut.String())
3039+
err := e.Run(context.Background(), &ast.Call{Task: test.name})
3040+
if test.wantErr {
3041+
require.Error(t, err)
3042+
} else {
3043+
require.NoError(t, err)
3044+
}
3045+
assert.Equal(t, test.expectedOutput, buf.String())
30333046
})
30343047
}
30353048
}
@@ -3040,6 +3053,7 @@ func TestForDeps(t *testing.T) {
30403053
tests := []struct {
30413054
name string
30423055
expectedOutputContains []string
3056+
wantErr bool
30433057
}{
30443058
{
30453059
name: "loop-explicit",
@@ -3056,6 +3070,21 @@ func TestForDeps(t *testing.T) {
30563070
"darwin/arm64\n",
30573071
},
30583072
},
3073+
{
3074+
name: "loop-matrix-ref",
3075+
expectedOutputContains: []string{
3076+
"windows/amd64\n",
3077+
"windows/arm64\n",
3078+
"linux/amd64\n",
3079+
"linux/arm64\n",
3080+
"darwin/amd64\n",
3081+
"darwin/arm64\n",
3082+
},
3083+
},
3084+
{
3085+
name: "loop-matrix-ref-error",
3086+
wantErr: true,
3087+
},
30593088
{
30603089
name: "loop-sources",
30613090
expectedOutputContains: []string{"bar\n", "foo\n"},
@@ -3091,20 +3120,25 @@ func TestForDeps(t *testing.T) {
30913120
t.Parallel()
30923121

30933122
// We need to use a sync buffer here as deps are run concurrently
3094-
var buff SyncBuffer
3095-
e := task.Executor{
3123+
buf := &SyncBuffer{}
3124+
e := &task.Executor{
30963125
Dir: "testdata/for/deps",
3097-
Stdout: &buff,
3098-
Stderr: &buff,
3126+
Stdout: buf,
3127+
Stderr: buf,
30993128
Silent: true,
31003129
Force: true,
31013130
// Force output of each dep to be grouped together to prevent interleaving
31023131
OutputStyle: ast.Output{Name: "group"},
31033132
}
31043133
require.NoError(t, e.Setup())
3105-
require.NoError(t, e.Run(context.Background(), &ast.Call{Task: test.name}))
3134+
err := e.Run(context.Background(), &ast.Call{Task: test.name})
3135+
if test.wantErr {
3136+
require.Error(t, err)
3137+
} else {
3138+
require.NoError(t, err)
3139+
}
31063140
for _, expectedOutputContains := range test.expectedOutputContains {
3107-
assert.Contains(t, buff.buf.String(), expectedOutputContains)
3141+
assert.Contains(t, buf.buf.String(), expectedOutputContains)
31083142
}
31093143
})
31103144
}

taskfile/ast/matrix.go

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,25 @@ import (
1010
"github.com/go-task/task/v3/internal/deepcopy"
1111
)
1212

13-
type Matrix struct {
14-
om *orderedmap.OrderedMap[string, []any]
15-
}
16-
17-
type MatrixElement orderedmap.Element[string, []any]
13+
type (
14+
// Matrix is an ordered map of variable names to arrays of values.
15+
Matrix struct {
16+
om *orderedmap.OrderedMap[string, *MatrixRow]
17+
}
18+
// A MatrixElement is a key-value pair that is used for initializing a
19+
// Matrix structure.
20+
MatrixElement orderedmap.Element[string, *MatrixRow]
21+
// A MatrixRow list of values for a matrix key or a reference to another
22+
// variable.
23+
MatrixRow struct {
24+
Ref string
25+
Value []any
26+
}
27+
)
1828

1929
func NewMatrix(els ...*MatrixElement) *Matrix {
2030
matrix := &Matrix{
21-
om: orderedmap.NewOrderedMap[string, []any](),
31+
om: orderedmap.NewOrderedMap[string, *MatrixRow](),
2232
}
2333
for _, el := range els {
2434
matrix.Set(el.Key, el.Value)
@@ -33,27 +43,27 @@ func (matrix *Matrix) Len() int {
3343
return matrix.om.Len()
3444
}
3545

36-
func (matrix *Matrix) Get(key string) ([]any, bool) {
46+
func (matrix *Matrix) Get(key string) (*MatrixRow, bool) {
3747
if matrix == nil || matrix.om == nil {
3848
return nil, false
3949
}
4050
return matrix.om.Get(key)
4151
}
4252

43-
func (matrix *Matrix) Set(key string, value []any) bool {
53+
func (matrix *Matrix) Set(key string, value *MatrixRow) bool {
4454
if matrix == nil {
4555
matrix = NewMatrix()
4656
}
4757
if matrix.om == nil {
48-
matrix.om = orderedmap.NewOrderedMap[string, []any]()
58+
matrix.om = orderedmap.NewOrderedMap[string, *MatrixRow]()
4959
}
5060
return matrix.om.Set(key, value)
5161
}
5262

5363
// All returns an iterator that loops over all task key-value pairs.
54-
func (matrix *Matrix) All() iter.Seq2[string, []any] {
64+
func (matrix *Matrix) All() iter.Seq2[string, *MatrixRow] {
5565
if matrix == nil || matrix.om == nil {
56-
return func(yield func(string, []any) bool) {}
66+
return func(yield func(string, *MatrixRow) bool) {}
5767
}
5868
return matrix.om.AllFromFront()
5969
}
@@ -67,9 +77,9 @@ func (matrix *Matrix) Keys() iter.Seq[string] {
6777
}
6878

6979
// Values returns an iterator that loops over all task values.
70-
func (matrix *Matrix) Values() iter.Seq[[]any] {
80+
func (matrix *Matrix) Values() iter.Seq[*MatrixRow] {
7181
if matrix == nil || matrix.om == nil {
72-
return func(yield func([]any) bool) {}
82+
return func(yield func(*MatrixRow) bool) {}
7383
}
7484
return matrix.om.Values()
7585
}
@@ -93,14 +103,36 @@ func (matrix *Matrix) UnmarshalYAML(node *yaml.Node) error {
93103
keyNode := node.Content[i]
94104
valueNode := node.Content[i+1]
95105

96-
// Decode the value node into a Matrix struct
97-
var v []any
98-
if err := valueNode.Decode(&v); err != nil {
99-
return errors.NewTaskfileDecodeError(err, node)
106+
switch valueNode.Kind {
107+
case yaml.SequenceNode:
108+
// Decode the value node into a Matrix struct
109+
var v []any
110+
if err := valueNode.Decode(&v); err != nil {
111+
return errors.NewTaskfileDecodeError(err, node)
112+
}
113+
114+
// Add the row to the ordered map
115+
matrix.Set(keyNode.Value, &MatrixRow{
116+
Value: v,
117+
})
118+
119+
case yaml.MappingNode:
120+
// Decode the value node into a Matrix struct
121+
var refStruct struct {
122+
Ref string
123+
}
124+
if err := valueNode.Decode(&refStruct); err != nil {
125+
return errors.NewTaskfileDecodeError(err, node)
126+
}
127+
128+
// Add the reference to the ordered map
129+
matrix.Set(keyNode.Value, &MatrixRow{
130+
Ref: refStruct.Ref,
131+
})
132+
133+
default:
134+
return errors.NewTaskfileDecodeError(nil, node).WithMessage("matrix values must be an array or a reference")
100135
}
101-
102-
// Add the task to the ordered map
103-
matrix.Set(keyNode.Value, v)
104136
}
105137
return nil
106138
}

testdata/for/cmds/Taskfile.yml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
version: "3"
22

3+
vars:
4+
OS_VAR: ["windows", "linux", "darwin"]
5+
ARCH_VAR: ["amd64", "arm64"]
6+
NOT_A_LIST: "not a list"
7+
38
tasks:
49
# Loop over a list of values
510
loop-explicit:
@@ -15,6 +20,26 @@ tasks:
1520
ARCH: ["amd64", "arm64"]
1621
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
1722

23+
loop-matrix-ref:
24+
cmds:
25+
- for:
26+
matrix:
27+
OS:
28+
ref: .OS_VAR
29+
ARCH:
30+
ref: .ARCH_VAR
31+
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
32+
33+
loop-matrix-ref-error:
34+
cmds:
35+
- for:
36+
matrix:
37+
OS:
38+
ref: .OS_VAR
39+
ARCH:
40+
ref: .NOT_A_LIST
41+
cmd: echo "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
42+
1843
# Loop over the task's sources
1944
loop-sources:
2045
sources:

testdata/for/deps/Taskfile.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
version: "3"
22

3+
vars:
4+
OS_VAR: ["windows", "linux", "darwin"]
5+
ARCH_VAR: ["amd64", "arm64"]
6+
NOT_A_LIST: "not a list"
7+
38
tasks:
49
# Loop over a list of values
510
loop-explicit:
@@ -19,6 +24,30 @@ tasks:
1924
vars:
2025
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
2126

27+
loop-matrix-ref:
28+
deps:
29+
- for:
30+
matrix:
31+
OS:
32+
ref: .OS_VAR
33+
ARCH:
34+
ref: .ARCH_VAR
35+
task: echo
36+
vars:
37+
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
38+
39+
loop-matrix-ref-error:
40+
deps:
41+
- for:
42+
matrix:
43+
OS:
44+
ref: .OS_VAR
45+
ARCH:
46+
ref: .NOT_A_LIST
47+
task: echo
48+
vars:
49+
TEXT: "{{.ITEM.OS}}/{{.ITEM.ARCH}}"
50+
2251
# Loop over the task's sources
2352
loop-sources:
2453
sources:

0 commit comments

Comments
 (0)