mirror of
https://code.forgejo.org/forgejo/runner.git
synced 2025-08-11 17:50:58 +00:00
Refactor expression evaluator to use parser from actionlint package (#908)
* feat: implement expression evaluator Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * feat: integrate exprparser into act Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> * Escape { and }, do not fail on missing properties * Fix empty inputs context * fix: contains() comparison for complex values Co-authored-by: Markus Wolf <markus.wolf@new-work.se> Co-authored-by: Philipp Hinrichsen <philipp.hinrichsen@new-work.se> Co-authored-by: Christopher Homberger <christopher.homberger@web.de>
This commit is contained in:
parent
4920c29312
commit
61c66784dd
10 changed files with 2037 additions and 706 deletions
|
@ -12,7 +12,7 @@ import (
|
|||
yaml "gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func TestEvaluate(t *testing.T) {
|
||||
func createRunContext(t *testing.T) *RunContext {
|
||||
var yml yaml.Node
|
||||
err := yml.Encode(map[string][]interface{}{
|
||||
"os": {"Linux", "Windows"},
|
||||
|
@ -20,7 +20,7 @@ func TestEvaluate(t *testing.T) {
|
|||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
rc := &RunContext{
|
||||
return &RunContext{
|
||||
Config: &Config{
|
||||
Workdir: ".",
|
||||
Secrets: map[string]string{
|
||||
|
@ -71,54 +71,50 @@ func TestEvaluate(t *testing.T) {
|
|||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateRunContext(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
out string
|
||||
out interface{}
|
||||
errMesg string
|
||||
}{
|
||||
{" 1 ", "1", ""},
|
||||
{"1 + 3", "4", ""},
|
||||
{"(1 + 3) * -2", "-8", ""},
|
||||
{" 1 ", 1, ""},
|
||||
// {"1 + 3", "4", ""},
|
||||
// {"(1 + 3) * -2", "-8", ""},
|
||||
{"'my text'", "my text", ""},
|
||||
{"contains('my text', 'te')", "true", ""},
|
||||
{"contains('my TEXT', 'te')", "true", ""},
|
||||
{"contains(['my text'], 'te')", "false", ""},
|
||||
{"contains(['foo','bar'], 'bar')", "true", ""},
|
||||
{"startsWith('hello world', 'He')", "true", ""},
|
||||
{"endsWith('hello world', 'ld')", "true", ""},
|
||||
{"contains('my text', 'te')", true, ""},
|
||||
{"contains('my TEXT', 'te')", true, ""},
|
||||
{"contains(fromJSON('[\"my text\"]'), 'te')", false, ""},
|
||||
{"contains(fromJSON('[\"foo\",\"bar\"]'), 'bar')", true, ""},
|
||||
{"startsWith('hello world', 'He')", true, ""},
|
||||
{"endsWith('hello world', 'ld')", true, ""},
|
||||
{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
|
||||
{"join(['hello'],'octocat')", "hello octocat", ""},
|
||||
{"join(['hello','mona','the'],'octocat')", "hello mona the octocat", ""},
|
||||
{"join('hello','mona')", "hello mona", ""},
|
||||
{"toJSON({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
||||
{"toJson({'foo':'bar'})", "{\n \"foo\": \"bar\"\n}", ""},
|
||||
{"join(fromJSON('[\"hello\"]'),'octocat')", "hello", ""},
|
||||
{"join(fromJSON('[\"hello\",\"mona\",\"the\"]'),'octocat')", "hellooctocatmonaoctocatthe", ""},
|
||||
{"join('hello','mona')", "hello", ""},
|
||||
{"toJSON(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||
{"toJson(env)", "{\n \"ACT\": \"true\",\n \"key\": \"value\"\n}", ""},
|
||||
{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||
{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
|
||||
{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
|
||||
{"hashFiles('**/non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non.extant.files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"hashFiles('**/non''extant''files')", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", ""},
|
||||
{"success()", "true", ""},
|
||||
{"failure()", "false", ""},
|
||||
{"always()", "true", ""},
|
||||
{"cancelled()", "false", ""},
|
||||
// github does return an empty string for non-existent files
|
||||
{"hashFiles('**/non-extant-files')", "", ""},
|
||||
{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "", ""},
|
||||
{"hashFiles('**/non.extant.files')", "", ""},
|
||||
{"hashFiles('**/non''extant''files')", "", ""},
|
||||
{"success()", true, ""},
|
||||
{"failure()", false, ""},
|
||||
{"always()", true, ""},
|
||||
{"cancelled()", false, ""},
|
||||
{"github.workflow", "test-workflow", ""},
|
||||
{"github.actor", "nektos/act", ""},
|
||||
{"github.run_id", "1", ""},
|
||||
{"github.run_number", "1", ""},
|
||||
{"job.status", "success", ""},
|
||||
{"steps.idwithnothing.conclusion", "success", ""},
|
||||
{"steps.idwithnothing.outcome", "failure", ""},
|
||||
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
|
||||
{"steps.id-with-hyphens.conclusion", "success", ""},
|
||||
{"steps.id-with-hyphens.outcome", "failure", ""},
|
||||
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
|
||||
{"steps.id_with_underscores.conclusion", "success", ""},
|
||||
{"steps.id_with_underscores.outcome", "failure", ""},
|
||||
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
||||
{"runner.os", "Linux", ""},
|
||||
{"matrix.os", "Linux", ""},
|
||||
{"matrix.foo", "bar", ""},
|
||||
|
@ -139,7 +135,47 @@ func TestEvaluate(t *testing.T) {
|
|||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, _, err := ee.Evaluate(table.in)
|
||||
out, err := ee.evaluate(table.in, false)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
} else {
|
||||
assertObject.Error(err, table.in)
|
||||
assertObject.Equal(table.errMesg, err.Error(), table.in)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluateStepContext(t *testing.T) {
|
||||
rc := createRunContext(t)
|
||||
|
||||
sc := &StepContext{
|
||||
RunContext: rc,
|
||||
}
|
||||
ee := sc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
out interface{}
|
||||
errMesg string
|
||||
}{
|
||||
{"steps.idwithnothing.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.idwithnothing.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
|
||||
{"steps.id-with-hyphens.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.id-with-hyphens.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
|
||||
{"steps.id_with_underscores.conclusion", model.StepStatusSuccess, ""},
|
||||
{"steps.id_with_underscores.outcome", model.StepStatusFailure, ""},
|
||||
{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := ee.evaluate(table.in, false)
|
||||
if table.errMesg == "" {
|
||||
assertObject.NoError(err, table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
|
@ -181,7 +217,12 @@ func TestInterpolate(t *testing.T) {
|
|||
in string
|
||||
out string
|
||||
}{
|
||||
{" ${{1}} to ${{2}} ", " 1 to 2 "},
|
||||
{" text ", " text "},
|
||||
{" $text ", " $text "},
|
||||
{" ${text} ", " ${text} "},
|
||||
{" ${{ 1 }} to ${{2}} ", " 1 to 2 "},
|
||||
{" ${{ (true || false) }} to ${{2}} ", " true to 2 "},
|
||||
{" ${{ (false || '}}' ) }} to ${{2}} ", " }} to 2 "},
|
||||
{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
|
||||
{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
|
||||
{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
|
||||
|
@ -205,12 +246,13 @@ func TestInterpolate(t *testing.T) {
|
|||
{"${{ env.SOMETHING_TRUE || false }}", "true"},
|
||||
{"${{ env.SOMETHING_FALSE || false }}", "false"},
|
||||
{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
|
||||
{"${{ fromJSON('{}') < 2 }}", "false"},
|
||||
}
|
||||
|
||||
updateTestExpressionWorkflow(t, tables, rc)
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
t.Run("interpolate", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out := ee.Interpolate(table.in)
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
|
@ -247,7 +289,7 @@ jobs:
|
|||
`, envs)
|
||||
// editorconfig-checker-enable
|
||||
for _, table := range tables {
|
||||
expressionPattern = regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
|
||||
|
||||
expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
|
||||
return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
|
||||
|
@ -268,43 +310,56 @@ jobs:
|
|||
}
|
||||
}
|
||||
|
||||
func TestRewrite(t *testing.T) {
|
||||
rc := &RunContext{
|
||||
Config: &Config{},
|
||||
Run: &model.Run{
|
||||
JobID: "job1",
|
||||
Workflow: &model.Workflow{
|
||||
Jobs: map[string]*model.Job{
|
||||
"job1": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
ee := rc.NewExpressionEvaluator()
|
||||
|
||||
tables := []struct {
|
||||
in string
|
||||
re string
|
||||
func TestRewriteSubExpression(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"ecole", "ecole"},
|
||||
{"ecole.centrale", "ecole['centrale']"},
|
||||
{"ecole['centrale']", "ecole['centrale']"},
|
||||
{"ecole.centrale.paris", "ecole['centrale']['paris']"},
|
||||
{"ecole['centrale'].paris", "ecole['centrale']['paris']"},
|
||||
{"ecole.centrale['paris']", "ecole['centrale']['paris']"},
|
||||
{"ecole['centrale']['paris']", "ecole['centrale']['paris']"},
|
||||
{"ecole.centrale-paris", "ecole['centrale-paris']"},
|
||||
{"ecole['centrale-paris']", "ecole['centrale-paris']"},
|
||||
{"ecole.centrale_paris", "ecole['centrale_paris']"},
|
||||
{"ecole['centrale_paris']", "ecole['centrale_paris']"},
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "${{ true }}"},
|
||||
{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
|
||||
{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
|
||||
{in: "${{ '}}' }}", out: "${{ '}}' }}"},
|
||||
{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
|
||||
{in: "${{ '''' }}", out: "${{ '''' }}"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
|
||||
{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
|
||||
{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
}
|
||||
|
||||
for _, table := range tables {
|
||||
table := table
|
||||
t.Run(table.in, func(t *testing.T) {
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpression", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
re := ee.Rewrite(table.in)
|
||||
assertObject.Equal(table.re, re, table.in)
|
||||
out, err := rewriteSubExpression(table.in, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRewriteSubExpressionForceFormat(t *testing.T) {
|
||||
table := []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{in: "Hello World", out: "Hello World"},
|
||||
{in: "${{ true }}", out: "format('{0}', true)"},
|
||||
{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
|
||||
{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
|
||||
{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
|
||||
}
|
||||
|
||||
for _, table := range table {
|
||||
t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
|
||||
assertObject := assert.New(t)
|
||||
out, err := rewriteSubExpression(table.in, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assertObject.Equal(table.out, out, table.in)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue