github.com/nektos/act@v0.2.83/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 t.Run(table.in, func(t *testing.T) { 142 assertObject := assert.New(t) 143 out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone) 144 if table.errMesg == "" { 145 assertObject.NoError(err, table.in) 146 assertObject.Equal(table.out, out, table.in) 147 } else { 148 assertObject.Error(err, table.in) 149 assertObject.Equal(table.errMesg, err.Error(), table.in) 150 } 151 }) 152 } 153 } 154 155 func TestEvaluateStep(t *testing.T) { 156 rc := createRunContext(t) 157 step := &stepRun{ 158 RunContext: rc, 159 } 160 161 ee := rc.NewStepExpressionEvaluator(context.Background(), step) 162 163 tables := []struct { 164 in string 165 out interface{} 166 errMesg string 167 }{ 168 {"steps.idwithnothing.conclusion", model.StepStatusSuccess.String(), ""}, 169 {"steps.idwithnothing.outcome", model.StepStatusFailure.String(), ""}, 170 {"steps.idwithnothing.outputs.foowithnothing", "barwithnothing", ""}, 171 {"steps.id-with-hyphens.conclusion", model.StepStatusSuccess.String(), ""}, 172 {"steps.id-with-hyphens.outcome", model.StepStatusFailure.String(), ""}, 173 {"steps.id-with-hyphens.outputs.foo-with-hyphens", "bar-with-hyphens", ""}, 174 {"steps.id_with_underscores.conclusion", model.StepStatusSuccess.String(), ""}, 175 {"steps.id_with_underscores.outcome", model.StepStatusFailure.String(), ""}, 176 {"steps.id_with_underscores.outputs.foo_with_underscores", "bar_with_underscores", ""}, 177 } 178 179 for _, table := range tables { 180 t.Run(table.in, func(t *testing.T) { 181 assertObject := assert.New(t) 182 out, err := ee.evaluate(context.Background(), table.in, exprparser.DefaultStatusCheckNone) 183 if table.errMesg == "" { 184 assertObject.NoError(err, table.in) 185 assertObject.Equal(table.out, out, table.in) 186 } else { 187 assertObject.Error(err, table.in) 188 assertObject.Equal(table.errMesg, err.Error(), table.in) 189 } 190 }) 191 } 192 } 193 194 func TestInterpolate(t *testing.T) { 195 rc := &RunContext{ 196 Config: &Config{ 197 Workdir: ".", 198 Secrets: map[string]string{ 199 "CASE_INSENSITIVE_SECRET": "value", 200 }, 201 Vars: map[string]string{ 202 "CASE_INSENSITIVE_VAR": "value", 203 }, 204 }, 205 Env: map[string]string{ 206 "KEYWITHNOTHING": "valuewithnothing", 207 "KEY-WITH-HYPHENS": "value-with-hyphens", 208 "KEY_WITH_UNDERSCORES": "value_with_underscores", 209 "SOMETHING_TRUE": "true", 210 "SOMETHING_FALSE": "false", 211 }, 212 Run: &model.Run{ 213 JobID: "job1", 214 Workflow: &model.Workflow{ 215 Name: "test-workflow", 216 Jobs: map[string]*model.Job{ 217 "job1": {}, 218 }, 219 }, 220 }, 221 } 222 ee := rc.NewExpressionEvaluator(context.Background()) 223 tables := []struct { 224 in string 225 out string 226 }{ 227 {" text ", " text "}, 228 {" $text ", " $text "}, 229 {" ${text} ", " ${text} "}, 230 {" ${{ 1 }} to ${{2}} ", " 1 to 2 "}, 231 {" ${{ (true || false) }} to ${{2}} ", " true to 2 "}, 232 {" ${{ (false || '}}' ) }} to ${{2}} ", " }} to 2 "}, 233 {" ${{ env.KEYWITHNOTHING }} ", " valuewithnothing "}, 234 {" ${{ env.KEY-WITH-HYPHENS }} ", " value-with-hyphens "}, 235 {" ${{ env.KEY_WITH_UNDERSCORES }} ", " value_with_underscores "}, 236 {"${{ secrets.CASE_INSENSITIVE_SECRET }}", "value"}, 237 {"${{ secrets.case_insensitive_secret }}", "value"}, 238 {"${{ vars.CASE_INSENSITIVE_VAR }}", "value"}, 239 {"${{ vars.case_insensitive_var }}", "value"}, 240 {"${{ env.UNKNOWN }}", ""}, 241 {"${{ env.SOMETHING_TRUE }}", "true"}, 242 {"${{ env.SOMETHING_FALSE }}", "false"}, 243 {"${{ !env.SOMETHING_TRUE }}", "false"}, 244 {"${{ !env.SOMETHING_FALSE }}", "false"}, 245 {"${{ !env.SOMETHING_TRUE && true }}", "false"}, 246 {"${{ !env.SOMETHING_FALSE && true }}", "false"}, 247 {"${{ env.SOMETHING_TRUE && true }}", "true"}, 248 {"${{ env.SOMETHING_FALSE && true }}", "true"}, 249 {"${{ !env.SOMETHING_TRUE || true }}", "true"}, 250 {"${{ !env.SOMETHING_FALSE || true }}", "true"}, 251 {"${{ !env.SOMETHING_TRUE && false }}", "false"}, 252 {"${{ !env.SOMETHING_FALSE && false }}", "false"}, 253 {"${{ !env.SOMETHING_TRUE || false }}", "false"}, 254 {"${{ !env.SOMETHING_FALSE || false }}", "false"}, 255 {"${{ env.SOMETHING_TRUE || false }}", "true"}, 256 {"${{ env.SOMETHING_FALSE || false }}", "false"}, 257 {"${{ env.SOMETHING_FALSE }} && ${{ env.SOMETHING_TRUE }}", "false && true"}, 258 {"${{ fromJSON('{}') < 2 }}", "false"}, 259 } 260 261 updateTestExpressionWorkflow(t, tables, rc) 262 for _, table := range tables { 263 t.Run("interpolate", func(t *testing.T) { 264 assertObject := assert.New(t) 265 out := ee.Interpolate(context.Background(), table.in) 266 assertObject.Equal(table.out, out, table.in) 267 }) 268 } 269 } 270 271 func updateTestExpressionWorkflow(t *testing.T, tables []struct { 272 in string 273 out string 274 }, rc *RunContext) { 275 var envs string 276 keys := make([]string, 0, len(rc.Env)) 277 for k := range rc.Env { 278 keys = append(keys, k) 279 } 280 sort.Strings(keys) 281 for _, k := range keys { 282 envs += fmt.Sprintf(" %s: %s\n", k, rc.Env[k]) 283 } 284 285 // editorconfig-checker-disable 286 workflow := fmt.Sprintf(` 287 name: "Test how expressions are handled on GitHub" 288 on: push 289 290 env: 291 %s 292 293 jobs: 294 test-espressions: 295 runs-on: ubuntu-latest 296 steps: 297 `, envs) 298 // editorconfig-checker-enable 299 for _, table := range tables { 300 expressionPattern := regexp.MustCompile(`\${{\s*(.+?)\s*}}`) 301 302 expr := expressionPattern.ReplaceAllStringFunc(table.in, func(match string) string { 303 return fmt.Sprintf("€{{ %s }}", expressionPattern.ReplaceAllString(match, "$1")) 304 }) 305 name := fmt.Sprintf(`%s -> %s should be equal to %s`, expr, table.in, table.out) 306 echo := `run: echo "Done "` 307 workflow += fmt.Sprintf("\n - name: %s\n %s\n", name, echo) 308 } 309 310 file, err := os.Create("../../.github/workflows/test-expressions.yml") 311 if err != nil { 312 t.Fatal(err) 313 } 314 315 _, err = file.WriteString(workflow) 316 if err != nil { 317 t.Fatal(err) 318 } 319 } 320 321 func TestRewriteSubExpression(t *testing.T) { 322 table := []struct { 323 in string 324 out string 325 }{ 326 {in: "Hello World", out: "Hello World"}, 327 {in: "${{ true }}", out: "${{ true }}"}, 328 {in: "${{ true }} ${{ true }}", out: "format('{0} {1}', true, true)"}, 329 {in: "${{ true || false }} ${{ true && true }}", out: "format('{0} {1}', true || false, true && true)"}, 330 {in: "${{ '}}' }}", out: "${{ '}}' }}"}, 331 {in: "${{ '''}}''' }}", out: "${{ '''}}''' }}"}, 332 {in: "${{ '''' }}", out: "${{ '''' }}"}, 333 {in: `${{ fromJSON('"}}"') }}`, out: `${{ fromJSON('"}}"') }}`}, 334 {in: `${{ fromJSON('"\"}}\""') }}`, out: `${{ fromJSON('"\"}}\""') }}`}, 335 {in: `${{ fromJSON('"''}}"') }}`, out: `${{ fromJSON('"''}}"') }}`}, 336 {in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"}, 337 } 338 339 for _, table := range table { 340 t.Run("TestRewriteSubExpression", func(t *testing.T) { 341 assertObject := assert.New(t) 342 out, err := rewriteSubExpression(context.Background(), table.in, false) 343 if err != nil { 344 t.Fatal(err) 345 } 346 assertObject.Equal(table.out, out, table.in) 347 }) 348 } 349 } 350 351 func TestRewriteSubExpressionForceFormat(t *testing.T) { 352 table := []struct { 353 in string 354 out string 355 }{ 356 {in: "Hello World", out: "Hello World"}, 357 {in: "${{ true }}", out: "format('{0}', true)"}, 358 {in: "${{ '}}' }}", out: "format('{0}', '}}')"}, 359 {in: `${{ fromJSON('"}}"') }}`, out: `format('{0}', fromJSON('"}}"'))`}, 360 {in: "Hello ${{ 'World' }}", out: "format('Hello {0}', 'World')"}, 361 } 362 363 for _, table := range table { 364 t.Run("TestRewriteSubExpressionForceFormat", func(t *testing.T) { 365 assertObject := assert.New(t) 366 out, err := rewriteSubExpression(context.Background(), table.in, true) 367 if err != nil { 368 t.Fatal(err) 369 } 370 assertObject.Equal(table.out, out, table.in) 371 }) 372 } 373 }