Skip to content

Commit 0652063

Browse files
authored
test: add golden files to enterprise cli (#7924)
* test: Add golden files to enterprise cli
1 parent 4f9d315 commit 0652063

24 files changed

+956
-214
lines changed

Makefile

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,13 +525,17 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
525525
./scripts/apidocgen/generate.sh
526526
yarn run --cwd=site format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json
527527

528-
update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden
528+
update-golden-files: cli/testdata/.gen-golden helm/tests/testdata/.gen-golden scripts/ci-report/testdata/.gen-golden enterprise/cli/testdata/.gen-golden
529529
.PHONY: update-golden-files
530530

531531
cli/testdata/.gen-golden: $(wildcard cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES)
532532
go test ./cli -run="Test(CommandHelp|ServerYAML)" -update
533533
touch "$@"
534534

535+
enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES)
536+
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
537+
touch "$@"
538+
535539
helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.yaml) $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES)
536540
go test ./helm/tests -run=TestUpdateGoldenFiles -update
537541
touch "$@"

cli/clitest/golden.go

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package clitest
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"flag"
7+
"fmt"
8+
"os"
9+
"path/filepath"
10+
"regexp"
11+
"strings"
12+
"testing"
13+
14+
"github.com/charmbracelet/lipgloss"
15+
"github.com/muesli/termenv"
16+
"github.com/stretchr/testify/require"
17+
18+
"github.com/coder/coder/cli/clibase"
19+
"github.com/coder/coder/cli/config"
20+
"github.com/coder/coder/coderd/coderdtest"
21+
"github.com/coder/coder/coderd/database/dbtestutil"
22+
"github.com/coder/coder/codersdk"
23+
"github.com/coder/coder/testutil"
24+
)
25+
26+
// UpdateGoldenFiles indicates golden files should be updated.
27+
// To update the golden files:
28+
// make update-golden-files
29+
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")
30+
31+
var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z`)
32+
33+
type CommandHelpCase struct {
34+
Name string
35+
Cmd []string
36+
}
37+
38+
func DefaultCases() []CommandHelpCase {
39+
return []CommandHelpCase{
40+
{
41+
Name: "coder --help",
42+
Cmd: []string{"--help"},
43+
},
44+
{
45+
Name: "coder server --help",
46+
Cmd: []string{"server", "--help"},
47+
},
48+
}
49+
}
50+
51+
// TestCommandHelp will test the help output of the given commands
52+
// using golden files.
53+
//
54+
//nolint:tparallel,paralleltest
55+
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
56+
ogColorProfile := lipgloss.ColorProfile()
57+
// ANSI256 escape codes are far easier for humans to parse in a diff,
58+
// but TrueColor is probably more popular with modern terminals.
59+
lipgloss.SetColorProfile(termenv.ANSI)
60+
t.Cleanup(func() {
61+
lipgloss.SetColorProfile(ogColorProfile)
62+
})
63+
rootClient, replacements := prepareTestData(t)
64+
65+
root := getRoot(t)
66+
67+
ExtractCommandPathsLoop:
68+
for _, cp := range extractVisibleCommandPaths(nil, root.Children) {
69+
name := fmt.Sprintf("coder %s --help", strings.Join(cp, " "))
70+
cmd := append(cp, "--help")
71+
for _, tt := range cases {
72+
if tt.Name == name {
73+
continue ExtractCommandPathsLoop
74+
}
75+
}
76+
cases = append(cases, CommandHelpCase{Name: name, Cmd: cmd})
77+
}
78+
79+
for _, tt := range cases {
80+
tt := tt
81+
t.Run(tt.Name, func(t *testing.T) {
82+
t.Parallel()
83+
ctx := testutil.Context(t, testutil.WaitLong)
84+
85+
var outBuf bytes.Buffer
86+
87+
caseCmd := getRoot(t)
88+
89+
inv, cfg := NewWithCommand(t, caseCmd, tt.Cmd...)
90+
inv.Stderr = &outBuf
91+
inv.Stdout = &outBuf
92+
inv.Environ.Set("CODER_URL", rootClient.URL.String())
93+
inv.Environ.Set("CODER_SESSION_TOKEN", rootClient.SessionToken())
94+
inv.Environ.Set("CODER_CACHE_DIRECTORY", "~/.cache")
95+
96+
SetupConfig(t, rootClient, cfg)
97+
98+
StartWithWaiter(t, inv.WithContext(ctx)).RequireSuccess()
99+
100+
actual := outBuf.Bytes()
101+
if len(actual) == 0 {
102+
t.Fatal("no output")
103+
}
104+
105+
for k, v := range replacements {
106+
actual = bytes.ReplaceAll(actual, []byte(k), []byte(v))
107+
}
108+
109+
actual = NormalizeGoldenFile(t, actual)
110+
goldenPath := filepath.Join("testdata", strings.Replace(tt.Name, " ", "_", -1)+".golden")
111+
if *UpdateGoldenFiles {
112+
t.Logf("update golden file for: %q: %s", tt.Name, goldenPath)
113+
err := os.WriteFile(goldenPath, actual, 0o600)
114+
require.NoError(t, err, "update golden file")
115+
}
116+
117+
expected, err := os.ReadFile(goldenPath)
118+
require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes")
119+
120+
expected = NormalizeGoldenFile(t, expected)
121+
require.Equal(
122+
t, string(expected), string(actual),
123+
"golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes",
124+
goldenPath,
125+
)
126+
})
127+
}
128+
}
129+
130+
// NormalizeGoldenFile replaces any strings that are system or timing dependent
131+
// with a placeholder so that the golden files can be compared with a simple
132+
// equality check.
133+
func NormalizeGoldenFile(t *testing.T, byt []byte) []byte {
134+
// Replace any timestamps with a placeholder.
135+
byt = timestampRegex.ReplaceAll(byt, []byte("[timestamp]"))
136+
137+
homeDir, err := os.UserHomeDir()
138+
require.NoError(t, err)
139+
140+
configDir := config.DefaultDir()
141+
byt = bytes.ReplaceAll(byt, []byte(configDir), []byte("~/.config/coderv2"))
142+
143+
byt = bytes.ReplaceAll(byt, []byte(codersdk.DefaultCacheDir()), []byte("[cache dir]"))
144+
145+
// The home directory changes depending on the test environment.
146+
byt = bytes.ReplaceAll(byt, []byte(homeDir), []byte("~"))
147+
for _, r := range []struct {
148+
old string
149+
new string
150+
}{
151+
{"\r\n", "\n"},
152+
{`~\.cache\coder`, "~/.cache/coder"},
153+
{`C:\Users\RUNNER~1\AppData\Local\Temp`, "/tmp"},
154+
{os.TempDir(), "/tmp"},
155+
} {
156+
byt = bytes.ReplaceAll(byt, []byte(r.old), []byte(r.new))
157+
}
158+
return byt
159+
}
160+
161+
func extractVisibleCommandPaths(cmdPath []string, cmds []*clibase.Cmd) [][]string {
162+
var cmdPaths [][]string
163+
for _, c := range cmds {
164+
if c.Hidden {
165+
continue
166+
}
167+
cmdPath := append(cmdPath, c.Name())
168+
cmdPaths = append(cmdPaths, cmdPath)
169+
cmdPaths = append(cmdPaths, extractVisibleCommandPaths(cmdPath, c.Children)...)
170+
}
171+
return cmdPaths
172+
}
173+
174+
func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) {
175+
t.Helper()
176+
177+
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
178+
defer cancel()
179+
180+
db, pubsub := dbtestutil.NewDB(t)
181+
rootClient := coderdtest.New(t, &coderdtest.Options{
182+
Database: db,
183+
Pubsub: pubsub,
184+
IncludeProvisionerDaemon: true,
185+
})
186+
firstUser := coderdtest.CreateFirstUser(t, rootClient)
187+
secondUser, err := rootClient.CreateUser(ctx, codersdk.CreateUserRequest{
188+
Email: "testuser2@coder.com",
189+
Username: "testuser2",
190+
Password: coderdtest.FirstUserParams.Password,
191+
OrganizationID: firstUser.OrganizationID,
192+
})
193+
require.NoError(t, err)
194+
version := coderdtest.CreateTemplateVersion(t, rootClient, firstUser.OrganizationID, nil)
195+
version = coderdtest.AwaitTemplateVersionJob(t, rootClient, version.ID)
196+
template := coderdtest.CreateTemplate(t, rootClient, firstUser.OrganizationID, version.ID, func(req *codersdk.CreateTemplateRequest) {
197+
req.Name = "test-template"
198+
})
199+
workspace := coderdtest.CreateWorkspace(t, rootClient, firstUser.OrganizationID, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
200+
req.Name = "test-workspace"
201+
})
202+
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, rootClient, workspace.LatestBuild.ID)
203+
204+
replacements := map[string]string{
205+
firstUser.UserID.String(): "[first user ID]",
206+
secondUser.ID.String(): "[second user ID]",
207+
firstUser.OrganizationID.String(): "[first org ID]",
208+
version.ID.String(): "[version ID]",
209+
version.Name: "[version name]",
210+
version.Job.ID.String(): "[version job ID]",
211+
version.Job.FileID.String(): "[version file ID]",
212+
version.Job.WorkerID.String(): "[version worker ID]",
213+
template.ID.String(): "[template ID]",
214+
workspace.ID.String(): "[workspace ID]",
215+
workspaceBuild.ID.String(): "[workspace build ID]",
216+
workspaceBuild.Job.ID.String(): "[workspace build job ID]",
217+
workspaceBuild.Job.FileID.String(): "[workspace build file ID]",
218+
workspaceBuild.Job.WorkerID.String(): "[workspace build worker ID]",
219+
}
220+
221+
return rootClient, replacements
222+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy