diff options
Diffstat (limited to 'encode_test.go')
-rw-r--r-- | encode_test.go | 1326 |
1 files changed, 1326 insertions, 0 deletions
diff --git a/encode_test.go b/encode_test.go new file mode 100644 index 0000000..53e5901 --- /dev/null +++ b/encode_test.go @@ -0,0 +1,1326 @@ +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")) + } + }) +} |