1
0
Fork 0
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:
Björn Brauer 2022-01-21 17:07:20 +01:00 committed by GitHub
parent 4920c29312
commit 61c66784dd
10 changed files with 2037 additions and 706 deletions

View file

@ -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)
})
}
}