diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:02:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-16 17:02:14 +0000 |
commit | e61c5fa98419989a61b5ca4eb7749acbd37e0af6 (patch) | |
tree | ffe04b0283921bd40489aaa74dcee1f68b7b6b35 /decode_test.go | |
parent | Initial commit. (diff) | |
download | golang-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.go | 1238 |
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) +} |