Skip to content

test: add golden files to enterprise cli #7924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -525,13 +525,17 @@ coderd/apidoc/swagger.json: $(shell find ./scripts/apidocgen $(FIND_EXCLUSIONS)
./scripts/apidocgen/generate.sh
yarn run --cwd=site format:write:only ../docs/api ../docs/manifest.json ../coderd/apidoc/swagger.json

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

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

enterprise/cli/testdata/.gen-golden: $(wildcard enterprise/cli/testdata/*.golden) $(wildcard cli/*.tpl) $(GO_SRC_FILES)
go test ./enterprise/cli -run="TestEnterpriseCommandHelp" -update
touch "$@"

helm/tests/testdata/.gen-golden: $(wildcard helm/tests/testdata/*.yaml) $(wildcard helm/tests/testdata/*.golden) $(GO_SRC_FILES)
go test ./helm/tests -run=TestUpdateGoldenFiles -update
touch "$@"
Expand Down
222 changes: 222 additions & 0 deletions cli/clitest/golden.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package clitest

import (
"bytes"
"context"
"flag"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"testing"

"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
"github.com/stretchr/testify/require"

"github.com/coder/coder/cli/clibase"
"github.com/coder/coder/cli/config"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database/dbtestutil"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)

// UpdateGoldenFiles indicates golden files should be updated.
// To update the golden files:
// make update-golden-files
var UpdateGoldenFiles = flag.Bool("update", false, "update .golden files")

var timestampRegex = regexp.MustCompile(`(?i)\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(.\d+)?Z`)

type CommandHelpCase struct {
Name string
Cmd []string
}

func DefaultCases() []CommandHelpCase {
return []CommandHelpCase{
{
Name: "coder --help",
Cmd: []string{"--help"},
},
{
Name: "coder server --help",
Cmd: []string{"server", "--help"},
},
}
}

// TestCommandHelp will test the help output of the given commands
// using golden files.
//
//nolint:tparallel,paralleltest
func TestCommandHelp(t *testing.T, getRoot func(t *testing.T) *clibase.Cmd, cases []CommandHelpCase) {
ogColorProfile := lipgloss.ColorProfile()
// ANSI256 escape codes are far easier for humans to parse in a diff,
// but TrueColor is probably more popular with modern terminals.
lipgloss.SetColorProfile(termenv.ANSI)
t.Cleanup(func() {
lipgloss.SetColorProfile(ogColorProfile)
})
rootClient, replacements := prepareTestData(t)

root := getRoot(t)

ExtractCommandPathsLoop:
for _, cp := range extractVisibleCommandPaths(nil, root.Children) {
name := fmt.Sprintf("coder %s --help", strings.Join(cp, " "))
cmd := append(cp, "--help")
for _, tt := range cases {
if tt.Name == name {
continue ExtractCommandPathsLoop
}
}
cases = append(cases, CommandHelpCase{Name: name, Cmd: cmd})
}

for _, tt := range cases {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)

var outBuf bytes.Buffer

caseCmd := getRoot(t)

inv, cfg := NewWithCommand(t, caseCmd, tt.Cmd...)
inv.Stderr = &outBuf
inv.Stdout = &outBuf
inv.Environ.Set("CODER_URL", rootClient.URL.String())
inv.Environ.Set("CODER_SESSION_TOKEN", rootClient.SessionToken())
inv.Environ.Set("CODER_CACHE_DIRECTORY", "~/.cache")

SetupConfig(t, rootClient, cfg)

StartWithWaiter(t, inv.WithContext(ctx)).RequireSuccess()

actual := outBuf.Bytes()
if len(actual) == 0 {
t.Fatal("no output")
}

for k, v := range replacements {
actual = bytes.ReplaceAll(actual, []byte(k), []byte(v))
}

actual = NormalizeGoldenFile(t, actual)
goldenPath := filepath.Join("testdata", strings.Replace(tt.Name, " ", "_", -1)+".golden")
if *UpdateGoldenFiles {
t.Logf("update golden file for: %q: %s", tt.Name, goldenPath)
err := os.WriteFile(goldenPath, actual, 0o600)
require.NoError(t, err, "update golden file")
}

expected, err := os.ReadFile(goldenPath)
require.NoError(t, err, "read golden file, run \"make update-golden-files\" and commit the changes")

expected = NormalizeGoldenFile(t, expected)
require.Equal(
t, string(expected), string(actual),
"golden file mismatch: %s, run \"make update-golden-files\", verify and commit the changes",
goldenPath,
)
})
}
}

// NormalizeGoldenFile replaces any strings that are system or timing dependent
// with a placeholder so that the golden files can be compared with a simple
// equality check.
func NormalizeGoldenFile(t *testing.T, byt []byte) []byte {
// Replace any timestamps with a placeholder.
byt = timestampRegex.ReplaceAll(byt, []byte("[timestamp]"))

homeDir, err := os.UserHomeDir()
require.NoError(t, err)

configDir := config.DefaultDir()
byt = bytes.ReplaceAll(byt, []byte(configDir), []byte("~/.config/coderv2"))

byt = bytes.ReplaceAll(byt, []byte(codersdk.DefaultCacheDir()), []byte("[cache dir]"))

// The home directory changes depending on the test environment.
byt = bytes.ReplaceAll(byt, []byte(homeDir), []byte("~"))
for _, r := range []struct {
old string
new string
}{
{"\r\n", "\n"},
{`~\.cache\coder`, "~/.cache/coder"},
{`C:\Users\RUNNER~1\AppData\Local\Temp`, "/tmp"},
{os.TempDir(), "/tmp"},
} {
byt = bytes.ReplaceAll(byt, []byte(r.old), []byte(r.new))
}
return byt
}

func extractVisibleCommandPaths(cmdPath []string, cmds []*clibase.Cmd) [][]string {
var cmdPaths [][]string
for _, c := range cmds {
if c.Hidden {
continue
}
cmdPath := append(cmdPath, c.Name())
cmdPaths = append(cmdPaths, cmdPath)
cmdPaths = append(cmdPaths, extractVisibleCommandPaths(cmdPath, c.Children)...)
}
return cmdPaths
}

func prepareTestData(t *testing.T) (*codersdk.Client, map[string]string) {
t.Helper()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

db, pubsub := dbtestutil.NewDB(t)
rootClient := coderdtest.New(t, &coderdtest.Options{
Database: db,
Pubsub: pubsub,
IncludeProvisionerDaemon: true,
})
firstUser := coderdtest.CreateFirstUser(t, rootClient)
secondUser, err := rootClient.CreateUser(ctx, codersdk.CreateUserRequest{
Email: "testuser2@coder.com",
Username: "testuser2",
Password: coderdtest.FirstUserParams.Password,
OrganizationID: firstUser.OrganizationID,
})
require.NoError(t, err)
version := coderdtest.CreateTemplateVersion(t, rootClient, firstUser.OrganizationID, nil)
version = coderdtest.AwaitTemplateVersionJob(t, rootClient, version.ID)
template := coderdtest.CreateTemplate(t, rootClient, firstUser.OrganizationID, version.ID, func(req *codersdk.CreateTemplateRequest) {
req.Name = "test-template"
})
workspace := coderdtest.CreateWorkspace(t, rootClient, firstUser.OrganizationID, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.Name = "test-workspace"
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, rootClient, workspace.LatestBuild.ID)

replacements := map[string]string{
firstUser.UserID.String(): "[first user ID]",
secondUser.ID.String(): "[second user ID]",
firstUser.OrganizationID.String(): "[first org ID]",
version.ID.String(): "[version ID]",
version.Name: "[version name]",
version.Job.ID.String(): "[version job ID]",
version.Job.FileID.String(): "[version file ID]",
version.Job.WorkerID.String(): "[version worker ID]",
template.ID.String(): "[template ID]",
workspace.ID.String(): "[workspace ID]",
workspaceBuild.ID.String(): "[workspace build ID]",
workspaceBuild.Job.ID.String(): "[workspace build job ID]",
workspaceBuild.Job.FileID.String(): "[workspace build file ID]",
workspaceBuild.Job.WorkerID.String(): "[workspace build worker ID]",
}

return rootClient, replacements
}
Loading