main 98594d0

eval_test.go RawHistoryFolder

1// SPDX-License-Identifier: MIT
2
3package workflow
4
5import (
6	"context"
7	"strings"
8	"testing"
9
10	"bnema.dev/ermite-actions/expr"
11	"bnema.dev/ermite-actions/template"
12)
13
14func envWith(t testing.TB, contexts map[string]string) template.Env {
15	t.Helper()
16	env := template.Env{Contexts: map[string]expr.Value{}}
17	for k, v := range contexts {
18		env.Contexts[k] = jsonValue(t, v)
19	}
20	return env
21}
22
23func mustParse(t testing.TB, src string) *Workflow {
24	t.Helper()
25	w, errs := Parse("w.yml", []byte(src))
26	if len(errs) > 0 {
27		t.Fatalf("Parse: %v", errs)
28	}
29	return w
30}
31
32func TestEvaluateStrategyEndToEnd(t *testing.T) {
33	w := mustParse(t, `on: push
34jobs:
35  build:
36    name: Build
37    runs-on: ${{ matrix.os }}
38    strategy:
39      fail-fast: false
40      max-parallel: 2
41      matrix:
42        os: [ubuntu-latest, macos-latest]
43        node: ${{ fromJSON(needs.setup.outputs.versions) }}
44        include:
45          - os: ubuntu-latest
46            extra: ${{ github.ref_name }}
47    steps:
48      - run: echo
49  setup:
50    runs-on: x
51    steps: [{run: a}]
52`)
53	env := envWith(t, map[string]string{
54		"github": `{"ref_name": "main"}`,
55		"needs":  `{"setup": {"outputs": {"versions": "[18, 20]"}}}`,
56	})
57	job := w.Job("build")
58	s, err := job.EvaluateStrategy(context.Background(), env)
59	if err != nil {
60		t.Fatal(err)
61	}
62	if s.FailFast || s.MaxParallel != 2 || len(s.Combos) != 4 {
63		t.Fatalf("strategy = %+v", s)
64	}
65	names := make([]string, len(s.Combos))
66	runsOn := make([]string, len(s.Combos))
67	for i := range s.Combos {
68		e, err := s.MatrixEnv(env, i)
69		if err != nil {
70			t.Fatal(err)
71		}
72		names[i], err = job.DisplayName(context.Background(), e, s, i)
73		if err != nil {
74			t.Fatal(err)
75		}
76		runsOn[i], err = template.EvaluateString(context.Background(), job.RunsOn, e)
77		if err != nil {
78			t.Fatal(err)
79		}
80	}
81	wantNames := []string{
82		"Build (ubuntu-latest, 18, main)", "Build (ubuntu-latest, 20, main)",
83		"Build (macos-latest, 18)", "Build (macos-latest, 20)",
84	}
85	if strings.Join(names, "|") != strings.Join(wantNames, "|") {
86		t.Errorf("names = %q, want %q", names, wantNames)
87	}
88	if runsOn[0] != "ubuntu-latest" || runsOn[3] != "macos-latest" {
89		t.Errorf("runs-on = %q", runsOn)
90	}
91	// The strategy context.
92	schema, _ := template.WorkflowSchema()
93	e := template.NewExpression(template.Origin{}, "format('{0}/{1}', strategy.job-index, strategy.job-total)", schema.Info("runs-on"))
94	if got, err := template.EvaluateString(context.Background(), e, mustMatrixEnv(t, s, env, 2)); err != nil || got != "2/4" {
95		t.Errorf("strategy context = %q, %v", got, err)
96	}
97}
98
99func TestDisplayNameUsingMatrix(t *testing.T) {
100	w := mustParse(t, `on: push
101jobs:
102  a:
103    name: build-${{ matrix.os }}
104    runs-on: x
105    strategy:
106      matrix:
107        os: [l, m]
108    steps: [{run: a}]
109  b:
110    runs-on: x
111    steps: [{run: a}]
112`)
113	env := envWith(t, nil)
114	j := w.Job("a")
115	s, err := j.EvaluateStrategy(context.Background(), env)
116	if err != nil {
117		t.Fatal(err)
118	}
119	if got, _ := j.DisplayName(context.Background(), mustMatrixEnv(t, s, env, 1), s, 1); got != "build-m" {
120		t.Errorf("name = %q", got)
121	}
122	b := w.Job("b")
123	sb, _ := b.EvaluateStrategy(context.Background(), env)
124	if got, _ := b.DisplayName(context.Background(), env, sb, 0); got != "b" {
125		t.Errorf("name = %q", got)
126	}
127}
128
129func TestEvaluateStrategyErrors(t *testing.T) {
130	w := mustParse(t, `on: push
131jobs:
132  a:
133    runs-on: x
134    strategy:
135      matrix: ${{ fromJSON(inputs.m) }}
136    steps: [{run: a}]
137`)
138	env := envWith(t, map[string]string{"inputs": `{"m": "not json"}`})
139	_, err := w.Job("a").EvaluateStrategy(context.Background(), env)
140	if err == nil || !strings.Contains(err.Error(), "w.yml (Line: 6, Col: 15)") {
141		t.Errorf("err = %v", err)
142	}
143	ctx, cancel := context.WithCancel(context.Background())
144	cancel()
145	if _, err := w.Job("a").EvaluateStrategy(ctx, envWith(t, map[string]string{"inputs": `{"m": "{}"}`})); err == nil {
146		t.Error("cancelled context: no error")
147	}
148}
149
150func TestJobIfAndStatusFunctions(t *testing.T) {
151	w := mustParse(t, `on: push
152jobs:
153  a:
154    runs-on: x
155    steps: [{run: a}]
156  b:
157    needs: a
158    if: needs.a.result == 'failure'
159    runs-on: x
160    steps:
161      - run: a
162        if: always()
163      - run: b
164`)
165	b := w.Job("b")
166	env := envWith(t, map[string]string{"needs": `{"a": {"result": "failure"}}`})
167	funcs := map[string]expr.Func{
168		"success":   func(context.Context, []expr.Value) (expr.Value, error) { return expr.Boolean(true), nil },
169		"always":    func(context.Context, []expr.Value) (expr.Value, error) { return expr.Boolean(true), nil },
170		"failure":   func(context.Context, []expr.Value) (expr.Value, error) { return expr.Boolean(false), nil },
171		"cancelled": func(context.Context, []expr.Value) (expr.Value, error) { return expr.Boolean(false), nil },
172	}
173	env.Funcs = funcs
174	if b.If.Expression != "success() && (needs.a.result == 'failure')" {
175		t.Fatalf("if = %s", b.If.Expression)
176	}
177	ok, err := template.EvaluateBool(context.Background(), b.If, env)
178	if err != nil || !ok {
179		t.Errorf("job if = %v, %v", ok, err)
180	}
181	if b.Steps[0].If.Expression != "always()" || b.Steps[1].If.Expression != "success()" {
182		t.Errorf("step ifs = %s, %s", b.Steps[0].If.Expression, b.Steps[1].If.Expression)
183	}
184}
185
186func mustMatrixEnv(t testing.TB, s *Strategy, env template.Env, i int) template.Env {
187	t.Helper()
188	e, err := s.MatrixEnv(env, i)
189	if err != nil {
190		t.Fatal(err)
191	}
192	return e
193}

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