Repository: exercism/cli
Pull request #605 (2018-07-09 10:24:58)

Use sensible, os-specific workspace paths
This first adds the `Configuration` type, which is also added in https://github.com/exercism/cli/pull/604 (cherry-picked here). The `Configuration` type is a place to initialize all the environment-and-os-specific stuff, so that we can compute it only once and then pass around the object. It also lets us easily stub out env-related things for testing.

I also added stuff to that type in subsequent commits.

The sensible defaults that we've chosen are:

- Mac: `~/Exercism`
- Linux: `~/exercism`
- Windows: `%HOMEDRIVE%%HOMEPATH%\Exercism`

I'm using conditional compilation for the tests, since if we do `filepath.Join` on a *nix machine it will give `/` as the `os.PathSeparator`, and the same code will give `\` on a windows machine, so even stubbing out env things will give us nonsensical output. It doesn't make sense to make the test expectation check runtime environment, to determine which direction to make the slash (e.g. expect the windows path to be `"C:\\MyDocs/Exercism"` on *nix and `"C:\\MyDocs\\Exercism"` on windows). That just echoes the same underlying logic as the code, and doesn't really test anything.

Closes https://github.com/exercism/cli/issues/541

kytrinyx: @FabienTregan Would you be able to pull this down and run the `config/configuration_windows_test.go` to see if it makes sense?
kytrinyx: > On that note we should probably add Windows to Travis if supported for Go. I have not tested YET!!!!

Agreed. I've added AppVeyor on a separate branch.
FabienTregan: > On that note we should probably add Windows to Travis if supported for Go. I have not tested YET!!!!

Having a quick look, but https://docs.travis-ci.com/user/multi-os/ made me think it supports linux and osx only.

Patch:
diff --git a/config/configuration.go b/config/configuration.go
new file mode 100644
index 0000000..1cc00d8
--- /dev/null
+++ b/config/configuration.go
@@ -0,0 +1,106 @@
+package config
+
+import (
+	"os"
+	"path/filepath"
+	"runtime"
+	"strings"
+
+	"github.com/spf13/viper"
+)
+
+var (
+	// DefaultDirName is the default name used for config and workspace directories.
+	DefaultDirName string
+)
+
+// Configuration lets us inject configuration options into commands.
+// Note that we are slowly working towards getting rid of the
+// config.Config, config.UserConfig, and config.CLIConfig types.
+// Once we do, we can rename this type to Config, and get rid of the
+// User and CLI fields.
+type Configuration struct {
+	OS              string
+	Home            string
+	Dir             string
+	DefaultBaseURL  string
+	DefaultDirName  string
+	UserViperConfig *viper.Viper
+	UserConfig      *UserConfig
+	CLI             *CLIConfig
+}
+
+// NewConfiguration provides a configuration with default values.
+func NewConfiguration() Configuration {
+	home := userHome()
+
+	return Configuration{
+		OS:             runtime.GOOS,
+		Dir:            Dir(),
+		Home:           home,
+		DefaultBaseURL: defaultBaseURL,
+		DefaultDirName: DefaultDirName,
+	}
+}
+
+// SetDefaultDirName configures the default directory name based on the name of the binary.
+func SetDefaultDirName(binaryName string) {
+	DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1)
+}
+
+// Dir is the configured config home directory.
+// All the cli-related config files live in this directory.
+func Dir() string {
+	var dir string
+	if runtime.GOOS == "windows" {
+		dir = os.Getenv("APPDATA")
+		if dir != "" {
+			return filepath.Join(dir, DefaultDirName)
+		}
+	} else {
+		dir := os.Getenv("EXERCISM_CONFIG_HOME")
+		if dir != "" {
+			return dir
+		}
+		dir = os.Getenv("XDG_CONFIG_HOME")
+		if dir == "" {
+			dir = filepath.Join(os.Getenv("HOME"), ".config")
+		}
+		if dir != "" {
+			return filepath.Join(dir, DefaultDirName)
+		}
+	}
+	// If all else fails, use the current directory.
+	dir, _ = os.Getwd()
+	return dir
+}
+
+func userHome() string {
+	var dir string
+	if runtime.GOOS == "windows" {
+		dir = os.Getenv("USERPROFILE")
+		if dir != "" {
+			return dir
+		}
+		dir = filepath.Join(os.Getenv("HOMEDRIVE"), os.Getenv("HOMEPATH"))
+		if dir != "" {
+			return dir
+		}
+	} else {
+		dir = os.Getenv("HOME")
+		if dir != "" {
+			return dir
+		}
+	}
+	// If all else fails, use the current directory.
+	dir, _ = os.Getwd()
+	return dir
+}
+
+func DefaultWorkspaceDir(cfg Configuration) string {
+	dir := cfg.DefaultDirName
+	if cfg.OS != "linux" {
+		dir = strings.Title(dir)
+	}
+	return filepath.Join(cfg.Home, dir)
+}
diff --git a/config/dir.go b/config/dir.go
deleted file mode 100644
index 1cccc87..0000000
--- a/config/dir.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package config
-
-import (
-	"os"
-	"path/filepath"
-	"runtime"
-	"strings"
-)
-
-var (
-	// DefaultDirName is the default name used for config and workspace directories.
-	DefaultDirName string
-)
-
-// SetDefaultDirName configures the default directory name based on the name of the binary.
-func SetDefaultDirName(binaryName string) {
-	DefaultDirName = strings.Replace(filepath.Base(binaryName), ".exe", "", 1)
-}
-
-// Dir is the configured config home directory.
-// All the cli-related config files live in this directory.
-func Dir() string {
-	var dir string
-	if runtime.GOOS == "windows" {
-		dir = os.Getenv("APPDATA")
-		if dir != "" {
-			return filepath.Join(dir, DefaultDirName)
-		}
-	} else {
-		dir := os.Getenv("EXERCISM_CONFIG_HOME")
-		if dir != "" {
-			return dir
-		}
-		dir = os.Getenv("XDG_CONFIG_HOME")
-		if dir == "" {
-			dir = filepath.Join(os.Getenv("HOME"), ".config")
-		}
-		if dir != "" {
-			return filepath.Join(dir, DefaultDirName)
-		}
-	}
-	// If all else fails, use the current directory.
-	dir, _ = os.Getwd()
-	return dir
-}
diff --git a/config/user_config.go b/config/user_config.go
index 9e9013f..ef5b9b1 100644
--- a/config/user_config.go
+++ b/config/user_config.go
@@ -3,8 +3,6 @@ package config
 import (
 	"fmt"
 	"os"
-	"path/filepath"
-	"runtime"
 
 	"github.com/spf13/viper"
 )
@@ -20,6 +18,7 @@ type UserConfig struct {
 	Token      string
 	Home       string
 	APIBaseURL string
+	settings   Configuration
 }
 
 // NewUserConfig loads a user configuration if it exists.
@@ -36,7 +35,8 @@ func NewUserConfig() (*UserConfig, error) {
 // NewEmptyUserConfig creates a user configuration without loading it.
 func NewEmptyUserConfig() *UserConfig {
 	return &UserConfig{
-		Config: New(Dir(), "user"),
+		Config:   New(Dir(), "user"),
+		settings: NewConfiguration(),
 	}
 }
 
@@ -49,7 +49,14 @@ func (cfg *UserConfig) SetDefaults() {
 		cfg.APIBaseURL = defaultBaseURL
 	}
 	if cfg.Workspace == "" {
-		cfg.Workspace = defaultWorkspace(cfg.Home)
+		dir := DefaultWorkspaceDir(cfg.settings)
+
+		_, err := os.Stat(dir)
+		// Sorry about the double negative.
+		if !os.IsNotExist(err) {
+			dir = fmt.Sprintf("%s-1", dir)
+		}
+		cfg.Workspace = dir
 	}
 }
 
@@ -64,35 +71,3 @@ func (cfg *UserConfig) Load(v *viper.Viper) error {
 	cfg.readIn(v)
 	return v.Unmarshal(&cfg)
 }
-
-func userHome() string {
-	var dir string
-	if runtime.GOOS == "windows" {
-		dir = os.Getenv("USERPROFILE")
-		if dir != "" {
-			return dir
-		}
-		dir = os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
-		if dir != "" {
-			return dir
-		}
-	} else {
-		dir = os.Getenv("HOME")
-		if dir != "" {
-			return dir
-		}
-	}
-	// If all else fails, use the current directory.
-	dir, _ = os.Getwd()
-	return dir
-}
-
-func defaultWorkspace(home string) string {
-	dir := filepath.Join(home, DefaultDirName)
-	_, err := os.Stat(dir)
-	// Sorry about the double negative.
-	if !os.IsNotExist(err) {
-		dir = fmt.Sprintf("%s-1", dir)
-	}
-	return dir
-}


Test patch:
diff --git a/config/configuration_test.go b/config/configuration_test.go
new file mode 100644
index 0000000..ff64b49
--- /dev/null
+++ b/config/configuration_test.go
@@ -0,0 +1,30 @@
+// +build !windows
+
+package config
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultWorkspaceDir(t *testing.T) {
+	testCases := []struct {
+		cfg      Configuration
+		expected string
+	}{
+		{
+			cfg:      Configuration{OS: "darwin", Home: "/User/charlie", DefaultDirName: "apple"},
+			expected: "/User/charlie/Apple",
+		},
+		{
+			cfg:      Configuration{OS: "linux", Home: "/home/bob", DefaultDirName: "banana"},
+			expected: "/home/bob/banana",
+		},
+	}
+
+	for _, tc := range testCases {
+		assert.Equal(t, tc.expected, DefaultWorkspaceDir(tc.cfg), fmt.Sprintf("Operating System: %s", tc.cfg.OS))
+	}
+}
diff --git a/config/configuration_windows_test.go b/config/configuration_windows_test.go
new file mode 100644
index 0000000..b07ccab
--- /dev/null
+++ b/config/configuration_windows_test.go
@@ -0,0 +1,14 @@
+// +build windows
+
+package config
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDefaultWindowsWorkspaceDir(t *testing.T) {
+	cfg := Configuration{OS: "windows", Home: "C:\\Something", DefaultDirName: "basename"}
+	assert.Equal(t, "C:\\Something\\Basename", DefaultWorkspaceDir(cfg))
+}