mirror of
https://github.com/rclone/rclone.git
synced 2026-07-11 16:25:42 -04:00
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.
353 lines
10 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|