Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
105 changes: 105 additions & 0 deletions config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,111 @@ func TestApplyOverrides_Empty(t *testing.T) {
}
}

func TestApplyOverrides_StringSlice(t *testing.T) {
cases := []struct {
name string
in string
want []string
}{
{"single value", "kv", []string{"kv"}},
{"multi value", "kv,psql", []string{"kv", "psql"}},
{"trims whitespace", " kv , psql ", []string{"kv", "psql"}},
{"empty string yields empty slice", "", []string{}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := Default()
if err := ApplyOverrides(cfg, map[string]string{
"tx_index.indexer": tc.in,
}); err != nil {
t.Fatalf("ApplyOverrides: %v", err)
}
got := cfg.TxIndex.Indexer
if len(got) != len(tc.want) {
t.Fatalf("indexer: got %v (len %d), want %v (len %d)",
got, len(got), tc.want, len(tc.want))
}
for i := range got {
if got[i] != tc.want[i] {
t.Errorf("indexer[%d]: got %q, want %q", i, got[i], tc.want[i])
}
}
if got == nil {
t.Error("indexer slice must be non-nil to render into TOML")
}
})
}
}

func TestApplyOverrides_StringSliceRejectsEmptyEntries(t *testing.T) {
cases := []string{"kv,,psql", ",kv", "kv,", ",,,", "kv, ,psql"}
for _, in := range cases {
t.Run(in, func(t *testing.T) {
cfg := Default()
err := ApplyOverrides(cfg, map[string]string{
"tx_index.indexer": in,
})
if err == nil {
t.Fatalf("expected error for input %q, got nil", in)
}
})
}
}

func TestApplyOverrides_StringSliceOverwritesDefault(t *testing.T) {
cfg := Default()
if err := ApplyOverrides(cfg, map[string]string{
"tx_index.indexer": "kv",
}); err != nil {
t.Fatalf("ApplyOverrides: %v", err)
}
if len(cfg.TxIndex.Indexer) != 1 || cfg.TxIndex.Indexer[0] != "kv" {
t.Errorf("indexer: got %v, want [kv]", cfg.TxIndex.Indexer)
}
}

func TestApplyOverrides_StringSliceRoundTripTOML(t *testing.T) {
dir := t.TempDir()

cases := []struct {
name string
in string
want []string
}{
{"non-empty list survives round-trip", "kv,psql", []string{"kv", "psql"}},
{"empty list survives round-trip as []", "", []string{}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cfg := DefaultForMode(ModeFull)
if err := ApplyOverrides(cfg, map[string]string{
"tx_index.indexer": tc.in,
}); err != nil {
t.Fatalf("ApplyOverrides: %v", err)
}
subdir := t.TempDir()
if err := WriteConfigToDir(cfg, subdir); err != nil {
t.Fatalf("WriteConfigToDir: %v", err)
}
loaded, err := ReadConfigFromDir(subdir)
if err != nil {
t.Fatalf("ReadConfigFromDir: %v", err)
}
got := loaded.TxIndex.Indexer
if len(got) != len(tc.want) {
t.Fatalf("after round-trip: got %v (len %d), want %v (len %d)",
got, len(got), tc.want, len(tc.want))
}
for i := range got {
if got[i] != tc.want[i] {
t.Errorf("indexer[%d]: got %q, want %q", i, got[i], tc.want[i])
}
}
})
}
_ = dir
}

func TestResolveEnv(t *testing.T) {
cfg := Default()
t.Setenv("SEI_CHAIN_MIN_GAS_PRICES", "0.5usei")
Expand Down
33 changes: 33 additions & 0 deletions resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,19 @@ func setReflectValue(v reflect.Value, s string) error {
return fmt.Errorf("value %g overflows %s", n, v.Type())
}
v.SetFloat(n)
case reflect.Slice:
if v.Type().Elem().Kind() != reflect.String {
return fmt.Errorf("unsupported slice element kind: %s", v.Type().Elem().Kind())
}
out, err := parseStringSlice(s)
if err != nil {
return err
}
sliceVal := reflect.MakeSlice(v.Type(), len(out), len(out))
for i, p := range out {
sliceVal.Index(i).SetString(p)
}
v.Set(sliceVal)
default:
return fmt.Errorf("unsupported field type: %s", v.Type())
}
Expand All @@ -190,3 +203,23 @@ func parseFloat64(s string) (float64, error) {
_, err := fmt.Sscanf(s, "%f", &n)
return n, err
}

// parseStringSlice splits a comma-separated string, trims whitespace, and
// rejects empty entries so operator typos fail loudly. Empty input yields a
// non-nil zero-length slice: BurntSushi/toml encodes nil as omitted,
// []string{} as "field = []".
func parseStringSlice(s string) ([]string, error) {
if s == "" {
return []string{}, nil
}
parts := strings.Split(s, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed == "" {
return nil, fmt.Errorf("empty entry in string slice value %q", s)
}
out = append(out, trimmed)
}
return out, nil
}
93 changes: 93 additions & 0 deletions resolve_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package seiconfig

import (
"reflect"
"testing"
)

func TestSetReflectValue_StringSlice(t *testing.T) {
var s []string
v := reflect.ValueOf(&s).Elem()

if err := setReflectValue(v, "a, b ,c"); err != nil {
t.Fatalf("setReflectValue: %v", err)
}
want := []string{"a", "b", "c"}
if !reflect.DeepEqual(s, want) {
t.Errorf("got %v, want %v", s, want)
}
}

func TestSetReflectValue_RejectsNonStringSlice(t *testing.T) {
var s []int
v := reflect.ValueOf(&s).Elem()

err := setReflectValue(v, "1,2,3")
if err == nil {
t.Fatal("expected error for []int slice")
}
if got := err.Error(); got != "unsupported slice element kind: int" {
t.Errorf("got %q, want %q", got, "unsupported slice element kind: int")
}
}

func TestSetReflectValue_RejectsSliceOfSlice(t *testing.T) {
var s [][]string
v := reflect.ValueOf(&s).Elem()

err := setReflectValue(v, "anything")
if err == nil {
t.Fatal("expected error for [][]string")
}
if got := err.Error(); got != "unsupported slice element kind: slice" {
t.Errorf("got %q, want %q", got, "unsupported slice element kind: slice")
}
}

func TestParseStringSlice(t *testing.T) {
cases := []struct {
name string
in string
want []string
}{
{"empty yields non-nil empty", "", []string{}},
{"single value", "a", []string{"a"}},
{"multi value", "a,b,c", []string{"a", "b", "c"}},
{"trims whitespace", " a , b , c ", []string{"a", "b", "c"}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := parseStringSlice(tc.in)
if err != nil {
t.Fatalf("parseStringSlice(%q): %v", tc.in, err)
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parseStringSlice(%q): got %v, want %v", tc.in, got, tc.want)
}
if got == nil {
t.Errorf("parseStringSlice(%q) returned nil; want non-nil empty slice", tc.in)
}
})
}
}

func TestParseStringSlice_RejectsEmptyEntries(t *testing.T) {
cases := []struct {
name string
in string
}{
{"leading comma", ",a"},
{"trailing comma", "a,"},
{"consecutive commas", "a,,b"},
{"only whitespace entry", "a, ,b"},
{"only commas", ",,,"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
_, err := parseStringSlice(tc.in)
if err == nil {
t.Fatalf("parseStringSlice(%q): expected error, got nil", tc.in)
}
})
}
}
Loading