github.com/nektos/act@v0.2.63/pkg/runner/expression_test.go (about)

     1  package runner
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"regexp"
     8  	"sort"
     9  	"testing"
    10  
    11  	"github.com/nektos/act/pkg/exprparser"
    12  	"github.com/nektos/act/pkg/model"
    13  	assert "github.com/stretchr/testify/assert"
    14  	yaml "gopkg.in/yaml.v3"
    15  )
    16  
    17  func createRunContext(t *testing.T) *RunContext {
    18  	var yml yaml.Node
    19  	err := yml.Encode(map[string][]interface{}{
    20  		"os":  {"Linux", "Windows"},
    21  		"foo": {"bar", "baz"},
    22  	})
    23  	assert.NoError(t, err)
    24  
    25  	return &RunContext{
    26  		Config: &Config{
    27  			Workdir: ".",
    28  			Secrets: map[string]string{
    29  				"CASE_INSENSITIVE_SECRET": "value",
    30  			},
    31  			Vars: map[string]string{
    32  				"CASE_INSENSITIVE_VAR": "value",
    33  			},
    34  		},
    35  		Env: map[string]string{
    36  			"key": "value",
    37  		},
    38  		Run: &model.Run{
    39  			JobID: "job1",
    40  			Workflow: &model.Workflow{
    41  				Name: "test-workflow",
    42  				Jobs: map[string]*model.Job{
    43  					"job1": {
    44  						Strategy: &model.Strategy{
    45  							RawMatrix: yml,
    46  						},
    47  					},
    48  				},
    49  			},
    50  		},
    51  		Matrix: map[string]interface{}{
    52  			"os":  "Linux",
    53  			"foo": "bar",
    54  		},
    55  		StepResults: map[string]*model.StepResult{
    56  			"idwithnothing": {
    57  				Conclusion: model.StepStatusSuccess,
    58  				Outcome:    model.StepStatusFailure,
    59  				Outputs: map[string]string{
    60  					"foowithnothing": "barwithnothing",
    61  				},
    62  			},
    63  			"id-with-hyphens": {
    64  				Conclusion: model.StepStatusSuccess,
    65  				Outcome:    model.StepStatusFailure,
    66  				Outputs: map[string]string{
    67  					"foo-with-hyphens": "bar-with-hyphens",
    68  				},
    69  			},
    70  			"id_with_underscores": {
    71  				Conclusion: model.StepStatusSuccess,
    72  				Outcome:    model.StepStatusFailure,
    73  				Outputs: map[string]string{
    74  					"foo_with_underscores": "bar_with_underscores",
    75  				},
    76  			},
    77  		},
    78  	}
    79  }
    80  
    81  func TestEvaluateRunContext(t *testing.T) {
    82  	rc := createRunContext(t)
    83  	ee := rc.NewExpressionEvaluator(context.Background())
    84  
    85  	tables := []struct {
    86  		in      string
    87  		out     interface{}
    88  		errMesg string
    89  	}{
    90  		{" 1 ", 1, ""},
    91  		// {"1 + 3", "4", ""},
    92  		// {"(1 + 3) * -2", "-8", ""},
    93  		{"'my text'", "my text", ""},
    94  		{"contains('my text', 'te')", true, ""},
    95  		{"contains('my TEXT', 'te')", true, ""},
    96  		{"contains(fromJSON('[\"my text\"]'), 'te')", false, ""},
    97  		{"contains(fromJSON('[\"foo\",\"bar\"]'), 'bar')", true, ""},
    98  		{"startsWith('hello world', 'He')", true, ""},
    99  		{"endsWith('hello world', 'ld')", true, ""},
   100  		{"format('0:{0} 2:{2} 1:{1}', 'zero', 'one', 'two')", "0:zero 2:two 1:one", ""},
   101  		{"join(fromJSON('[\"hello\"]'),'octocat')", "hello", ""},
   102  		{"join(fromJSON('[\"hello\",\"mona\",\"the\"]'),'octocat')", "hellooctocatmonaoctocatthe", ""},
   103  		{"join('hello','mona')", "hello", ""},
   104  		{"toJSON(env)", "{\n  \"ACT\": \"true\",\n  \"key\": \"value\"\n}", ""},
   105  		{"toJson(env)", "{\n  \"ACT\": \"true\",\n  \"key\": \"value\"\n}", ""},
   106  		{"(fromJSON('{\"foo\":\"bar\"}')).foo", "bar", ""},
   107  		{"(fromJson('{\"foo\":\"bar\"}')).foo", "bar", ""},
   108  		{"(fromJson('[\"foo\",\"bar\"]'))[1]", "bar", ""},
   109  		// github does return an empty string for non-existent files
   110  		{"hashFiles('**/non-extant-files')", "", ""},
   111  		{"hashFiles('**/non-extant-files', '**/more-non-extant-files')", "", ""},
   112  		{"hashFiles('**/non.extant.files')", "", ""},
   113  		{"hashFiles('**/non''extant''files')", "", ""},
   114  		{"success()", true, ""},
   115  		{"failure()", false, ""},
   116  		{"always()", true, ""},
   117  		{"cancelled()", false, ""},
   118  		{"github.workflow", "test-workflow", ""},
   119  		{"github.actor", "nektos/act", ""},
   120  		{"github.run_id", "1", ""},
   121  		{"github.run_number", "1", ""},
   122  		{"job.status", "success", ""},
   123  		{"matrix.os", "Linux", ""},
   124  		{"matrix.foo", "bar", ""},
   125  		{"env.key", "value", ""},
   126  		{"secrets.CASE_INSENSITIVE_SECRET", "value", ""},
   127  		{"secrets.case_insensitive_secret", "value", ""},
   128  		{"vars.CASE_INSENSITIVE_VAR", "value", ""},
   129  		{"vars.case_insensitive_var", "value", ""},
   130  		{"format('{{0}}', 'test')", "{0}", ""},
   131  		{"format('{{{0}}}', 'test')", "{test}", ""},
   132  		{"format('}}')", "}", ""},
   133  		{"format('echo Hello {0} ${{Test}}', 'World')", "echo Hello World ${Test}", ""},
   134  		{"format('echo Hello {0} ${{Test}}', github.undefined_property)", "echo Hello  ${Test}", ""},
   135  		{"format('echo Hello {0}{1} ${{Te{0}st}}', github.undefined_property, 'World')", "echo Hello World ${Test}", ""},
   136  		{"format('{0}', '{1}', 'World')", "{1}", ""},
   137  		{"format('{{{0}', '{1}', 'World')", "{{1}", ""},
   138  	}
   139  
   140  	for _, table := range tables {
   141  		table := table
   142  		t.Run(table.in, func(t *testing.T) {
   143  			assertObject := assert.New(t)
   144  			out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
   145  			if table.errMesg == "" {
   146  				assertObject.NoError(err, table.in)
   147  				assertObject.Equal(table.out, out, table.in)
   148  			} else {
   149  				assertObject.Error(err, table.in)
   150  				assertObject.Equal(table.errMesg, err.Error(), table.in)
   151  			}
   152  		})
   153  	}
   154  }
   155  
   156  func TestEvaluateStep(t *testing.T) {
   157  	rc := createRunContext(t)
   158  	step := &stepRun{
   159  		RunContext: rc,
   160  	}
   161  
   162  	ee := rc.NewStepExpressionEvaluator(context.Background(), step)
   163  
   164  	tables := []struct {
   165  		in      string
   166  		out     interface{}
   167  		errMesg string
   168  	}{
   169  		{"steps.idwithnothing.conclusion", model.StepStatusSuccess.String(), ""},
   170  		{"steps.idwithnothing.outcome", model.StepStatusFailure.String(), ""},
   171  		{"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""},
   172  		{"steps.id-with-hyphens.conclusion", model.StepStatusSuccess.String(), ""},
   173  		{"steps.id-with-hyphens.outcome", model.StepStatusFailure.String(), ""},
   174  		{"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""},
   175  		{"steps.id_with_underscores.conclusion", model.StepStatusSuccess.String(), ""},
   176  		{"steps.id_with_underscores.outcome", model.StepStatusFailure.String(), ""},
   177  		{"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""},
   178  	}
   179  
   180  	for _, table := range tables {
   181  		table := table
   182  		t.Run(table.in, func(t *testing.T) {
   183  			assertObject := assert.New(t)
   184  			out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone)
   185  			if table.errMesg == "" {
   186  				assertObject.NoError(err, table.in)
   187  				assertObject.Equal(table.out, out, table.in)
   188  			} else {
   189  				assertObject.Error(err, table.in)
   190  				assertObject.Equal(table.errMesg, err.Error(), table.in)
   191  			}
   192  		})
   193  	}
   194  }
   195  
   196  func TestInterpolate(t *testing.T) {
   197  	rc := &RunContext{
   198  		Config: &Config{
   199  			Workdir: ".",
   200  			Secrets: map[string]string{
   201  				"CASE_INSENSITIVE_SECRET": "value",
   202  			},
   203  			Vars: map[string]string{
   204  				"CASE_INSENSITIVE_VAR": "value",
   205  			},
   206  		},
   207  		Env: map[string]string{
   208  			"KEYWITHNOTHING":       "valuewithnothing",
   209  			"KEY-WITH-HYPHENS":     "value-with-hyphens",
   210  			"KEY_WITH_UNDERSCORES": "value_with_underscores",
   211  			"SOMETHING_TRUE":       "true",
   212  			"SOMETHING_FALSE":      "false",
   213  		},
   214  		Run: &model.Run{
   215  			JobID: "job1",
   216  			Workflow: &model.Workflow{
   217  				Name: "test-workflow",
   218  				Jobs: map[string]*model.Job{
   219  					"job1": {},
   220  				},
   221  			},
   222  		},
   223  	}
   224  	ee := rc.NewExpressionEvaluator(context.Background())
   225  	tables := []struct {
   226  		in  string
   227  		out string
   228  	}{
   229  		{" text ", " text "},
   230  		{" $text ", " $text "},
   231  		{" ${text} ", " ${text} "},
   232  		{" ${{          1                         }} to ${{2}} ", " 1 to 2 "},
   233  		{" ${{  (true || false)  }} to ${{2}} ", " true to 2 "},
   234  		{" ${{  (false   ||  '}}'  )    }} to ${{2}} ", " }} to 2 "},
   235  		{" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "},
   236  		{" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "},
   237  		{" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "},
   238  		{"${{ secrets.CASE_INSENSITIVE_SECRET }}", "value"},
   239  		{"${{ secrets.case_insensitive_secret }}", "value"},
   240  		{"${{ vars.CASE_INSENSITIVE_VAR }}", "value"},
   241  		{"${{ vars.case_insensitive_var }}", "value"},
   242  		{"${{ env.UNKNOWN }}", ""},
   243  		{"${{ env.SOMETHING_TRUE }}", "true"},
   244  		{"${{ env.SOMETHING_FALSE }}", "false"},
   245  		{"${{ !env.SOMETHING_TRUE }}", "false"},
   246  		{"${{ !env.SOMETHING_FALSE }}", "false"},
   247  		{"${{ !env.SOMETHING_TRUE && true }}", "false"},
   248  		{"${{ !env.SOMETHING_FALSE && true }}", "false"},
   249  		{"${{ env.SOMETHING_TRUE && true }}", "true"},
   250  		{"${{ env.SOMETHING_FALSE && true }}", "true"},
   251  		{"${{ !env.SOMETHING_TRUE || true }}", "true"},
   252  		{"${{ !env.SOMETHING_FALSE || true }}", "true"},
   253  		{"${{ !env.SOMETHING_TRUE && false }}", "false"},
   254  		{"${{ !env.SOMETHING_FALSE && false }}", "false"},
   255  		{"${{ !env.SOMETHING_TRUE || false }}", "false"},
   256  		{"${{ !env.SOMETHING_FALSE || false }}", "false"},
   257  		{"${{ env.SOMETHING_TRUE || false }}", "true"},
   258  		{"${{ env.SOMETHING_FALSE || false }}", "false"},
   259  		{"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"},
   260  		{"${{ fromJSON('{}') < 2 }}", "false"},
   261  	}
   262  
   263  	updateTestExpressionWorkflow(t, tables, rc)
   264  	for _, table := range tables {
   265  		table := table
   266  		t.Run("interpolate", func(t *testing.T) {
   267  			assertObject := assert.New(t)
   268  			out := ee.Interpolate(context.Background(), table.in)
   269  			assertObject.Equal(table.out, out, table.in)
   270  		})
   271  	}
   272  }
   273  
   274  func updateTestExpressionWorkflow(t *testing.T, tables []struct {
   275  	in  string
   276  	out string
   277  }, rc *RunContext) {
   278  	var envs string
   279  	keys := make([]string, 0, len(rc.Env))
   280  	for k := range rc.Env {
   281  		keys = append(keys, k)
   282  	}
   283  	sort.Strings(keys)
   284  	for _, k := range keys {
   285  		envs += fmt.Sprintf("  %s: %s\n", k, rc.Env[k])
   286  	}
   287  
   288  	// editorconfig-checker-disable
   289  	workflow := fmt.Sprintf(`
   290  name: "Test how expressions are handled on GitHub"
   291  on: push
   292  
   293  env:
   294  %s
   295  
   296  jobs:
   297    test-espressions:
   298      runs-on: ubuntu-latest
   299      steps:
   300  `, envs)
   301  	// editorconfig-checker-enable
   302  	for _, table := range tables {
   303  		expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`)
   304  
   305  		expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string {
   306  			return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1"))
   307  		})
   308  		name := fmt.Sprintf(`%s -> %s should be equal to %s`, expr, table.in, table.out)
   309  		echo := `run: echo "Done "`
   310  		workflow += fmt.Sprintf("\n      - name: %s\n        %s\n", name, echo)
   311  	}
   312  
   313  	file, err := os.Create("../../.github/workflows/test-expressions.yml")
   314  	if err != nil {
   315  		t.Fatal(err)
   316  	}
   317  
   318  	_, err = file.WriteString(workflow)
   319  	if err != nil {
   320  		t.Fatal(err)
   321  	}
   322  }
   323  
   324  func TestRewriteSubExpression(t *testing.T) {
   325  	table := []struct {
   326  		in  string
   327  		out string
   328  	}{
   329  		{in: "Hello World", out: "Hello World"},
   330  		{in: "${{ true }}", out: "${{ true }}"},
   331  		{in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"},
   332  		{in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"},
   333  		{in: "${{ '}}' }}", out: "${{ '}}' }}"},
   334  		{in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"},
   335  		{in: "${{ '''' }}", out: "${{ '''' }}"},
   336  		{in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`},
   337  		{in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`},
   338  		{in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`},
   339  		{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
   340  	}
   341  
   342  	for _, table := range table {
   343  		t.Run("TestRewriteSubExpression", func(t *testing.T) {
   344  			assertObject := assert.New(t)
   345  			out, err := rewriteSubExpression(context.Background(), table.in, false)
   346  			if err != nil {
   347  				t.Fatal(err)
   348  			}
   349  			assertObject.Equal(table.out, out, table.in)
   350  		})
   351  	}
   352  }
   353  
   354  func TestRewriteSubExpressionForceFormat(t *testing.T) {
   355  	table := []struct {
   356  		in  string
   357  		out string
   358  	}{
   359  		{in: "Hello World", out: "Hello World"},
   360  		{in: "${{ true }}", out: "format('{0}', true)"},
   361  		{in: "${{ '}}' }}", out: "format('{0}', '}}')"},
   362  		{in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`},
   363  		{in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"},
   364  	}
   365  
   366  	for _, table := range table {
   367  		t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) {
   368  			assertObject := assert.New(t)
   369  			out, err := rewriteSubExpression(context.Background(), table.in, true)
   370  			if err != nil {
   371  				t.Fatal(err)
   372  			}
   373  			assertObject.Equal(table.out, out, table.in)
   374  		})
   375  	}
   376  }