feat: dotnet segment for .NET SDK display

New segment for .NET SDK version (or unsupported version) display.

Includes update for handling command execution errors so segments
can act differently based on exit codes. Using a custom error
type to make it testable rather than passing the OS error directly
to the segment.
This commit is contained in:
Travis Illig 2020-10-16 08:43:02 -07:00 committed by Jan De Dobbeleer
parent 23233bc383
commit 5844faa54d
18 changed files with 224 additions and 35 deletions

View file

@ -0,0 +1,31 @@
---
id: dotnet
title: Dotnet
sidebar_label: Dotnet
---
## What
Display the currently active .NET SDK version.
## Sample Configuration
```json
{
"type": "dotnet",
"style": "powerline",
"powerline_symbol": "",
"foreground": "#000000",
"background": "#00ffff",
"properties": {
"prefix": " \uE77F "
}
}
```
## Properties
- display_version: `boolean` - display the active version or not; useful if all you need is an icon indicating `dotnet`
is present - defaults to `true`
- unsupported_version_icon: `string` - text/icon that is displayed when the active .NET SDK version (e.g., one specified
by `global.json`) is not installed/supported - defaults to `\u2327` (X in a rectangle box)

View file

@ -17,6 +17,7 @@ module.exports = {
"az",
"battery",
"command",
"dotnet",
"environment",
"exit",
"git",

View file

@ -1,6 +1,8 @@
package main
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
@ -26,7 +28,7 @@ type environmentInfo interface {
getHostName() (string, error)
getRuntimeGOOS() string
hasCommand(command string) bool
runCommand(command string, args ...string) string
runCommand(command string, args ...string) (string, error)
runShellCommand(shell string, command string) string
lastErrorCode() int
getArgs() *args
@ -39,6 +41,14 @@ type environment struct {
cwd string
}
type commandError struct {
exitCode int
}
func (e *commandError) Error() string {
return fmt.Sprintf("%d", e.exitCode)
}
func (env *environment) getenv(key string) string {
return os.Getenv(key)
}
@ -110,12 +120,18 @@ func (env *environment) getRuntimeGOOS() string {
return runtime.GOOS
}
func (env *environment) runCommand(command string, args ...string) string {
func (env *environment) runCommand(command string, args ...string) (string, error) {
out, err := exec.Command(command, args...).Output()
if err != nil {
return ""
var exerr *exec.ExitError
if errors.As(err, &exerr) {
return "", &commandError{exitCode: exerr.ExitCode()}
}
return strings.TrimSpace(string(out))
if err != nil {
return "", err
}
return strings.TrimSpace(string(out)), nil
}
func (env *environment) runShellCommand(shell string, command string) string {

View file

@ -20,6 +20,8 @@ const (
ColorBackground Property = "color_background"
//IgnoreFolders folders to ignore and not run the segment logic
IgnoreFolders Property = "ignore_folders"
//DisplayVersion show the version number or not
DisplayVersion Property = "display_version"
)
type properties struct {

View file

@ -67,6 +67,8 @@ const (
Az SegmentType = "az"
//Kubectl writes the Kubernetes context we're currently in
Kubectl SegmentType = "kubectl"
//Dotnet writes which dotnet version is currently active
Dotnet SegmentType = "dotnet"
//Powerline writes it Powerline style
Powerline SegmentStyle = "powerline"
//Plain writes it without ornaments
@ -123,6 +125,7 @@ func (segment *Segment) mapSegmentWithWriter(env environmentInfo) error {
EnvVar: &envvar{},
Az: &az{},
Kubectl: &kubectl{},
Dotnet: &dotnet{},
}
if writer, ok := functions[segment.Type]; ok {
props := &properties{

View file

@ -40,7 +40,7 @@ func (a *az) enabled() bool {
return false
}
output := a.env.runCommand("az", "account", "show", "--query=[name,id]", "-o=tsv")
output, _ := a.env.runCommand("az", "account", "show", "--query=[name,id]", "-o=tsv")
if output == "" {
return false
}

58
segment_dotnet.go Normal file
View file

@ -0,0 +1,58 @@
package main
import (
"errors"
)
type dotnet struct {
props *properties
env environmentInfo
activeVersion string
unsupportedVersion bool
}
const (
//UnsupportedDotnetVersionIcon is displayed when the dotnet version in
//the current folder isn't supported by the installed dotnet SDK set.
UnsupportedDotnetVersionIcon Property = "unsupported_version_icon"
)
func (d *dotnet) string() string {
if d.unsupportedVersion {
return d.props.getString(UnsupportedDotnetVersionIcon, "\u2327")
}
if d.props.getBool(DisplayVersion, true) {
return d.activeVersion
}
return ""
}
func (d *dotnet) init(props *properties, env environmentInfo) {
d.props = props
d.env = env
}
func (d *dotnet) enabled() bool {
if !d.env.hasCommand("dotnet") {
return false
}
output, err := d.env.runCommand("dotnet", "--version")
if err == nil {
d.activeVersion = output
return true
}
// Exit code 145 is a special indicator that dotnet
// ran, but the current project config settings specify
// use of an SDK that isn't installed.
var exerr *commandError
if errors.As(err, &exerr) && exerr.exitCode == 145 {
d.unsupportedVersion = true
return true
}
return false
}

81
segment_dotnet_test.go Normal file
View file

@ -0,0 +1,81 @@
package main
import (
"testing"
"github.com/stretchr/testify/assert"
)
type dotnetArgs struct {
enabled bool
version string
unsupported bool
unsupportedIcon string
displayVersion bool
}
func bootStrapDotnetTest(args *dotnetArgs) *dotnet {
env := new(MockedEnvironment)
env.On("hasCommand", "dotnet").Return(args.enabled)
if args.unsupported {
err := &commandError{exitCode: 145}
env.On("runCommand", "dotnet", []string{"--version"}).Return("", err)
} else {
env.On("runCommand", "dotnet", []string{"--version"}).Return(args.version, nil)
}
props := &properties{
values: map[Property]interface{}{
DisplayVersion: args.displayVersion,
UnsupportedDotnetVersionIcon: args.unsupportedIcon,
},
}
a := &dotnet{
env: env,
props: props,
}
return a
}
func TestEnabledDotnetNotFound(t *testing.T) {
args := &dotnetArgs{
enabled: false,
}
dotnet := bootStrapDotnetTest(args)
assert.False(t, dotnet.enabled())
}
func TestDotnetVersionNotDisplayed(t *testing.T) {
args := &dotnetArgs{
enabled: true,
displayVersion: false,
version: "3.1.402",
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, "", dotnet.string())
}
func TestDotnetVersionDisplayed(t *testing.T) {
expected := "3.1.402"
args := &dotnetArgs{
enabled: true,
displayVersion: true,
version: expected,
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, expected, dotnet.string())
}
func TestDotnetVersionUnsupported(t *testing.T) {
expected := "x"
args := &dotnetArgs{
enabled: true,
displayVersion: true,
unsupported: true,
unsupportedIcon: expected,
}
dotnet := bootStrapDotnetTest(args)
assert.True(t, dotnet.enabled())
assert.Equal(t, expected, dotnet.string())
}

View file

@ -101,7 +101,7 @@ func (g *git) enabled() bool {
if !g.env.hasCommand("git") {
return false
}
output := g.env.runCommand("git", "rev-parse", "--is-inside-work-tree")
output, _ := g.env.runCommand("git", "rev-parse", "--is-inside-work-tree")
return output == "true"
}
@ -183,7 +183,8 @@ func (g *git) setGitStatus() {
func (g *git) getGitCommandOutput(args ...string) string {
args = append([]string{"-c", "core.quotepath=false", "-c", "color.status=false"}, args...)
return g.env.runCommand("git", args...)
val, _ := g.env.runCommand("git", args...)
return val
}
func (g *git) getGitHEADContext(ref string) string {

View file

@ -18,7 +18,7 @@ func TestEnabledGitNotFound(t *testing.T) {
func TestEnabledInWorkingDirectory(t *testing.T) {
env := new(MockedEnvironment)
env.On("hasCommand", "git").Return(true)
env.On("runCommand", "git", []string{"rev-parse", "--is-inside-work-tree"}).Return("true")
env.On("runCommand", "git", []string{"rev-parse", "--is-inside-work-tree"}).Return("true", nil)
g := &git{
env: env,
}
@ -30,7 +30,7 @@ func TestGetGitOutputForCommand(t *testing.T) {
commandArgs := []string{"symbolic-ref", "--short", "HEAD"}
want := "je suis le output"
env := new(MockedEnvironment)
env.On("runCommand", "git", append(args, commandArgs...)).Return(want)
env.On("runCommand", "git", append(args, commandArgs...)).Return(want, nil)
g := &git{
env: env,
}
@ -70,12 +70,12 @@ func setupHEADContextEnv(context *detachedContext) *git {
env.On("getFileContent", "/.git/MERGE_HEAD").Return(context.mergeHEAD)
env.On("hasFiles", "/.git/CHERRY_PICK_HEAD").Return(context.cherryPick)
env.On("hasFiles", "/.git/MERGE_HEAD").Return(context.merge)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-parse", "--short", "HEAD"}).Return(context.currentCommit)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "describe", "--tags", "--exact-match"}).Return(context.tagName)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.origin}).Return(context.origin)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.onto}).Return(context.onto)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.cherryPickSHA}).Return(context.cherryPickSHA)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.mergeHEAD}).Return(context.mergeHEAD)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-parse", "--short", "HEAD"}).Return(context.currentCommit, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "describe", "--tags", "--exact-match"}).Return(context.tagName, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.origin}).Return(context.origin, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.onto}).Return(context.onto, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.cherryPickSHA}).Return(context.cherryPickSHA, nil)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "name-rev", "--name-only", "--exclude=tags/*", context.mergeHEAD}).Return(context.mergeHEAD, nil)
g := &git{
env: env,
repo: &gitRepo{
@ -200,7 +200,7 @@ func TestGetGitHEADContextMergeTag(t *testing.T) {
func TestGetStashContextZeroEntries(t *testing.T) {
want := ""
env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("")
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("", nil)
g := &git{
env: env,
}
@ -211,7 +211,7 @@ func TestGetStashContextZeroEntries(t *testing.T) {
func TestGetStashContextMultipleEntries(t *testing.T) {
want := "2"
env := new(MockedEnvironment)
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("2")
env.On("runCommand", "git", []string{"-c", "core.quotepath=false", "-c", "color.status=false", "rev-list", "--walk-reflogs", "--count", "refs/stash"}).Return("2", nil)
g := &git{
env: env,
}

View file

@ -19,6 +19,6 @@ func (k *kubectl) enabled() bool {
if !k.env.hasCommand("kubectl") {
return false
}
k.contextName = k.env.runCommand("kubectl", "config", "current-context")
k.contextName, _ = k.env.runCommand("kubectl", "config", "current-context")
return true
}

View file

@ -14,7 +14,7 @@ type kubectlArgs struct {
func bootStrapKubectlTest(args *kubectlArgs) *kubectl {
env := new(MockedEnvironment)
env.On("hasCommand", "kubectl").Return(args.enabled)
env.On("runCommand", "kubectl", []string{"config", "current-context"}).Return(args.contextName)
env.On("runCommand", "kubectl", []string{"config", "current-context"}).Return(args.contextName, nil)
k := &kubectl{
env: env,
props: &properties{},

View file

@ -6,11 +6,6 @@ type node struct {
nodeVersion string
}
const (
//DisplayVersion show the version number or not
DisplayVersion Property = "display_version"
)
func (n *node) string() string {
if n.props.getBool(DisplayVersion, true) {
return n.nodeVersion
@ -30,6 +25,6 @@ func (n *node) enabled() bool {
if !n.env.hasCommand("node") {
return false
}
n.nodeVersion = n.env.runCommand("node", "--version")
n.nodeVersion, _ = n.env.runCommand("node", "--version")
return true
}

View file

@ -17,7 +17,7 @@ type nodeArgs struct {
func bootStrapNodeTest(args *nodeArgs) *node {
env := new(MockedEnvironment)
env.On("hasCommand", "node").Return(args.enabled)
env.On("runCommand", "node", []string{"--version"}).Return(args.nodeVersion)
env.On("runCommand", "node", []string{"--version"}).Return(args.nodeVersion, nil)
env.On("hasFiles", "*.js").Return(args.hasJS)
env.On("hasFiles", "*.ts").Return(args.hasTS)
props := &properties{

View file

@ -68,9 +68,9 @@ func (env *MockedEnvironment) hasCommand(command string) bool {
return args.Bool(0)
}
func (env *MockedEnvironment) runCommand(command string, args ...string) string {
func (env *MockedEnvironment) runCommand(command string, args ...string) (string, error) {
arguments := env.Called(command, args)
return arguments.String(0)
return arguments.String(0), arguments.Error(1)
}
func (env *MockedEnvironment) runShellCommand(shell string, command string) string {

View file

@ -38,7 +38,7 @@ func (p *python) enabled() bool {
"python",
}
for index, python := range pythonVersions {
version := p.env.runCommand(python, "--version")
version, _ := p.env.runCommand(python, "--version")
if version != "" {
rawVersion := strings.TrimLeft(version, "Python")
p.pythonVersion = strings.Trim(rawVersion, " ")

View file

@ -15,8 +15,8 @@ type pythonArgs struct {
pathSeparator string
pythonVersion string
python3Version string
hasPyFiles bool
hasNotebookFiles bool
hasPyFiles bool
hasNotebookFiles bool
}
func newPythonArgs() *pythonArgs {
@ -37,8 +37,8 @@ func bootStrapPythonTest(args *pythonArgs) *python {
env := new(MockedEnvironment)
env.On("hasFiles", "*.py").Return(args.hasPyFiles)
env.On("hasFiles", "*.ipynb").Return(args.hasNotebookFiles)
env.On("runCommand", "python", []string{"--version"}).Return(args.pythonVersion)
env.On("runCommand", "python3", []string{"--version"}).Return(args.python3Version)
env.On("runCommand", "python", []string{"--version"}).Return(args.pythonVersion, nil)
env.On("runCommand", "python3", []string{"--version"}).Return(args.python3Version, nil)
env.On("getenv", "VIRTUAL_ENV").Return(args.virtualEnvName)
env.On("getenv", "CONDA_ENV_PATH").Return(args.condaEnvName)
env.On("getenv", "CONDA_DEFAULT_ENV").Return(args.condaDefaultName)

View file

@ -61,5 +61,6 @@ func (s *spotify) init(props *properties, env environmentInfo) {
}
func (s *spotify) runAppleScriptCommand(command string) string {
return s.env.runCommand("osascript", "-e", command)
val, _ := s.env.runCommand("osascript", "-e", command)
return val
}