Skip to content

Commit 9da798c

Browse files
authored
chore: add helm tests and update ci to check for changes (#204)
* helm tests added * \n * many > two
1 parent 3385149 commit 9da798c

9 files changed

Lines changed: 806 additions & 0 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,31 @@ jobs:
7676
- name: Run integration tests
7777
run: go test -tags=integration -v -timeout=8m ./...
7878

79+
helm-test:
80+
runs-on: ubuntu-latest
81+
steps:
82+
- name: Checkout
83+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
84+
- name: Setup Go
85+
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
86+
with:
87+
go-version: "~1.26"
88+
- name: Setup Helm
89+
uses: azure/setup-helm@f0accbfd55e3332a28f721b8202b1016cecf90d5 # v5
90+
with:
91+
version: "v3.18.3"
92+
- name: Run Helm chart tests
93+
run: go test ./helm/tests/...
94+
- name: Check for unstaged files
95+
run: ./scripts/check_unstaged.sh
96+
7997
required:
8098
runs-on: ubuntu-latest
8199
needs:
82100
- test
83101
- lint
84102
- integration-test
103+
- helm-test
85104
# Allow this job to run even if the needed jobs fail, are skipped or
86105
# cancelled.
87106
if: always()

helm/tests/chart_test.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package tests // nolint: testpackage
2+
3+
import (
4+
"bytes"
5+
"flag"
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"runtime"
10+
"testing"
11+
12+
"github.com/stretchr/testify/require"
13+
"golang.org/x/xerrors"
14+
)
15+
16+
// These tests run `helm template` with the values file specified in each test
17+
// and compare the output to the contents of the corresponding golden file.
18+
// All values and golden files are located in the `testdata` directory.
19+
// To update golden files, run `go test . -update`.
20+
21+
// updateGoldenFiles is a flag that can be set to update golden files.
22+
var updateGoldenFiles = flag.Bool("update", false, "Update golden files")
23+
24+
var namespaces = []string{
25+
"default",
26+
"coder",
27+
}
28+
29+
var testCases = []testCase{
30+
{
31+
name: "default_values",
32+
expectedError: "",
33+
},
34+
{
35+
name: "all_values",
36+
expectedError: "",
37+
},
38+
}
39+
40+
type testCase struct {
41+
name string // Name of the test case. This is used to control which values and golden file are used.
42+
namespace string // Namespace is the name of the namespace the resources should be generated within
43+
expectedError string // Expected error from running `helm template`.
44+
}
45+
46+
func (tc testCase) valuesFilePath() string {
47+
return filepath.Join("./testdata", tc.name+".yaml")
48+
}
49+
50+
func (tc testCase) goldenFilePath() string {
51+
if tc.namespace == "default" {
52+
return filepath.Join("./testdata", tc.name+".golden")
53+
}
54+
55+
return filepath.Join("./testdata", tc.name+"_"+tc.namespace+".golden")
56+
}
57+
58+
func inCI() bool { return os.Getenv("CI") != "" }
59+
60+
func TestRenderChart(t *testing.T) {
61+
t.Parallel()
62+
if *updateGoldenFiles {
63+
t.Skip("Golden files are being updated. Skipping test.")
64+
}
65+
if inCI() {
66+
switch runtime.GOOS {
67+
case "windows", "darwin":
68+
t.Skip("Skipping tests on Windows and macOS in CI")
69+
}
70+
}
71+
72+
// Ensure that Helm is available in $PATH
73+
helmPath := lookupHelm(t)
74+
err := updateHelmDependencies(t, helmPath, "..")
75+
require.NoError(t, err, "failed to build Helm dependencies")
76+
77+
for _, tc := range testCases {
78+
for _, ns := range namespaces {
79+
tc.namespace = ns
80+
81+
t.Run(tc.namespace+"/"+tc.name, func(t *testing.T) {
82+
t.Parallel()
83+
84+
// Ensure that the values file exists.
85+
valuesFilePath := tc.valuesFilePath()
86+
if _, err := os.Stat(valuesFilePath); os.IsNotExist(err) {
87+
t.Fatalf("values file %q does not exist", valuesFilePath)
88+
}
89+
90+
// Run helm template with the values file.
91+
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesFilePath, tc.namespace)
92+
if tc.expectedError != "" {
93+
require.Error(t, err, "helm template should have failed")
94+
require.Contains(t, templateOutput, tc.expectedError, "helm template output should contain expected error")
95+
} else {
96+
require.NoError(t, err, "helm template should not have failed")
97+
require.NotEmpty(t, templateOutput, "helm template output should not be empty")
98+
goldenFilePath := tc.goldenFilePath()
99+
goldenBytes, err := os.ReadFile(goldenFilePath)
100+
require.NoError(t, err, "failed to read golden file %q", goldenFilePath)
101+
102+
// Remove carriage returns to make tests pass on Windows.
103+
goldenBytes = bytes.ReplaceAll(goldenBytes, []byte("\r"), []byte(""))
104+
expected := string(goldenBytes)
105+
106+
require.NoError(t, err, "failed to load golden file %q")
107+
require.Equal(t, expected, templateOutput)
108+
}
109+
})
110+
}
111+
}
112+
}
113+
114+
func TestUpdateGoldenFiles(t *testing.T) {
115+
t.Parallel()
116+
if !*updateGoldenFiles {
117+
t.Skip("Run with -update to update golden files")
118+
}
119+
120+
helmPath := lookupHelm(t)
121+
err := updateHelmDependencies(t, helmPath, "..")
122+
require.NoError(t, err, "failed to build Helm dependencies")
123+
124+
for _, tc := range testCases {
125+
if tc.expectedError != "" {
126+
t.Logf("skipping test case %q with render error", tc.name)
127+
continue
128+
}
129+
130+
for _, ns := range namespaces {
131+
tc.namespace = ns
132+
133+
valuesPath := tc.valuesFilePath()
134+
templateOutput, err := runHelmTemplate(t, helmPath, "..", valuesPath, tc.namespace)
135+
if err != nil {
136+
t.Logf("error running `helm template -f %q`: %v", valuesPath, err)
137+
t.Logf("output: %s", templateOutput)
138+
}
139+
require.NoError(t, err, "failed to run `helm template -f %q`", valuesPath)
140+
141+
goldenFilePath := tc.goldenFilePath()
142+
err = os.WriteFile(goldenFilePath, []byte(templateOutput), 0o644) // nolint:gosec
143+
require.NoError(t, err, "failed to write golden file %q", goldenFilePath)
144+
}
145+
}
146+
t.Log("Golden files updated. Please review the changes and commit them.")
147+
}
148+
149+
// updateHelmDependencies runs `helm dependency update .` on the given chartDir.
150+
func updateHelmDependencies(t testing.TB, helmPath, chartDir string) error {
151+
// Remove charts/ from chartDir if it exists.
152+
err := os.RemoveAll(filepath.Join(chartDir, "charts"))
153+
if err != nil {
154+
return xerrors.Errorf("failed to remove charts/ directory: %w", err)
155+
}
156+
157+
// Regenerate the chart dependencies.
158+
cmd := exec.Command(helmPath, "dependency", "update", "--skip-refresh", ".")
159+
cmd.Dir = chartDir
160+
t.Logf("exec command: %v", cmd.Args)
161+
out, err := cmd.CombinedOutput()
162+
if err != nil {
163+
return xerrors.Errorf("failed to run `helm dependency build`: %w\noutput: %s", err, out)
164+
}
165+
166+
return nil
167+
}
168+
169+
// runHelmTemplate runs helm template on the given chart with the given values and
170+
// returns the raw output.
171+
func runHelmTemplate(t testing.TB, helmPath, chartDir, valuesFilePath, namespace string) (string, error) {
172+
// Ensure that valuesFilePath exists
173+
if _, err := os.Stat(valuesFilePath); err != nil {
174+
return "", xerrors.Errorf("values file %q does not exist: %w", valuesFilePath, err)
175+
}
176+
177+
cmd := exec.Command(helmPath, "template", chartDir, "-f", valuesFilePath, "--namespace", namespace)
178+
t.Logf("exec command: %v", cmd.Args)
179+
out, err := cmd.CombinedOutput()
180+
return string(out), err
181+
}
182+
183+
// lookupHelm ensures that Helm is available in $PATH and returns the path to the
184+
// Helm executable.
185+
func lookupHelm(t testing.TB) string {
186+
helmPath, err := exec.LookPath("helm")
187+
if err != nil {
188+
t.Fatalf("helm not found in $PATH: %v", err)
189+
return ""
190+
}
191+
t.Logf("Using helm at %q", helmPath)
192+
return helmPath
193+
}
194+
195+
func TestMain(m *testing.M) {
196+
flag.Parse()
197+
os.Exit(m.Run())
198+
}

0 commit comments

Comments
 (0)