summaryrefslogtreecommitdiffstats
path: root/pkg/runner/expression_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'pkg/runner/expression_test.go')
-rw-r--r--pkg/runner/expression_test.go376
1 files changed, 376 insertions, 0 deletions
diff --git a/pkg/runner/expression_test.go b/pkg/runner/expression_test.go
new file mode 100644
index 0000000..5cc2f7b
--- /dev/null
+++ b/pkg/runner/expression_test.go
@@ -0,0 +1,376 @@
+package runner
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "regexp"
+ "sort"
+ "testing"
+
+ "github.com/nektos/act/pkg/exprparser"
+ "github.com/nektos/act/pkg/model"
+ assert "github.com/stretchr/testify/assert"
+ yaml "gopkg.in/yaml.v3"
+)
+
+func createRunContext(t *testing.T) *RunContext {
+ var yml yaml.Node
+ err := yml.Encode(map[string][]interface{}{
+ "os": {"Linux", "Windows"},
+ "foo": {"bar", "baz"},
+ })
+ assert.NoError(t, err)
+
+ return &RunContext{
+ Config: &Config{
+ Workdir: ".",
+ Secrets: map[string]string{
+ "CASE_INSENSITIVE_SECRET": "value",
+ },
+ Vars: map[string]string{
+ "CASE_INSENSITIVE_VAR": "value",
+ },
+ },
+ Env: map[string]string{
+ "key": "value",
+ },
+ Run: &model.Run{
+ JobID: "job1",
+ Workflow: &model.Workflow{
+ Name: "test-workflow",
+ Jobs: map[string]*model.Job{
+ "job1": {
+ Strategy: &model.Strategy{
+ RawMatrix: yml,
+ },
+ },
+ },
+ },
+ },
+ Matrix: map[string]interface{}{
+ "os": "Linux",
+ "foo": "bar",
+ },
+ StepResults: map[string]*model.StepResult{
+ "idwithnothing": {
+ Conclusion: model.StepStatusSuccess,
+ Outcome: model.StepStatusFailure,
+ Outputs: map[string]string{
+ "foowithnothing": "barwithnothing",
+ },
+ },
+ "id-with-hyphens": {
+ Conclusion: model.StepStatusSuccess,
+ Outcome: model.StepStatusFailure,
+ Outputs: map[string]string{
+ "foo-with-hyphens": "bar-with-hyphens",
+ },
+ },
+ "id_with_underscores": {
+ Conclusion: model.StepStatusSuccess,
+ Outcome: model.StepStatusFailure,
+ Outputs: map[string]string{
+ "foo_with_underscores": "bar_with_underscores",
+ },
+ },
+ },
+ }
+}
+
+func TestEvaluateRunContext(t *testing.T) {
+ rc := createRunContext(t)
+ ee := rc.NewExpressionEvaluator(context.Background())
+
+ tables := []struct {
+ in string
+ out interface{}
+ errMesg string
+ }{
+ {" 1 ", 1, ""},
+ // {"1 + 3", "4", ""},
+ // {"(1 + 3) * -2", "-8", ""},
+ {"'my text'", "my text", ""},
+ {"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(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", ""},
+ // 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", ""},
+ {"matrix.os", "Linux", ""},
+ {"matrix.foo", "bar", ""},
+ {"env.key", "value", ""},
+ {"secrets.CASE_INSENSITIVE_SECRET", "value", ""},
+ {"secrets.case_insensitive_secret", "value", ""},
+ {"vars.CASE_INSENSITIVE_VAR", "value", ""},
+ {"vars.case_insensitive_var", "value", ""},
+ {"format('{{0}}', 'test')", "{0}", ""},
+ {"format('{{{0}}}', 'test')", "{test}", ""},
+ {"format('}}')", "}", ""},
+ {"format('echo Hello {0} ${{Test}}', 'World')", "echo Hello World ${Test}", ""},
+ {"format('echo Hello {0} ${{Test}}', github.undefined_property)", "echo Hello ${Test}", ""},
+ {"format('echo Hello {0}{1} ${{Te{0}st}}', github.undefined_property, 'World')", "echo Hello World ${Test}", ""},
+ {"format('{0}', '{1}', 'World')", "{1}", ""},
+ {"format('{{{0}', '{1}', 'World')", "{{1}", ""},
+ }
+
+ for _, table := range tables {
+ table := table
+ t.Run(table.in, func(t *testing.T) {
+ assertObject := assert.New(t)
+ out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
+ 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 TestEvaluateStep(t *testing.T) {
+ rc := createRunContext(t)
+ step := &stepRun{
+ RunContext: rc,
+ }
+
+ ee := rc.NewStepExpressionEvaluator(context.Background(), step)
+
+ tables := []struct {
+ in string
+ out interface{}
+ errMesg string
+ }{
+ {"steps.idwithnothing.conclusion", model.StepStatusSuccess.String(), ""},
+ {"steps.idwithnothing.outcome", model.StepStatusFailure.String(), ""},
+ {"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
+ {"steps.id-with-hyphens.conclusion", model.StepStatusSuccess.String(), ""},
+ {"steps.id-with-hyphens.outcome", model.StepStatusFailure.String(), ""},
+ {"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
+ {"steps.id_with_underscores.conclusion", model.StepStatusSuccess.String(), ""},
+ {"steps.id_with_underscores.outcome", model.StepStatusFailure.String(), ""},
+ {"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(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
+ 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 TestInterpolate(t *testing.T) {
+ rc := &RunContext{
+ Config: &Config{
+ Workdir: ".",
+ Secrets: map[string]string{
+ "CASE_INSENSITIVE_SECRET": "value",
+ },
+ Vars: map[string]string{
+ "CASE_INSENSITIVE_VAR": "value",
+ },
+ },
+ Env: map[string]string{
+ "KEYWITHNOTHING": "valuewithnothing",
+ "KEY-WITH-HYPHENS": "value-with-hyphens",
+ "KEY_WITH_UNDERSCORES": "value_with_underscores",
+ "SOMETHING_TRUE": "true",
+ "SOMETHING_FALSE": "false",
+ },
+ Run: &model.Run{
+ JobID: "job1",
+ Workflow: &model.Workflow{
+ Name: "test-workflow",
+ Jobs: map[string]*model.Job{
+ "job1": {},
+ },
+ },
+ },
+ }
+ ee := rc.NewExpressionEvaluator(context.Background())
+ tables := []struct {
+ in string
+ out string
+ }{
+ {" 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 "},
+ {"${{ secrets.CASE_INSENSITIVE_SECRET }}", "value"},
+ {"${{ secrets.case_insensitive_secret }}", "value"},
+ {"${{ vars.CASE_INSENSITIVE_VAR }}", "value"},
+ {"${{ vars.case_insensitive_var }}", "value"},
+ {"${{ env.UNKNOWN }}", ""},
+ {"${{ env.SOMETHING_TRUE }}", "true"},
+ {"${{ env.SOMETHING_FALSE }}", "false"},
+ {"${{ !env.SOMETHING_TRUE }}", "false"},
+ {"${{ !env.SOMETHING_FALSE }}", "false"},
+ {"${{ !env.SOMETHING_TRUE && true }}", "false"},
+ {"${{ !env.SOMETHING_FALSE && true }}", "false"},
+ {"${{ env.SOMETHING_TRUE && true }}", "true"},
+ {"${{ env.SOMETHING_FALSE && true }}", "true"},
+ {"${{ !env.SOMETHING_TRUE || true }}", "true"},
+ {"${{ !env.SOMETHING_FALSE || true }}", "true"},
+ {"${{ !env.SOMETHING_TRUE && false }}", "false"},
+ {"${{ !env.SOMETHING_FALSE && false }}", "false"},
+ {"${{ !env.SOMETHING_TRUE || false }}", "false"},
+ {"${{ !env.SOMETHING_FALSE || false }}", "false"},
+ {"${{ 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("interpolate", func(t *testing.T) {
+ assertObject := assert.New(t)
+ out := ee.Interpolate(context.Background(), table.in)
+ assertObject.Equal(table.out, out, table.in)
+ })
+ }
+}
+
+func updateTestExpressionWorkflow(t *testing.T, tables []struct {
+ in string
+ out string
+}, rc *RunContext) {
+ var envs string
+ keys := make([]string, 0, len(rc.Env))
+ for k := range rc.Env {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ for _, k := range keys {
+ envs += fmt.Sprintf(" %s: %s\n", k, rc.Env[k])
+ }
+
+ // editorconfig-checker-disable
+ workflow := fmt.Sprintf(`
+name: "Test how expressions are handled on GitHub"
+on: push
+
+env:
+%s
+
+jobs:
+ test-espressions:
+ runs-on: ubuntu-latest
+ steps:
+`, envs)
+ // editorconfig-checker-enable
+ for _, table := range tables {
+ expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
+
+ expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
+ return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
+ })
+ name := fmt.Sprintf(`%s -> %s should be equal to %s`, expr, table.in, table.out)
+ echo := `run: echo "Done "`
+ workflow += fmt.Sprintf("\n - name: %s\n %s\n", name, echo)
+ }
+
+ file, err := os.Create("../../.github/workflows/test-expressions.yml")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ _, err = file.WriteString(workflow)
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestRewriteSubExpression(t *testing.T) {
+ table := []struct {
+ in string
+ out string
+ }{
+ {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 table {
+ t.Run("TestRewriteSubExpression", func(t *testing.T) {
+ assertObject := assert.New(t)
+ out, err := rewriteSubExpression(context.Background(), 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(context.Background(), table.in, true)
+ if err != nil {
+ t.Fatal(err)
+ }
+ assertObject.Equal(table.out, out, table.in)
+ })
+ }
+}