manifest_test.go
1package manifest
2
3import (
4 "errors"
5 "strings"
6 "testing"
7)
8
9const sovrn = `
10when:
11 - event: [push]
12 branch: [main]
13engine: devenv
14repos:
15 - { repo: servers, path: ../servers }
16 - { repo: data, path: ../data, env: SECRETS_DIR }
17steps: [test, plan, deploy, health-check, record, notify]
18actions:
19 test: { command: just test }
20 plan: { dir: ../servers, command: just plan-project sovrn }
21 deploy: { dir: ../servers, command: just deploy-project sovrn,
22 gate: { prompt: "Deploy sovrn?" }, concurrency: fleet,
23 audited: true }
24 health-check: { dir: ../servers, command: just prod-health-check sovrn }
25 record: { dir: ../servers, command: just record-deploy sovrn }
26 notify: { command: just notify-deploy }
27 rollback: { dir: ../servers, command: just rollback, style: danger,
28 manual: true, audited: true,
29 gate: { prompt: "Roll back sovrn?" } }
30`
31
32func TestParseDesignExample(t *testing.T) {
33 m, err := Parse("deploy", []byte(sovrn))
34 if err != nil {
35 t.Fatal(err)
36 }
37 if got := m.Actions["deploy"].Concurrency; got == nil || got.Group != "fleet" {
38 t.Errorf("concurrency shorthand: %+v", got)
39 }
40 if m.Actions["rollback"].Gate.Prompt != "Roll back sovrn?" || !m.Actions["rollback"].Manual {
41 t.Errorf("rollback: %+v", m.Actions["rollback"])
42 }
43 if i, ok := m.IsStep("health-check"); !ok || i != 3 {
44 t.Errorf("IsStep(health-check) = %d, %v", i, ok)
45 }
46 if _, ok := m.IsStep("rollback"); ok {
47 t.Error("rollback is not a step")
48 }
49 if m.Repos[1].Env != "SECRETS_DIR" {
50 t.Errorf("repos: %+v", m.Repos)
51 }
52}
53
54func TestShorthands(t *testing.T) {
55 m, err := Parse("w", []byte(`
56when: [{ event: push, branch: main, tag: "v*" }]
57engine: devenv
58environment: { A: 1, B: true, C: x }
59steps: [a]
60actions:
61 a: { command: "true", gate: true }
62 b: { command: "true", gate: }
63 c: { command: "true", gate: false }
64`))
65 if err != nil {
66 t.Fatal(err)
67 }
68 w := m.When[0]
69 if len(w.Event) != 1 || w.Event[0] != "push" || w.Branch[0] != "main" || w.Tag[0] != "v*" {
70 t.Errorf("when: %+v", w)
71 }
72 if m.Environment["A"] != "1" || m.Environment["B"] != "true" {
73 t.Errorf("environment: %+v", m.Environment)
74 }
75 if m.Actions["a"].Gate == nil || m.Actions["b"].Gate == nil || m.Actions["c"].Gate != nil {
76 t.Errorf("gates: a=%v b=%v c=%v", m.Actions["a"].Gate, m.Actions["b"].Gate, m.Actions["c"].Gate)
77 }
78 if m.GateTimeout("a") != DefaultGateTimeout {
79 t.Errorf("default timeout")
80 }
81}
82
83func TestOtherEngine(t *testing.T) {
84 _, err := Parse("w", []byte("engine: nixery\nsteps: [{name: x, command: y}]\n"))
85 if !errors.Is(err, ErrOtherEngine) {
86 t.Fatalf("got %v", err)
87 }
88}
89
90func TestInvalid(t *testing.T) {
91 cases := map[string]string{
92 "undeclared step": `engine: devenv
93steps: [a, b]
94actions: { a: { command: x } }`,
95 "duplicate step": `engine: devenv
96steps: [a, a]
97actions: { a: { command: x } }`,
98 "no steps": `engine: devenv
99steps: []
100actions: { a: { command: x } }`,
101 "unknown field": `engine: devenv
102steps: [a]
103actions: { a: { command: x, comand: y } }`,
104 "empty command": `engine: devenv
105steps: [a]
106actions: { a: { command: " " } }`,
107 "reserved name": `engine: devenv
108steps: [a]
109actions: { a: { command: x }, stop: { command: x } }`,
110 "bad style": `engine: devenv
111steps: [a]
112actions: { a: { command: x, style: loud } }`,
113 "bad timeout": `engine: devenv
114steps: [a]
115actions: { a: { command: x, gate: { timeout: soon } } }`,
116 "absolute dir": `engine: devenv
117steps: [a]
118actions: { a: { command: x, dir: /etc } }`,
119 "repo at root": `engine: devenv
120repos: [{ repo: r, path: . }]
121steps: [a]
122actions: { a: { command: x } }`,
123 "bad env name": `engine: devenv
124repos: [{ repo: r, path: ../r, env: 1X }]
125steps: [a]
126actions: { a: { command: x } }`,
127 "unknown event": `engine: devenv
128when: [{ event: [pr] }]
129steps: [a]
130actions: { a: { command: x } }`,
131 "bad glob": `engine: devenv
132when: [{ event: [push], branch: ["["] }]
133steps: [a]
134actions: { a: { command: x } }`,
135 }
136 for name, src := range cases {
137 if _, err := Parse("w", []byte(src)); err == nil {
138 t.Errorf("%s: accepted", name)
139 } else if errors.Is(err, ErrOtherEngine) {
140 t.Errorf("%s: %v", name, err)
141 }
142 }
143}
144
145func TestErrorsAreJoined(t *testing.T) {
146 _, err := Parse("w", []byte(`engine: devenv
147steps: [a, b]
148actions: { a: { command: "", style: loud } }`))
149 if err == nil {
150 t.Fatal("accepted")
151 }
152 for _, want := range []string{`"b" is not a declared action`, "command is required", "style"} {
153 if !strings.Contains(err.Error(), want) {
154 t.Errorf("error %q lacks %q", err, want)
155 }
156 }
157}
158
159func TestMatches(t *testing.T) {
160 m, err := Parse("w", []byte(`engine: devenv
161when:
162 - { event: [push], branch: ["main", "release/**"], tag: ["v*"] }
163 - { event: [push], branch: ["docs"], paths: ["docs/**"] }
164steps: [a]
165actions: { a: { command: x } }`))
166 if err != nil {
167 t.Fatal(err)
168 }
169 cases := []struct {
170 ev Event
171 want bool
172 }{
173 {Event{Kind: "push", Ref: "refs/heads/main"}, true},
174 {Event{Kind: "push", Ref: "refs/heads/release/1/x"}, true},
175 {Event{Kind: "push", Ref: "refs/heads/feature"}, false},
176 {Event{Kind: "push", Ref: "refs/tags/v1.2"}, true},
177 {Event{Kind: "push", Ref: "refs/tags/main"}, false},
178 {Event{Kind: "push", Ref: "refs/heads/docs", ChangedFiles: []string{"docs/a/b.md"}}, true},
179 {Event{Kind: "push", Ref: "refs/heads/docs", ChangedFiles: []string{"src/x.go"}}, false},
180 {Event{Kind: "push", Ref: "refs/heads/docs"}, true}, // changed files unknown
181 {Event{Kind: "manual"}, true},
182 {Event{Kind: "pull_request"}, false},
183 }
184 for _, c := range cases {
185 if got := m.Matches(c.ev); got != c.want {
186 t.Errorf("%+v: got %v", c.ev, got)
187 }
188 }
189 none, _ := Parse("w", []byte("engine: devenv\nsteps: [a]\nactions: { a: { command: x } }"))
190 if !none.Matches(Event{Kind: "push", Ref: "refs/heads/x"}) {
191 t.Error("a workflow without conditions always runs")
192 }
193}
194
195func TestNameOf(t *testing.T) {
196 for in, want := range map[string]string{"a/b/deploy.yml": "deploy", "x.yaml": "x", "y": "y"} {
197 if got := NameOf(in); got != want {
198 t.Errorf("NameOf(%q) = %q", in, got)
199 }
200 }
201}