1// SPDX-License-Identifier: MIT
2
3package expr
4
5import (
6 "context"
7 "strings"
8 "testing"
9)
10
11// Ports of the expression-semantic L0 tests of actions/runner
12// (commit 5f0b3ddcc6ee9fe9441e00d55e85b94ea4aad798, MIT, GitHub, Inc.):
13//
14// src/Test/L0/Sdk/ExpressionParserL0.cs
15// src/Test/L0/Worker/Expressions/ConditionFunctionsL0.cs
16
17// TestRunnerExpressionParserL0 ports ExpressionParserL0: the parser must
18// reject names that were not declared (the regression was a positional
19// argument that turned on "allow unknown keywords" for every parse).
20func TestRunnerExpressionParserL0(t *testing.T) {
21 tests := []struct {
22 name string // the C# test method
23 src string
24 contexts []string
25 wantErr string // substring of the error; empty means success
26 }{
27 {"CreateTree_RejectsUnrecognizedNamedValue", "github.event.repository.private", []string{"inputs"}, "Unrecognized named-value"},
28 {"CreateTree_AcceptsRecognizedNamedValue", "inputs.foo", []string{"inputs"}, ""},
29 {"CreateTree_CaseFunctionWorks", "case(github.event_name, 'push', 'Push Event')", []string{"github"}, ""},
30 {"CreateTree_CaseFunctionDoesNotAffectUnknownKeywords", "github.ref", []string{"inputs"}, "Unrecognized named-value"},
31 }
32 for _, tc := range tests {
33 t.Run(tc.name, func(t *testing.T) {
34 node, err := Parse(tc.src, tc.contexts, nil)
35 if tc.wantErr != "" {
36 if err == nil || !strings.Contains(err.Error(), tc.wantErr) {
37 t.Fatalf("Parse(%q) error = %v, want one containing %q", tc.src, err, tc.wantErr)
38 }
39 return
40 }
41 if err != nil || node == nil {
42 t.Fatalf("Parse(%q) = %v, %v; want a tree", tc.src, node, err)
43 }
44 })
45 }
46}
47
48// Job and action results, as the runner's ActionResult enum names them.
49const (
50 resNone = ""
51 resSuccess = "Success"
52 resFailure = "Failure"
53 resCancelled = "Cancelled"
54)
55
56// statusFuncs mirrors src/Runner.Worker/Expressions/{Always,Cancelled,Failure,
57// Success}Function.cs. A missing job status counts as Success. In a composite
58// action's main step (isEmbedded and main stage) failure() and success() read
59// the action status instead of the job status; cancelled() and always() never do.
60func statusFuncs(jobStatus, actionStatus string, compositeMain bool) (map[string]Func, []FuncInfo) {
61 orSuccess := func(s string) string {
62 if s == resNone {
63 return resSuccess
64 }
65 return s
66 }
67 status := func() string {
68 if compositeMain {
69 return orSuccess(actionStatus)
70 }
71 return orSuccess(jobStatus)
72 }
73 fn := func(f func() bool) Func {
74 return func(context.Context, []Value) (Value, error) { return Boolean(f()), nil }
75 }
76 funcs := map[string]Func{
77 "always": fn(func() bool { return true }),
78 "cancelled": fn(func() bool { return orSuccess(jobStatus) == resCancelled }),
79 "failure": fn(func() bool { return status() == resFailure }),
80 "success": fn(func() bool { return status() == resSuccess }),
81 }
82 var infos []FuncInfo
83 for _, n := range []string{"always", "cancelled", "failure", "success"} {
84 infos = append(infos, FuncInfo{Name: n, MinArgs: 0, MaxArgs: 0})
85 }
86 return funcs, infos
87}
88
89func evalCondition(t *testing.T, src, jobStatus, actionStatus string, compositeMain bool) bool {
90 t.Helper()
91 funcs, infos := statusFuncs(jobStatus, actionStatus, compositeMain)
92 node, err := Parse(src, nil, infos)
93 if err != nil {
94 t.Fatalf("Parse(%q): %v", src, err)
95 }
96 v, err := Evaluate(context.Background(), node, EvalContext{Funcs: funcs})
97 if err != nil {
98 t.Fatalf("Evaluate(%q): %v", src, err)
99 }
100 return Truthy(v)
101}
102
103// TestRunnerConditionFunctionsL0 ports AlwaysFunction, CancelledFunction,
104// FailureFunction and SuccessFunction of ConditionFunctionsL0, and the two
105// composite-action theories. The functions themselves live in the runner; what
106// is checked here is that package expr carries zero-argument extension
107// functions the way the runner declares them (FunctionInfo(name, 0, 0)).
108func TestRunnerConditionFunctionsL0(t *testing.T) {
109 jobStatuses := []string{resNone, resCancelled, resFailure, resSuccess}
110 want := map[string]map[string]bool{
111 "always": {resNone: true, resCancelled: true, resFailure: true, resSuccess: true},
112 "cancelled": {resNone: false, resCancelled: true, resFailure: false, resSuccess: false},
113 "failure": {resNone: false, resCancelled: false, resFailure: true, resSuccess: false},
114 "success": {resNone: true, resCancelled: false, resFailure: false, resSuccess: true},
115 }
116 for name, byStatus := range want {
117 for _, js := range jobStatuses {
118 if got := evalCondition(t, name+"()", js, "", false); got != byStatus[js] {
119 t.Errorf("%s() with job status %q = %v, want %v", name, js, got, byStatus[js])
120 }
121 // Names are case-insensitive, as in the runner's dictionaries.
122 if got := evalCondition(t, strings.ToUpper(name)+"()", js, "", false); got != byStatus[js] {
123 t.Errorf("%s() with job status %q = %v, want %v", strings.ToUpper(name), js, got, byStatus[js])
124 }
125 }
126 }
127
128 composite := []struct {
129 fn string
130 job, action string
131 want bool
132 }{
133 {"failure", resFailure, resFailure, true},
134 {"failure", resFailure, resSuccess, false},
135 {"failure", resSuccess, resFailure, true},
136 {"failure", resSuccess, resSuccess, false},
137 {"failure", resSuccess, resNone, false},
138 {"success", resFailure, resFailure, false},
139 {"success", resFailure, resSuccess, true},
140 {"success", resSuccess, resFailure, false},
141 {"success", resSuccess, resSuccess, true},
142 {"success", resSuccess, resNone, true},
143 }
144 for _, tc := range composite {
145 if got := evalCondition(t, tc.fn+"()", tc.job, tc.action, true); got != tc.want {
146 t.Errorf("%s() in a composite main step, job %q action %q = %v, want %v", tc.fn, tc.job, tc.action, got, tc.want)
147 }
148 }
149
150 // Arity is declared 0..0: the parser rejects arguments.
151 _, infos := statusFuncs("", "", false)
152 if _, err := Parse("success(1)", nil, infos); err == nil || !strings.Contains(err.Error(), "Too many parameters supplied") {
153 t.Errorf("success(1) = %v, want Too many parameters supplied", err)
154 }
155 // A condition combining them: runner conditions are ordinary expressions.
156 if !evalCondition(t, "failure() || cancelled()", resCancelled, "", false) || evalCondition(t, "!always()", resFailure, "", false) {
157 t.Error("combined status conditions")
158 }
159}