Make config list less fallible

This commit is contained in:
William Martin 2024-05-08 13:47:34 +02:00
parent 8a82e3a856
commit 8a4f32b4e2
6 changed files with 280 additions and 106 deletions

View file

@ -42,7 +42,7 @@ type cfg struct {
cfg *ghConfig.Config
}
func (c *cfg) Get(hostname, key string) o.Option[string] {
func (c *cfg) get(hostname, key string) o.Option[string] {
if hostname != "" {
val, err := c.cfg.Get([]string{hostsKey, hostname, key})
if err == nil {
@ -59,7 +59,7 @@ func (c *cfg) Get(hostname, key string) o.Option[string] {
}
func (c *cfg) GetOrDefault(hostname, key string) o.Option[string] {
if val := c.Get(hostname, key); val.IsSome() {
if val := c.get(hostname, key); val.IsSome() {
return val
}
@ -126,7 +126,7 @@ func (c *cfg) Prompt(hostname string) string {
}
func (c *cfg) Version() o.Option[string] {
return c.Get("", versionKey)
return c.get("", versionKey)
}
func (c *cfg) Migrate(m gh.Migration) error {
@ -513,6 +513,7 @@ type ConfigOption struct {
Description string
DefaultValue string
AllowedValues []string
CurrentValue func(c gh.Config, hostname string) string
}
func ConfigOptions() []ConfigOption {
@ -522,32 +523,50 @@ func ConfigOptions() []ConfigOption {
Description: "the protocol to use for git clone and push operations",
DefaultValue: "https",
AllowedValues: []string{"https", "ssh"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.GitProtocol(hostname)
},
},
{
Key: editorKey,
Description: "the text editor program to use for authoring text",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Editor(hostname)
},
},
{
Key: promptKey,
Description: "toggle interactive prompting in the terminal",
DefaultValue: "enabled",
AllowedValues: []string{"enabled", "disabled"},
CurrentValue: func(c gh.Config, hostname string) string {
return c.Prompt(hostname)
},
},
{
Key: pagerKey,
Description: "the terminal pager program to send standard output to",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Pager(hostname)
},
},
{
Key: httpUnixSocketKey,
Description: "the path to a Unix socket through which to make an HTTP connection",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.HTTPUnixSocket(hostname)
},
},
{
Key: browserKey,
Description: "the web browser to use for opening URLs",
DefaultValue: "",
CurrentValue: func(c gh.Config, hostname string) string {
return c.Browser(hostname)
},
},
}
}

View file

@ -32,67 +32,6 @@ func TestNewConfigProvidesFallback(t *testing.T) {
requireKeyWithValue(t, spiedCfg, []string{browserKey}, "")
}
func TestGetNonExistentKey(t *testing.T) {
// Given we have no top level configuration
cfg := newTestConfig()
// When we get a key that has no value
optionalVal := cfg.Get("", "non-existent-key")
// Then it returns a None variant
require.True(t, optionalVal.IsNone(), "expected there to be no value")
}
func TestGetNonExistentHostSpecificKey(t *testing.T) {
// Given have no top level configuration
cfg := newTestConfig()
// When we get a key for a host that has no value
optionalVal := cfg.Get("non-existent-host", "non-existent-key")
// Then it returns a None variant
require.True(t, optionalVal.IsNone(), "expected there to be no value")
}
func TestGetExistingTopLevelKey(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "top-level-key", "top-level-value")
// When we get that key
optionalVal := cfg.Get("non-existent-host", "top-level-key")
// Then it returns a Some variant containing the correct value
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "top-level-value", optionalVal.Unwrap())
}
func TestGetExistingHostSpecificKey(t *testing.T) {
// Given have a host specific config entry
cfg := newTestConfig()
cfg.Set("github.com", "host-specific-key", "host-specific-value")
// When we get that key
optionalVal := cfg.Get("github.com", "host-specific-key")
// Then it returns a Some variant containing the correct value
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "host-specific-value", optionalVal.Unwrap())
}
func TestGetHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "key", "value")
// When we get that key on a specific host
optionalVal := cfg.Get("github.com", "key")
// Then it returns a Some variant containing the correct value by falling back to the top level config
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "value", optionalVal.Unwrap())
}
func TestGetOrDefaultApplicationDefaults(t *testing.T) {
tests := []struct {
key string
@ -121,29 +60,65 @@ func TestGetOrDefaultApplicationDefaults(t *testing.T) {
}
}
func TestGetOrDefaultExistingKey(t *testing.T) {
// Given have a top level config entry
func TestGetOrDefaultNonExistentKey(t *testing.T) {
// Given we have no top level configuration
cfg := newTestConfig()
cfg.Set("", gitProtocolKey, "ssh")
// When we get that key
optionalVal := cfg.GetOrDefault("", gitProtocolKey)
// When we get a key that has no value
optionalVal := cfg.GetOrDefault("", "non-existent-key")
// Then it returns successfully with the correct value, and doesn't fall back
// to the default
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "ssh", optionalVal.Unwrap())
// Then it returns a None variant
require.True(t, optionalVal.IsNone(), "expected there to be no value")
}
func TestGetOrDefaultNotFoundAndNoDefault(t *testing.T) {
// Given have no configuration
func TestGetOrDefaultNonExistentHostSpecificKey(t *testing.T) {
// Given have no top level configuration
cfg := newTestConfig()
// When we get a non-existent-key that has no default
optionalEntry := cfg.GetOrDefault("", "non-existent-key")
// When we get a key for a host that has no value
optionalVal := cfg.GetOrDefault("non-existent-host", "non-existent-key")
// Then it returns with no entry
require.False(t, optionalEntry.IsSome(), "expected the config to not contain a value")
// Then it returns a None variant
require.True(t, optionalVal.IsNone(), "expected there to be no value")
}
func TestGetOrDefaultExistingTopLevelKey(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "top-level-key", "top-level-value")
// When we get that key
optionalVal := cfg.GetOrDefault("non-existent-host", "top-level-key")
// Then it returns a Some variant containing the correct value
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "top-level-value", optionalVal.Unwrap())
}
func TestGetOrDefaultExistingHostSpecificKey(t *testing.T) {
// Given have a host specific config entry
cfg := newTestConfig()
cfg.Set("github.com", "host-specific-key", "host-specific-value")
// When we get that key
optionalVal := cfg.GetOrDefault("github.com", "host-specific-key")
// Then it returns a Some variant containing the correct value
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "host-specific-value", optionalVal.Unwrap())
}
func TestGetOrDefaultHostnameSpecificKeyFallsBackToTopLevel(t *testing.T) {
// Given have a top level config entry
cfg := newTestConfig()
cfg.Set("", "key", "value")
// When we get that key on a specific host
optionalVal := cfg.GetOrDefault("github.com", "key")
// Then it returns a Some variant containing the correct value by falling back to the top level config
require.True(t, optionalVal.IsSome(), "expected there to be a value")
require.Equal(t, "value", optionalVal.Unwrap())
}
func TestFallbackConfig(t *testing.T) {

View file

@ -57,13 +57,8 @@ func listRun(opts *ListOptions) error {
configOptions := config.ConfigOptions()
for _, key := range configOptions {
optionalValue := cfg.GetOrDefault(host, key.Key)
if optionalValue.IsNone() {
return fmt.Errorf("invalid key: %s", key.Key)
}
fmt.Fprintf(opts.IO.Out, "%s=%s\n", key.Key, optionalValue.Unwrap())
for _, option := range configOptions {
fmt.Fprintf(opts.IO.Out, "%s=%s\n", option.Key, option.CurrentValue(cfg, host))
}
return nil

View file

@ -10,6 +10,7 @@ import (
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewCmdConfigList(t *testing.T) {
@ -108,9 +109,8 @@ browser=brave
t.Run(tt.name, func(t *testing.T) {
err := listRun(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.stdout, stdout.String())
//assert.Equal(t, tt.stderr, stderr.String())
require.NoError(t, err)
require.Equal(t, tt.stdout, stdout.String())
})
}
}

View file

@ -1,3 +1,24 @@
// MIT License
// Copyright (c) 2022 Tom Godkin
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package o
import "fmt"
@ -76,15 +97,6 @@ func (o Option[T]) IsSome() bool {
return o.present
}
// IsSome returns true if the [Option] is a [Some] variant and the value inside of it equals the provided value.
// func (o Option[T]) Is(t T) bool {
// return o.present && o.value == t
// }
func (o Option[T]) IsSomeAnd(f func(T) bool) bool {
return o.present && f(o.value)
}
// IsNone returns true if the [Option] is a [None] variant.
func (o Option[T]) IsNone() bool {
return !o.present
@ -105,11 +117,3 @@ func (o Option[T]) Expect(message string) T {
panic(message)
}
func Map[T any, U any](f func(T) U, o Option[T]) Option[U] {
if o.present {
return Some(f(o.value))
}
return None[U]()
}

181
pkg/option/option_test.go Normal file
View file

@ -0,0 +1,181 @@
// MIT License
// Copyright (c) 2022 Tom Godkin
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package o_test
import (
"fmt"
"testing"
o "github.com/cli/cli/v2/pkg/option"
"github.com/stretchr/testify/require"
)
func ExampleOption_Unwrap() {
fmt.Println(o.Some(4).Unwrap())
// Output: 4
}
func ExampleOption_UnwrapOr() {
fmt.Println(o.Some(4).UnwrapOr(3))
fmt.Println(o.None[int]().UnwrapOr(3))
// Output:
// 4
// 3
}
func ExampleOption_UnwrapOrElse() {
fmt.Println(o.Some(4).UnwrapOrElse(func() int {
return 3
}))
fmt.Println(o.None[int]().UnwrapOrElse(func() int {
return 3
}))
// Output:
// 4
// 3
}
func ExampleOption_UnwrapOrZero() {
fmt.Println(o.Some(4).UnwrapOrZero())
fmt.Println(o.None[int]().UnwrapOrZero())
// Output
// 4
// 0
}
func ExampleOption_IsSome() {
fmt.Println(o.Some(4).IsSome())
fmt.Println(o.None[int]().IsSome())
// Output:
// true
// false
}
func ExampleOption_IsNone() {
fmt.Println(o.Some(4).IsNone())
fmt.Println(o.None[int]().IsNone())
// Output:
// false
// true
}
func ExampleOption_Value() {
value, ok := o.Some(4).Value()
fmt.Println(value)
fmt.Println(ok)
// Output:
// 4
// true
}
func ExampleOption_Expect() {
fmt.Println(o.Some(4).Expect("oops"))
// Output: 4
}
func TestSomeStringer(t *testing.T) {
require.Equal(t, fmt.Sprintf("%s", o.Some("foo")), "Some(foo)") //nolint:gosimple
require.Equal(t, fmt.Sprintf("%s", o.Some(42)), "Some(42)") //nolint:gosimple
}
func TestNoneStringer(t *testing.T) {
require.Equal(t, fmt.Sprintf("%s", o.None[string]()), "None") //nolint:gosimple
}
func TestSomeUnwrap(t *testing.T) {
require.Equal(t, o.Some(42).Unwrap(), 42)
}
func TestNoneUnwrap(t *testing.T) {
defer func() {
require.Equal(t, fmt.Sprint(recover()), "called `Option.Unwrap()` on a `None` value")
}()
o.None[string]().Unwrap()
t.Error("did not panic")
}
func TestSomeUnwrapOr(t *testing.T) {
require.Equal(t, o.Some(42).UnwrapOr(3), 42)
}
func TestNoneUnwrapOr(t *testing.T) {
require.Equal(t, o.None[int]().UnwrapOr(3), 3)
}
func TestSomeUnwrapOrElse(t *testing.T) {
require.Equal(t, o.Some(42).UnwrapOrElse(func() int { return 41 }), 42)
}
func TestNoneUnwrapOrElse(t *testing.T) {
require.Equal(t, o.None[int]().UnwrapOrElse(func() int { return 41 }), 41)
}
func TestSomeUnwrapOrZero(t *testing.T) {
require.Equal(t, o.Some(42).UnwrapOrZero(), 42)
}
func TestNoneUnwrapOrZero(t *testing.T) {
require.Equal(t, o.None[int]().UnwrapOrZero(), 0)
}
func TestIsSome(t *testing.T) {
require.True(t, o.Some(42).IsSome())
require.False(t, o.None[int]().IsSome())
}
func TestIsNone(t *testing.T) {
require.False(t, o.Some(42).IsNone())
require.True(t, o.None[int]().IsNone())
}
func TestSomeValue(t *testing.T) {
value, ok := o.Some(42).Value()
require.Equal(t, value, 42)
require.True(t, ok)
}
func TestNoneValue(t *testing.T) {
value, ok := o.None[int]().Value()
require.Equal(t, value, 0)
require.False(t, ok)
}
func TestSomeExpect(t *testing.T) {
require.Equal(t, o.Some(42).Expect("oops"), 42)
}
func TestNoneExpect(t *testing.T) {
defer func() {
require.Equal(t, fmt.Sprint(recover()), "oops")
}()
o.None[int]().Expect("oops")
t.Error("did not panic")
}