Files
rclone/fs/config/configstruct/configstruct_test.go
Hakan İSMAİL b36b679202 rc: allow setting rc config and filter options as flat parameters
This commit allows global config options to be set as flat parameters on all rc
commands. This makes the rc commands much more like command line parameters and
will aid understanding of how the rc is used.

It is backwards compatible with the old method using _config and _filter.
2026-06-30 17:04:33 +01:00

353 lines
10 KiB
Go

package configstruct_test
import (
"fmt"
"testing"
"time"
"github.com/rclone/rclone/fs"
"github.com/rclone/rclone/fs/config/configstruct"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type Conf struct {
A string
B string
}
type Conf2 struct {
PotatoPie string `config:"spud_pie"`
BeanStew bool
RaisinRoll int
SausageOnStick int64
ForbiddenFruit uint
CookingTime fs.Duration
TotalWeight fs.SizeSuffix
}
type ConfNested struct {
Conf // embedded struct with no tag
Sub1 Conf `config:"sub"` // member struct with tag
Sub2 Conf2 // member struct without tag
C string // normal item
D fs.Tristate // an embedded struct which we don't want to recurse
}
func TestItemsError(t *testing.T) {
_, err := configstruct.Items(nil)
assert.EqualError(t, err, "argument must be a pointer")
_, err = configstruct.Items(new(int))
assert.EqualError(t, err, "argument must be a pointer to a struct")
}
// Check each item has a Set function pointer then clear it for the assert.Equal
func cleanItems(t *testing.T, items []configstruct.Item) []configstruct.Item {
for i := range items {
item := &items[i]
assert.NotNil(t, item.Set)
item.Set = nil
}
return items
}
func TestItems(t *testing.T) {
in := &Conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
}
got, err := configstruct.Items(in)
require.NoError(t, err)
want := []configstruct.Item{
{Name: "spud_pie", Field: "PotatoPie", Value: string("yum")},
{Name: "bean_stew", Field: "BeanStew", Value: true},
{Name: "raisin_roll", Field: "RaisinRoll", Value: int(42)},
{Name: "sausage_on_stick", Field: "SausageOnStick", Value: int64(101)},
{Name: "forbidden_fruit", Field: "ForbiddenFruit", Value: uint(6)},
{Name: "cooking_time", Field: "CookingTime", Value: fs.Duration(42 * time.Second)},
{Name: "total_weight", Field: "TotalWeight", Value: fs.SizeSuffix(17 << 20)},
}
assert.Equal(t, want, cleanItems(t, got))
}
func TestItemsNested(t *testing.T) {
in := ConfNested{
Conf: Conf{
A: "1",
B: "2",
},
Sub1: Conf{
A: "3",
B: "4",
},
Sub2: Conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
},
C: "normal",
D: fs.Tristate{Value: true, Valid: true},
}
got, err := configstruct.Items(&in)
require.NoError(t, err)
want := []configstruct.Item{
{Name: "a", Field: "Conf.A", Value: string("1")},
{Name: "b", Field: "Conf.B", Value: string("2")},
{Name: "sub_a", Field: "Sub1.A", Value: string("3")},
{Name: "sub_b", Field: "Sub1.B", Value: string("4")},
{Name: "spud_pie", Field: "Sub2.PotatoPie", Value: string("yum")},
{Name: "bean_stew", Field: "Sub2.BeanStew", Value: true},
{Name: "raisin_roll", Field: "Sub2.RaisinRoll", Value: int(42)},
{Name: "sausage_on_stick", Field: "Sub2.SausageOnStick", Value: int64(101)},
{Name: "forbidden_fruit", Field: "Sub2.ForbiddenFruit", Value: uint(6)},
{Name: "cooking_time", Field: "Sub2.CookingTime", Value: fs.Duration(42 * time.Second)},
{Name: "total_weight", Field: "Sub2.TotalWeight", Value: fs.SizeSuffix(17 << 20)},
{Name: "c", Field: "C", Value: string("normal")},
{Name: "d", Field: "D", Value: fs.Tristate{Value: true, Valid: true}},
}
assert.Equal(t, want, cleanItems(t, got))
}
func TestSetBasics(t *testing.T) {
c := &Conf{A: "one", B: "two"}
err := configstruct.Set(configMap{}, c)
require.NoError(t, err)
assert.Equal(t, &Conf{A: "one", B: "two"}, c)
}
// a simple configmap.Getter for testing
type configMap map[string]string
// Get the value
func (c configMap) Get(key string) (value string, ok bool) {
value, ok = c[key]
return value, ok
}
func TestSetMore(t *testing.T) {
c := &Conf{A: "one", B: "two"}
m := configMap{
"a": "ONE",
}
err := configstruct.Set(m, c)
require.NoError(t, err)
assert.Equal(t, &Conf{A: "ONE", B: "two"}, c)
}
func TestSetFull(t *testing.T) {
in := &Conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
}
m := configMap{
"spud_pie": "YUM",
"bean_stew": "FALSE",
"raisin_roll": "43 ",
"sausage_on_stick": " 102 ",
"forbidden_fruit": "0x7",
"cooking_time": "43s",
"total_weight": "18M",
}
want := &Conf2{
PotatoPie: "YUM",
BeanStew: false,
RaisinRoll: 43,
SausageOnStick: 102,
ForbiddenFruit: 7,
CookingTime: fs.Duration(43 * time.Second),
TotalWeight: fs.SizeSuffix(18 << 20),
}
err := configstruct.Set(m, in)
require.NoError(t, err)
assert.Equal(t, want, in)
}
func TestSetAnyFull(t *testing.T) {
in := &Conf2{
PotatoPie: "yum",
BeanStew: true,
RaisinRoll: 42,
SausageOnStick: 101,
ForbiddenFruit: 6,
CookingTime: fs.Duration(42 * time.Second),
TotalWeight: fs.SizeSuffix(17 << 20),
}
m := map[string]any{
"spud_pie": "YUM",
"bean_stew": false,
"raisin_roll": "43 ",
"sausage_on_stick": " 102 ",
"forbidden_fruit": "0x7",
"cooking_time": 43 * time.Second,
"total_weight": "18M",
}
want := &Conf2{
PotatoPie: "YUM",
BeanStew: false,
RaisinRoll: 43,
SausageOnStick: 102,
ForbiddenFruit: 7,
CookingTime: fs.Duration(43 * time.Second),
TotalWeight: fs.SizeSuffix(18 << 20),
}
err := configstruct.SetAny(m, in)
require.NoError(t, err)
assert.Equal(t, want, in)
}
func TestSetAnyWithSlice(t *testing.T) {
type ConfWithSlice struct {
ExcludeRule []string `config:"exclude"`
}
// 1. Test []any (JSON array of strings)
in := &ConfWithSlice{
ExcludeRule: []string{"original"},
}
m := map[string]any{
"exclude": []any{"foo", "bar"},
}
err := configstruct.SetAny(m, in)
require.NoError(t, err)
assert.Equal(t, []string{"foo", "bar"}, in.ExcludeRule)
// 2. Test []string directly
in2 := &ConfWithSlice{
ExcludeRule: []string{"original"},
}
m2 := map[string]any{
"exclude": []string{"hello", "world"},
}
err = configstruct.SetAny(m2, in2)
require.NoError(t, err)
assert.Equal(t, []string{"hello", "world"}, in2.ExcludeRule)
// 3. Test []any with mixed convertible types (e.g. ints/floats/bools)
in3 := &ConfWithSlice{
ExcludeRule: []string{"original"},
}
m3 := map[string]any{
"exclude": []any{"foo", 123, true, 45.6},
}
err = configstruct.SetAny(m3, in3)
require.NoError(t, err)
assert.Equal(t, []string{"foo", "123", "true", "45.6"}, in3.ExcludeRule)
}
func TestStringToInterface(t *testing.T) {
item := struct{ A int }{2}
for _, test := range []struct {
in string
def any
want any
err string
}{
{"", string(""), "", ""},
{" string ", string(""), " string ", ""},
{"123", int(0), int(123), ""},
{"0x123", int(0), int(0x123), ""},
{" 0x123 ", int(0), int(0x123), ""},
{"-123", int(0), int(-123), ""},
{"0", false, false, ""},
{"1", false, true, ""},
{"7", false, true, `parsing "7" as bool failed: strconv.ParseBool: parsing "7": invalid syntax`},
{"FALSE", false, false, ""},
{"true", false, true, ""},
{"123", uint(0), uint(123), ""},
{"123", int64(0), int64(123), ""},
{"123x", int64(0), nil, "parsing \"123x\" as int64 failed: expected newline"},
{"truth", false, nil, "parsing \"truth\" as bool failed: strconv.ParseBool: parsing \"truth\": invalid syntax"},
{"struct", item, nil, "parsing \"struct\" as struct { A int } failed: don't know how to parse this type"},
{"1s", fs.Duration(0), fs.Duration(time.Second), ""},
{"1m1s", fs.Duration(0), fs.Duration(61 * time.Second), ""},
{"1potato", fs.Duration(0), nil, `parsing "1potato" as fs.Duration failed: parsing time "1potato" as "2006-01-02": cannot parse "1potato" as "2006"`},
{``, []string{}, []string{}, ""},
{`""`, []string(nil), []string{""}, ""},
{`hello`, []string{}, []string{"hello"}, ""},
{`"hello"`, []string{}, []string{"hello"}, ""},
{`hello,world!`, []string(nil), []string{"hello", "world!"}, ""},
{`"hello","world!"`, []string(nil), []string{"hello", "world!"}, ""},
{"1s", time.Duration(0), time.Second, ""},
{"1m1s", time.Duration(0), 61 * time.Second, ""},
{"1potato", time.Duration(0), nil, `parsing "1potato" as time.Duration failed: time: unknown unit "potato" in duration "1potato"`},
{"1M", fs.SizeSuffix(0), fs.Mebi, ""},
{"1G", fs.SizeSuffix(0), fs.Gibi, ""},
{"1potato", fs.SizeSuffix(0), nil, `parsing "1potato" as fs.SizeSuffix failed: bad suffix 'o'`},
} {
what := fmt.Sprintf("parse %q as %T", test.in, test.def)
got, err := configstruct.StringToInterface(test.def, test.in)
if test.err == "" {
require.NoError(t, err, what)
assert.Equal(t, test.want, got, what)
} else {
assert.Nil(t, got, what)
assert.EqualError(t, err, test.err, what)
}
}
}
func TestInterfaceToString(t *testing.T) {
item := struct{ A int }{2}
for _, test := range []struct {
in any
want string
err string
}{
{nil, "", "interpreting <nil> as string failed: don't know how to convert this"},
{"", "", ""},
{" string ", " string ", ""},
{int(123), "123", ""},
{int(0x123), "291", ""},
{int(-123), "-123", ""},
{false, "false", ""},
{true, "true", ""},
{uint(123), "123", ""},
{int64(123), "123", ""},
{item, "", "interpreting struct { A int } as string failed: don't know how to convert this"},
{fs.Duration(time.Second), "1s", ""},
{fs.Duration(61 * time.Second), "1m1s", ""},
{[]string{}, ``, ""},
{[]string{""}, `""`, ""},
{[]string{"", ""}, `,`, ""},
{[]string{"hello"}, `hello`, ""},
{[]string{"hello", "world"}, `hello,world`, ""},
{[]string{"hello", "", "world"}, `hello,,world`, ""},
{[]string{`hello, world`, `goodbye, world!`}, `"hello, world","goodbye, world!"`, ""},
{[]any{"hello", "world"}, `hello,world`, ""},
{[]any{123, 456}, `123,456`, ""},
{[]any{"hello", 45.6, true}, `hello,45.6,true`, ""},
{[2]string{"a", "b"}, `a,b`, ""},
{[2]any{"x", 12}, `x,12`, ""},
{fs.SpaceSepList{"a", "b"}, `a b`, ""},
{fs.CommaSepList{"x", "y"}, `x,y`, ""},
{time.Second, "1s", ""},
{61 * time.Second, "1m1s", ""},
{fs.Mebi, "1Mi", ""},
{fs.Gibi, "1Gi", ""},
} {
what := fmt.Sprintf("interpret %#v as string", test.in)
got, err := configstruct.InterfaceToString(test.in)
if test.err == "" {
require.NoError(t, err, what)
assert.Equal(t, test.want, got, what)
} else {
assert.Equal(t, "", got)
assert.EqualError(t, err, test.err, what)
}
}
}