main 98594d0

runner_l0_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 3

↑↓ moveenter openesc close