Skip to content

feat: add JSON output format to many CLI commands #6082

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 8 commits into from
Feb 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
chore: add golden files for json commands
  • Loading branch information
deansheather committed Feb 8, 2023
commit 3eb681312b214d9f79b6af715b7dbca1448be045
94 changes: 86 additions & 8 deletions cli/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package cli_test

import (
"bytes"
"context"
"flag"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"testing"
Expand All @@ -20,6 +22,8 @@ import (
"github.com/coder/coder/buildinfo"
"github.com/coder/coder/cli"
"github.com/coder/coder/cli/clitest"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database/dbtestutil"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/testutil"
)
Expand All @@ -28,13 +32,17 @@ import (
// 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`)

//nolint:tparallel,paralleltest // These test sets env vars.
func TestCommandHelp(t *testing.T) {
commonEnv := map[string]string{
"HOME": "~",
"CODER_CONFIG_DIR": "~/.config/coderv2",
}

rootClient, replacements := prepareTestData(t)

type testCase struct {
name string
cmd []string
Expand All @@ -59,6 +67,14 @@ func TestCommandHelp(t *testing.T) {
"CODER_AGENT_LOG_DIR": "/tmp",
},
},
{
name: "coder list --output json",
cmd: []string{"list", "--output", "json"},
},
{
name: "coder users list --output json",
cmd: []string{"users", "list", "--output", "json"},
},
}

root := cli.Root(cli.AGPL())
Expand Down Expand Up @@ -111,21 +127,33 @@ ExtractCommandPathsLoop:
}
err := os.Chdir(tmpwd)
var buf bytes.Buffer
root, _ := clitest.New(t, tt.cmd...)
root.SetOut(&buf)
cmd, cfg := clitest.New(t, tt.cmd...)
clitest.SetupConfig(t, rootClient, cfg)
cmd.SetOut(&buf)
assert.NoError(t, err)
err = root.ExecuteContext(ctx)
err = cmd.ExecuteContext(ctx)
err2 := os.Chdir(wd)
require.NoError(t, err)
require.NoError(t, err2)

got := buf.Bytes()
// Remove CRLF newlines (Windows).
got = bytes.ReplaceAll(got, []byte{'\r', '\n'}, []byte{'\n'})

// The `coder templates create --help` command prints the path
// to the working directory (--directory flag default value).
got = bytes.ReplaceAll(got, []byte(fmt.Sprintf("%q", tmpwd)), []byte("\"[current directory]\""))
replace := map[string][]byte{
// Remove CRLF newlines (Windows).
string([]byte{'\r', '\n'}): []byte("\n"),
// The `coder templates create --help` command prints the path
// to the working directory (--directory flag default value).
fmt.Sprintf("%q", tmpwd): []byte("\"[current directory]\""),
}
for k, v := range replacements {
replace[k] = []byte(v)
}
for k, v := range replace {
got = bytes.ReplaceAll(got, []byte(k), v)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can just use a map[string]string and strings.ReplaceAll to simplify and avoid the byte/string conversions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd have to convert got to a string in that case:

gotStr := strings.ReplaceAll([]byte(got), k, v)
got = []byte(gotStr)

}

// Replace any timestamps with a placeholder.
got = timestampRegex.ReplaceAll(got, []byte("[timestamp]"))

gf := filepath.Join("testdata", strings.Replace(tt.name, " ", "_", -1)+".golden")
if *updateGoldenFiles {
Expand Down Expand Up @@ -156,6 +184,56 @@ func extractVisibleCommandPaths(cmdPath []string, cmds []*cobra.Command) [][]str
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
}

func TestRoot(t *testing.T) {
t.Parallel()
t.Run("FormatCobraError", func(t *testing.T) {
Expand Down
51 changes: 51 additions & 0 deletions cli/testdata/coder_list_--output_json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
[
{
"id": "[workspace ID]",
"created_at": "[timestamp]",
"updated_at": "[timestamp]",
"owner_id": "[first user ID]",
"owner_name": "testuser",
"template_id": "[template ID]",
"template_name": "test-template",
"template_display_name": "",
"template_icon": "",
"template_allow_user_cancel_workspace_jobs": false,
"latest_build": {
"id": "[workspace build ID]",
"created_at": "[timestamp]",
"updated_at": "[timestamp]",
"workspace_id": "[workspace ID]",
"workspace_name": "test-workspace",
"workspace_owner_id": "[first user ID]",
"workspace_owner_name": "testuser",
"template_version_id": "[version ID]",
"template_version_name": "[version name]",
"build_number": 1,
"transition": "start",
"initiator_id": "[first user ID]",
"initiator_name": "testuser",
"job": {
"id": "[workspace build job ID]",
"created_at": "[timestamp]",
"started_at": "[timestamp]",
"completed_at": "[timestamp]",
"status": "succeeded",
"worker_id": "[workspace build worker ID]",
"file_id": "[workspace build file ID]",
"tags": {
"scope": "organization"
}
},
"reason": "initiator",
"resources": [],
"deadline": "[timestamp]",
"status": "running",
"daily_cost": 0
},
"outdated": false,
"name": "test-workspace",
"autostart_schedule": "CRON_TZ=US/Central 30 9 * * 1-5",
"ttl_ms": 28800000,
"last_used_at": "[timestamp]"
}
]
33 changes: 33 additions & 0 deletions cli/testdata/coder_users_list_--output_json.golden
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
[
{
"id": "[first user ID]",
"username": "testuser",
"email": "testuser@coder.com",
"created_at": "[timestamp]",
"last_seen_at": "[timestamp]",
"status": "active",
"organization_ids": [
"[first org ID]"
],
"roles": [
{
"name": "owner",
"display_name": "Owner"
}
],
"avatar_url": ""
},
{
"id": "[second user ID]",
"username": "testuser2",
"email": "testuser2@coder.com",
"created_at": "[timestamp]",
"last_seen_at": "[timestamp]",
"status": "active",
"organization_ids": [
"[first org ID]"
],
"roles": [],
"avatar_url": ""
}
]