v0.1.0 28f9d6e

eval_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package template
4
5import (
6	"context"
7	"errors"
8	"strings"
9	"testing"
10
11	"bnema.dev/ermite-actions/expr"
12)
13
14func testEnv(t testing.TB) Env {
15	t.Helper()
16	gh, err := expr.FromJSON([]byte(`{"ref": "refs/heads/main", "n": 3, "flag": true}`))
17	if err != nil {
18		t.Fatal(err)
19	}
20	m, _ := expr.FromJSON([]byte(`{"os": "linux", "extra": {"a": "1", "b": "2"}}`))
21	return Env{Contexts: map[string]expr.Value{"github": gh, "matrix": m}}
22}
23
24func stepOf(t testing.TB, src string) *Mapping {
25	t.Helper()
26	tok, errs := readWorkflow(t, head+src)
27	if len(errs) > 0 {
28		t.Fatalf("read: %q", errs)
29	}
30	return tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping)
31}
32
33func TestEvaluateTokens(t *testing.T) {
34	step := stepOf(t, "      - name: n-${{ github.ref }}-${{ matrix.os }}\n        run: echo\n        env:\n          A: ${{ github.n }}\n          B: ${{ github.flag }}\n          C: x\n")
35	env := testEnv(t)
36	ctx := context.Background()
37	name, err := EvaluateString(ctx, step.Find("name"), env)
38	if err != nil || name != "n-refs/heads/main-linux" {
39		t.Errorf("name = %q, %v", name, err)
40	}
41	v, err := Evaluate(ctx, step.Find("env"), env)
42	if err != nil {
43		t.Fatal(err)
44	}
45	if got := expr.ToJSON(v); got != "{\n  \"A\": 3,\n  \"B\": true,\n  \"C\": \"x\"\n}" {
46		t.Errorf("env = %s", got)
47	}
48	s, err := EvaluateString(ctx, step.Find("env").(*Mapping).Find("A"), env)
49	if err != nil || s != "3" {
50		t.Errorf("A = %q, %v", s, err)
51	}
52}
53
54func TestInsertAndExpressionKeys(t *testing.T) {
55	tok, errs := readWorkflow(t, head+"      - run: x\n        env:\n          ${{ insert }}: ${{ matrix.extra }}\n          ${{ github.ref }}: v\n          K: w\n")
56	if len(errs) > 0 {
57		t.Fatal(errs)
58	}
59	env := tok.(*Mapping).Find("jobs").(*Mapping).Find("a").(*Mapping).Find("steps").(*Sequence).Items[0].(*Mapping).Find("env")
60	v, err := Evaluate(context.Background(), env, testEnv(t))
61	if err != nil {
62		t.Fatal(err)
63	}
64	if got := strings.ReplaceAll(strings.ReplaceAll(expr.ToJSON(v), "\n", ""), "  ", ""); got != `{"a": "1","b": "2","refs/heads/main": "v","K": "w"}` {
65		t.Errorf("env = %s", got)
66	}
67	// insert needs a mapping.
68	bad := NewMapping(Origin{})
69	bad.Add(&InsertExpression{}, NewString(Origin{}, "x"))
70	if _, err := Evaluate(context.Background(), bad, testEnv(t)); err == nil || !strings.Contains(err.Error(), "insert") {
71		t.Errorf("err = %v", err)
72	}
73}
74
75func TestEvaluateTypedHelpers(t *testing.T) {
76	ctx := context.Background()
77	env := testEnv(t)
78	ex := func(s string) *BasicExpression {
79		return NewExpression(Origin{File: "f.yml", Pos: Pos{4, 2}}, s, mustInfo(t, "step-env"))
80	}
81	if b, err := EvaluateBool(ctx, ex("github.flag"), env); err != nil || !b {
82		t.Errorf("bool = %v, %v", b, err)
83	}
84	if b, err := EvaluateBool(ctx, ex("'FALSE'"), env); err != nil || b {
85		t.Errorf("bool string = %v, %v", b, err)
86	}
87	if _, err := EvaluateBool(ctx, ex("github.n"), env); err == nil || !strings.Contains(err.Error(), "f.yml (Line: 4, Col: 2): Unexpected type 'NumberToken'") {
88		t.Errorf("bool number: %v", err)
89	}
90	if n, err := EvaluateNumber(ctx, ex("github.n"), env); err != nil || n != 3 {
91		t.Errorf("number = %v, %v", n, err)
92	}
93	if n, err := EvaluateNumber(ctx, ex("'12'"), env); err != nil || n != 12 {
94		t.Errorf("number string = %v, %v", n, err)
95	}
96	if _, err := EvaluateNumber(ctx, ex("'x'"), env); err == nil {
97		t.Error("number x: no error")
98	}
99	if _, err := EvaluateString(ctx, ex("matrix.extra"), env); err == nil || !strings.Contains(err.Error(), "MappingToken") {
100		t.Errorf("string of mapping: %v", err)
101	}
102	if s, err := EvaluateString(ctx, ex("null"), env); err != nil || s != "" {
103		t.Errorf("string of null = %q, %v", s, err)
104	}
105}
106
107func mustInfo(t testing.TB, name string) *DefinitionInfo {
108	t.Helper()
109	s, err := WorkflowSchema()
110	if err != nil {
111		t.Fatal(err)
112	}
113	return s.Info(name)
114}
115
116func TestExpressionErrors(t *testing.T) {
117	ctx := context.Background()
118	bad := NewExpression(Origin{File: "f.yml", Pos: Pos{1, 1}}, "github.", mustInfo(t, "step-env"))
119	if bad.Err() == nil {
120		t.Fatal("parse error expected")
121	}
122	_, err := Evaluate(ctx, bad, testEnv(t))
123	var te *Error
124	if !errors.As(err, &te) || !strings.HasPrefix(err.Error(), "f.yml (Line: 1, Col: 1): ") {
125		t.Errorf("err = %v", err)
126	}
127	// Not allowed context: secrets in job-if.
128	e := NewExpression(Origin{}, "secrets.x", mustInfo(t, "job-if"))
129	if e.Err() == nil || !strings.Contains(e.Err().Error(), "Unrecognized named-value: 'secrets'") {
130		t.Errorf("err = %v", e.Err())
131	}
132	// Evaluation errors wrap the expr error.
133	ev := NewExpression(Origin{}, "fromJSON('x')", mustInfo(t, "step-env"))
134	_, err = Evaluate(ctx, ev, testEnv(t))
135	var ee *expr.EvalError
136	if !errors.As(err, &ee) {
137		t.Errorf("err = %v (%T)", err, err)
138	}
139	// Cancellation.
140	cctx, cancel := context.WithCancel(ctx)
141	cancel()
142	if _, err := Evaluate(cctx, NewExpression(Origin{}, "github.ref", mustInfo(t, "step-env")), testEnv(t)); err == nil {
143		t.Error("cancelled: no error")
144	}
145	// Stray insert.
146	if _, err := Evaluate(ctx, &InsertExpression{}, testEnv(t)); err == nil {
147		t.Error("insert alone: no error")
148	}
149}
150
151func TestEvaluateDepthLimit(t *testing.T) {
152	var tok Token = NewString(Origin{}, "x")
153	for i := 0; i < MaxEvalDepth+5; i++ {
154		tok = NewSequence(Origin{}, tok)
155	}
156	if _, err := Evaluate(context.Background(), tok, Env{}); err == nil {
157		t.Error("deep token: no error")
158	}
159}
160
161func TestResultBudget(t *testing.T) {
162	text := "'xxxxxxxxxxxxxxxx'"
163	for i := 0; i < 7; i++ {
164		text = "format('{0}{0}{0}{0}', " + text + ")"
165	}
166	e := NewExpression(Origin{}, text, mustInfo(t, "step-env"))
167	if e.Err() != nil {
168		t.Fatal(e.Err())
169	}
170	env := testEnv(t)
171	env.MaxResultBytes = 1000
172	_, err := Evaluate(context.Background(), e, env)
173	var ee *expr.EvalError
174	if !errors.As(err, &ee) {
175		t.Fatalf("err = %v", err)
176	}
177	env.MaxResultBytes = -1
178	if _, err := Evaluate(context.Background(), e, env); err != nil {
179		t.Errorf("no limit: %v", err)
180	}
181}
182
183func TestMatchDirective(t *testing.T) {
184	tests := []struct {
185		in      string
186		match   bool
187		wantErr string
188	}{
189		{"insert", true, ""},
190		{"insert ", true, ""},
191		{"inserted", false, ""},
192		{"insert x", false, "Exactly 0 parameter(s) were expected following the directive 'insert'. Actual parameter count: 1"},
193		{"insert x y", false, "Actual parameter count: 1"},
194		{"github.ref", false, ""},
195	}
196	for _, tc := range tests {
197		got, err := matchDirective(tc.in, "insert", 0)
198		if got != tc.match || (tc.wantErr == "") != (err == nil) || (err != nil && !strings.Contains(err.Error(), tc.wantErr)) {
199			t.Errorf("matchDirective(%q) = %v, %v", tc.in, got, err)
200		}
201	}
202}
203
204func TestJSNumberString(t *testing.T) {
205	for in, want := range map[float64]string{
206		0: "0", 1: "1", -1: "-1", 1.5: "1.5", 100: "100", 1e21: "1e+21", 1e-7: "1e-7", 123456789012: "123456789012", 0.000001: "0.000001",
207	} {
208		if got := JSNumberString(in); got != want {
209			t.Errorf("JSNumberString(%v) = %s, want %s", in, got, want)
210		}
211	}
212}
213
214func TestUsesContext(t *testing.T) {
215	info := mustInfo(t, "step-env")
216	tests := []struct {
217		expr, ctx string
218		want      bool
219	}{
220		{"matrix.os", "matrix", true},
221		{"MATRIX.os", "matrix", true},
222		{"github.ref == 'x' && matrix.os", "matrix", true},
223		{"format('{0}', fromJSON(matrix.a))", "matrix", true},
224		{"!(matrix.a)", "matrix", true},
225		{"github.event.matrix", "matrix", false},
226		{"github['matrix']", "matrix", false},
227		{"github[matrix.k]", "matrix", true},
228		{"'matrix.os'", "matrix", false},
229		{"contains(github.ref, 'matrix.')", "matrix", false},
230		{"matrix", "matrix", true},
231		{"env.matrix", "matrix", false},
232	}
233	for _, tc := range tests {
234		e := NewExpression(Origin{}, tc.expr, info)
235		if e.Err() != nil {
236			t.Errorf("%s: %v", tc.expr, e.Err())
237			continue
238		}
239		if got := e.UsesContext(tc.ctx); got != tc.want {
240			t.Errorf("UsesContext(%q, %q) = %v, want %v", tc.expr, tc.ctx, got, tc.want)
241		}
242	}
243	if (&BasicExpression{}).UsesContext("matrix") {
244		t.Error("empty expression uses a context")
245	}
246}

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