mirror of
https://gitea.com/gitea/act_runner.git
synced 2026-03-20 03:46:09 +08: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
|
||||
with:
|
||||
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
|
||||
if: '!env.NO_QEMU'
|
||||
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], 3)", false},
|
||||
{"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 {
|
||||
|
||||
@@ -2,6 +2,7 @@ package v2
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
}
|
||||
|
||||
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] {
|
||||
return CaseInsensitiveObject[Function](map[string]Function{
|
||||
"fromjson": &FromJSON{},
|
||||
@@ -172,5 +197,6 @@ func GetFunctions() CaseInsensitiveObject[Function] {
|
||||
"endswith": &EndsWith{},
|
||||
"format": &Format{},
|
||||
"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
|
||||
}
|
||||
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) {
|
||||
switch el := node.(type) {
|
||||
|
||||
@@ -198,12 +198,44 @@ type Node struct {
|
||||
Context []string
|
||||
}
|
||||
|
||||
type FunctionInfo struct {
|
||||
type FunctionInfo interface {
|
||||
GetName() string
|
||||
Check(args []exprparser.Node) error
|
||||
}
|
||||
|
||||
type BasicFunctionInfo struct {
|
||||
Name string
|
||||
Min 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 {
|
||||
if len(s.Context) == 0 {
|
||||
switch exprNode.(type) {
|
||||
@@ -220,13 +252,8 @@ func (s *Node) checkSingleExpression(exprNode exprparser.Node) error {
|
||||
exprparser.VisitNode(exprNode, func(node exprparser.Node) {
|
||||
if funcCallNode, ok := node.(*exprparser.FunctionNode); ok {
|
||||
for _, v := range funcs {
|
||||
if strings.EqualFold(funcCallNode.Name, v.Name) {
|
||||
if v.Min > len(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)))
|
||||
}
|
||||
if strings.EqualFold(funcCallNode.Name, v.GetName()) {
|
||||
err = v.Check(funcCallNode.Args)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -255,6 +282,13 @@ func (s *Node) GetFunctions() []FunctionInfo {
|
||||
AddFunction(&funcs, "startsWith", 2, 2)
|
||||
AddFunction(&funcs, "toJson", 1, 1)
|
||||
AddFunction(&funcs, "fromJson", 1, 1)
|
||||
funcs = append(funcs, &OddFunctionInfo{
|
||||
BasicFunctionInfo: BasicFunctionInfo{
|
||||
Name: "case",
|
||||
Min: 3,
|
||||
Max: 255,
|
||||
},
|
||||
})
|
||||
for _, v := range s.Context {
|
||||
i := strings.Index(v, "(")
|
||||
if i == -1 {
|
||||
@@ -271,7 +305,7 @@ func (s *Node) GetFunctions() []FunctionInfo {
|
||||
} else {
|
||||
maxParameters, _ = strconv.ParseInt(maxParametersRaw, 10, 32)
|
||||
}
|
||||
funcs = append(funcs, FunctionInfo{
|
||||
funcs = append(funcs, &BasicFunctionInfo{
|
||||
Name: functionName,
|
||||
Min: int(minParameters),
|
||||
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) {
|
||||
*funcs = append(*funcs, FunctionInfo{
|
||||
*funcs = append(*funcs, &BasicFunctionInfo{
|
||||
Name: s,
|
||||
Min: i1,
|
||||
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 {
|
||||
// clear, we have better matching ones
|
||||
|
||||
@@ -148,3 +148,78 @@ jobs:
|
||||
}).UnmarshalYAML(&node)
|
||||
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