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 }