313 lines
8.5 KiB
Go
313 lines
8.5 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Test sections exercising every field shape section.go supports.
|
|
type testInner struct {
|
|
Name string `cfg:"name" default:"inner"`
|
|
}
|
|
|
|
type testEmbedded struct {
|
|
On bool `cfg:"on" default:"true"`
|
|
}
|
|
|
|
type testSection struct {
|
|
testEmbedded
|
|
Str string `cfg:"str" default:"hello"`
|
|
Int int `cfg:"int" default:"-42"`
|
|
Int8 int8 `cfg:"int8"`
|
|
Uint uint `cfg:"uint" default:"7"`
|
|
Uint16 uint16 `cfg:"uint16"`
|
|
Float float64 `cfg:"float" default:"1.5"`
|
|
Float32 float32 `cfg:"float32"`
|
|
Dur time.Duration `cfg:"dur" default:"90s"`
|
|
When time.Time `cfg:"when"`
|
|
Slice []string `cfg:"slice"`
|
|
Inner testInner `cfg:"inner"`
|
|
NoTag string
|
|
Skipped string `cfg:"-"`
|
|
Empty string `cfg:""`
|
|
unexport string `cfg:"unexported"` //nolint:unused // checks unexported fields are ignored
|
|
}
|
|
|
|
type testValidated struct {
|
|
Value string `cfg:"value"`
|
|
}
|
|
|
|
var errRejected = errors.New("rejected")
|
|
|
|
func (c *testValidated) validate(key string, value any) error {
|
|
if key == "value" && value.(string) == "bad" {
|
|
return errRejected
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestKeysOf(t *testing.T) {
|
|
got := keysOf(&testSection{})
|
|
want := []string{"on", "str", "int", "int8", "uint", "uint16", "float",
|
|
"float32", "dur", "when", "slice", "inner"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("keysOf = %v; want %v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestFieldsOfSkipsUntagged(t *testing.T) {
|
|
for _, f := range fieldsOf(reflect.TypeOf(testSection{})) {
|
|
switch f.key {
|
|
case "", "-", "unexported":
|
|
t.Errorf("fieldsOf included field with key %q", f.key)
|
|
}
|
|
if f.key == "when" && f.section {
|
|
t.Error("time.Time field treated as a section")
|
|
}
|
|
if f.key == "inner" && !f.section {
|
|
t.Error("struct field not treated as a section")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMapOf(t *testing.T) {
|
|
var s testSection
|
|
if err := applyDefaults(&s); err != nil {
|
|
t.Fatalf("applyDefaults: %v", err)
|
|
}
|
|
m := mapOf(s)
|
|
if got := m["str"]; got != "hello" {
|
|
t.Errorf("m[str] = %#v", got)
|
|
}
|
|
if got := m["on"]; got != true {
|
|
t.Errorf("m[on] (promoted) = %#v", got)
|
|
}
|
|
if got := m["dur"]; got != "1m30s" {
|
|
t.Errorf("m[dur] = %#v; want the duration as a string", got)
|
|
}
|
|
inner, ok := m["inner"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("m[inner] = %#v; want nested map", m["inner"])
|
|
}
|
|
if inner["name"] != "inner" {
|
|
t.Errorf("m[inner][name] = %#v", inner["name"])
|
|
}
|
|
if _, ok := m["inner.name"]; ok {
|
|
t.Error("mapOf produced a flattened key")
|
|
}
|
|
// Pointer and value give the same result.
|
|
if !reflect.DeepEqual(m, mapOf(&s)) {
|
|
t.Error("mapOf(&s) != mapOf(s)")
|
|
}
|
|
}
|
|
|
|
func TestFlattenOfPrefix(t *testing.T) {
|
|
m := flattenOf("p", testSection{Dur: 90 * time.Second})
|
|
for _, k := range []string{"p.str", "p.on", "p.inner.name"} {
|
|
if _, ok := m[k]; !ok {
|
|
t.Errorf("flattenOf missing %q in %v", k, m)
|
|
}
|
|
}
|
|
if got := m["p.dur"]; got != "1m30s" {
|
|
t.Errorf("m[p.dur] = %#v; want the duration as a string", got)
|
|
}
|
|
}
|
|
|
|
func TestStructValuePanics(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Error("structValue(int) did not panic")
|
|
}
|
|
}()
|
|
structValue(42)
|
|
}
|
|
|
|
func TestApplyDefaults(t *testing.T) {
|
|
var s testSection
|
|
if err := applyDefaults(&s); err != nil {
|
|
t.Fatalf("applyDefaults: %v", err)
|
|
}
|
|
want := testSection{
|
|
testEmbedded: testEmbedded{On: true},
|
|
Str: "hello",
|
|
Int: -42,
|
|
Uint: 7,
|
|
Float: 1.5,
|
|
Dur: 90 * time.Second,
|
|
Inner: testInner{Name: "inner"},
|
|
}
|
|
if !reflect.DeepEqual(s, want) {
|
|
t.Errorf("applyDefaults = %+v; want %+v", s, want)
|
|
}
|
|
}
|
|
|
|
func TestApplyDefaultsInvalid(t *testing.T) {
|
|
var bad struct {
|
|
N int `cfg:"n" default:"nope"`
|
|
}
|
|
err := applyDefaults(&bad)
|
|
if err == nil || !strings.Contains(err.Error(), "invalid default for n") {
|
|
t.Errorf("applyDefaults = %v; want invalid default error", err)
|
|
}
|
|
|
|
var badNested struct {
|
|
Sub struct {
|
|
D time.Duration `cfg:"d" default:"forever"`
|
|
} `cfg:"sub"`
|
|
}
|
|
err = applyDefaults(&badNested)
|
|
if err == nil || !strings.Contains(err.Error(), "sub.d") {
|
|
t.Errorf("applyDefaults nested = %v; want error naming sub.d", err)
|
|
}
|
|
}
|
|
|
|
func TestTypeOf(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
want reflect.Kind
|
|
}{
|
|
{"str", reflect.String},
|
|
{"int", reflect.Int},
|
|
{"uint", reflect.Uint},
|
|
{"float", reflect.Float64},
|
|
{"on", reflect.Bool},
|
|
{"dur", reflect.String}, // durations are read as strings
|
|
{"when", reflect.Struct},
|
|
{"slice", reflect.Slice},
|
|
{"inner.name", reflect.String},
|
|
}
|
|
for _, tt := range tests {
|
|
got, err := typeOf(testSection{}, tt.key)
|
|
if err != nil || got != tt.want {
|
|
t.Errorf("typeOf(%q) = %v, %v; want %v", tt.key, got, err, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnknownKeys(t *testing.T) {
|
|
keys := []string{
|
|
"missing",
|
|
"inner", // section without sub-key
|
|
"str.sub", // value with a sub-key
|
|
"inner.missing", // unknown nested key
|
|
"NoTag", // untagged field
|
|
"unexported",
|
|
"",
|
|
}
|
|
var s testSection
|
|
for _, key := range keys {
|
|
if k, err := typeOf(s, key); !errors.Is(err, errUnknownKey) || k != reflect.Invalid {
|
|
t.Errorf("typeOf(%q) = %v, %v; want Invalid, errUnknownKey", key, k, err)
|
|
}
|
|
if v, err := valueOf(s, key); !errors.Is(err, errUnknownKey) || v != nil {
|
|
t.Errorf("valueOf(%q) = %v, %v; want nil, errUnknownKey", key, v, err)
|
|
}
|
|
if err := setValue(&s, key, "x"); !errors.Is(err, errUnknownKey) {
|
|
t.Errorf("setValue(%q) = %v; want errUnknownKey", key, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetValue(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
value any
|
|
want any
|
|
}{
|
|
// strings are parsed
|
|
{"str", "world", "world"},
|
|
{"on", "false", false},
|
|
{"int", "123", 123},
|
|
{"int8", "-128", int8(-128)},
|
|
{"uint", "9", uint(9)},
|
|
{"uint16", "65535", uint16(65535)},
|
|
{"float", "2.25", 2.25},
|
|
{"float32", "0.5", float32(0.5)},
|
|
{"dur", "1h30m", "1h30m0s"}, // durations are given out as strings
|
|
{"inner.name", "nested", "nested"},
|
|
// values of the field's type are used as-is
|
|
{"int", 5, 5},
|
|
{"on", true, true},
|
|
{"dur", 3 * time.Second, "3s"},
|
|
{"slice", []string{"a", "b"}, []string{"a", "b"}},
|
|
{"when", time.Unix(0, 0).UTC(), time.Unix(0, 0).UTC()},
|
|
}
|
|
for _, tt := range tests {
|
|
var s testSection
|
|
if err := setValue(&s, tt.key, tt.value); err != nil {
|
|
t.Errorf("setValue(%q, %#v): %v", tt.key, tt.value, err)
|
|
continue
|
|
}
|
|
got, err := valueOf(s, tt.key)
|
|
if err != nil || !reflect.DeepEqual(got, tt.want) {
|
|
t.Errorf("after setValue(%q, %#v): valueOf = %#v, %v; want %#v",
|
|
tt.key, tt.value, got, err, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetValueErrors(t *testing.T) {
|
|
tests := []struct {
|
|
key string
|
|
value any
|
|
want string
|
|
}{
|
|
{"int", "abc", "unable to convert"},
|
|
{"int8", "200", "unable to convert"}, // out of range
|
|
{"uint", "-1", "unable to convert"},
|
|
{"float", "x", "unable to convert"},
|
|
{"on", "maybe", "unable to convert"},
|
|
{"dur", "soon", "unable to convert to duration"},
|
|
{"slice", "a,b", "unsupported type string"},
|
|
{"when", "2024-01-01", "unsupported type string"},
|
|
{"int", 1.5, "unsupported type float64"},
|
|
{"int", int64(1), "unsupported type int64"},
|
|
{"str", nil, "unsupported type <nil>"},
|
|
}
|
|
for _, tt := range tests {
|
|
var s testSection
|
|
before := s
|
|
err := setValue(&s, tt.key, tt.value)
|
|
if err == nil || !strings.Contains(err.Error(), tt.want) {
|
|
t.Errorf("setValue(%q, %#v) = %v; want error containing %q",
|
|
tt.key, tt.value, err, tt.want)
|
|
}
|
|
if !reflect.DeepEqual(s, before) {
|
|
t.Errorf("setValue(%q, %#v) modified the section on error", tt.key, tt.value)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetValueValidator(t *testing.T) {
|
|
var s testValidated
|
|
if err := setValue(&s, "value", "good"); err != nil || s.Value != "good" {
|
|
t.Errorf("setValue(good) = %v, Value = %q", err, s.Value)
|
|
}
|
|
if err := setValue(&s, "value", "bad"); !errors.Is(err, errRejected) {
|
|
t.Errorf("setValue(bad) = %v; want errRejected", err)
|
|
}
|
|
if s.Value != "good" {
|
|
t.Errorf("rejected value was stored: %q", s.Value)
|
|
}
|
|
|
|
// The validator of the section holding the field is used for nested keys.
|
|
var outer struct {
|
|
V testValidated `cfg:"v"`
|
|
}
|
|
if err := setValue(&outer, "v.value", "bad"); !errors.Is(err, errRejected) {
|
|
t.Errorf("nested setValue(bad) = %v; want errRejected", err)
|
|
}
|
|
}
|
|
|
|
// parseString handles string fields too, although convert assigns a string
|
|
// to a string field before reaching it.
|
|
func TestParseStringString(t *testing.T) {
|
|
v, err := parseString("text", reflect.TypeOf(""))
|
|
if err != nil || v.Interface() != "text" {
|
|
t.Errorf("parseString(text, string) = %v, %v", v, err)
|
|
}
|
|
}
|