package toml import ( "bytes" "encoding/json" "fmt" "math" "net" "os" "strconv" "strings" "testing" "time" ) func TestEncodeRoundTrip(t *testing.T) { type Config struct { Age int Cats []string Pi float64 Perfection []int DOB time.Time Ipaddress net.IP } var inputs = Config{ Age: 13, Cats: []string{"one", "two", "three"}, Pi: 3.145, Perfection: []int{11, 2, 3, 4}, DOB: time.Now(), Ipaddress: net.ParseIP("192.168.59.254"), } var ( firstBuffer bytes.Buffer secondBuffer bytes.Buffer outputs Config ) err := NewEncoder(&firstBuffer).Encode(inputs) if err != nil { t.Fatal(err) } _, err = Decode(firstBuffer.String(), &outputs) if err != nil { t.Logf("Could not decode:\n%s\n", firstBuffer.String()) t.Fatal(err) } err = NewEncoder(&secondBuffer).Encode(outputs) if err != nil { t.Fatal(err) } if firstBuffer.String() != secondBuffer.String() { t.Errorf("%s\n\nIS NOT IDENTICAL TO\n\n%s", firstBuffer.String(), secondBuffer.String()) } } func TestEncodeNestedTableArrays(t *testing.T) { type song struct { Name string `toml:"name"` } type album struct { Name string `toml:"name"` Songs []song `toml:"songs"` } type springsteen struct { Albums []album `toml:"albums"` } value := springsteen{ []album{ {"Born to Run", []song{{"Jungleland"}, {"Meeting Across the River"}}}, {"Born in the USA", []song{{"Glory Days"}, {"Dancing in the Dark"}}}, }, } expected := `[[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" ` encodeExpected(t, "nested table arrays", value, expected, nil) } func TestEncodeArrayHashWithNormalHashOrder(t *testing.T) { type Alpha struct { V int } type Beta struct { V int } type Conf struct { V int A Alpha B []Beta } val := Conf{ V: 1, A: Alpha{2}, B: []Beta{{3}}, } expected := "V = 1\n\n[A]\n V = 2\n\n[[B]]\n V = 3\n" encodeExpected(t, "array hash with normal hash order", val, expected, nil) } func TestEncodeOmitEmptyStruct(t *testing.T) { type ( T struct{ Int int } Tpriv struct { Int int private int } Ttime struct { Time time.Time } ) tests := []struct { in interface{} want string }{ {struct { F T `toml:"f,omitempty"` }{}, ""}, {struct { F T `toml:"f,omitempty"` }{T{1}}, "[f]\n Int = 1"}, {struct { F Tpriv `toml:"f,omitempty"` }{}, ""}, {struct { F Tpriv `toml:"f,omitempty"` }{Tpriv{1, 0}}, "[f]\n Int = 1"}, // Private field being set also counts as "not empty". {struct { F Tpriv `toml:"f,omitempty"` }{Tpriv{0, 1}}, "[f]\n Int = 0"}, // time.Time is common use case, so test that explicitly. {struct { F Ttime `toml:"t,omitempty"` }{}, ""}, {struct { F Ttime `toml:"t,omitempty"` }{Ttime{time.Time{}.Add(1)}}, "[t]\n Time = 0001-01-01T00:00:00.000000001Z"}, // TODO: also test with MarshalText, MarshalTOML returning non-zero // value. } for _, tt := range tests { t.Run("", func(t *testing.T) { buf := new(bytes.Buffer) err := NewEncoder(buf).Encode(tt.in) if err != nil { t.Fatal(err) } have := strings.TrimSpace(buf.String()) if have != tt.want { t.Errorf("\nhave:\n%s\nwant:\n%s", have, tt.want) } }) } } func TestEncodeOmitEmpty(t *testing.T) { type compareable struct { Bool bool `toml:"bool,omitempty"` } type uncomparable struct { Field []string `toml:"field,omitempty"` } type nestedUncomparable struct { Field uncomparable `toml:"uncomparable,omitempty"` Bool bool `toml:"bool,omitempty"` } type simple struct { Bool bool `toml:"bool,omitempty"` String string `toml:"string,omitempty"` Array [0]byte `toml:"array,omitempty"` Slice []int `toml:"slice,omitempty"` Map map[string]string `toml:"map,omitempty"` Time time.Time `toml:"time,omitempty"` Compareable1 compareable `toml:"compareable1,omitempty"` Compareable2 compareable `toml:"compareable2,omitempty"` Uncomparable1 uncomparable `toml:"uncomparable1,omitempty"` Uncomparable2 uncomparable `toml:"uncomparable2,omitempty"` NestedUncomparable1 nestedUncomparable `toml:"nesteduncomparable1,omitempty"` NestedUncomparable2 nestedUncomparable `toml:"nesteduncomparable2,omitempty"` } var v simple encodeExpected(t, "fields with omitempty are omitted when empty", v, "", nil) v = simple{ Bool: true, String: " ", Slice: []int{2, 3, 4}, Map: map[string]string{"foo": "bar"}, Time: time.Date(1985, 6, 18, 15, 16, 17, 0, time.UTC), Compareable2: compareable{true}, Uncomparable2: uncomparable{[]string{"XXX"}}, NestedUncomparable1: nestedUncomparable{uncomparable{[]string{"XXX"}}, false}, NestedUncomparable2: nestedUncomparable{uncomparable{}, true}, } expected := `bool = true string = " " slice = [2, 3, 4] time = 1985-06-18T15:16:17Z [map] foo = "bar" [compareable2] bool = true [uncomparable2] field = ["XXX"] [nesteduncomparable1] [nesteduncomparable1.uncomparable] field = ["XXX"] [nesteduncomparable2] bool = true ` encodeExpected(t, "fields with omitempty are not omitted when non-empty", v, expected, nil) } func TestEncodeOmitEmptyPointer(t *testing.T) { type s struct { String *string `toml:"string,omitempty"` } t.Run("nil pointers", func(t *testing.T) { var v struct { String *string `toml:"string,omitempty"` Slice *[]string `toml:"slice,omitempty"` Map *map[string]string `toml:"map,omitempty"` Struct *s `toml:"struct,omitempty"` } encodeExpected(t, "", v, ``, nil) }) t.Run("zero values", func(t *testing.T) { str := "" sl := []string{} m := map[string]string{} v := struct { String *string `toml:"string,omitempty"` Slice *[]string `toml:"slice,omitempty"` Map *map[string]string `toml:"map,omitempty"` Struct *s `toml:"struct,omitempty"` }{&str, &sl, &m, &s{&str}} want := `string = "" slice = [] [map] [struct] string = "" ` encodeExpected(t, "", v, want, nil) }) t.Run("with values", func(t *testing.T) { str := "XXX" sl := []string{"XXX"} m := map[string]string{"XXX": "XXX"} v := struct { String *string `toml:"string,omitempty"` Slice *[]string `toml:"slice,omitempty"` Map *map[string]string `toml:"map,omitempty"` Struct *s `toml:"struct,omitempty"` }{&str, &sl, &m, &s{&str}} want := `string = "XXX" slice = ["XXX"] [map] XXX = "XXX" [struct] string = "XXX" ` encodeExpected(t, "", v, want, nil) }) } func TestEncodeOmitZero(t *testing.T) { type simple struct { Number int `toml:"number,omitzero"` Real float64 `toml:"real,omitzero"` Unsigned uint `toml:"unsigned,omitzero"` } value := simple{0, 0.0, uint(0)} expected := "" encodeExpected(t, "simple with omitzero, all zero", value, expected, nil) value.Number = 10 value.Real = 20 value.Unsigned = 5 expected = `number = 10 real = 20.0 unsigned = 5 ` encodeExpected(t, "simple with omitzero, non-zero", value, expected, nil) } func TestEncodeOmitemptyEmptyName(t *testing.T) { type simple struct { S []int `toml:",omitempty"` } v := simple{[]int{1, 2, 3}} expected := "S = [1, 2, 3]\n" encodeExpected(t, "simple with omitempty, no name, non-empty field", v, expected, nil) } func TestEncodeAnonymousStruct(t *testing.T) { type Inner struct{ N int } type inner struct{ B int } type Embedded struct { Inner1 Inner Inner2 Inner } type Outer0 struct { Inner inner } type Outer1 struct { Inner `toml:"inner"` inner `toml:"innerb"` } type Outer3 struct { Embedded } v0 := Outer0{Inner{3}, inner{4}} expected := "N = 3\nB = 4\n" encodeExpected(t, "embedded anonymous untagged struct", v0, expected, nil) v1 := Outer1{Inner{3}, inner{4}} expected = "[inner]\n N = 3\n\n[innerb]\n B = 4\n" encodeExpected(t, "embedded anonymous tagged struct", v1, expected, nil) v3 := Outer3{Embedded: Embedded{Inner{3}, Inner{4}}} expected = "[Inner1]\n N = 3\n\n[Inner2]\n N = 4\n" encodeExpected(t, "embedded anonymous multiple fields", v3, expected, nil) } func TestEncodeAnonymousStructPointerField(t *testing.T) { type Inner struct{ N int } type Outer0 struct{ *Inner } type Outer1 struct { *Inner `toml:"inner"` } v0 := Outer0{} expected := "" encodeExpected(t, "nil anonymous untagged struct pointer field", v0, expected, nil) v0 = Outer0{&Inner{3}} expected = "N = 3\n" encodeExpected(t, "non-nil anonymous untagged struct pointer field", v0, expected, nil) v1 := Outer1{} expected = "" encodeExpected(t, "nil anonymous tagged struct pointer field", v1, expected, nil) v1 = Outer1{&Inner{3}} expected = "[inner]\n N = 3\n" encodeExpected(t, "non-nil anonymous tagged struct pointer field", v1, expected, nil) } func TestEncodeNestedAnonymousStructs(t *testing.T) { type A struct{ A string } type B struct{ B string } type C struct{ C string } type BC struct { B C } type Outer struct { A BC } v := &Outer{ A: A{ A: "a", }, BC: BC{ B: B{ B: "b", }, C: C{ C: "c", }, }, } expected := "A = \"a\"\nB = \"b\"\nC = \"c\"\n" encodeExpected(t, "nested anonymous untagged structs", v, expected, nil) } type InnerForNextTest struct{ N int } func (InnerForNextTest) F() {} func (InnerForNextTest) G() {} func TestEncodeAnonymousNoStructField(t *testing.T) { type Inner interface{ F() } type inner interface{ G() } type IntS []int type intS []int type Outer0 struct { Inner inner IntS intS } v0 := Outer0{ Inner: InnerForNextTest{3}, inner: InnerForNextTest{4}, IntS: []int{5, 6}, intS: []int{7, 8}, } expected := "IntS = [5, 6]\n\n[Inner]\n N = 3\n" encodeExpected(t, "non struct anonymous field", v0, expected, nil) } func TestEncodeIgnoredFields(t *testing.T) { type simple struct { Number int `toml:"-"` } value := simple{} expected := "" encodeExpected(t, "ignored field", value, expected, nil) } func TestEncodeNaN(t *testing.T) { s1 := struct { Nan float64 `toml:"nan"` Inf float64 `toml:"inf"` }{math.NaN(), math.Inf(1)} s2 := struct { Nan float32 `toml:"nan"` Inf float32 `toml:"inf"` }{float32(math.NaN()), float32(math.Inf(-1))} encodeExpected(t, "", s1, "nan = nan\ninf = +inf\n", nil) encodeExpected(t, "", s2, "nan = nan\ninf = -inf\n", nil) } func TestEncodePrimitive(t *testing.T) { type MyStruct struct { Data Primitive DataA int DataB string } decodeAndEncode := func(toml string) string { var s MyStruct _, err := Decode(toml, &s) if err != nil { t.Fatal(err) } var buf bytes.Buffer err = NewEncoder(&buf).Encode(s) if err != nil { t.Fatal(err) } return buf.String() } original := `DataA = 1 DataB = "bbb" Data = ["Foo", "Bar"] ` reEncoded := decodeAndEncode(decodeAndEncode(original)) if reEncoded != original { t.Errorf( "re-encoded not the same as original\noriginal: %q\nre-encoded: %q", original, reEncoded) } } func TestEncodeError(t *testing.T) { tests := []struct { in interface{} wantErr string }{ {make(chan int), "unsupported type for key '': chan"}, {struct{ C complex128 }{0}, "unsupported type: complex128"}, {[]complex128{0}, "unsupported type: complex128"}, } for _, tt := range tests { t.Run("", func(t *testing.T) { err := NewEncoder(os.Stderr).Encode(tt.in) if err == nil { t.Fatal("err is nil") } if !errorContains(err, tt.wantErr) { t.Errorf("wrong error\nhave: %q\nwant: %q", err, tt.wantErr) } }) } } type ( sound struct{ S string } food struct{ F []string } fun func() cplx complex128 ints []int sound2 struct{ S string } food2 struct{ F []string } fun2 func() cplx2 complex128 ints2 []int ) // This is intentionally wrong (pointer receiver) func (s *sound) MarshalText() ([]byte, error) { return []byte(s.S), nil } func (f food) MarshalText() ([]byte, error) { return []byte(strings.Join(f.F, ", ")), nil } func (f fun) MarshalText() ([]byte, error) { return []byte("why would you do this?"), nil } func (c cplx) MarshalText() ([]byte, error) { cplx := complex128(c) return []byte(fmt.Sprintf("(%f+%fi)", real(cplx), imag(cplx))), nil } func intsValue(is []int) []byte { var buf bytes.Buffer buf.WriteByte('<') for i, v := range is { if i > 0 { buf.WriteByte(',') } buf.WriteString(strconv.Itoa(v)) } buf.WriteByte('>') return buf.Bytes() } func (is *ints) MarshalText() ([]byte, error) { if is == nil { return []byte("[]"), nil } return intsValue(*is), nil } func (s *sound2) MarshalTOML() ([]byte, error) { return []byte("\"" + s.S + "\""), nil } func (f food2) MarshalTOML() ([]byte, error) { return []byte("[\"" + strings.Join(f.F, "\", \"") + "\"]"), nil } func (f fun2) MarshalTOML() ([]byte, error) { return []byte("\"why would you do this?\""), nil } func (c cplx2) MarshalTOML() ([]byte, error) { cplx := complex128(c) return []byte(fmt.Sprintf("\"(%f+%fi)\"", real(cplx), imag(cplx))), nil } func (is *ints2) MarshalTOML() ([]byte, error) { // MarshalTOML must quote by self if is == nil { return []byte(`"[]"`), nil } return []byte(fmt.Sprintf(`"%s"`, intsValue(*is))), nil } func TestEncodeTextMarshaler(t *testing.T) { x := struct { Name string Labels map[string]string Sound sound Sound2 *sound Food food Food2 *food Complex cplx Fun fun Ints ints Ints2 *ints2 }{ Name: "Goblok", Sound: sound{"miauw"}, Sound2: &sound{"miauw"}, Labels: map[string]string{ "type": "cat", "color": "black", }, Food: food{[]string{"chicken", "fish"}}, Food2: &food{[]string{"chicken", "fish"}}, Complex: complex(42, 666), Fun: func() { panic("x") }, Ints: ints{1, 2, 3, 4}, Ints2: &ints2{1, 2, 3, 4}, } var buf bytes.Buffer if err := NewEncoder(&buf).Encode(&x); err != nil { t.Fatal(err) } want := `Name = "Goblok" Sound = "miauw" Sound2 = "miauw" Food = "chicken, fish" Food2 = "chicken, fish" Complex = "(42.000000+666.000000i)" Fun = "why would you do this?" Ints = "<1,2,3,4>" Ints2 = "<1,2,3,4>" [Labels] color = "black" type = "cat" ` if buf.String() != want { t.Error("\n" + buf.String()) } } func TestEncodeTOMLMarshaler(t *testing.T) { x := struct { Name string Labels map[string]string Sound sound2 Sound2 *sound2 Food food2 Food2 *food2 Complex cplx2 Fun fun2 }{ Name: "Goblok", Sound: sound2{"miauw"}, Sound2: &sound2{"miauw"}, Labels: map[string]string{ "type": "cat", "color": "black", }, Food: food2{[]string{"chicken", "fish"}}, Food2: &food2{[]string{"chicken", "fish"}}, Complex: complex(42, 666), Fun: func() { panic("x") }, } var buf bytes.Buffer if err := NewEncoder(&buf).Encode(x); err != nil { t.Fatal(err) } want := `Name = "Goblok" Sound2 = "miauw" Food = ["chicken", "fish"] Food2 = ["chicken", "fish"] Complex = "(42.000000+666.000000i)" Fun = "why would you do this?" [Labels] color = "black" type = "cat" [Sound] S = "miauw" ` if buf.String() != want { t.Error("\n" + buf.String()) } } type ( retNil1 string retNil2 string ) func (r retNil1) MarshalText() ([]byte, error) { return nil, nil } func (r retNil2) MarshalTOML() ([]byte, error) { return nil, nil } func TestEncodeEmpty(t *testing.T) { t.Run("text", func(t *testing.T) { var ( s struct{ Text retNil1 } buf bytes.Buffer ) err := NewEncoder(&buf).Encode(s) if err == nil { t.Fatalf("no error, but expected an error; output:\n%s", buf.String()) } if buf.String() != "" { t.Error("\n" + buf.String()) } }) t.Run("toml", func(t *testing.T) { var ( s struct{ Text retNil2 } buf bytes.Buffer ) err := NewEncoder(&buf).Encode(s) if err == nil { t.Fatalf("no error, but expected an error; output:\n%s", buf.String()) } if buf.String() != "" { t.Error("\n" + buf.String()) } }) } // Would previously fail on 32bit architectures; can test with: // // GOARCH=386 go test -c && ./toml.test // GOARCH=arm GOARM=7 go test -c && qemu-arm ./toml.test func TestEncode32bit(t *testing.T) { type Inner struct { A, B, C string } type Outer struct{ Inner } encodeExpected(t, "embedded anonymous untagged struct", Outer{Inner{"a", "b", "c"}}, "A = \"a\"\nB = \"b\"\nC = \"c\"\n", nil) } // Skip invalid types if it has toml:"-" // // https://github.com/BurntSushi/toml/issues/345 func TestEncodeSkipInvalidType(t *testing.T) { buf := new(bytes.Buffer) err := NewEncoder(buf).Encode(struct { Str string `toml:"str"` Arr []func() `toml:"-"` Map map[string]interface{} `toml:"-"` Func func() `toml:"-"` }{ Str: "a", Arr: []func(){func() {}}, Map: map[string]interface{}{"f": func() {}}, Func: func() {}, }) if err != nil { t.Fatal(err) } have := buf.String() want := "str = \"a\"\n" if have != want { t.Errorf("\nwant: %q\nhave: %q\n", want, have) } } func TestEncodeDuration(t *testing.T) { tests := []time.Duration{ 0, time.Second, time.Minute, time.Hour, 248*time.Hour + 45*time.Minute + 24*time.Second, 12345678 * time.Nanosecond, 12345678 * time.Second, 4*time.Second + 2*time.Nanosecond, } for _, tt := range tests { encodeExpected(t, tt.String(), struct{ Dur time.Duration }{Dur: tt}, fmt.Sprintf("Dur = %q", tt), nil) } } type jsonT struct { Num json.Number NumP *json.Number Arr []json.Number ArrP []*json.Number Tbl map[string]json.Number TblP map[string]*json.Number } var ( n2, n4, n6 = json.Number("2"), json.Number("4"), json.Number("6") f2, f4, f6 = json.Number("2.2"), json.Number("4.4"), json.Number("6.6") ) func TestEncodeJSONNumber(t *testing.T) { tests := []struct { in jsonT want string }{ {jsonT{}, "Num = 0"}, {jsonT{ Num: "1", NumP: &n2, Arr: []json.Number{"3"}, ArrP: []*json.Number{&n4}, Tbl: map[string]json.Number{"k1": "5"}, TblP: map[string]*json.Number{"k2": &n6}}, ` Num = 1 NumP = 2 Arr = [3] ArrP = [4] [Tbl] k1 = 5 [TblP] k2 = 6 `}, {jsonT{ Num: "1.1", NumP: &f2, Arr: []json.Number{"3.3"}, ArrP: []*json.Number{&f4}, Tbl: map[string]json.Number{"k1": "5.5"}, TblP: map[string]*json.Number{"k2": &f6}}, ` Num = 1.1 NumP = 2.2 Arr = [3.3] ArrP = [4.4] [Tbl] k1 = 5.5 [TblP] k2 = 6.6 `}, } for _, tt := range tests { t.Run("", func(t *testing.T) { var buf bytes.Buffer err := NewEncoder(&buf).Encode(tt.in) if err != nil { t.Fatal(err) } have := strings.TrimSpace(buf.String()) want := strings.ReplaceAll(strings.TrimSpace(tt.want), "\t", "") if have != want { t.Errorf("\nwant:\n%s\nhave:\n%s\n", want, have) } }) } } func TestEncode(t *testing.T) { type Embedded struct { Int int `toml:"_int"` } type NonStruct int date := time.Date(2014, 5, 11, 19, 30, 40, 0, time.UTC) dateStr := "2014-05-11T19:30:40Z" tests := map[string]struct { input interface{} wantOutput string wantError error }{ "bool field": { input: struct { BoolTrue bool BoolFalse bool }{true, false}, wantOutput: "BoolTrue = true\nBoolFalse = false\n", }, "int fields": { input: struct { Int int Int8 int8 Int16 int16 Int32 int32 Int64 int64 }{1, 2, 3, 4, 5}, wantOutput: "Int = 1\nInt8 = 2\nInt16 = 3\nInt32 = 4\nInt64 = 5\n", }, "uint fields": { input: struct { Uint uint Uint8 uint8 Uint16 uint16 Uint32 uint32 Uint64 uint64 }{1, 2, 3, 4, 5}, wantOutput: "Uint = 1\nUint8 = 2\nUint16 = 3\nUint32 = 4" + "\nUint64 = 5\n", }, "float fields": { input: struct { Float32 float32 Float64 float64 }{1.5, 2.5}, wantOutput: "Float32 = 1.5\nFloat64 = 2.5\n", }, "string field": { input: struct{ String string }{"foo"}, wantOutput: "String = \"foo\"\n", }, "string field with \\n escape": { input: struct{ String string }{"foo\n"}, wantOutput: "String = \"foo\\n\"\n", }, "string field and unexported field": { input: struct { String string unexported int }{"foo", 0}, wantOutput: "String = \"foo\"\n", }, "datetime field in UTC": { input: struct{ Date time.Time }{date}, wantOutput: fmt.Sprintf("Date = %s\n", dateStr), }, "datetime field as primitive": { // Using a map here to fail if isStructOrMap() returns true for // time.Time. input: map[string]interface{}{ "Date": date, "Int": 1, }, wantOutput: fmt.Sprintf("Date = %s\nInt = 1\n", dateStr), }, "array fields": { input: struct { IntArray0 [0]int IntArray3 [3]int }{[0]int{}, [3]int{1, 2, 3}}, wantOutput: "IntArray0 = []\nIntArray3 = [1, 2, 3]\n", }, "slice fields": { input: struct{ IntSliceNil, IntSlice0, IntSlice3 []int }{ nil, []int{}, []int{1, 2, 3}, }, wantOutput: "IntSlice0 = []\nIntSlice3 = [1, 2, 3]\n", }, "datetime slices": { input: struct{ DatetimeSlice []time.Time }{ []time.Time{date, date}, }, wantOutput: fmt.Sprintf("DatetimeSlice = [%s, %s]\n", dateStr, dateStr), }, "nested arrays and slices": { input: struct { SliceOfArrays [][2]int ArrayOfSlices [2][]int SliceOfArraysOfSlices [][2][]int ArrayOfSlicesOfArrays [2][][2]int SliceOfMixedArrays [][2]interface{} ArrayOfMixedSlices [2][]interface{} }{ [][2]int{{1, 2}, {3, 4}}, [2][]int{{1, 2}, {3, 4}}, [][2][]int{ { {1, 2}, {3, 4}, }, { {5, 6}, {7, 8}, }, }, [2][][2]int{ { {1, 2}, {3, 4}, }, { {5, 6}, {7, 8}, }, }, [][2]interface{}{ {1, 2}, {"a", "b"}, }, [2][]interface{}{ {1, 2}, {"a", "b"}, }, }, wantOutput: `SliceOfArrays = [[1, 2], [3, 4]] ArrayOfSlices = [[1, 2], [3, 4]] SliceOfArraysOfSlices = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] ArrayOfSlicesOfArrays = [[[1, 2], [3, 4]], [[5, 6], [7, 8]]] SliceOfMixedArrays = [[1, 2], ["a", "b"]] ArrayOfMixedSlices = [[1, 2], ["a", "b"]] `, }, "empty slice": { input: struct{ Empty []interface{} }{[]interface{}{}}, wantOutput: "Empty = []\n", }, "(error) slice with element type mismatch (string and integer)": { input: struct{ Mixed []interface{} }{[]interface{}{1, "a"}}, wantOutput: "Mixed = [1, \"a\"]\n", }, "(error) slice with element type mismatch (integer and float)": { input: struct{ Mixed []interface{} }{[]interface{}{1, 2.5}}, wantOutput: "Mixed = [1, 2.5]\n", }, "slice with elems of differing Go types, same TOML types": { input: struct { MixedInts []interface{} MixedFloats []interface{} }{ []interface{}{ int(1), int8(2), int16(3), int32(4), int64(5), uint(1), uint8(2), uint16(3), uint32(4), uint64(5), }, []interface{}{float32(1.5), float64(2.5)}, }, wantOutput: "MixedInts = [1, 2, 3, 4, 5, 1, 2, 3, 4, 5]\n" + "MixedFloats = [1.5, 2.5]\n", }, "(error) slice w/ element type mismatch (one is nested array)": { input: struct{ Mixed []interface{} }{ []interface{}{1, []interface{}{2}}, }, wantOutput: "Mixed = [1, [2]]\n", }, "(error) slice with 1 nil element": { input: struct{ NilElement1 []interface{} }{[]interface{}{nil}}, wantError: errArrayNilElement, }, "(error) slice with 1 nil element (and other non-nil elements)": { input: struct{ NilElement []interface{} }{ []interface{}{1, nil}, }, wantError: errArrayNilElement, }, "simple map": { input: map[string]int{"a": 1, "b": 2}, wantOutput: "a = 1\nb = 2\n", }, "map with interface{} value type": { input: map[string]interface{}{"a": 1, "b": "c"}, wantOutput: "a = 1\nb = \"c\"\n", }, "map with interface{} value type, some of which are structs": { input: map[string]interface{}{ "a": struct{ Int int }{2}, "b": 1, }, wantOutput: "b = 1\n\n[a]\n Int = 2\n", }, "nested map": { input: map[string]map[string]int{ "a": {"b": 1}, "c": {"d": 2}, }, wantOutput: "[a]\n b = 1\n\n[c]\n d = 2\n", }, "nested struct": { input: struct{ Struct struct{ Int int } }{ struct{ Int int }{1}, }, wantOutput: "[Struct]\n Int = 1\n", }, "nested struct and non-struct field": { input: struct { Struct struct{ Int int } Bool bool }{struct{ Int int }{1}, true}, wantOutput: "Bool = true\n\n[Struct]\n Int = 1\n", }, "2 nested structs": { input: struct{ Struct1, Struct2 struct{ Int int } }{ struct{ Int int }{1}, struct{ Int int }{2}, }, wantOutput: "[Struct1]\n Int = 1\n\n[Struct2]\n Int = 2\n", }, "deeply nested structs": { input: struct { Struct1, Struct2 struct{ Struct3 *struct{ Int int } } }{ struct{ Struct3 *struct{ Int int } }{&struct{ Int int }{1}}, struct{ Struct3 *struct{ Int int } }{nil}, }, wantOutput: "[Struct1]\n [Struct1.Struct3]\n Int = 1" + "\n\n[Struct2]\n", }, "nested struct with nil struct elem": { input: struct { Struct struct{ Inner *struct{ Int int } } }{ struct{ Inner *struct{ Int int } }{nil}, }, wantOutput: "[Struct]\n", }, "nested struct with no fields": { input: struct { Struct struct{ Inner struct{} } }{ struct{ Inner struct{} }{struct{}{}}, }, wantOutput: "[Struct]\n [Struct.Inner]\n", }, "struct with tags": { input: struct { Struct struct { Int int `toml:"_int"` } `toml:"_struct"` Bool bool `toml:"_bool"` }{ struct { Int int `toml:"_int"` }{1}, true, }, wantOutput: "_bool = true\n\n[_struct]\n _int = 1\n", }, "embedded struct": { input: struct{ Embedded }{Embedded{1}}, wantOutput: "_int = 1\n", }, "embedded *struct": { input: struct{ *Embedded }{&Embedded{1}}, wantOutput: "_int = 1\n", }, "nested embedded struct": { input: struct { Struct struct{ Embedded } `toml:"_struct"` }{struct{ Embedded }{Embedded{1}}}, wantOutput: "[_struct]\n _int = 1\n", }, "nested embedded *struct": { input: struct { Struct struct{ *Embedded } `toml:"_struct"` }{struct{ *Embedded }{&Embedded{1}}}, wantOutput: "[_struct]\n _int = 1\n", }, "embedded non-struct": { input: struct{ NonStruct }{5}, wantOutput: "NonStruct = 5\n", }, "array of tables": { input: struct { Structs []*struct{ Int int } `toml:"struct"` }{ []*struct{ Int int }{{1}, {3}}, }, wantOutput: "[[struct]]\n Int = 1\n\n[[struct]]\n Int = 3\n", }, "array of tables order": { input: map[string]interface{}{ "map": map[string]interface{}{ "zero": 5, "arr": []map[string]int{ { "friend": 5, }, }, }, }, wantOutput: "[map]\n zero = 5\n\n [[map.arr]]\n friend = 5\n", }, "empty key name": { input: map[string]int{"": 1}, wantOutput: `"" = 1` + "\n", }, "key with \\n escape": { input: map[string]string{"\n": "\n"}, wantOutput: `"\n" = "\n"` + "\n", }, "empty map name": { input: map[string]interface{}{ "": map[string]int{"v": 1}, }, wantOutput: "[\"\"]\n v = 1\n", }, "(error) top-level slice": { input: []struct{ Int int }{{1}, {2}, {3}}, wantError: errNoKey, }, "(error) map no string key": { input: map[int]string{1: ""}, wantError: errNonString, }, "tbl-in-arr-struct": { input: struct { Arr [][]struct{ A, B, C int } }{[][]struct{ A, B, C int }{{{1, 2, 3}, {4, 5, 6}}}}, wantOutput: "Arr = [[{A = 1, B = 2, C = 3}, {A = 4, B = 5, C = 6}]]", }, "tbl-in-arr-map": { input: map[string]interface{}{ "arr": []interface{}{[]interface{}{ map[string]interface{}{ "a": []interface{}{"hello", "world"}, "b": []interface{}{1.12, 4.1}, "c": 1, "d": map[string]interface{}{"e": "E"}, "f": struct{ A, B int }{1, 2}, "g": []struct{ A, B int }{{3, 4}, {5, 6}}, }, }}, }, wantOutput: `arr = [[{a = ["hello", "world"], b = [1.12, 4.1], c = 1, d = {e = "E"}, f = {A = 1, B = 2}, g = [{A = 3, B = 4}, {A = 5, B = 6}]}]]`, }, "slice of slice": { input: struct { Slices [][]struct{ Int int } }{ [][]struct{ Int int }{{{1}}, {{2}}, {{3}}}, }, wantOutput: "Slices = [[{Int = 1}], [{Int = 2}], [{Int = 3}]]", }, } for label, test := range tests { encodeExpected(t, label, test.input, test.wantOutput, test.wantError) } } func TestEncodeDoubleTags(t *testing.T) { // TODO: this needs fixing; it shouldn't emit two 'a =' keys. s := struct { A int `toml:"a"` B int `toml:"a"` C int `toml:"c"` }{1, 2, 3} buf := new(strings.Builder) err := NewEncoder(buf).Encode(s) if err != nil { t.Fatal(err) } want := `a = 1 a = 2 c = 3 ` if want != buf.String() { t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want) } } type ( Doc1 struct{ N string } Doc2 struct{ N string } ) func (d Doc1) MarshalTOML() ([]byte, error) { return []byte(`marshal_toml = "` + d.N + `"`), nil } func (d Doc2) MarshalText() ([]byte, error) { return []byte(`marshal_text = "` + d.N + `"`), nil } // MarshalTOML and MarshalText on the top level type, rather than a field. func TestMarshalDoc(t *testing.T) { t.Run("toml", func(t *testing.T) { var buf bytes.Buffer err := NewEncoder(&buf).Encode(Doc1{"asd"}) if err != nil { t.Fatal(err) } want := `marshal_toml = "asd"` if want != buf.String() { t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want) } }) t.Run("text", func(t *testing.T) { var buf bytes.Buffer err := NewEncoder(&buf).Encode(Doc2{"asd"}) if err != nil { t.Fatal(err) } want := `"marshal_text = \"asd\""` if want != buf.String() { t.Errorf("\nhave: %s\nwant: %s\n", buf.String(), want) } }) } func encodeExpected(t *testing.T, label string, val interface{}, want string, wantErr error) { t.Helper() t.Run(label, func(t *testing.T) { t.Helper() var buf bytes.Buffer err := NewEncoder(&buf).Encode(val) if err != wantErr { if wantErr != nil { if wantErr == errAnything && err != nil { return } t.Errorf("want Encode error %v, got %v", wantErr, err) } else { t.Errorf("Encode failed: %s", err) } } if err != nil { return } have := strings.TrimSpace(buf.String()) want = strings.TrimSpace(want) if want != have { t.Errorf("\nhave:\n%s\nwant:\n%s\n", "\t"+strings.ReplaceAll(have, "\n", "\n\t"), "\t"+strings.ReplaceAll(want, "\n", "\n\t")) } }) }