summaryrefslogtreecommitdiffstats
path: root/decode_test.go
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:02:14 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 17:02:14 +0000
commite61c5fa98419989a61b5ca4eb7749acbd37e0af6 (patch)
treeffe04b0283921bd40489aaa74dcee1f68b7b6b35 /decode_test.go
parentInitial commit. (diff)
downloadgolang-toml-e61c5fa98419989a61b5ca4eb7749acbd37e0af6.tar.xz
golang-toml-e61c5fa98419989a61b5ca4eb7749acbd37e0af6.zip
Adding upstream version 1.3.2.upstream/1.3.2upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'decode_test.go')
-rw-r--r--decode_test.go1238
1 files changed, 1238 insertions, 0 deletions
diff --git a/decode_test.go b/decode_test.go
new file mode 100644
index 0000000..6f08d3a
--- /dev/null
+++ b/decode_test.go
@@ -0,0 +1,1238 @@
+package toml
+
+import (
+ "bytes"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "math"
+ "os"
+ "reflect"
+ "strconv"
+ "strings"
+ "sync"
+ "testing"
+ "time"
+
+ "github.com/BurntSushi/toml/internal"
+)
+
+func WithTomlNext(f func()) {
+ os.Setenv("BURNTSUSHI_TOML_110", "")
+ defer func() { os.Unsetenv("BURNTSUSHI_TOML_110") }()
+ f()
+}
+
+func TestDecodeReader(t *testing.T) {
+ var i struct{ A int }
+ meta, err := DecodeReader(strings.NewReader("a = 42"), &i)
+ if err != nil {
+ t.Fatal(err)
+ }
+ have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+ want := "{42} [a] Integer"
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+func TestDecodeFile(t *testing.T) {
+ tmp, err := ioutil.TempFile("", "toml-")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.Remove(tmp.Name())
+ if _, err := tmp.WriteString("a = 42"); err != nil {
+ t.Fatal(err)
+ }
+ if err := tmp.Close(); err != nil {
+ t.Fatal(err)
+ }
+
+ var i struct{ A int }
+ meta, err := DecodeFile(tmp.Name(), &i)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%v %v %v", i, meta.Keys(), meta.Type("a"))
+ want := "{42} [a] Integer"
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+func TestDecodeBOM(t *testing.T) {
+ for _, tt := range [][]byte{
+ []byte("\xff\xfea = \"b\""),
+ []byte("\xfe\xffa = \"b\""),
+ []byte("\xef\xbb\xbfa = \"b\""),
+ } {
+ t.Run("", func(t *testing.T) {
+ var s struct{ A string }
+ _, err := Decode(string(tt), &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if s.A != "b" {
+ t.Errorf(`s.A is not "b" but %q`, s.A)
+ }
+ })
+ }
+}
+
+func TestDecodeEmbedded(t *testing.T) {
+ type Dog struct{ Name string }
+ type Age int
+ type cat struct{ Name string }
+
+ for _, test := range []struct {
+ label string
+ input string
+ decodeInto interface{}
+ wantDecoded interface{}
+ }{
+ {
+ label: "embedded struct",
+ input: `Name = "milton"`,
+ decodeInto: &struct{ Dog }{},
+ wantDecoded: &struct{ Dog }{Dog{"milton"}},
+ },
+ {
+ label: "embedded non-nil pointer to struct",
+ input: `Name = "milton"`,
+ decodeInto: &struct{ *Dog }{},
+ wantDecoded: &struct{ *Dog }{&Dog{"milton"}},
+ },
+ {
+ label: "embedded nil pointer to struct",
+ input: ``,
+ decodeInto: &struct{ *Dog }{},
+ wantDecoded: &struct{ *Dog }{nil},
+ },
+ {
+ label: "unexported embedded struct",
+ input: `Name = "socks"`,
+ decodeInto: &struct{ cat }{},
+ wantDecoded: &struct{ cat }{cat{"socks"}},
+ },
+ {
+ label: "embedded int",
+ input: `Age = -5`,
+ decodeInto: &struct{ Age }{},
+ wantDecoded: &struct{ Age }{-5},
+ },
+ } {
+ _, err := Decode(test.input, test.decodeInto)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(test.wantDecoded, test.decodeInto) {
+ t.Errorf("%s: want decoded == %+v, got %+v",
+ test.label, test.wantDecoded, test.decodeInto)
+ }
+ }
+}
+
+func TestDecodeErrors(t *testing.T) {
+ tests := []struct {
+ s interface{}
+ toml string
+ wantErr string
+ }{
+ {
+ &struct{ V int8 }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): 999 is out of range for int8`,
+ },
+ {
+ &struct{ V float32 }{},
+ `V = 999999999999999`,
+ `toml: line 1 (last key "V"): 999999999999999 is out of range for float32`,
+ },
+ {
+ &struct{ V string }{},
+ `V = 5`,
+ `toml: line 1 (last key "V"): incompatible types: TOML value has type int64; destination has type string`,
+ },
+ {
+ &struct{ V interface{ ASD() } }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): unsupported type interface { ASD() }`,
+ },
+ {
+ &struct{ V struct{ V int } }{},
+ `V = 999`,
+ `toml: line 1 (last key "V"): type mismatch for struct { V int }: expected table but found int64`,
+ },
+ {
+ &struct{ V [1]int }{},
+ `V = [1,2,3]`,
+ `toml: line 1 (last key "V"): expected array length 1; got TOML array of length 3`,
+ },
+ {
+ &struct{ V struct{ N int8 } }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): 999 is out of range for int8`,
+ },
+ {
+ &struct{ V struct{ N float32 } }{},
+ `V.N = 999999999999999`,
+ `toml: line 1 (last key "V.N"): 999999999999999 is out of range for float32`,
+ },
+ {
+ &struct{ V struct{ N string } }{},
+ `V.N = 5`,
+ `toml: line 1 (last key "V.N"): incompatible types: TOML value has type int64; destination has type string`,
+ },
+ {
+ &struct {
+ V struct{ N interface{ ASD() } }
+ }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): unsupported type interface { ASD() }`,
+ },
+ {
+ &struct{ V struct{ N struct{ V int } } }{},
+ `V.N = 999`,
+ `toml: line 1 (last key "V.N"): type mismatch for struct { V int }: expected table but found int64`,
+ },
+ {
+ &struct{ V struct{ N [1]int } }{},
+ `V.N = [1,2,3]`,
+ `toml: line 1 (last key "V.N"): expected array length 1; got TOML array of length 3`,
+ },
+ }
+
+ for _, tt := range tests {
+ _, err := Decode(tt.toml, tt.s)
+ if err == nil {
+ t.Fatal("err is nil")
+ }
+ if err.Error() != tt.wantErr {
+ t.Errorf("\nhave: %q\nwant: %q", err, tt.wantErr)
+ }
+ }
+}
+
+func TestDecodeIgnoreFields(t *testing.T) {
+ const input = `
+Number = 123
+- = 234
+`
+ var s struct {
+ Number int `toml:"-"`
+ }
+ if _, err := Decode(input, &s); err != nil {
+ t.Fatal(err)
+ }
+ if s.Number != 0 {
+ t.Errorf("got: %d; want 0", s.Number)
+ }
+}
+
+func TestDecodeTableArrays(t *testing.T) {
+ var tomlTableArrays = `
+[[albums]]
+name = "Born to Run"
+
+ [[albums.songs]]
+ name = "Jungleland"
+
+ [[albums.songs]]
+ name = "Meeting Across the River"
+
+[[albums]]
+name = "Born in the USA"
+
+ [[albums.songs]]
+ name = "Glory Days"
+
+ [[albums.songs]]
+ name = "Dancing in the Dark"
+`
+
+ type Song struct {
+ Name string
+ }
+
+ type Album struct {
+ Name string
+ Songs []Song
+ }
+
+ type Music struct {
+ Albums []Album
+ }
+
+ expected := Music{[]Album{
+ {"Born to Run", []Song{{"Jungleland"}, {"Meeting Across the River"}}},
+ {"Born in the USA", []Song{{"Glory Days"}, {"Dancing in the Dark"}}},
+ }}
+ var got Music
+ if _, err := Decode(tomlTableArrays, &got); err != nil {
+ t.Fatal(err)
+ }
+ if !reflect.DeepEqual(expected, got) {
+ t.Fatalf("\n%#v\n!=\n%#v\n", expected, got)
+ }
+}
+
+func TestDecodePointers(t *testing.T) {
+ type Object struct {
+ Type string
+ Description string
+ }
+
+ type Dict struct {
+ NamedObject map[string]*Object
+ BaseObject *Object
+ Strptr *string
+ Strptrs []*string
+ }
+ s1, s2, s3 := "blah", "abc", "def"
+ expected := &Dict{
+ Strptr: &s1,
+ Strptrs: []*string{&s2, &s3},
+ NamedObject: map[string]*Object{
+ "foo": {"FOO", "fooooo!!!"},
+ "bar": {"BAR", "ba-ba-ba-ba-barrrr!!!"},
+ },
+ BaseObject: &Object{"BASE", "da base"},
+ }
+
+ ex1 := `
+Strptr = "blah"
+Strptrs = ["abc", "def"]
+
+[NamedObject.foo]
+Type = "FOO"
+Description = "fooooo!!!"
+
+[NamedObject.bar]
+Type = "BAR"
+Description = "ba-ba-ba-ba-barrrr!!!"
+
+[BaseObject]
+Type = "BASE"
+Description = "da base"
+`
+ dict := new(Dict)
+ _, err := Decode(ex1, dict)
+ if err != nil {
+ t.Errorf("Decode error: %v", err)
+ }
+ if !reflect.DeepEqual(expected, dict) {
+ t.Fatalf("\n%#v\n!=\n%#v\n", expected, dict)
+ }
+}
+
+func TestDecodeBadDatetime(t *testing.T) {
+ var x struct{ T time.Time }
+ for _, s := range []string{"123", "1230"} {
+ input := "T = " + s
+ if _, err := Decode(input, &x); err == nil {
+ t.Errorf("Expected invalid DateTime error for %q", s)
+ }
+ }
+}
+
+type sphere struct {
+ Center [3]float64
+ Radius float64
+}
+
+func TestDecodeArrayWrongSize(t *testing.T) {
+ var s1 sphere
+ if _, err := Decode(`center = [0.1, 2.3]`, &s1); err == nil {
+ t.Fatal("Expected array type mismatch error")
+ }
+}
+
+func TestDecodeIntOverflow(t *testing.T) {
+ type table struct {
+ Value int8
+ }
+ var tab table
+ if _, err := Decode(`value = 500`, &tab); err == nil {
+ t.Fatal("Expected integer out-of-bounds error.")
+ }
+}
+
+func TestDecodeFloatOverflow(t *testing.T) {
+ tests := []struct {
+ value string
+ overflow bool
+ }{
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat64), true},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat64), true},
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat32*1.1), true},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat32*1.1), true},
+ {fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int+1), true},
+ {fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int-1), true},
+ {fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int+1), true},
+ {fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int-1), true},
+
+ {fmt.Sprintf(`F32 = %f`, math.MaxFloat32), false},
+ {fmt.Sprintf(`F32 = %f`, -math.MaxFloat32), false},
+ {fmt.Sprintf(`F32 = %d`, maxSafeFloat32Int), false},
+ {fmt.Sprintf(`F32 = %d`, -maxSafeFloat32Int), false},
+ {fmt.Sprintf(`F64 = %f`, math.MaxFloat64), false},
+ {fmt.Sprintf(`F64 = %f`, -math.MaxFloat64), false},
+ {fmt.Sprintf(`F64 = %f`, math.MaxFloat32), false},
+ {fmt.Sprintf(`F64 = %f`, -math.MaxFloat32), false},
+ {fmt.Sprintf(`F64 = %d`, maxSafeFloat64Int), false},
+ {fmt.Sprintf(`F64 = %d`, -maxSafeFloat64Int), false},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var tab struct {
+ F32 float32
+ F64 float64
+ }
+ _, err := Decode(tt.value, &tab)
+
+ if tt.overflow && err == nil {
+ t.Fatal("expected error, but err is nil")
+ }
+ if (tt.overflow && !errorContains(err, "out of range")) || (!tt.overflow && err != nil) {
+ t.Fatalf("unexpected error:\n%v", err)
+ }
+ })
+ }
+}
+
+func TestDecodeSizedInts(t *testing.T) {
+ type table struct {
+ U8 uint8
+ U16 uint16
+ U32 uint32
+ U64 uint64
+ U uint
+ I8 int8
+ I16 int16
+ I32 int32
+ I64 int64
+ I int
+ }
+ answer := table{1, 1, 1, 1, 1, -1, -1, -1, -1, -1}
+ toml := `
+ u8 = 1
+ u16 = 1
+ u32 = 1
+ u64 = 1
+ u = 1
+ i8 = -1
+ i16 = -1
+ i32 = -1
+ i64 = -1
+ i = -1
+ `
+ var tab table
+ if _, err := Decode(toml, &tab); err != nil {
+ t.Fatal(err.Error())
+ }
+ if answer != tab {
+ t.Fatalf("Expected %#v but got %#v", answer, tab)
+ }
+}
+
+type NopUnmarshalTOML int
+
+func (n *NopUnmarshalTOML) UnmarshalTOML(p interface{}) error {
+ *n = 42
+ return nil
+}
+
+func TestDecodeTypes(t *testing.T) {
+ type (
+ mystr string
+ myiface interface{}
+ )
+
+ for _, tt := range []struct {
+ v interface{}
+ want string
+ wantErr string
+ }{
+ {new(map[string]bool), "&map[F:true]", ""},
+ {new(map[mystr]bool), "&map[F:true]", ""},
+ {new(NopUnmarshalTOML), "42", ""},
+ {new(map[interface{}]bool), "&map[F:true]", ""},
+ {new(map[myiface]bool), "&map[F:true]", ""},
+
+ {3, "", `toml: cannot decode to non-pointer "int"`},
+ {map[string]interface{}{}, "", `toml: cannot decode to non-pointer "map[string]interface {}"`},
+
+ {(*int)(nil), "", `toml: cannot decode to nil value of "*int"`},
+ {(*Unmarshaler)(nil), "", `toml: cannot decode to nil value of "*toml.Unmarshaler"`},
+ {nil, "", `toml: cannot decode to non-pointer <nil>`},
+
+ {new(map[int]string), "", "toml: cannot decode to a map with non-string key type"},
+
+ {new(struct{ F int }), "", `toml: line 1 (last key "F"): incompatible types: TOML value has type bool; destination has type integer`},
+ {new(map[string]int), "", `toml: line 1 (last key "F"): incompatible types: TOML value has type bool; destination has type integer`},
+ {new(int), "", `toml: cannot decode to type int`},
+ {new([]int), "", "toml: cannot decode to type []int"},
+ } {
+ t.Run(fmt.Sprintf("%T", tt.v), func(t *testing.T) {
+ _, err := Decode(`F = true`, tt.v)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatalf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr)
+ }
+
+ if err == nil {
+ have := fmt.Sprintf("%v", tt.v)
+ if n, ok := tt.v.(*NopUnmarshalTOML); ok {
+ have = fmt.Sprintf("%v", *n)
+ }
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ }
+ })
+ }
+}
+
+func TestUnmarshaler(t *testing.T) {
+ var tomlBlob = `
+[dishes.hamboogie]
+name = "Hamboogie with fries"
+price = 10.99
+
+[[dishes.hamboogie.ingredients]]
+name = "Bread Bun"
+
+[[dishes.hamboogie.ingredients]]
+name = "Lettuce"
+
+[[dishes.hamboogie.ingredients]]
+name = "Real Beef Patty"
+
+[[dishes.hamboogie.ingredients]]
+name = "Tomato"
+
+[dishes.eggsalad]
+name = "Egg Salad with rice"
+price = 3.99
+
+[[dishes.eggsalad.ingredients]]
+name = "Egg"
+
+[[dishes.eggsalad.ingredients]]
+name = "Mayo"
+
+[[dishes.eggsalad.ingredients]]
+name = "Rice"
+`
+ m := &menu{}
+ if _, err := Decode(tomlBlob, m); err != nil {
+ t.Fatal(err)
+ }
+
+ if len(m.Dishes) != 2 {
+ t.Log("two dishes should be loaded with UnmarshalTOML()")
+ t.Errorf("expected %d but got %d", 2, len(m.Dishes))
+ }
+
+ eggSalad := m.Dishes["eggsalad"]
+ if _, ok := interface{}(eggSalad).(dish); !ok {
+ t.Errorf("expected a dish")
+ }
+
+ if eggSalad.Name != "Egg Salad with rice" {
+ t.Errorf("expected the dish to be named 'Egg Salad with rice'")
+ }
+
+ if len(eggSalad.Ingredients) != 3 {
+ t.Log("dish should be loaded with UnmarshalTOML()")
+ t.Errorf("expected %d but got %d", 3, len(eggSalad.Ingredients))
+ }
+
+ found := false
+ for _, i := range eggSalad.Ingredients {
+ if i.Name == "Rice" {
+ found = true
+ break
+ }
+ }
+ if !found {
+ t.Error("Rice was not loaded in UnmarshalTOML()")
+ }
+
+ // test on a value - must be passed as *
+ o := menu{}
+ if _, err := Decode(tomlBlob, &o); err != nil {
+ t.Fatal(err)
+ }
+
+}
+
+func TestDecodeInlineTable(t *testing.T) {
+ input := `
+[CookieJar]
+Types = {Chocolate = "yummy", Oatmeal = "best ever"}
+
+[Seasons]
+Locations = {NY = {Temp = "not cold", Rating = 4}, MI = {Temp = "freezing", Rating = 9}}
+`
+ type cookieJar struct {
+ Types map[string]string
+ }
+ type properties struct {
+ Temp string
+ Rating int
+ }
+ type seasons struct {
+ Locations map[string]properties
+ }
+ type wrapper struct {
+ CookieJar cookieJar
+ Seasons seasons
+ }
+ var got wrapper
+
+ meta, err := Decode(input, &got)
+ if err != nil {
+ t.Fatal(err)
+ }
+ want := wrapper{
+ CookieJar: cookieJar{
+ Types: map[string]string{
+ "Chocolate": "yummy",
+ "Oatmeal": "best ever",
+ },
+ },
+ Seasons: seasons{
+ Locations: map[string]properties{
+ "NY": {
+ Temp: "not cold",
+ Rating: 4,
+ },
+ "MI": {
+ Temp: "freezing",
+ Rating: 9,
+ },
+ },
+ },
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("after decode, got:\n\n%#v\n\nwant:\n\n%#v", got, want)
+ }
+ if len(meta.keys) != 12 {
+ t.Errorf("after decode, got %d meta keys; want 12", len(meta.keys))
+ }
+ if len(meta.keyInfo) != 12 {
+ t.Errorf("after decode, got %d meta keyInfo; want 12", len(meta.keyInfo))
+ }
+}
+
+func TestDecodeInlineTableArray(t *testing.T) {
+ type point struct {
+ X, Y, Z int
+ }
+ var got struct {
+ Points []point
+ }
+ // Example inline table array from the spec.
+ const in = `
+points = [ { x = 1, y = 2, z = 3 },
+ { x = 7, y = 8, z = 9 },
+ { x = 2, y = 4, z = 8 } ]
+
+`
+ if _, err := Decode(in, &got); err != nil {
+ t.Fatal(err)
+ }
+ want := []point{
+ {X: 1, Y: 2, Z: 3},
+ {X: 7, Y: 8, Z: 9},
+ {X: 2, Y: 4, Z: 8},
+ }
+ if !reflect.DeepEqual(got.Points, want) {
+ t.Errorf("got %#v; want %#v", got.Points, want)
+ }
+}
+
+type menu struct {
+ Dishes map[string]dish
+}
+
+func (m *menu) UnmarshalTOML(p interface{}) error {
+ m.Dishes = make(map[string]dish)
+ data, _ := p.(map[string]interface{})
+ dishes := data["dishes"].(map[string]interface{})
+ for n, v := range dishes {
+ if d, ok := v.(map[string]interface{}); ok {
+ nd := dish{}
+ nd.UnmarshalTOML(d)
+ m.Dishes[n] = nd
+ } else {
+ return fmt.Errorf("not a dish")
+ }
+ }
+ return nil
+}
+
+type dish struct {
+ Name string
+ Price float32
+ Ingredients []ingredient
+}
+
+func (d *dish) UnmarshalTOML(p interface{}) error {
+ data, _ := p.(map[string]interface{})
+ d.Name, _ = data["name"].(string)
+ d.Price, _ = data["price"].(float32)
+ ingredients, _ := data["ingredients"].([]map[string]interface{})
+ for _, e := range ingredients {
+ n, _ := interface{}(e).(map[string]interface{})
+ name, _ := n["name"].(string)
+ i := ingredient{name}
+ d.Ingredients = append(d.Ingredients, i)
+ }
+ return nil
+}
+
+type ingredient struct {
+ Name string
+}
+
+func TestDecodeSlices(t *testing.T) {
+ type (
+ T struct {
+ Arr []string
+ Tbl map[string]interface{}
+ }
+ M map[string]interface{}
+ )
+ tests := []struct {
+ input string
+ in, want T
+ }{
+ {"",
+ T{}, T{}},
+
+ // Leave existing values alone.
+ {"",
+ T{[]string{}, M{"arr": []string{}}},
+ T{[]string{}, M{"arr": []string{}}}},
+ {"",
+ T{[]string{"a"}, M{"arr": []string{"b"}}},
+ T{[]string{"a"}, M{"arr": []string{"b"}}}},
+
+ // Empty array always allocates (see #339)
+ {`arr = []
+ tbl = {arr = []}`,
+ T{},
+ T{[]string{}, M{"arr": []interface{}{}}}},
+ {`arr = []
+ tbl = {}`,
+ T{[]string{}, M{}},
+ T{[]string{}, M{}}},
+
+ {`arr = []`,
+ T{[]string{"a"}, M{}},
+ T{[]string{}, M{}}},
+
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{[]string{}, M{}},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ {`arr = ["x"]
+ tbl = {arr=["y"]}`,
+ T{[]string{"a", "b"}, M{"arr": []interface{}{"c", "d"}}},
+ T{[]string{"x"}, M{"arr": []interface{}{"y"}}}},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.input, &tt.in)
+ if err != nil {
+ t.Error(err)
+ }
+ if !reflect.DeepEqual(tt.in, tt.want) {
+ t.Errorf("\nhave: %#v\nwant: %#v", tt.in, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodePrimitive(t *testing.T) {
+ type S struct {
+ P Primitive
+ }
+ type T struct {
+ S []int
+ }
+ slicep := func(s []int) *[]int { return &s }
+ arrayp := func(a [2]int) *[2]int { return &a }
+ mapp := func(m map[string]int) *map[string]int { return &m }
+ for i, tt := range []struct {
+ v interface{}
+ input string
+ want interface{}
+ }{
+ // slices
+ {slicep(nil), "", slicep(nil)},
+ {slicep([]int{}), "", slicep([]int{})},
+ {slicep([]int{1, 2, 3}), "", slicep([]int{1, 2, 3})},
+ {slicep(nil), "P = [1,2]", slicep([]int{1, 2})},
+ {slicep([]int{}), "P = [1,2]", slicep([]int{1, 2})},
+ {slicep([]int{1, 2, 3}), "P = [1,2]", slicep([]int{1, 2})},
+
+ // arrays
+ {arrayp([2]int{2, 3}), "", arrayp([2]int{2, 3})},
+ {arrayp([2]int{2, 3}), "P = [3,4]", arrayp([2]int{3, 4})},
+
+ // maps
+ {mapp(nil), "", mapp(nil)},
+ {mapp(map[string]int{}), "", mapp(map[string]int{})},
+ {mapp(map[string]int{"a": 1}), "", mapp(map[string]int{"a": 1})},
+ {mapp(nil), "[P]\na = 2", mapp(map[string]int{"a": 2})},
+ {mapp(map[string]int{}), "[P]\na = 2", mapp(map[string]int{"a": 2})},
+ {mapp(map[string]int{"a": 1, "b": 3}), "[P]\na = 2", mapp(map[string]int{"a": 2, "b": 3})},
+
+ // structs
+ {&T{nil}, "[P]", &T{nil}},
+ {&T{[]int{}}, "[P]", &T{[]int{}}},
+ {&T{[]int{1, 2, 3}}, "[P]", &T{[]int{1, 2, 3}}},
+ {&T{nil}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ {&T{[]int{}}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ {&T{[]int{1, 2, 3}}, "[P]\nS = [1,2]", &T{[]int{1, 2}}},
+ } {
+ var s S
+ md, err := Decode(tt.input, &s)
+ if err != nil {
+ t.Errorf("[%d] Decode error: %s", i, err)
+ continue
+ }
+ if err := md.PrimitiveDecode(s.P, tt.v); err != nil {
+ t.Errorf("[%d] PrimitiveDecode error: %s", i, err)
+ continue
+ }
+ if !reflect.DeepEqual(tt.v, tt.want) {
+ t.Errorf("[%d] got %#v; want %#v", i, tt.v, tt.want)
+ }
+ }
+}
+
+func TestDecodeDatetime(t *testing.T) {
+ // Test here in addition to toml-test to ensure the TZs are correct.
+ tz7 := time.FixedZone("", -3600*7)
+
+ for _, tt := range []struct {
+ in string
+ want time.Time
+ }{
+ // Offset datetime
+ {"1979-05-27T07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+ {"1979-05-27T07:32:00.999999Z", time.Date(1979, 05, 27, 07, 32, 0, 999999000, time.UTC)},
+ {"1979-05-27T00:32:00-07:00", time.Date(1979, 05, 27, 00, 32, 0, 0, tz7)},
+ {"1979-05-27T00:32:00.999999-07:00", time.Date(1979, 05, 27, 00, 32, 0, 999999000, tz7)},
+ {"1979-05-27T00:32:00.24-07:00", time.Date(1979, 05, 27, 00, 32, 0, 240000000, tz7)},
+ {"1979-05-27 07:32:00Z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+ {"1979-05-27t07:32:00z", time.Date(1979, 05, 27, 07, 32, 0, 0, time.UTC)},
+
+ // Make sure the space between the datetime and "#" isn't lexed.
+ {"1979-05-27T07:32:12-07:00 # c", time.Date(1979, 05, 27, 07, 32, 12, 0, tz7)},
+
+ // Local times.
+ {"1979-05-27T07:32:00", time.Date(1979, 05, 27, 07, 32, 0, 0, internal.LocalDatetime)},
+ {"1979-05-27T07:32:00.999999", time.Date(1979, 05, 27, 07, 32, 0, 999999000, internal.LocalDatetime)},
+ {"1979-05-27T07:32:00.25", time.Date(1979, 05, 27, 07, 32, 0, 250000000, internal.LocalDatetime)},
+ {"1979-05-27", time.Date(1979, 05, 27, 0, 0, 0, 0, internal.LocalDate)},
+ {"07:32:00", time.Date(0, 1, 1, 07, 32, 0, 0, internal.LocalTime)},
+ {"07:32:00.999999", time.Date(0, 1, 1, 07, 32, 0, 999999000, internal.LocalTime)},
+ } {
+ t.Run(tt.in, func(t *testing.T) {
+ var x struct{ D time.Time }
+ input := "d = " + tt.in
+ if _, err := Decode(input, &x); err != nil {
+ t.Fatalf("got error: %s", err)
+ }
+
+ if h, w := x.D.Format(time.RFC3339Nano), tt.want.Format(time.RFC3339Nano); h != w {
+ t.Errorf("\nhave: %s\nwant: %s", h, w)
+ }
+ })
+ }
+}
+
+func TestDecodeTextUnmarshaler(t *testing.T) {
+ tests := []struct {
+ name string
+ t interface{}
+ toml string
+ want string
+ }{
+ {
+ "time.Time",
+ struct{ Time time.Time }{},
+ "Time = 1987-07-05T05:45:00Z",
+ "map[Time:1987-07-05 05:45:00 +0000 UTC]",
+ },
+ {
+ "*time.Time",
+ struct{ Time *time.Time }{},
+ "Time = 1988-07-05T05:45:00Z",
+ "map[Time:1988-07-05 05:45:00 +0000 UTC]",
+ },
+ {
+ "map[string]time.Time",
+ struct{ Times map[string]time.Time }{},
+ "Times.one = 1989-07-05T05:45:00Z\nTimes.two = 1990-07-05T05:45:00Z",
+ "map[Times:map[one:1989-07-05 05:45:00 +0000 UTC two:1990-07-05 05:45:00 +0000 UTC]]",
+ },
+ {
+ "map[string]*time.Time",
+ struct{ Times map[string]*time.Time }{},
+ "Times.one = 1989-07-05T05:45:00Z\nTimes.two = 1990-07-05T05:45:00Z",
+ "map[Times:map[one:1989-07-05 05:45:00 +0000 UTC two:1990-07-05 05:45:00 +0000 UTC]]",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ _, err := Decode(tt.toml, &tt.t)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%v", tt.t)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeDuration(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ toml, want, wantErr string
+ }{
+ {&struct{ T time.Duration }{}, `t = "0s"`,
+ "&{0s}", ""},
+ {&struct{ T time.Duration }{}, `t = "5m4s"`,
+ "&{5m4s}", ""},
+ {&struct{ T time.Duration }{}, `t = "4.000000002s"`,
+ "&{4.000000002s}", ""},
+
+ {&struct{ T time.Duration }{}, `t = 0`,
+ "&{0s}", ""},
+ {&struct{ T time.Duration }{}, `t = 12345678`,
+ "&{12.345678ms}", ""},
+
+ {&struct{ T *time.Duration }{}, `T = "5s"`,
+ "&{5s}", ""},
+ {&struct{ T *time.Duration }{}, `T = 5`,
+ "&{5ns}", ""},
+
+ {&struct{ T map[string]time.Duration }{}, `T.dur = "5s"`,
+ "&{map[dur:5s]}", ""},
+ {&struct{ T map[string]*time.Duration }{}, `T.dur = "5s"`,
+ "&{map[dur:5s]}", ""},
+
+ {&struct{ T []time.Duration }{}, `T = ["5s"]`,
+ "&{[5s]}", ""},
+ {&struct{ T []*time.Duration }{}, `T = ["5s"]`,
+ "&{[5s]}", ""},
+
+ {&struct{ T time.Duration }{}, `t = "99 bottles of beer"`, "&{0s}", `invalid duration: "99 bottles of beer"`},
+ {&struct{ T time.Duration }{}, `t = "one bottle of beer"`, "&{0s}", `invalid duration: "one bottle of beer"`},
+ {&struct{ T time.Duration }{}, `t = 1.2`, "&{0s}", "incompatible types:"},
+ {&struct{ T time.Duration }{}, `t = {}`, "&{0s}", "incompatible types:"},
+ {&struct{ T time.Duration }{}, `t = []`, "&{0s}", "incompatible types:"},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.toml, tt.in)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%s", tt.in)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeJSONNumber(t *testing.T) {
+ tests := []struct {
+ in interface{}
+ toml, want, wantErr string
+ }{
+ {&struct{ D json.Number }{}, `D = 2`, "&{2}", ""},
+ {&struct{ D json.Number }{}, `D = 2.002`, "&{2.002}", ""},
+ {&struct{ D *json.Number }{}, `D = 2`, "&{2}", ""},
+ {&struct{ D *json.Number }{}, `D = 2.002`, "&{2.002}", ""},
+ {&struct{ D []json.Number }{}, `D = [2, 3.03]`, "&{[2 3.03]}", ""},
+ {&struct{ D []*json.Number }{}, `D = [2, 3.03]`, "&{[2 3.03]}", ""},
+ {&struct{ D map[string]json.Number }{}, `D = {a=2, b=3.03}`, "&{map[a:2 b:3.03]}", ""},
+ {&struct{ D map[string]*json.Number }{}, `D = {a=2, b=3.03}`, "&{map[a:2 b:3.03]}", ""},
+
+ {&struct{ D json.Number }{}, `D = {}`, "&{}", "incompatible types"},
+ {&struct{ D json.Number }{}, `D = []`, "&{}", "incompatible types"},
+ {&struct{ D json.Number }{}, `D = "2"`, "&{}", "incompatible types"},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ _, err := Decode(tt.toml, tt.in)
+ if !errorContains(err, tt.wantErr) {
+ t.Fatal(err)
+ }
+
+ have := fmt.Sprintf("%s", tt.in)
+ if have != tt.want {
+ t.Errorf("\nhave: %s\nwant: %s", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestMetaDotConflict(t *testing.T) {
+ var m map[string]interface{}
+ meta, err := Decode(`
+ "a.b" = "str"
+ a.b = 1
+ "" = 2
+ `, &m)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `"a.b"=String; a.b=Integer; ""=Integer`
+ have := ""
+ for i, k := range meta.Keys() {
+ if i > 0 {
+ have += "; "
+ }
+ have += k.String() + "=" + meta.Type(k...)
+ }
+ if have != want {
+ t.Errorf("\nhave: %s\nwant: %s", have, want)
+ }
+}
+
+type (
+ Outer struct {
+ Int *InnerInt
+ Enum *Enum
+ Slice *InnerArrayString
+ }
+ Enum int
+ InnerString struct{ value string }
+ InnerInt struct{ value int }
+ InnerBool struct{ value bool }
+ InnerArrayString struct{ value []string }
+)
+
+const (
+ NoValue Enum = iota
+ OtherValue
+)
+
+func (e *Enum) Value() string {
+ switch *e {
+ case OtherValue:
+ return "OTHER_VALUE"
+ }
+ return ""
+}
+
+func (e *Enum) MarshalTOML() ([]byte, error) {
+ return []byte(`"` + e.Value() + `"`), nil
+}
+
+func (e *Enum) UnmarshalTOML(value interface{}) error {
+ sValue, ok := value.(string)
+ if !ok {
+ return fmt.Errorf("value %v is not a string type", value)
+ }
+ for _, enum := range []Enum{NoValue, OtherValue} {
+ if enum.Value() == sValue {
+ *e = enum
+ return nil
+ }
+ }
+ return errors.New("invalid enum value")
+}
+
+func (i *InnerInt) MarshalTOML() ([]byte, error) {
+ return []byte(strconv.Itoa(i.value)), nil
+}
+func (i *InnerInt) UnmarshalTOML(value interface{}) error {
+ iValue, ok := value.(int64)
+ if !ok {
+ return fmt.Errorf("value %v is not a int type", value)
+ }
+ i.value = int(iValue)
+ return nil
+}
+
+func (as *InnerArrayString) MarshalTOML() ([]byte, error) {
+ return []byte("[\"" + strings.Join(as.value, "\", \"") + "\"]"), nil
+}
+
+func (as *InnerArrayString) UnmarshalTOML(value interface{}) error {
+ if value != nil {
+ asValue, ok := value.([]interface{})
+ if !ok {
+ return fmt.Errorf("value %v is not a [] type", value)
+ }
+ as.value = []string{}
+ for _, value := range asValue {
+ as.value = append(as.value, value.(string))
+ }
+ }
+ return nil
+}
+
+// Test for #341
+func TestCustomEncode(t *testing.T) {
+ enum := OtherValue
+ outer := Outer{
+ Int: &InnerInt{value: 10},
+ Enum: &enum,
+ Slice: &InnerArrayString{value: []string{"text1", "text2"}},
+ }
+
+ var buf bytes.Buffer
+ err := NewEncoder(&buf).Encode(outer)
+ if err != nil {
+ t.Errorf("Encode failed: %s", err)
+ }
+
+ have := strings.TrimSpace(buf.String())
+ want := strings.ReplaceAll(strings.TrimSpace(`
+ Int = 10
+ Enum = "OTHER_VALUE"
+ Slice = ["text1", "text2"]
+ `), "\t", "")
+ if want != have {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, want)
+ }
+}
+
+// Test for #341
+func TestCustomDecode(t *testing.T) {
+ var outer Outer
+ _, err := Decode(`
+ Int = 10
+ Enum = "OTHER_VALUE"
+ Slice = ["text1", "text2"]
+ `, &outer)
+ if err != nil {
+ t.Fatalf("Decode failed: %s", err)
+ }
+
+ if outer.Int.value != 10 {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Int.value, 10)
+ }
+ if *outer.Enum != OtherValue {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Enum, OtherValue)
+ }
+ if fmt.Sprint(outer.Slice.value) != fmt.Sprint([]string{"text1", "text2"}) {
+ t.Errorf("\nhave:\n%v\nwant:\n%v\n", outer.Slice.value, []string{"text1", "text2"})
+ }
+}
+
+// TODO: this should be improved for v2:
+// https://github.com/BurntSushi/toml/issues/384
+func TestDecodeDoubleTags(t *testing.T) {
+ var s struct {
+ A int `toml:"a"`
+ B int `toml:"a"`
+ C int `toml:"c"`
+ }
+ _, err := Decode(`
+ a = 1
+ b = 2
+ c = 3
+ `, &s)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ want := `{0 0 3}`
+ have := fmt.Sprintf("%v", s)
+ if want != have {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, want)
+ }
+}
+
+func TestMetaKeys(t *testing.T) {
+ tests := []struct {
+ in string
+ want []Key
+ }{
+ {"", []Key{}},
+ {"b=1\na=1", []Key{Key{"b"}, Key{"a"}}},
+ {"a.b=1\na.a=1", []Key{Key{"a", "b"}, Key{"a", "a"}}}, // TODO: should include "a"
+ {"[tbl]\na=1", []Key{Key{"tbl"}, Key{"tbl", "a"}}},
+ {"[tbl]\na.a=1", []Key{Key{"tbl"}, Key{"tbl", "a", "a"}}}, // TODO: should include "a.a"
+ {"tbl={a=1}", []Key{Key{"tbl"}, Key{"tbl", "a"}}},
+ {"tbl={a={b=1}}", []Key{Key{"tbl"}, Key{"tbl", "a"}, Key{"tbl", "a", "b"}}},
+ }
+
+ for _, tt := range tests {
+ t.Run("", func(t *testing.T) {
+ var x interface{}
+ meta, err := Decode(tt.in, &x)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ have := meta.Keys()
+ if !reflect.DeepEqual(tt.want, have) {
+ t.Errorf("\nhave: %s\nwant: %s\n", have, tt.want)
+ }
+ })
+ }
+}
+
+func TestDecodeParallel(t *testing.T) {
+ doc, err := os.ReadFile("testdata/ja-JP.toml")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ var wg sync.WaitGroup
+ for i := 0; i < 10; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ err := Unmarshal(doc, new(map[string]interface{}))
+ if err != nil {
+ t.Fatal(err)
+ }
+ }()
+ }
+ wg.Wait()
+}
+
+// errorContains checks if the error message in have contains the text in
+// want.
+//
+// This is safe when have is nil. Use an empty string for want if you want to
+// test that err is nil.
+func errorContains(have error, want string) bool {
+ if have == nil {
+ return want == ""
+ }
+ if want == "" {
+ return false
+ }
+ return strings.Contains(have.Error(), want)
+}