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}