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}