mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-24 15:55:03 +01:00
feat: implement case function (#16)
Follow GitHub Actions * added unit tests Closes #15 Reviewed-on: https://gitea.com/actions-oss/act-cli/pulls/16 Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-committed-by: ChristopherHX <christopher.homberger@web.de>
This commit is contained in:
committed by
ChristopherHX
parent
83cbf1f2b8
commit
ee2e0135d5
@@ -43,6 +43,18 @@ jobs:
|
|||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
fetch-depth: 2
|
fetch-depth: 2
|
||||||
|
- name: Cleanup Docker Engine
|
||||||
|
run: |
|
||||||
|
docker ps -a --format '{{ if eq (truncate .Names 4) "act-" }}
|
||||||
|
{{ .ID }}
|
||||||
|
{{end}}' | xargs -r docker rm -f || :
|
||||||
|
docker volume ls --format '{{ if eq (truncate .Name 4) "act-" }}
|
||||||
|
{{ .Name }}
|
||||||
|
{{ end }}' | xargs -r docker volume rm -f || :
|
||||||
|
docker images --format '{{ if eq (truncate .Repository 4) "act-" }}
|
||||||
|
{{ .ID }}
|
||||||
|
{{ end }}' | xargs -r docker rmi -f || :
|
||||||
|
docker images -q | xargs -r docker rmi || :
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: '!env.NO_QEMU'
|
if: '!env.NO_QEMU'
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|||||||
@@ -97,6 +97,12 @@ func TestEvaluator_Raw(t *testing.T) {
|
|||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], 6)", true},
|
{"contains(fromjson('[[3,5],[5,6]]').*[1], 6)", true},
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], 3)", false},
|
{"contains(fromjson('[[3,5],[5,6]]').*[1], 3)", false},
|
||||||
{"contains(fromjson('[[3,5],[5,6]]').*[1], '6')", true},
|
{"contains(fromjson('[[3,5],[5,6]]').*[1], '6')", true},
|
||||||
|
{"case(6 == 6, 0, 1)", 0.0},
|
||||||
|
{"case(6 != 6, 0, 1)", 1.0},
|
||||||
|
{"case(6 != 6, 0, 'test')", "test"},
|
||||||
|
{"case(contains(fromjson('[\"ac\"]'), 'a'), 0, 'test')", "test"},
|
||||||
|
{"case(0 == 1, 0, 2 == 2, 1, 0)", 1.0},
|
||||||
|
{"case(0 == 1, 0, 2 != 2, 1, 0)", 0.0},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/actions-oss/act-cli/internal/eval/functions"
|
"github.com/actions-oss/act-cli/internal/eval/functions"
|
||||||
@@ -163,6 +164,30 @@ func (Join) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult
|
|||||||
return CreateIntermediateResult(eval.Context(), ""), nil
|
return CreateIntermediateResult(eval.Context(), ""), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Case struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Case) Evaluate(eval *Evaluator, args []exprparser.Node) (*EvaluationResult, error) {
|
||||||
|
if len(args)%2 == 0 {
|
||||||
|
return nil, errors.New("case function requires an odd number of arguments")
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(args)-1; i += 2 {
|
||||||
|
condition, err := eval.Evaluate(args[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if condition.kind != ValueKindBoolean {
|
||||||
|
return nil, errors.New("case function conditions must evaluate to boolean")
|
||||||
|
}
|
||||||
|
if condition.IsTruthy() {
|
||||||
|
return eval.Evaluate(args[i+1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return eval.Evaluate(args[len(args)-1])
|
||||||
|
}
|
||||||
|
|
||||||
func GetFunctions() CaseInsensitiveObject[Function] {
|
func GetFunctions() CaseInsensitiveObject[Function] {
|
||||||
return CaseInsensitiveObject[Function](map[string]Function{
|
return CaseInsensitiveObject[Function](map[string]Function{
|
||||||
"fromjson": &FromJSON{},
|
"fromjson": &FromJSON{},
|
||||||
@@ -172,5 +197,6 @@ func GetFunctions() CaseInsensitiveObject[Function] {
|
|||||||
"endswith": &EndsWith{},
|
"endswith": &EndsWith{},
|
||||||
"format": &Format{},
|
"format": &Format{},
|
||||||
"join": &Join{},
|
"join": &Join{},
|
||||||
|
"case": &Case{},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (ee ExpressionEvaluator) canEvaluate(parsed exprparser.Node, snode *schema.
|
|||||||
canEvaluate = canEvaluate && ee.EvaluationContext.Variables.Get(v) != nil
|
canEvaluate = canEvaluate && ee.EvaluationContext.Variables.Get(v) != nil
|
||||||
}
|
}
|
||||||
for _, v := range snode.GetFunctions() {
|
for _, v := range snode.GetFunctions() {
|
||||||
canEvaluate = canEvaluate && ee.EvaluationContext.Functions.Get(v.Name) != nil
|
canEvaluate = canEvaluate && ee.EvaluationContext.Functions.Get(v.GetName()) != nil
|
||||||
}
|
}
|
||||||
exprparser.VisitNode(parsed, func(node exprparser.Node) {
|
exprparser.VisitNode(parsed, func(node exprparser.Node) {
|
||||||
switch el := node.(type) {
|
switch el := node.(type) {
|
||||||
|
|||||||
@@ -198,12 +198,44 @@ type Node struct {
|
|||||||
Context []string
|
Context []string
|
||||||
}
|
}
|
||||||
|
|
||||||
type FunctionInfo struct {
|
type FunctionInfo interface {
|
||||||
|
GetName() string
|
||||||
|
Check(args []exprparser.Node) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type BasicFunctionInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Min int
|
Min int
|
||||||
Max int
|
Max int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f BasicFunctionInfo) GetName() string {
|
||||||
|
return f.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f BasicFunctionInfo) Check(args []exprparser.Node) error {
|
||||||
|
var err error
|
||||||
|
if f.Min > len(args) {
|
||||||
|
err = errors.Join(err, fmt.Errorf("missing parameters for %s expected >= %v got %v", f.Name, f.Min, len(args)))
|
||||||
|
}
|
||||||
|
if f.Max < len(args) {
|
||||||
|
err = errors.Join(err, fmt.Errorf("too many parameters for %s expected <= %v got %v", f.Name, f.Max, len(args)))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type OddFunctionInfo struct {
|
||||||
|
BasicFunctionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f OddFunctionInfo) Check(args []exprparser.Node) error {
|
||||||
|
var err error
|
||||||
|
if len(args)%2 == 0 {
|
||||||
|
err = errors.Join(err, fmt.Errorf("expected odd number of parameters for %s got %v", f.Name, len(args)))
|
||||||
|
}
|
||||||
|
return errors.Join(err, f.BasicFunctionInfo.Check(args))
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Node) checkSingleExpression(exprNode exprparser.Node) error {
|
func (s *Node) checkSingleExpression(exprNode exprparser.Node) error {
|
||||||
if len(s.Context) == 0 {
|
if len(s.Context) == 0 {
|
||||||
switch exprNode.(type) {
|
switch exprNode.(type) {
|
||||||
@@ -220,13 +252,8 @@ func (s *Node) checkSingleExpression(exprNode exprparser.Node) error {
|
|||||||
exprparser.VisitNode(exprNode, func(node exprparser.Node) {
|
exprparser.VisitNode(exprNode, func(node exprparser.Node) {
|
||||||
if funcCallNode, ok := node.(*exprparser.FunctionNode); ok {
|
if funcCallNode, ok := node.(*exprparser.FunctionNode); ok {
|
||||||
for _, v := range funcs {
|
for _, v := range funcs {
|
||||||
if strings.EqualFold(funcCallNode.Name, v.Name) {
|
if strings.EqualFold(funcCallNode.Name, v.GetName()) {
|
||||||
if v.Min > len(funcCallNode.Args) {
|
err = v.Check(funcCallNode.Args)
|
||||||
err = errors.Join(err, fmt.Errorf("missing parameters for %s expected >= %v got %v", funcCallNode.Name, v.Min, len(funcCallNode.Args)))
|
|
||||||
}
|
|
||||||
if v.Max < len(funcCallNode.Args) {
|
|
||||||
err = errors.Join(err, fmt.Errorf("too many parameters for %s expected <= %v got %v", funcCallNode.Name, v.Max, len(funcCallNode.Args)))
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -255,6 +282,13 @@ func (s *Node) GetFunctions() []FunctionInfo {
|
|||||||
AddFunction(&funcs, "startsWith", 2, 2)
|
AddFunction(&funcs, "startsWith", 2, 2)
|
||||||
AddFunction(&funcs, "toJson", 1, 1)
|
AddFunction(&funcs, "toJson", 1, 1)
|
||||||
AddFunction(&funcs, "fromJson", 1, 1)
|
AddFunction(&funcs, "fromJson", 1, 1)
|
||||||
|
funcs = append(funcs, &OddFunctionInfo{
|
||||||
|
BasicFunctionInfo: BasicFunctionInfo{
|
||||||
|
Name: "case",
|
||||||
|
Min: 3,
|
||||||
|
Max: 255,
|
||||||
|
},
|
||||||
|
})
|
||||||
for _, v := range s.Context {
|
for _, v := range s.Context {
|
||||||
i := strings.Index(v, "(")
|
i := strings.Index(v, "(")
|
||||||
if i == -1 {
|
if i == -1 {
|
||||||
@@ -271,7 +305,7 @@ func (s *Node) GetFunctions() []FunctionInfo {
|
|||||||
} else {
|
} else {
|
||||||
maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
|
maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
|
||||||
}
|
}
|
||||||
funcs = append(funcs, FunctionInfo{
|
funcs = append(funcs, &BasicFunctionInfo{
|
||||||
Name: functionName,
|
Name: functionName,
|
||||||
Min: int(minParameters),
|
Min: int(minParameters),
|
||||||
Max: int(maxParameters),
|
Max: int(maxParameters),
|
||||||
@@ -330,7 +364,7 @@ func (s *Node) checkExpression(node *yaml.Node) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
|
func AddFunction(funcs *[]FunctionInfo, s string, i1, i2 int) {
|
||||||
*funcs = append(*funcs, FunctionInfo{
|
*funcs = append(*funcs, &BasicFunctionInfo{
|
||||||
Name: s,
|
Name: s,
|
||||||
Min: i1,
|
Min: i1,
|
||||||
Max: i2,
|
Max: i2,
|
||||||
@@ -451,9 +485,6 @@ func (s *Node) checkOneOf(def Definition, node *yaml.Node) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if matched == 0 {
|
|
||||||
matched = math.MaxInt
|
|
||||||
}
|
|
||||||
if matched <= invalidProps {
|
if matched <= invalidProps {
|
||||||
if matched < invalidProps {
|
if matched < invalidProps {
|
||||||
// clear, we have better matching ones
|
// clear, we have better matching ones
|
||||||
|
|||||||
@@ -148,3 +148,78 @@ jobs:
|
|||||||
}).UnmarshalYAML(&node)
|
}).UnmarshalYAML(&node)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSchemaErrors(t *testing.T) {
|
||||||
|
table := []struct {
|
||||||
|
name string // test name
|
||||||
|
input string // workflow yaml input
|
||||||
|
err string // error message substring
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "case even parameters is error",
|
||||||
|
input: `
|
||||||
|
${{ 'on' }}: push
|
||||||
|
jobs:
|
||||||
|
job-with-condition:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- run: echo ${{ case(1 == 1, 'zero', 2 == 2, 'one', 'two', '') }}
|
||||||
|
`,
|
||||||
|
err: "expected odd number of parameters for case got 6",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case odd parameters no error",
|
||||||
|
input: `
|
||||||
|
${{ 'on' }}: push
|
||||||
|
jobs:
|
||||||
|
job-with-condition:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- run: echo ${{ case(1 == 1, 'zero', 2 == 2, 'one', 'two') }}
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "case 1 parameters error",
|
||||||
|
input: `
|
||||||
|
${{ 'on' }}: push
|
||||||
|
jobs:
|
||||||
|
job-with-condition:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- run: echo ${{ case(1 == 1) }}
|
||||||
|
`,
|
||||||
|
err: "missing parameters for case expected >= 3 got 1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid expression in step uses",
|
||||||
|
input: `
|
||||||
|
on: push
|
||||||
|
jobs:
|
||||||
|
job-with-condition:
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- uses: ${{ format('actions/checkout@v%s', 'v2') }}
|
||||||
|
`,
|
||||||
|
err: "Line: 7 Column 17: expressions are not allowed here",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range table {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var node yaml.Node
|
||||||
|
err := yaml.Unmarshal([]byte(test.input), &node)
|
||||||
|
if !assert.NoError(t, err) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = (&Node{
|
||||||
|
Definition: "workflow-root-strict",
|
||||||
|
Schema: GetWorkflowSchema(),
|
||||||
|
}).UnmarshalYAML(&node)
|
||||||
|
if test.err != "" {
|
||||||
|
assert.ErrorContains(t, err, test.err)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user