Repository: exercism/cli
Pull request #588 (2018-06-25 05:32:55)

Inline all the status stuff into the troubleshoot command
This one might be a wee bit more controversial, but ... also maybe not.

The status check code all lived inside the `cli` package, but it was only used from the troubleshoot command. This moves it into the troubleshoot file, and makes it unexported in the `cmd` package.

In addition to this feeling like it kind of belongs together with the troubleshoot command, the `cli` package no longer imports the `config` package, which feels cleaner to me.


Patch:
diff --git a/cli/status.go b/cli/status.go
deleted file mode 100644
index f79f410..0000000
--- a/cli/status.go
+++ /dev/null
@@ -1,223 +0,0 @@
-package cli
-
-import (
-	"bytes"
-	"fmt"
-	"html/template"
-	"runtime"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/exercism/cli/config"
-)
-
-// Status represents the results of a CLI self test.
-type Status struct {
-	Censor          bool
-	Version         versionStatus
-	System          systemStatus
-	Configuration   configurationStatus
-	APIReachability apiReachabilityStatus
-	cli             *CLI
-	cfg             config.UserConfig
-}
-
-type versionStatus struct {
-	Current  string
-	Latest   string
-	Status   string
-	Error    error
-	UpToDate bool
-}
-
-type systemStatus struct {
-	OS           string
-	Architecture string
-	Build        string
-}
-
-type configurationStatus struct {
-	Home      string
-	Workspace string
-	File      string
-	Token     string
-	TokenURL  string
-}
-
-type apiReachabilityStatus struct {
-	Services []*apiPing
-}
-
-type apiPing struct {
-	Service string
-	URL     string
-	Status  string
-	Latency time.Duration
-}
-
-// NewStatus prepares a value to perform a diagnostic self-check.
-func NewStatus(c *CLI, uc config.UserConfig) Status {
-	status := Status{
-		cli: c,
-		cfg: uc,
-	}
-	return status
-}
-
-// Check runs the CLI's diagnostic self-check.
-func (status *Status) Check() (string, error) {
-	status.Version = newVersionStatus(status.cli)
-	status.System = newSystemStatus()
-
-	cs, err := newConfigurationStatus(status)
-	if err != nil {
-		return "", err
-	}
-	status.Configuration = cs
-
-	ar, err := newAPIReachabilityStatus(status.cfg.APIBaseURL)
-	if err != nil {
-		return "", err
-	}
-	status.APIReachability = ar
-
-	return status.compile()
-}
-func (status *Status) compile() (string, error) {
-	t, err := template.New("self-test").Parse(tmplSelfTest)
-	if err != nil {
-		return "", err
-	}
-
-	var bb bytes.Buffer
-	t.Execute(&bb, status)
-	return bb.String(), nil
-}
-
-func newAPIReachabilityStatus(baseURL string) (apiReachabilityStatus, error) {
-	ar := apiReachabilityStatus{
-		Services: []*apiPing{
-			{Service: "GitHub", URL: "https://api.github.com"},
-			{Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)},
-		},
-	}
-	var wg sync.WaitGroup
-	wg.Add(len(ar.Services))
-	for _, service := range ar.Services {
-		go service.Call(&wg)
-	}
-	wg.Wait()
-	return ar, nil
-}
-
-func newVersionStatus(cli *CLI) versionStatus {
-	vs := versionStatus{
-		Current: cli.Version,
-	}
-	ok, err := cli.IsUpToDate()
-	if err == nil {
-		vs.Latest = cli.LatestRelease.Version()
-	} else {
-		vs.Error = fmt.Errorf("Error: %s", err)
-	}
-	vs.UpToDate = ok
-	return vs
-}
-
-func newSystemStatus() systemStatus {
-	ss := systemStatus{
-		OS:           runtime.GOOS,
-		Architecture: runtime.GOARCH,
-	}
-	if BuildOS != "" && BuildARCH != "" {
-		ss.Build = fmt.Sprintf("%s/%s", BuildOS, BuildARCH)
-	}
-	if BuildARM != "" {
-		ss.Build = fmt.Sprintf("%s ARMv%s", ss.Build, BuildARM)
-	}
-	return ss
-}
-
-func newConfigurationStatus(status *Status) (configurationStatus, error) {
-	cs := configurationStatus{
-		Home:      status.cfg.Home,
-		Workspace: status.cfg.Workspace,
-		File:      status.cfg.File(),
-		Token:     status.cfg.Token,
-		TokenURL:  config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings",
-	}
-	if status.Censor && status.cfg.Token != "" {
-		cs.Token = redactToken(status.cfg.Token)
-	}
-	return cs, nil
-}
-
-func (ping *apiPing) Call(wg *sync.WaitGroup) {
-	defer wg.Done()
-
-	now := time.Now()
-	res, err := HTTPClient.Get(ping.URL)
-	delta := time.Since(now)
-	ping.Latency = delta
-	if err != nil {
-		ping.Status = err.Error()
-		return
-	}
-	res.Body.Close()
-	ping.Status = "connected"
-}
-
-func redactToken(token string) string {
-	str := token[4 : len(token)-3]
-	redaction := strings.Repeat("*", len(str))
-	return string(token[:4]) + redaction + string(token[len(token)-3:])
-}
-
-const tmplSelfTest = `
-Troubleshooting Information
-===========================
-
-Version
-----------------
-Current: {{ .Version.Current }}
-Latest:  {{ with .Version.Latest }}{{ . }}{{ else }}<unknown>{{ end }}
-{{ with .Version.Error }}
-{{ . }}
-{{ end -}}
-{{ if not .Version.UpToDate }}
-Call 'exercism upgrade' to get the latest version.
-See the release notes at https://github.com/exercism/cli/releases/tag/{{ .Version.Latest }} for details.
-{{ end }}
-
-Operating System
-----------------
-OS:           {{ .System.OS }}
-Architecture: {{ .System.Architecture }}
-{{ with .System.Build }}
-Build: {{ . }}
-{{ end }}
-
-Configuration
-----------------
-Home:      {{ .Configuration.Home }}
-Workspace: {{ .Configuration.Workspace }}
-Config:    {{ .Configuration.File }}
-API key:   {{ with .Configuration.Token }}{{ . }}{{ else }}<not configured>
-Find your API key at {{ .Configuration.TokenURL }}{{ end }}
-
-API Reachability
-----------------
-{{ range .APIReachability.Services }}
-{{ .Service }}:
-    * {{ .URL }}
-    * [{{ .Status }}]
-    * {{ .Latency }}
-{{ end }}
-
-If you are having trouble please file a GitHub issue at
-https://github.com/exercism/exercism.io/issues and include
-this information.
-{{ if not .Censor }}
-Don't share your API key. Keep that private.
-{{ end }}`
diff --git a/cmd/troubleshoot.go b/cmd/troubleshoot.go
index 9b72042..b03242a 100644
--- a/cmd/troubleshoot.go
+++ b/cmd/troubleshoot.go
@@ -1,8 +1,13 @@
 package cmd
 
 import (
+	"bytes"
 	"fmt"
+	"html/template"
 	"net/http"
+	"runtime"
+	"strings"
+	"sync"
 	"time"
 
 	"github.com/exercism/cli/cli"
@@ -32,9 +37,9 @@ command into a GitHub issue so we can help figure out what's going on.
 			return err
 		}
 
-		status := cli.NewStatus(c, *cfg)
+		status := newStatus(c, *cfg)
 		status.Censor = !fullAPIKey
-		s, err := status.Check()
+		s, err := status.check()
 		if err != nil {
 			return err
 		}
@@ -44,6 +49,206 @@ command into a GitHub issue so we can help figure out what's going on.
 	},
 }
 
+// Status represents the results of a CLI self test.
+type Status struct {
+	Censor          bool
+	Version         versionStatus
+	System          systemStatus
+	Configuration   configurationStatus
+	APIReachability apiReachabilityStatus
+	cli             *cli.CLI
+	cfg             config.UserConfig
+}
+
+type versionStatus struct {
+	Current  string
+	Latest   string
+	Status   string
+	Error    error
+	UpToDate bool
+}
+
+type systemStatus struct {
+	OS           string
+	Architecture string
+	Build        string
+}
+
+type configurationStatus struct {
+	Home      string
+	Workspace string
+	File      string
+	Token     string
+	TokenURL  string
+}
+
+type apiReachabilityStatus struct {
+	Services []*apiPing
+}
+
+type apiPing struct {
+	Service string
+	URL     string
+	Status  string
+	Latency time.Duration
+}
+
+// newStatus prepares a value to perform a diagnostic self-check.
+func newStatus(c *cli.CLI, uc config.UserConfig) Status {
+	status := Status{
+		cli: c,
+		cfg: uc,
+	}
+	return status
+}
+
+// check runs the CLI's diagnostic self-check.
+func (status *Status) check() (string, error) {
+	status.Version = newVersionStatus(status.cli)
+	status.System = newSystemStatus()
+	status.Configuration = newConfigurationStatus(status)
+	status.APIReachability = newAPIReachabilityStatus(status.cfg.APIBaseURL)
+
+	return status.compile()
+}
+func (status *Status) compile() (string, error) {
+	t, err := template.New("self-test").Parse(tmplSelfTest)
+	if err != nil {
+		return "", err
+	}
+
+	var bb bytes.Buffer
+	t.Execute(&bb, status)
+	return bb.String(), nil
+}
+
+func newAPIReachabilityStatus(baseURL string) apiReachabilityStatus {
+	ar := apiReachabilityStatus{
+		Services: []*apiPing{
+			{Service: "GitHub", URL: "https://api.github.com"},
+			{Service: "Exercism", URL: fmt.Sprintf("%s/ping", baseURL)},
+		},
+	}
+	var wg sync.WaitGroup
+	wg.Add(len(ar.Services))
+	for _, service := range ar.Services {
+		go service.Call(&wg)
+	}
+	wg.Wait()
+	return ar
+}
+
+func newVersionStatus(c *cli.CLI) versionStatus {
+	vs := versionStatus{
+		Current: c.Version,
+	}
+	ok, err := c.IsUpToDate()
+	if err == nil {
+		vs.Latest = c.LatestRelease.Version()
+	} else {
+		vs.Error = fmt.Errorf("Error: %s", err)
+	}
+	vs.UpToDate = ok
+	return vs
+}
+
+func newSystemStatus() systemStatus {
+	ss := systemStatus{
+		OS:           runtime.GOOS,
+		Architecture: runtime.GOARCH,
+	}
+	if cli.BuildOS != "" && cli.BuildARCH != "" {
+		ss.Build = fmt.Sprintf("%s/%s", cli.BuildOS, cli.BuildARCH)
+	}
+	if cli.BuildARM != "" {
+		ss.Build = fmt.Sprintf("%s ARMv%s", ss.Build, cli.BuildARM)
+	}
+	return ss
+}
+
+func newConfigurationStatus(status *Status) configurationStatus {
+	cs := configurationStatus{
+		Home:      status.cfg.Home,
+		Workspace: status.cfg.Workspace,
+		File:      status.cfg.File(),
+		Token:     status.cfg.Token,
+		TokenURL:  config.InferSiteURL(status.cfg.APIBaseURL) + "/my/settings",
+	}
+	if status.Censor && status.cfg.Token != "" {
+		cs.Token = redact(status.cfg.Token)
+	}
+	return cs
+}
+
+func (ping *apiPing) Call(wg *sync.WaitGroup) {
+	defer wg.Done()
+
+	now := time.Now()
+	res, err := cli.HTTPClient.Get(ping.URL)
+	delta := time.Since(now)
+	ping.Latency = delta
+	if err != nil {
+		ping.Status = err.Error()
+		return
+	}
+	res.Body.Close()
+	ping.Status = "connected"
+}
+
+func redact(token string) string {
+	str := token[4 : len(token)-3]
+	redaction := strings.Repeat("*", len(str))
+	return string(token[:4]) + redaction + string(token[len(token)-3:])
+}
+
+const tmplSelfTest = `
+Troubleshooting Information
+===========================
+
+Version
+----------------
+Current: {{ .Version.Current }}
+Latest:  {{ with .Version.Latest }}{{ . }}{{ else }}<unknown>{{ end }}
+{{ with .Version.Error }}
+{{ . }}
+{{ end -}}
+{{ if not .Version.UpToDate }}
+Call 'exercism upgrade' to get the latest version.
+See the release notes at https://github.com/exercism/cli/releases/tag/{{ .Version.Latest }} for details.
+{{ end }}
+
+Operating System
+----------------
+OS:           {{ .System.OS }}
+Architecture: {{ .System.Architecture }}
+{{ with .System.Build }}
+Build: {{ . }}
+{{ end }}
+
+Configuration
+----------------
+Home:      {{ .Configuration.Home }}
+Workspace: {{ .Configuration.Workspace }}
+Config:    {{ .Configuration.File }}
+API key:   {{ with .Configuration.Token }}{{ . }}{{ else }}<not configured>
+Find your API key at {{ .Configuration.TokenURL }}{{ end }}
+
+API Reachability
+----------------
+{{ range .APIReachability.Services }}
+{{ .Service }}:
+    * {{ .URL }}
+    * [{{ .Status }}]
+    * {{ .Latency }}
+{{ end }}
+
+If you are having trouble please file a GitHub issue at
+https://github.com/exercism/exercism.io/issues and include
+this information.
+{{ if not .Censor }}
+Don't share your API key. Keep that private.
+{{ end }}`
+
 func init() {
 	RootCmd.AddCommand(troubleshootCmd)
 	troubleshootCmd.Flags().BoolVarP(&fullAPIKey, "full-api-key", "f", false, "display the user's full API key, censored by default")


Test patch:
diff --git a/cli/status_test.go b/cli/status_test.go
deleted file mode 100644
index 07b9011..0000000
--- a/cli/status_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package cli
-
-import (
-	"testing"
-
-	"github.com/exercism/cli/config"
-	"github.com/stretchr/testify/assert"
-)
-
-func TestCensor(t *testing.T) {
-	fakeToken := "1a11111aaaa111aa1a11111a11111aa1"
-	expected := "1a11*************************aa1"
-
-	c := New("")
-
-	cfg := config.NewEmptyUserConfig()
-	cfg.Token = fakeToken
-
-	status := NewStatus(c, *cfg)
-	status.Censor = true
-	status.Check()
-
-	assert.Equal(t, expected, status.Configuration.Token)
-}
diff --git a/cmd/troubleshoot_test.go b/cmd/troubleshoot_test.go
new file mode 100644
index 0000000..e244602
--- /dev/null
+++ b/cmd/troubleshoot_test.go
@@ -0,0 +1,14 @@
+package cmd
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRedact(t *testing.T) {
+	fakeToken := "1a11111aaaa111aa1a11111a11111aa1"
+	expected := "1a11*************************aa1"
+
+	assert.Equal(t, expected, redact(fakeToken))
+}