2287 lines
64 KiB
Go
2287 lines
64 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package interpres
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"math"
|
|
"net"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestMarshalScalars(t *testing.T) {
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Count int `toml:"count"`
|
|
Unsigned uint64 `toml:"unsigned"`
|
|
Ratio float64 `toml:"ratio"`
|
|
Enabled bool `toml:"enabled"`
|
|
Disabled bool `toml:"disabled"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Title: "demo",
|
|
Count: 42,
|
|
Unsigned: 99,
|
|
Ratio: 3.14,
|
|
Enabled: true,
|
|
Disabled: false,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "title = \"demo\"\ncount = 42\nunsigned = 99\nratio = 3.14\nenabled = true\ndisabled = false\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalFloatSpecials(t *testing.T) {
|
|
type Cfg struct {
|
|
PosInf float64 `toml:"pos_inf"`
|
|
NegInf float64 `toml:"neg_inf"`
|
|
NaN float64 `toml:"nan"`
|
|
Zero float64 `toml:"zero"`
|
|
IntVal float64 `toml:"int_val"`
|
|
NegZ float64 `toml:"neg_zero"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
PosInf: math.Inf(1),
|
|
NegInf: math.Inf(-1),
|
|
NaN: math.NaN(),
|
|
Zero: 0,
|
|
IntVal: 7,
|
|
NegZ: math.Copysign(0, -1),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "pos_inf = inf\nneg_inf = -inf\nnan = nan\nzero = 0.0\nint_val = 7.0\nneg_zero = 0.0\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalFloatNormalizesNegativeZero(t *testing.T) {
|
|
// The output contract normalises negative zero to "0.0".
|
|
type Cfg struct {
|
|
Z float64 `toml:"z"`
|
|
}
|
|
out, err := Marshal(Cfg{Z: math.Copysign(0, -1)})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "z = 0.0\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalContextHonoursCancellation(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
type C struct {
|
|
A int `toml:"a"`
|
|
}
|
|
if _, err := MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("MarshalContext returned %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLayoutGroupedDefault(t *testing.T) {
|
|
// NewEncoder must default to LayoutKindGrouped so legacy callers keep the
|
|
// scalars-first ordering.
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
S struct {
|
|
Host string `toml:"host"`
|
|
} `toml:"s"`
|
|
}
|
|
out, err := Marshal(Cfg{Name: "x", S: struct {
|
|
Host string `toml:"host"`
|
|
}{Host: "h"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"x\"\n\n[s]\nhost = \"h\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLayoutDeclarationPreservesOrder(t *testing.T) {
|
|
type Inner struct {
|
|
Host string `toml:"host"`
|
|
}
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
Server Inner `toml:"server"`
|
|
Debug bool `toml:"debug"`
|
|
}
|
|
in := Cfg{
|
|
Name: "x",
|
|
Server: Inner{Host: "h"},
|
|
Debug: true,
|
|
}
|
|
out, err := Marshal(in, Layout(LayoutKindDeclaration))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// With Layout(LayoutKindDeclaration) the encoder walks entries in declaration order.
|
|
// The output is still parseable, but a scalar that follows a header is
|
|
// parsed as a sub-table key. That is the user's trade-off; see
|
|
// docs/API.md.
|
|
want := "name = \"x\"\n\n[server]\nhost = \"h\"\ndebug = true\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLayoutGroupedDefaultOrder(t *testing.T) {
|
|
// The default (LayoutKindGrouped) must lift the trailing scalar ahead of
|
|
// the [server] block so the document round-trips losslessly.
|
|
type Inner struct {
|
|
Host string `toml:"host"`
|
|
}
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
Server Inner `toml:"server"`
|
|
Debug bool `toml:"debug"`
|
|
}
|
|
in := Cfg{
|
|
Name: "x",
|
|
Server: Inner{Host: "h"},
|
|
Debug: true,
|
|
}
|
|
out, err := Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"x\"\ndebug = true\n\n[server]\nhost = \"h\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderOmitEmptyArrays(t *testing.T) {
|
|
type Cfg struct {
|
|
Tags []string `toml:"tags"`
|
|
Secrets []string `toml:"secrets"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Tags: []string{"a", "b"},
|
|
Secrets: []string{},
|
|
}, OmitEmptyArrays(true))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "tags = [\"a\", \"b\"]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderDefaultEmitsEmptyArray(t *testing.T) {
|
|
type Cfg struct {
|
|
Tags []string `toml:"tags"`
|
|
}
|
|
out, err := Marshal(Cfg{Tags: []string{}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "tags = []\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderOmitEmptyArrayOfTablesStillSkipped(t *testing.T) {
|
|
type Item struct {
|
|
Name string `toml:"name"`
|
|
}
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Items []Item `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Title: "demo",
|
|
Items: nil,
|
|
}, OmitEmptyArrays(true))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "title = \"demo\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLiteralMultiline(t *testing.T) {
|
|
type Cfg struct {
|
|
Long string `toml:"long"`
|
|
}
|
|
long := strings.Repeat("a", 50) + "\nline two\nline three"
|
|
out, err := Marshal(Cfg{Long: long}, LiteralMultiline(20))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "long = '''\n" + long + "\n'''\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLiteralMultilineBelowThreshold(t *testing.T) {
|
|
// A multi-line value shorter than the threshold must remain escaped.
|
|
type Cfg struct {
|
|
Short string `toml:"short"`
|
|
}
|
|
out, err := Marshal(Cfg{Short: "one\ntwo"}, LiteralMultiline(1000))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "short = \"one\\ntwo\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLiteralMultilineThresholdZero(t *testing.T) {
|
|
// LiteralMultiline(0) disables the literal form entirely.
|
|
type Cfg struct {
|
|
S string `toml:"s"`
|
|
}
|
|
out, err := Marshal(Cfg{S: "a\nb\nc\nd"}, LiteralMultiline(0))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if !bytes.HasPrefix(out, []byte("s = \"")) {
|
|
t.Errorf("output mismatch, expected basic quoted form:\ngot: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestEncoderLiteralMultilineFallsBackWhenUnsafe(t *testing.T) {
|
|
// The literal form carries the value verbatim, so content it cannot
|
|
// represent must fall back to the escaped basic string instead of
|
|
// producing output that does not re-parse.
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
}{
|
|
{"embedded delimiter", "before ''' after\nsecond line"},
|
|
{"control character", "a\x01b\nsecond"},
|
|
{"delete character", "a\x7fb\nsecond"},
|
|
{"lone carriage return", "first\rsecond\nthird"},
|
|
}
|
|
for _, c := range cases {
|
|
out, err := Marshal(map[string]any{"s": c.in}, LiteralMultiline(5))
|
|
if err != nil {
|
|
t.Fatalf("%s: marshal: %v", c.name, err)
|
|
}
|
|
if !bytes.HasPrefix(out, []byte("s = \"")) {
|
|
t.Errorf("%s: expected the basic quoted form, got:\n%s", c.name, out)
|
|
}
|
|
re, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Errorf("%s: re-parse: %v\ndoc:\n%s", c.name, err, out)
|
|
continue
|
|
}
|
|
if re["s"] != c.in {
|
|
t.Errorf("%s: round-trip changed the value: %q", c.name, re["s"])
|
|
}
|
|
}
|
|
}
|
|
|
|
// marshalerFunc adapts a plain function value to the Marshaler interface.
|
|
// Tests use it to express "this field produces this TOML value" without a
|
|
// dedicated struct definition.
|
|
type marshalerFunc func() (any, error)
|
|
|
|
func (f marshalerFunc) MarshalTOML() (any, error) { return f() }
|
|
|
|
// failingMarshalerFunc invokes MarshalTOML to a fixed error; it lets us check
|
|
// that a MarshalTOML failure propagates back to Marshal.
|
|
type failingMarshalerFunc struct{}
|
|
|
|
func (failingMarshalerFunc) MarshalTOML() (any, error) {
|
|
return nil, errors.New("oops")
|
|
}
|
|
|
|
func TestMarshalerReturningTime(t *testing.T) {
|
|
// A Marshaler may return a date-time scalar; the encoder must emit it
|
|
// using its canonical form.
|
|
when := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
|
type Cfg struct {
|
|
M marshalerFunc `toml:"m"`
|
|
}
|
|
out, err := Marshal(Cfg{M: marshalerFunc(func() (any, error) { return when, nil })})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "m = 2026-06-26T10:00Z\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerReturningDifferentStruct(t *testing.T) {
|
|
// A Marshaler returning a struct (not a scalar) is treated as a sub-table
|
|
// by the encoder.
|
|
type Inner struct {
|
|
V string `toml:"v"`
|
|
}
|
|
type Cfg struct {
|
|
P marshalerFunc `toml:"p"`
|
|
}
|
|
out, err := Marshal(Cfg{P: marshalerFunc(func() (any, error) {
|
|
return Inner{V: "x"}, nil
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[p]\nv = \"x\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerReturningSliceOfMaps(t *testing.T) {
|
|
// A Marshaler returning []map[string]any becomes an array of tables.
|
|
type Cfg struct {
|
|
Items marshalerFunc `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{Items: marshalerFunc(func() (any, error) {
|
|
return []map[string]any{
|
|
{"k": "a"},
|
|
{"k": "b"},
|
|
}, nil
|
|
})})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[[items]]\nk = \"a\"\n\n[[items]]\nk = \"b\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerErrorPropagates(t *testing.T) {
|
|
type Cfg struct {
|
|
F failingMarshalerFunc `toml:"f"`
|
|
}
|
|
if _, err := Marshal(Cfg{F: failingMarshalerFunc{}}); err == nil {
|
|
t.Fatal("expected an error from MarshalTOML")
|
|
} else if !strings.Contains(err.Error(), "oops") {
|
|
t.Errorf("err = %v, want substring \"oops\"", err)
|
|
}
|
|
}
|
|
|
|
// nilMarshalerFunc is a Marshaler whose method returns nil with no error.
|
|
type nilMarshalerFunc struct{}
|
|
|
|
func (nilMarshalerFunc) MarshalTOML() (any, error) { return nil, nil }
|
|
|
|
func TestMarshalRejectsNilMarshalerResult(t *testing.T) {
|
|
// nil has no TOML representation, so a MarshalTOML result of nil is an
|
|
// error, not a silently dropped field.
|
|
_, err := Marshal(struct {
|
|
F nilMarshalerFunc `toml:"f"`
|
|
}{})
|
|
if err == nil {
|
|
t.Fatal("expected an error for a nil MarshalTOML result")
|
|
}
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "f" {
|
|
t.Fatalf("Path = %v, want f", ee.Path)
|
|
}
|
|
|
|
// Inside a value array the nil result used to reach reflection as a zero
|
|
// Value and panic.
|
|
_, err = Marshal(map[string]any{"arr": []any{1, nilMarshalerFunc{}}})
|
|
if err == nil {
|
|
t.Fatal("expected an error for a nil MarshalTOML result in an array")
|
|
}
|
|
if !strings.Contains(err.Error(), "MarshalTOML returned a nil value") {
|
|
t.Errorf("err = %v, want the nil-result message", err)
|
|
}
|
|
}
|
|
|
|
// Two fields that resolve to one TOML key must marshal as one key, resolved
|
|
// the way the decoder resolves it, or the output would carry a duplicate key
|
|
// and never re-parse.
|
|
func TestMarshalDuplicateKeyResolvesToOneField(t *testing.T) {
|
|
type SameLevel struct {
|
|
First int `toml:"v"`
|
|
Second string `toml:"v"`
|
|
}
|
|
out, err := Marshal(SameLevel{First: 1, Second: "s"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if want := "v = \"s\"\n"; string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
type Base struct {
|
|
Name string `toml:"name"`
|
|
}
|
|
type Embedded struct {
|
|
Base
|
|
Name string `toml:"name"`
|
|
}
|
|
out, err = Marshal(Embedded{Base: Base{Name: "inner"}, Name: "outer"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// The shallower field wins, matching the decoder.
|
|
if want := "name = \"outer\"\n"; string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
if _, err := ParseMap(out); err != nil {
|
|
t.Errorf("re-parse: %v\ndoc:\n%s", err, out)
|
|
}
|
|
}
|
|
|
|
func TestMarshalEmbeddedScalarStruct(t *testing.T) {
|
|
// A field declared directly as a scalar-struct type (here LocalDateTime)
|
|
// must be encoded as a TOML scalar at the parent level, not rendered as
|
|
// a sub-table.
|
|
ldt := LocalDateTime{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)}
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
S LocalDateTime `toml:"s"`
|
|
}
|
|
out, err := Marshal(Cfg{Name: "x", S: ldt})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"x\"\ns = 2026-06-26T00:00\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEncoderChainedOptions(t *testing.T) {
|
|
// All chainable options combined; verify they compose without errors.
|
|
type Inner struct {
|
|
V string `toml:"v"`
|
|
}
|
|
type Cfg struct {
|
|
S string `toml:"s"`
|
|
I Inner `toml:"i"`
|
|
}
|
|
long := strings.Repeat("x", 200)
|
|
out, err := Marshal(Cfg{S: "short", I: Inner{V: long}},
|
|
Layout(LayoutKindDeclaration),
|
|
OmitEmptyArrays(true),
|
|
LiteralMultiline(50))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
_ = out // success path is enough; per-option correctness is exercised above.
|
|
}
|
|
|
|
func TestMarshalStringEscapes(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
want string // the TOML scalar value (without "s = " prefix)
|
|
}{
|
|
{"plain", "hello", `"hello"`},
|
|
{"quote", `say "hi"`, `"say \"hi\""`},
|
|
{"backslash", `a\b`, `"a\\b"`},
|
|
{"newline", "line1\nline2", `"line1\nline2"`},
|
|
{"tab", "col1\tcol2", `"col1\tcol2"`},
|
|
{"cr", "line\rmore", `"line\rmore"`},
|
|
{"control", "a\x01b", `"a\u0001b"`},
|
|
{"unicode", "\u201csmart\u201d", `"“smart”"`}, // printable unicode; not escaped
|
|
{"empty", "", `""`},
|
|
{"slash_only", "a/b", `"a/b"`},
|
|
}
|
|
for _, c := range cases {
|
|
out, err := Marshal(struct {
|
|
S string `toml:"s"`
|
|
}{S: c.in})
|
|
if err != nil {
|
|
t.Fatalf("%s: marshal: %v", c.name, err)
|
|
}
|
|
got := strings.TrimSuffix(string(out), "\n")
|
|
want := "s = " + c.want
|
|
if got != want {
|
|
t.Errorf("%s:\ngot: %s\nwant: %s", c.name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarshalDateTime(t *testing.T) {
|
|
type Cfg struct {
|
|
Offset time.Time `toml:"offset"`
|
|
Local LocalDateTime `toml:"local"`
|
|
Day LocalDate `toml:"day"`
|
|
Clock LocalTime `toml:"clock"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Offset: time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC),
|
|
Local: LocalDateTime{Time: time.Date(2026, 6, 26, 7, 32, 0, 0, time.UTC)},
|
|
Day: LocalDate{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)},
|
|
Clock: LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "offset = 2026-06-26T10:00Z\nlocal = 2026-06-26T07:32\nday = 2026-06-26\nclock = 07:32\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalDateTimeFractional(t *testing.T) {
|
|
out, err := Marshal(struct {
|
|
LDT LocalDateTime `toml:"ldt"`
|
|
LT LocalTime `toml:"lt"`
|
|
}{
|
|
LDT: LocalDateTime{Time: time.Date(2026, 6, 26, 7, 32, 0, 123456789, time.UTC)},
|
|
LT: LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 123, time.UTC)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "ldt = 2026-06-26T07:32:00.123456789\nlt = 07:32:00.000000123\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalArraysOfScalars(t *testing.T) {
|
|
type Cfg struct {
|
|
Tags []string `toml:"tags"`
|
|
Ports []int `toml:"ports"`
|
|
Mixed []any `toml:"mixed"`
|
|
Empty []int `toml:"empty"`
|
|
EmptyS []string `toml:"empty_s"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Tags: []string{"a", "b"},
|
|
Ports: []int{80, 443},
|
|
Mixed: []any{int64(1), "x", true},
|
|
Empty: nil,
|
|
EmptyS: []string{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "tags = [\"a\", \"b\"]\nports = [80, 443]\nmixed = [1, \"x\", true]\nempty_s = []\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNestedArrays(t *testing.T) {
|
|
type Cfg struct {
|
|
Matrix [][]int `toml:"matrix"`
|
|
Words [][]string `toml:"words"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Matrix: [][]int{{1, 2}, {3, 4}},
|
|
Words: [][]string{{"a", "b"}, {"c"}},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "matrix = [[1, 2], [3, 4]]\nwords = [[\"a\", \"b\"], [\"c\"]]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalMixedArrayWithInlineTable(t *testing.T) {
|
|
// Parse accepts a mixed array (TOML allows any value kinds in one array),
|
|
// so Marshal of the parsed tree must re-emit it. The table element has no
|
|
// header form inside a value array and renders inline.
|
|
tree, err := ParseMap([]byte("arr = [1, {a = 2}, \"x\"]\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
out, err := Marshal(tree)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "arr = [1, {a = 2}, \"x\"]\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
re, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("re-parse: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(tree, re) {
|
|
t.Fatalf("round-trip changed the tree:\nwas: %#v\nnow: %#v", tree, re)
|
|
}
|
|
}
|
|
|
|
// A []any of tables is what Parse produces for a value array of inline
|
|
// tables; it must stay in the value-array form, or the output would re-parse
|
|
// as []map[string]any and the round-trip would change the value's type.
|
|
func TestMarshalValueArrayOfTablesStaysInline(t *testing.T) {
|
|
for _, doc := range []string{
|
|
"0=[{}]",
|
|
"a = [{x = 1}, {x = 2}]\n",
|
|
"b = [{x = 1}, 2, \"three\"]\n",
|
|
} {
|
|
tree, err := ParseMap([]byte(doc))
|
|
if err != nil {
|
|
t.Fatalf("%s: parse: %v", doc, err)
|
|
}
|
|
out, err := Marshal(tree)
|
|
if err != nil {
|
|
t.Fatalf("%s: marshal: %v", doc, err)
|
|
}
|
|
if bytes.HasPrefix(out, []byte("[[")) {
|
|
t.Errorf("%s: emitted the [[header]] form for a value array:\n%s", doc, out)
|
|
}
|
|
re, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("%s: re-parse: %v\ndoc:\n%s", doc, err, out)
|
|
}
|
|
if !tomlEqual(tree, re) {
|
|
t.Errorf("%s: round-trip changed the tree:\nwas: %#v\nnow: %#v\ndoc:\n%s", doc, tree, re, out)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarshalNestedInlineTables(t *testing.T) {
|
|
tree := map[string]any{
|
|
"mix": []any{
|
|
int64(1),
|
|
map[string]any{"deep": map[string]any{"n": int64(0)}, "list": []any{"a", true}},
|
|
map[string]any{},
|
|
},
|
|
}
|
|
out, err := Marshal(tree)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "mix = [1, {deep = {n = 0}, list = [\"a\", true]}, {}]\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalInlineTableWithDatetime(t *testing.T) {
|
|
when := time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)
|
|
tree := map[string]any{
|
|
"mix": []any{when, map[string]any{"t": LocalDateTime{when}}},
|
|
}
|
|
out, err := Marshal(tree)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "mix = [1979-05-27T07:32Z, {t = 1979-05-27T07:32}]\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalArrayOfTablesStaysHeaderForm(t *testing.T) {
|
|
tree, err := ParseMap([]byte("[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
out, err := Marshal(tree)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[[items]]\nname = \"a\"\n\n[[items]]\nname = \"b\"\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalFloatExponentNoLeadingZero(t *testing.T) {
|
|
// strconv.FormatFloat with 'g' would produce "1e+06" (leading zero in
|
|
// exponent). The encoder must strip it so the output is "1e+6".
|
|
type Cfg struct {
|
|
Large float64 `toml:"large"`
|
|
Small float64 `toml:"small"`
|
|
}
|
|
out, err := Marshal(Cfg{Large: 1e6, Small: 1e-5})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// Parse to check the output is valid TOML (for a strict parser that
|
|
// rejects leading zeros in exponents).
|
|
if _, err := ParseMap(out); err != nil {
|
|
t.Fatalf("marshalled output is not valid TOML:\n%s\nerror: %v", out, err)
|
|
}
|
|
if string(out) != "large = 1e+6\nsmall = 1e-5\n" {
|
|
t.Errorf("output mismatch:\ngot: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestMarshalStructAsTable(t *testing.T) {
|
|
type Server struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
}
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Server Server `toml:"server"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Title: "demo",
|
|
Server: Server{Host: "127.0.0.1", Port: 9090},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "title = \"demo\"\n\n[server]\nhost = \"127.0.0.1\"\nport = 9090\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalArrayOfTables(t *testing.T) {
|
|
type Item struct {
|
|
Name string `toml:"name"`
|
|
Qty int `toml:"qty"`
|
|
}
|
|
type Cfg struct {
|
|
Items []Item `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Items: []Item{
|
|
{Name: "a", Qty: 1},
|
|
{Name: "b", Qty: 2},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[[items]]\nname = \"a\"\nqty = 1\n\n[[items]]\nname = \"b\"\nqty = 2\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalEmptyArrayOfTablesIsSkipped(t *testing.T) {
|
|
type Item struct {
|
|
Name string `toml:"name"`
|
|
}
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Items []Item `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Title: "demo",
|
|
Items: nil,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "title = \"demo\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNestedTablesAndArrays(t *testing.T) {
|
|
type SMTP struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
}
|
|
type Form struct {
|
|
Name string `toml:"name"`
|
|
SMTP SMTP `toml:"smtp"`
|
|
}
|
|
type Cfg struct {
|
|
Port int `toml:"port"`
|
|
Forms []Form `toml:"forms"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Port: 8080,
|
|
Forms: []Form{
|
|
{Name: "contact", SMTP: SMTP{Host: "h1", Port: 587}},
|
|
{Name: "feedback", SMTP: SMTP{Host: "h2", Port: 25}},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "port = 8080\n\n[[forms]]\nname = \"contact\"\n\n[forms.smtp]\nhost = \"h1\"\nport = 587\n\n[[forms]]\nname = \"feedback\"\n\n[forms.smtp]\nhost = \"h2\"\nport = 25\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalStructTags(t *testing.T) {
|
|
type Cfg struct {
|
|
Keep string `toml:"keep"`
|
|
Rename string `toml:"renamed"`
|
|
Skip string `toml:"-"`
|
|
Untagged string
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Keep: "k", Rename: "r", Skip: "s", Untagged: "u",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "keep = \"k\"\nrenamed = \"r\"\nuntagged = \"u\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalEmbeddedStructPromoted(t *testing.T) {
|
|
type Base struct {
|
|
ID int `toml:"id"`
|
|
}
|
|
type Derived struct {
|
|
Base
|
|
Name string `toml:"name"`
|
|
}
|
|
out, err := Marshal(Derived{ID: 1, Name: "x"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "id = 1\nname = \"x\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalEmbeddedStructAsTable(t *testing.T) {
|
|
type Inner struct {
|
|
Host string `toml:"host"`
|
|
}
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner"`
|
|
Name string `toml:"name"`
|
|
}
|
|
out, err := Marshal(Cfg{Inner: Inner{Host: "h"}, Name: "n"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"n\"\n\n[inner]\nhost = \"h\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTagOptionOmitZero(t *testing.T) {
|
|
type Server struct {
|
|
Host string `toml:"host"`
|
|
}
|
|
type Cfg struct {
|
|
Name string `toml:"name,omitzero"`
|
|
Count int `toml:"count,omitzero"`
|
|
Ratio float64 `toml:"ratio,omitzero"`
|
|
When time.Time `toml:"when,omitzero"`
|
|
Server Server `toml:"server,omitzero"`
|
|
Always string `toml:"always"`
|
|
}
|
|
out, err := Marshal(Cfg{Always: "kept"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// Every omitzero field sits at its zero value, so only always is emitted.
|
|
want := "always = \"kept\"\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
when := time.Date(2026, 9, 17, 12, 0, 0, 0, time.UTC)
|
|
out, err = Marshal(Cfg{Name: "x", Count: 1, Ratio: 0.5, When: when, Server: Server{Host: "h"}, Always: "kept"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want = "name = \"x\"\ncount = 1\nratio = 0.5\nwhen = 2026-09-17T12:00Z\nalways = \"kept\"\n\n[server]\nhost = \"h\"\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTagOptionOmitEmpty(t *testing.T) {
|
|
type Cfg struct {
|
|
Tags []string `toml:"tags,omitempty"`
|
|
Ports []int `toml:"ports,omitempty"`
|
|
Matrix [][]int `toml:"matrix,omitempty"`
|
|
Extra map[string]any `toml:"extra,omitempty"`
|
|
Name string `toml:"name,omitempty"`
|
|
Keep []string `toml:"keep"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Ports: []int{},
|
|
Matrix: [][]int{{1}},
|
|
Extra: map[string]any{},
|
|
Name: "set",
|
|
Keep: []string{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// tags is nil (omitted anyway), ports and extra are empty collections
|
|
// dropped by omitempty, matrix is populated, name is a string the option
|
|
// does not cover, keep is empty but carries no option so it emits [].
|
|
want := "matrix = [[1]]\nname = \"set\"\nkeep = []\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTagOptionOnTaggedEmbeddedStruct(t *testing.T) {
|
|
type Inner struct {
|
|
N int `toml:"n"`
|
|
}
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner,omitzero"`
|
|
Name string `toml:"name"`
|
|
}
|
|
out, err := Marshal(Cfg{Name: "x"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"x\"\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalMapKeysSorted(t *testing.T) {
|
|
m := map[string]any{
|
|
"zeta": 1,
|
|
"alpha": 2,
|
|
"mu": 3,
|
|
}
|
|
out, err := Marshal(m)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "alpha = 2\nmu = 3\nzeta = 1\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalMapWithSubMap(t *testing.T) {
|
|
m := map[string]any{
|
|
"meta": map[string]any{"x": 1, "y": 2},
|
|
"a": "z",
|
|
}
|
|
out, err := Marshal(m)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "a = \"z\"\n\n[meta]\nx = 1\ny = 2\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalMarshaler(t *testing.T) {
|
|
type Port int
|
|
type Cfg struct {
|
|
P Port `toml:"p"`
|
|
}
|
|
out, err := Marshal(Cfg{P: 8080})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "p = 8080\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalMarshalerReturningScalar(t *testing.T) {
|
|
type Wrapped struct {
|
|
Value string `toml:"value"`
|
|
}
|
|
type Alias struct{}
|
|
out, err := Marshal(struct {
|
|
W Wrapped `toml:"w"`
|
|
}{W: Wrapped{Value: "hello"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[w]\nvalue = \"hello\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
_ = Alias{}
|
|
}
|
|
|
|
func TestMarshalMarshalerReturningDifferentShape(t *testing.T) {
|
|
out, err := Marshal(struct {
|
|
C Custom `toml:"c"`
|
|
}{C: Custom{tag: "x"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// Custom returns a string from MarshalTOML.
|
|
want := "c = \"x\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNilPointerFieldSkipped(t *testing.T) {
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
Hidden *string `toml:"hidden"`
|
|
}
|
|
out, err := Marshal(Cfg{Name: "x"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"x\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNonNilPointerFollowed(t *testing.T) {
|
|
v := "v"
|
|
type Cfg struct {
|
|
Name string `toml:"name"`
|
|
Hidden *string `toml:"hidden"`
|
|
}
|
|
out, err := Marshal(Cfg{Name: "n", Hidden: &v})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "name = \"n\"\nhidden = \"v\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTopLevelMustBeStructOrMap(t *testing.T) {
|
|
if _, err := Marshal(42); err == nil {
|
|
t.Errorf("expected error marshalling int at top level")
|
|
}
|
|
if _, err := Marshal("hello"); err == nil {
|
|
t.Errorf("expected error marshalling string at top level")
|
|
}
|
|
if _, err := Marshal(nil); err == nil {
|
|
t.Errorf("expected error marshalling nil")
|
|
}
|
|
}
|
|
|
|
func TestMarshalMapKeyMustBeString(t *testing.T) {
|
|
m := map[int]any{1: "x"}
|
|
if _, err := Marshal(m); err == nil {
|
|
t.Errorf("expected error for non-string map key")
|
|
}
|
|
}
|
|
|
|
func TestMarshalUnexportedFieldSkipped(t *testing.T) {
|
|
type Cfg struct {
|
|
Pub string `toml:"pub"`
|
|
priv string
|
|
}
|
|
out, err := Marshal(Cfg{Pub: "p", priv: "s"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "pub = \"p\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalBareAndQuotedKeys(t *testing.T) {
|
|
type Cfg struct {
|
|
Bare string `toml:"bare_key"`
|
|
Dash string `toml:"with-dash"`
|
|
Num string `toml:"num123"`
|
|
Q string `toml:"needs space"`
|
|
Dot string `toml:"needs.dot"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Bare: "a", Dash: "b", Num: "c", Q: "d", Dot: "e",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "bare_key = \"a\"\nwith-dash = \"b\"\nnum123 = \"c\"\n\"needs space\" = \"d\"\n\"needs.dot\" = \"e\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalThenParseRoundTrip(t *testing.T) {
|
|
type Server struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
Enabled bool `toml:"enabled"`
|
|
Tags []string `toml:"tags"`
|
|
}
|
|
type Form struct {
|
|
Name string `toml:"name"`
|
|
Allowed []string `toml:"allowed"`
|
|
}
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Count int `toml:"count"`
|
|
Ratio float64 `toml:"ratio"`
|
|
Server Server `toml:"server"`
|
|
Forms []Form `toml:"forms"`
|
|
Due time.Time `toml:"due"`
|
|
Day LocalDate `toml:"day"`
|
|
}
|
|
in := Cfg{
|
|
Title: "demo",
|
|
Count: 42,
|
|
Ratio: 3.14,
|
|
Server: Server{
|
|
Host: "127.0.0.1", Port: 9090, Enabled: true,
|
|
Tags: []string{"a", "b"},
|
|
},
|
|
Forms: []Form{
|
|
{Name: "contact", Allowed: []string{"x"}},
|
|
{Name: "feedback", Allowed: nil},
|
|
},
|
|
Due: time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC),
|
|
Day: LocalDate{Time: time.Date(2026, 6, 26, 0, 0, 0, 0, time.UTC)},
|
|
}
|
|
out, err := Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
tree1, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("parse of marshalled: %v\noutput:\n%s", err, out)
|
|
}
|
|
// Decode back into the struct.
|
|
var out2 Cfg
|
|
if err := Unmarshal(out, &out2); err != nil {
|
|
t.Fatalf("unmarshal of marshalled: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(in, out2) {
|
|
t.Errorf("round-trip mismatch:\nin: %#v\nout: %#v", in, out2)
|
|
}
|
|
_ = tree1
|
|
}
|
|
|
|
func TestMarshalRoundTripFromUntypedTree(t *testing.T) {
|
|
src := []byte(`title = "demo"
|
|
count = 42
|
|
ratio = 3.14
|
|
enabled = true
|
|
|
|
[server]
|
|
host = "127.0.0.1"
|
|
port = 9090
|
|
|
|
[[items]]
|
|
name = "a"
|
|
qty = 1
|
|
|
|
[[items]]
|
|
name = "b"
|
|
qty = 2
|
|
|
|
[meta]
|
|
created = 2026-06-26T10:00:00Z
|
|
|
|
mixed = [1, {n = 1, name = "a value long enough to push this line well past the one hundred column limit"}]
|
|
`)
|
|
tree1, err := ParseMap(src)
|
|
if err != nil {
|
|
t.Fatalf("parse src: %v", err)
|
|
}
|
|
out, err := Marshal(tree1)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
tree2, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("re-parse marshalled: %v\noutput:\n%s", err, out)
|
|
}
|
|
if !reflect.DeepEqual(tree1, tree2) {
|
|
t.Errorf("round-trip mismatch:\nbefore: %#v\nafter: %#v", tree1, tree2)
|
|
}
|
|
}
|
|
|
|
func TestMarshalUintOverflow(t *testing.T) {
|
|
type Cfg struct {
|
|
Big uint64 `toml:"big"`
|
|
}
|
|
if _, err := Marshal(Cfg{Big: 1<<63 + 1}); err == nil {
|
|
t.Errorf("expected overflow error")
|
|
}
|
|
}
|
|
|
|
func TestMarshalKeyRequiresUTF8(t *testing.T) {
|
|
m := map[string]any{"\xff": "x"}
|
|
if _, err := Marshal(m); err == nil {
|
|
t.Errorf("expected error for invalid UTF-8 key")
|
|
}
|
|
// The check must reach the keys of table headers and of inline tables
|
|
// nested inside value arrays, not only scalar keys: both write keys
|
|
// through the same path.
|
|
nested := map[string]any{"\xff": map[string]any{"k": "v"}}
|
|
if _, err := Marshal(nested); err == nil {
|
|
t.Errorf("expected error for invalid UTF-8 table header key")
|
|
}
|
|
inline := map[string]any{"mix": []any{1, map[string]any{"\xff": 1}}}
|
|
if _, err := Marshal(inline); err == nil {
|
|
t.Errorf("expected error for invalid UTF-8 inline table key")
|
|
}
|
|
}
|
|
|
|
func TestMarshalStringRequiresUTF8(t *testing.T) {
|
|
type Cfg struct {
|
|
S string `toml:"s"`
|
|
}
|
|
if _, err := Marshal(Cfg{S: "abc\xff"}); err == nil {
|
|
t.Errorf("expected error for invalid UTF-8 string")
|
|
}
|
|
}
|
|
|
|
func TestLocalDateString(t *testing.T) {
|
|
ld := LocalDate{Time: time.Date(1979, 5, 27, 0, 0, 0, 0, time.UTC)}
|
|
if got := ld.String(); got != "1979-05-27" {
|
|
t.Errorf("LocalDate.String() = %q, want 1979-05-27", got)
|
|
}
|
|
}
|
|
|
|
func TestLocalDateTimeString(t *testing.T) {
|
|
// The rendering drops zero seconds and the trailing zeros of a fraction,
|
|
// which TOML 1.1 allows and which keeps a value written without seconds
|
|
// written without them.
|
|
ldt := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)}
|
|
if got := ldt.String(); got != "1979-05-27T07:32" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32", got)
|
|
}
|
|
ldt2 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 5, time.UTC)}
|
|
if got := ldt2.String(); got != "1979-05-27T07:32:00.000000005" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000005", got)
|
|
}
|
|
ldt3 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 500, time.UTC)}
|
|
if got := ldt3.String(); got != "1979-05-27T07:32:00.0000005" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.0000005", got)
|
|
}
|
|
ldt4 := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 30, 500000000, time.UTC)}
|
|
if got := ldt4.String(); got != "1979-05-27T07:32:30.5" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:30.5", got)
|
|
}
|
|
}
|
|
|
|
func TestLocalTimeString(t *testing.T) {
|
|
lt := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)}
|
|
if got := lt.String(); got != "07:32" {
|
|
t.Errorf("LocalTime.String() = %q, want 07:32", got)
|
|
}
|
|
lt2 := LocalTime{Time: time.Date(0, 1, 1, 7, 32, 15, 250000000, time.UTC)}
|
|
if got := lt2.String(); got != "07:32:15.25" {
|
|
t.Errorf("LocalTime.String() = %q, want 07:32:15.25", got)
|
|
}
|
|
}
|
|
|
|
func TestEncoderEquivalenceToMarshal(t *testing.T) {
|
|
type Cfg struct {
|
|
Title string `toml:"title"`
|
|
Count int `toml:"count"`
|
|
}
|
|
in := Cfg{Title: "x", Count: 7}
|
|
a, err := Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
b, err := Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("encoder marshal: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(a, b) {
|
|
t.Errorf("Marshal and Encoder disagree:\n%s\n%s", a, b)
|
|
}
|
|
}
|
|
|
|
type Custom struct {
|
|
tag string
|
|
}
|
|
|
|
func (c Custom) MarshalTOML() (any, error) { return c.tag, nil }
|
|
|
|
// encodeErrBad is a Marshaler whose MarshalTOML always fails.
|
|
type encodeErrBad struct {
|
|
msg string
|
|
}
|
|
|
|
func (encodeErrBad) MarshalTOML() (any, error) { return nil, errors.New("bad timestamp") }
|
|
|
|
func TestEncodeErrorCarriesPath(t *testing.T) {
|
|
type Inner struct {
|
|
Port encodeErrBad `toml:"port"`
|
|
}
|
|
type Cfg struct {
|
|
Server Inner `toml:"server"`
|
|
}
|
|
_, err := Marshal(Cfg{Server: Inner{Port: encodeErrBad{}}})
|
|
if err == nil {
|
|
t.Fatal("expected a marshal error")
|
|
}
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "server.port" {
|
|
t.Fatalf("Path = %v, want server.port", ee.Path)
|
|
}
|
|
if ee.Err == nil || ee.Err.Error() != "bad timestamp" {
|
|
t.Fatalf("Err = %v", ee.Err)
|
|
}
|
|
if err.Error() != "interpres: server.port: bad timestamp" {
|
|
t.Fatalf("message = %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestEncodeErrorTopLevelPathHasNoLeadingDot(t *testing.T) {
|
|
type Cfg struct {
|
|
Port encodeErrBad `toml:"port"`
|
|
}
|
|
_, err := Marshal(Cfg{})
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "port" {
|
|
t.Fatalf("Path = %v, want port", ee.Path)
|
|
}
|
|
if err.Error() != "interpres: port: bad timestamp" {
|
|
t.Fatalf("message = %q", err.Error())
|
|
}
|
|
}
|
|
|
|
func TestEncodeErrorHeterogeneousArrayPath(t *testing.T) {
|
|
type Item struct {
|
|
N int `toml:"n"`
|
|
}
|
|
cfg := map[string]any{
|
|
"items": []any{Item{}, 3},
|
|
}
|
|
_, err := Marshal(cfg)
|
|
if err == nil {
|
|
t.Fatal("expected a heterogeneous array error")
|
|
}
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "items[0]" {
|
|
t.Fatalf("Path = %v, want items[0]", ee.Path)
|
|
}
|
|
}
|
|
|
|
// --- encoding.TextMarshaler and time.Duration ------------------------------
|
|
|
|
// textTag is a value-receiver encoding.TextMarshaler, so the encoder finds the
|
|
// method on the value itself.
|
|
type textTag string
|
|
|
|
func (t textTag) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil }
|
|
|
|
// textPointer carries MarshalText on the pointer receiver only, so the encoder
|
|
// has to look at the address of an addressable field.
|
|
type textPointer struct{ V string }
|
|
|
|
func (t *textPointer) MarshalText() ([]byte, error) { return []byte(strings.ToUpper(t.V)), nil }
|
|
|
|
// textAndTOML implements both encoding interfaces; the TOML method wins.
|
|
type textAndTOML struct{}
|
|
|
|
func (textAndTOML) MarshalTOML() (any, error) { return "toml", nil }
|
|
func (textAndTOML) MarshalText() ([]byte, error) { return []byte("text"), nil }
|
|
|
|
// brokenText fails the marshal from MarshalText.
|
|
type brokenText struct{}
|
|
|
|
func (brokenText) MarshalText() ([]byte, error) { return nil, errors.New("text boom") }
|
|
|
|
// notUTF8 renders bytes that no TOML string can carry.
|
|
type notUTF8 struct{}
|
|
|
|
func (notUTF8) MarshalText() ([]byte, error) { return []byte{0xff, 0xfe}, nil }
|
|
|
|
// textTagBoth renders itself with a prefix and strips it again on decode, so
|
|
// the round trip through a TOML string is lossless.
|
|
type textTagBoth string
|
|
|
|
func (t textTagBoth) MarshalText() ([]byte, error) { return []byte("tag:" + string(t)), nil }
|
|
|
|
func (t *textTagBoth) UnmarshalText(text []byte) error {
|
|
trimmed, ok := strings.CutPrefix(string(text), "tag:")
|
|
if !ok {
|
|
return errors.New("textTagBoth: missing the tag prefix")
|
|
}
|
|
*t = textTagBoth(trimmed)
|
|
return nil
|
|
}
|
|
|
|
func TestMarshalTextValues(t *testing.T) {
|
|
// The pointer receiver is reachable only through an addressable field, so
|
|
// the whole value is marshalled through a pointer here.
|
|
type Cfg struct {
|
|
IP net.IP `toml:"ip"`
|
|
Duration time.Duration `toml:"duration"`
|
|
Tag textTag `toml:"tag"`
|
|
Pointer textPointer `toml:"pointer"`
|
|
Both textAndTOML `toml:"both"`
|
|
}
|
|
out, err := Marshal(&Cfg{
|
|
IP: net.IPv4(192, 0, 2, 1),
|
|
Duration: 90 * time.Minute,
|
|
Tag: "x",
|
|
Pointer: textPointer{V: "abc"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "ip = \"192.0.2.1\"\nduration = \"1h30m0s\"\ntag = \"tag:x\"\npointer = \"ABC\"\nboth = \"toml\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextValuesInContainers(t *testing.T) {
|
|
// Slice elements are addressable, so a pointer-receiver MarshalText is used
|
|
// there too, and an array of such values stays a value array: each element's
|
|
// TOML form is a string, so the [[header]] form cannot carry it.
|
|
type Cfg struct {
|
|
Map map[string]net.IP `toml:"map"`
|
|
Durs []time.Duration `toml:"durs"`
|
|
Ptrs []textPointer `toml:"ptrs"`
|
|
Empty []textPointer `toml:"empty"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Map: map[string]net.IP{"a": net.IPv4(10, 0, 0, 1)},
|
|
Durs: []time.Duration{0, 250 * time.Millisecond},
|
|
Ptrs: []textPointer{{V: "a"}, {V: "b"}},
|
|
Empty: []textPointer{},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "durs = [\"0s\", \"250ms\"]\nptrs = [\"A\", \"B\"]\nempty = []\n\n[map]\na = \"10.0.0.1\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextLeavesDateTimesAlone(t *testing.T) {
|
|
// The four date-time types carry time.Time's text methods through an
|
|
// embedded field; their TOML form is a bare atom, never a quoted string.
|
|
stamp := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
|
type Cfg struct {
|
|
Stamp time.Time `toml:"stamp"`
|
|
Ptr *time.Time `toml:"ptr"`
|
|
Day LocalDate `toml:"day"`
|
|
At LocalDateTime `toml:"at"`
|
|
Clock LocalTime `toml:"clock"`
|
|
}
|
|
out, err := Marshal(&Cfg{
|
|
Stamp: stamp,
|
|
Ptr: &stamp,
|
|
Day: LocalDate{time.Date(1979, 5, 27, 0, 0, 0, 0, time.UTC)},
|
|
At: LocalDateTime{time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)},
|
|
Clock: LocalTime{time.Date(0, 1, 1, 7, 32, 0, 0, time.UTC)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "stamp = 2026-06-26T10:00Z\nptr = 2026-06-26T10:00Z\nday = 1979-05-27\nat = 1979-05-27T07:32\nclock = 07:32\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextNilPointerOmitted(t *testing.T) {
|
|
type Cfg struct {
|
|
P *textPointer `toml:"p"`
|
|
K string `toml:"k"`
|
|
}
|
|
out, err := Marshal(&Cfg{K: "x"})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if want := "k = \"x\"\n"; string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextErrorCarriesPath(t *testing.T) {
|
|
type Inner struct {
|
|
F brokenText `toml:"f"`
|
|
}
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner"`
|
|
}
|
|
_, err := Marshal(Cfg{})
|
|
if err == nil {
|
|
t.Fatal("expected an error from MarshalText")
|
|
}
|
|
if !strings.Contains(err.Error(), "text boom") {
|
|
t.Errorf("err = %v, want substring \"text boom\"", err)
|
|
}
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "inner.f" {
|
|
t.Fatalf("Path = %v, want inner.f", ee.Path)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextRejectsInvalidUTF8(t *testing.T) {
|
|
// A TOML string holds UTF-8 only, so text that is not gets an error rather
|
|
// than replacement characters.
|
|
_, err := Marshal(struct {
|
|
V notUTF8 `toml:"v"`
|
|
}{})
|
|
if err == nil {
|
|
t.Fatal("expected an error for text that is not valid UTF-8")
|
|
}
|
|
if !strings.Contains(err.Error(), "UTF-8") {
|
|
t.Errorf("err = %v, want a UTF-8 message", err)
|
|
}
|
|
}
|
|
|
|
func TestMarshalTextValuesRoundTrip(t *testing.T) {
|
|
type Cfg struct {
|
|
Duration time.Duration `toml:"duration"`
|
|
IP net.IP `toml:"ip"`
|
|
Tag textTagBoth `toml:"tag"`
|
|
}
|
|
in := Cfg{Duration: 90 * time.Minute, IP: net.IPv4(198, 51, 100, 7), Tag: "y"}
|
|
out, err := Marshal(&in)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var back Cfg
|
|
if err := Unmarshal(out, &back); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if back.Duration != in.Duration {
|
|
t.Errorf("Duration = %v, want %v", back.Duration, in.Duration)
|
|
}
|
|
if !back.IP.Equal(in.IP) {
|
|
t.Errorf("IP = %v, want %v", back.IP, in.IP)
|
|
}
|
|
if back.Tag != in.Tag {
|
|
t.Errorf("Tag = %q, want %q", back.Tag, in.Tag)
|
|
}
|
|
}
|
|
|
|
// --- TOML 1.1 output forms -------------------------------------------------
|
|
|
|
func TestMarshalDateTimeRendering(t *testing.T) {
|
|
// The seconds are written only when the value carries them, and a fraction
|
|
// drops its trailing zeros. Both are the same value either way; the shorter
|
|
// form is the one TOML 1.1 allows.
|
|
base := time.Date(2026, 6, 26, 10, 0, 0, 0, time.UTC)
|
|
cases := []struct {
|
|
name string
|
|
val any
|
|
want string
|
|
}{
|
|
{"offset-zero-seconds", base, "v = 2026-06-26T10:00Z\n"},
|
|
{"offset-wrapper", OffsetDateTime{Time: base}, "v = 2026-06-26T10:00Z\n"},
|
|
{"offset-seconds", base.Add(30 * time.Second), "v = 2026-06-26T10:00:30Z\n"},
|
|
{"offset-fraction", base.Add(500 * time.Millisecond), "v = 2026-06-26T10:00:00.5Z\n"},
|
|
{"offset-zone", time.Date(2026, 6, 26, 10, 0, 0, 0, time.FixedZone("", -7*3600)), "v = 2026-06-26T10:00-07:00\n"},
|
|
{"local-zero-seconds", LocalDateTime{Time: base}, "v = 2026-06-26T10:00\n"},
|
|
{"local-fraction", LocalDateTime{Time: base.Add(2500 * time.Millisecond)}, "v = 2026-06-26T10:00:02.5\n"},
|
|
{"date", LocalDate{Time: base}, "v = 2026-06-26\n"},
|
|
{"time-zero-seconds", LocalTime{Time: base}, "v = 10:00\n"},
|
|
{"time-seconds", LocalTime{Time: base.Add(15 * time.Second)}, "v = 10:00:15\n"},
|
|
{"time-nanoseconds", LocalTime{Time: base.Add(123456789 * time.Nanosecond)}, "v = 10:00:00.123456789\n"},
|
|
}
|
|
for _, c := range cases {
|
|
out, err := Marshal(map[string]any{"v": c.val})
|
|
if err != nil {
|
|
t.Errorf("%s: marshal: %v", c.name, err)
|
|
continue
|
|
}
|
|
if string(out) != c.want {
|
|
t.Errorf("%s: output mismatch:\ngot: %q\nwant: %q", c.name, out, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMarshalInlineTableBreaksWhenLong(t *testing.T) {
|
|
// A table element of a value array is written inline; a long one carries
|
|
// newlines and a trailing comma instead of running past the line limit,
|
|
// which TOML 1.1 allows an inline table to do.
|
|
const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit"
|
|
type Cfg struct {
|
|
Arr []any `toml:"arr"`
|
|
}
|
|
out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": long}}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "arr = [1, {\n\tn = 1,\n\tname = \"" + long + "\",\n}]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
// The same values without the long string stay on one line.
|
|
out, err = Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "name": "short"}}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if want := "arr = [1, {n = 1, name = \"short\"}]\n"; string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
// The broken form parses back to the same tree.
|
|
tree, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("parse of the encoder output: %v", err)
|
|
}
|
|
if got := len(tree["arr"].([]any)); got != 2 {
|
|
t.Fatalf("arr has %d elements, want 2", got)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNestedInlineTableBreaksIndependently(t *testing.T) {
|
|
// A nested table breaks on its own measure, so a table whose entries stay
|
|
// short keeps the one-line form inside a parent that broke.
|
|
const long = "a-very-long-value-that-pushes-the-line-well-past-the-one-hundred-column-limit"
|
|
type Cfg struct {
|
|
Arr []any `toml:"arr"`
|
|
}
|
|
out, err := Marshal(Cfg{Arr: []any{int64(1), map[string]any{"n": int64(1), "sub": map[string]any{"name": long}}}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "arr = [1, {\n\tn = 1,\n\tsub = {name = \"" + long + "\"},\n}]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
type inlineTLS struct {
|
|
On bool `toml:"on"`
|
|
}
|
|
|
|
type inlineServer struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
TLS inlineTLS `toml:"tls"`
|
|
}
|
|
|
|
type inlineBig struct {
|
|
A int `toml:"a"`
|
|
B int `toml:"b"`
|
|
C int `toml:"c"`
|
|
}
|
|
|
|
func TestEncoderInlineTables(t *testing.T) {
|
|
type Cfg struct {
|
|
Server inlineServer `toml:"server"`
|
|
Big inlineBig `toml:"big"`
|
|
}
|
|
cfg := Cfg{Server: inlineServer{Host: "127.0.0.1", Port: 9090}, Big: inlineBig{A: 1, B: 2, C: 3}}
|
|
|
|
// The default keeps every sub-table a header section.
|
|
headerForm, err := Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[server]\nhost = \"127.0.0.1\"\nport = 9090\n\n[server.tls]\non = false\n\n[big]\na = 1\nb = 2\nc = 3\n"
|
|
if string(headerForm) != want {
|
|
t.Errorf("default output mismatch:\ngot: %q\nwant: %q", headerForm, want)
|
|
}
|
|
|
|
// With the option both fit the threshold and become inline tables, nested
|
|
// ones included.
|
|
out, err := Marshal(cfg, InlineTables(60))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want = "server = {host = \"127.0.0.1\", port = 9090, tls = {on = false}}\nbig = {a = 1, b = 2, c = 3}\n"
|
|
if string(out) != want {
|
|
t.Errorf("compact output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
// A threshold below the rendering keeps the header form.
|
|
out, err = Marshal(cfg, InlineTables(10))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if string(out) != string(headerForm) {
|
|
t.Errorf("small threshold output mismatch:\ngot: %q\nwant: %q", out, headerForm)
|
|
}
|
|
}
|
|
|
|
func TestEncoderInlineTablesOrderAndRoundTrip(t *testing.T) {
|
|
// An inlined sub-table is a value line, so it precedes every header of the
|
|
// document; written after a header it would be read back as part of that
|
|
// table. The compact form and the header form parse to the same tree.
|
|
type Four struct {
|
|
A int `toml:"a"`
|
|
B int `toml:"b"`
|
|
C int `toml:"c"`
|
|
D int `toml:"d"`
|
|
}
|
|
type Cfg struct {
|
|
Small inlineTLS `toml:"small"`
|
|
Big Four `toml:"big"`
|
|
}
|
|
cfg := Cfg{Small: inlineTLS{On: true}, Big: Four{A: 1, B: 2, C: 3, D: 4}}
|
|
|
|
headerForm, err := Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
compact, err := Marshal(cfg, InlineTables(20))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "small = {on = true}\n\n[big]\na = 1\nb = 2\nc = 3\nd = 4\n"
|
|
if string(compact) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", compact, want)
|
|
}
|
|
|
|
got, err := ParseMap(compact)
|
|
if err != nil {
|
|
t.Fatalf("parse of the compact output: %v", err)
|
|
}
|
|
ref, err := ParseMap(headerForm)
|
|
if err != nil {
|
|
t.Fatalf("parse of the header output: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(got, ref) {
|
|
t.Errorf("the compact form changed the tree:\ncompact: %#v\nheaders: %#v", got, ref)
|
|
}
|
|
if _, ok := got["big"].(map[string]any); !ok {
|
|
t.Errorf("big = %#v, want a table", got["big"])
|
|
}
|
|
}
|
|
|
|
func TestEncoderInlineTablesKeepsArraysOfTables(t *testing.T) {
|
|
// An array of tables has no inline form that keeps the value's type, so the
|
|
// option leaves it alone and the tree keeps its []map[string]any shape.
|
|
type Item struct {
|
|
N int `toml:"n"`
|
|
}
|
|
type Cfg struct {
|
|
Items []Item `toml:"items"`
|
|
Small inlineTLS `toml:"small"`
|
|
}
|
|
cfg := Cfg{Items: []Item{{N: 1}}, Small: inlineTLS{On: true}}
|
|
out, err := Marshal(cfg, InlineTables(60))
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "small = {on = true}\n\n[[items]]\nn = 1\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
tree, err := ParseMap(out)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if _, ok := tree["items"].([]map[string]any); !ok {
|
|
t.Errorf("items = %#v, want []map[string]any", tree["items"])
|
|
}
|
|
}
|
|
|
|
// --- Marshaler on array elements -------------------------------------------
|
|
|
|
// countingMarshaler reports how often its method ran, so a test can check that
|
|
// the encoder resolves an element once.
|
|
type countingMarshaler struct{ calls *int }
|
|
|
|
func (c countingMarshaler) MarshalTOML() (any, error) {
|
|
*c.calls++
|
|
return map[string]any{"n": int64(*c.calls)}, nil
|
|
}
|
|
|
|
// ptrMarshaler carries MarshalTOML on the pointer receiver only.
|
|
type ptrMarshaler struct{ V string }
|
|
|
|
func (p *ptrMarshaler) MarshalTOML() (any, error) { return map[string]any{"v": p.V}, nil }
|
|
|
|
func TestMarshalerElementOfArrayOfTables(t *testing.T) {
|
|
// An element is classified by what MarshalTOML returns, so methods that
|
|
// render tables keep the [[header]] form the Go kind would have given them.
|
|
type Cfg struct {
|
|
Items []marshalerFunc `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{Items: []marshalerFunc{
|
|
func() (any, error) { return map[string]any{"k": "a"}, nil },
|
|
func() (any, error) { return map[string]any{"k": "b"}, nil },
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[[items]]\nk = \"a\"\n\n[[items]]\nk = \"b\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerElementScalarResultMakesValueArray(t *testing.T) {
|
|
// One element rendering itself as a scalar turns the whole array into a
|
|
// value array, with the table elements written inline.
|
|
type Cfg struct {
|
|
Items []marshalerFunc `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{Items: []marshalerFunc{
|
|
func() (any, error) { return map[string]any{"k": "a"}, nil },
|
|
func() (any, error) { return "x", nil },
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "items = [{k = \"a\"}, \"x\"]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerElementRunsOnce(t *testing.T) {
|
|
// Classification and emission share one result, so the method runs exactly
|
|
// once per element even when it decides the array's form.
|
|
calls := 0
|
|
type Cfg struct {
|
|
Items []countingMarshaler `toml:"items"`
|
|
}
|
|
out, err := Marshal(Cfg{Items: []countingMarshaler{{calls: &calls}, {calls: &calls}, {calls: &calls}}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if calls != 3 {
|
|
t.Errorf("MarshalTOML ran %d times, want 3", calls)
|
|
}
|
|
want := "[[items]]\nn = 1\n\n[[items]]\nn = 2\n\n[[items]]\nn = 3\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerPointerReceiverOnElement(t *testing.T) {
|
|
// A slice element is addressable, so a pointer-receiver MarshalTOML is
|
|
// found there, and the method's table keeps the [[header]] form.
|
|
type Cfg struct {
|
|
Items []ptrMarshaler `toml:"items"`
|
|
}
|
|
out, err := Marshal(&Cfg{Items: []ptrMarshaler{{V: "a"}, {V: "b"}}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "[[items]]\nv = \"a\"\n\n[[items]]\nv = \"b\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
|
|
// The same method is found on an addressable struct field, whose result is
|
|
// a table and so keeps a header section.
|
|
type Field struct {
|
|
F ptrMarshaler `toml:"f"`
|
|
}
|
|
out, err = Marshal(&Field{F: ptrMarshaler{V: "c"}})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if want := "[f]\nv = \"c\"\n"; string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerElementErrorCarriesPath(t *testing.T) {
|
|
type Cfg struct {
|
|
Items []any `toml:"items"`
|
|
}
|
|
_, err := Marshal(Cfg{Items: []any{map[string]any{"k": "a"}, failingMarshalerFunc{}}})
|
|
if err == nil {
|
|
t.Fatal("expected an error from MarshalTOML")
|
|
}
|
|
if !strings.Contains(err.Error(), "oops") {
|
|
t.Errorf("err = %v, want substring \"oops\"", err)
|
|
}
|
|
ee, ok := errors.AsType[*EncodeError](err)
|
|
if !ok {
|
|
t.Fatalf("expected an *EncodeError, got %T: %v", err, err)
|
|
}
|
|
if ee.Path.String() != "items[1]" {
|
|
t.Fatalf("Path = %v, want items[1]", ee.Path)
|
|
}
|
|
}
|
|
|
|
func TestMarshalerResultIsNormalised(t *testing.T) {
|
|
// A result is normalised like any other value, so a method may return a
|
|
// plain int or a duration where the Go kind alone would not encode.
|
|
type Cfg struct {
|
|
Plain marshalerFunc `toml:"plain"`
|
|
Duration marshalerFunc `toml:"duration"`
|
|
Elements []any `toml:"elements"`
|
|
}
|
|
out, err := Marshal(Cfg{
|
|
Plain: func() (any, error) { return 7, nil },
|
|
Duration: func() (any, error) { return 90 * time.Minute, nil },
|
|
Elements: []any{marshalerFunc(func() (any, error) { return 8, nil })},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
want := "plain = 7\nduration = \"1h30m0s\"\nelements = [8]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalNumber(t *testing.T) {
|
|
t.Run("the literal is written as it is", func(t *testing.T) {
|
|
out, err := Marshal(map[string]any{
|
|
"hex": Number("0x1f"), "sep": Number("1_000"),
|
|
"signed": Number("+1.0"), "inf": Number("inf"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "hex = 0x1f\ninf = inf\nsep = 1_000\nsigned = +1.0\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
})
|
|
t.Run("a Number field round-trips", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Rate Number `toml:"rate"`
|
|
}
|
|
out, err := Marshal(Cfg{Rate: "1_000"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "rate = 1_000\n" {
|
|
t.Fatalf("output %q", out)
|
|
}
|
|
var back map[string]any
|
|
if err := Unmarshal(out, &back, NumbersAsLiterals(true)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, ok := back["rate"].(Number); !ok || got != "1_000" {
|
|
t.Errorf("round trip = %#v, want Number(\"1_000\")", back["rate"])
|
|
}
|
|
})
|
|
t.Run("a Number inside a value array", func(t *testing.T) {
|
|
out, err := Marshal(map[string]any{"vals": []any{Number("0x1f"), "s", int64(2)}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "vals = [0x1f, \"s\", 2]\n"
|
|
if string(out) != want {
|
|
t.Errorf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
})
|
|
t.Run("an invalid literal is an error", func(t *testing.T) {
|
|
for _, lit := range []Number{"01", "1__0", "abc", "1.2.3"} {
|
|
if _, err := Marshal(map[string]any{"n": lit}); err == nil {
|
|
t.Errorf("Number(%q) encoded without an error", lit)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestMarshalAppend(t *testing.T) {
|
|
buf := []byte("preamble\n")
|
|
out, err := MarshalAppend(buf, map[string]any{"a": int64(1)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "preamble\na = 1\n"
|
|
if string(out) != want {
|
|
t.Errorf("output %q, want %q", out, want)
|
|
}
|
|
if &out[0] != &buf[0] {
|
|
t.Log("append reallocated; capacity differed")
|
|
}
|
|
out2, err := MarshalAppend(out, map[string]any{"b": true})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out2) != want+"b = true\n" {
|
|
t.Errorf("second append %q", out2)
|
|
}
|
|
buf = []byte("keep\n")
|
|
if out3, err := MarshalAppend(buf, Document{}); err == nil {
|
|
t.Errorf("MarshalAppend with an unencodable value = %q, want an error", out3)
|
|
}
|
|
}
|
|
|
|
func TestMarshalCyclicData(t *testing.T) {
|
|
t.Run("a cyclic struct is an error, not a crash", func(t *testing.T) {
|
|
type Node struct {
|
|
Name string `toml:"name"`
|
|
Next *Node `toml:"next"`
|
|
}
|
|
a := &Node{Name: "a"}
|
|
b := &Node{Name: "b"}
|
|
a.Next = b
|
|
b.Next = a
|
|
_, err := Marshal(a)
|
|
if err == nil {
|
|
t.Fatal("Marshal(cyclic) succeeded, want an error")
|
|
}
|
|
if !strings.Contains(err.Error(), "may be cyclic") {
|
|
t.Errorf("err = %v, want it to name the cycle", err)
|
|
}
|
|
})
|
|
t.Run("a cyclic map is an error", func(t *testing.T) {
|
|
m := map[string]any{}
|
|
m["self"] = m
|
|
if _, err := Marshal(m); err == nil {
|
|
t.Fatal("Marshal(cyclic map) succeeded, want an error")
|
|
}
|
|
})
|
|
t.Run("a cyclic value array is an error", func(t *testing.T) {
|
|
m := map[string]any{}
|
|
m["items"] = []any{int64(1), m}
|
|
if _, err := Marshal(map[string]any{"outer": m}); err == nil {
|
|
t.Fatal("Marshal(cyclic array) succeeded, want an error")
|
|
}
|
|
})
|
|
t.Run("a deeply nested but finite value encodes", func(t *testing.T) {
|
|
type Node struct {
|
|
Next *Node `toml:"next"`
|
|
}
|
|
root := &Node{}
|
|
cur := root
|
|
for range 5000 {
|
|
cur.Next = &Node{}
|
|
cur = cur.Next
|
|
}
|
|
if _, err := Marshal(root); err != nil {
|
|
t.Errorf("Marshal(deep) = %v, want nil", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestUnmarshalOptionsShape(t *testing.T) {
|
|
data := []byte("host = \"db\"\nextra = 1\n")
|
|
type Config struct {
|
|
Host string `toml:"host,required"`
|
|
}
|
|
t.Run("the zero value takes the defaults", func(t *testing.T) {
|
|
var cfg struct {
|
|
Host string `toml:"host"`
|
|
Extra int `toml:"extra"`
|
|
}
|
|
if err := Unmarshal(data, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Host != "db" || cfg.Extra != 1 {
|
|
t.Errorf("decoded %+v", cfg)
|
|
}
|
|
})
|
|
t.Run("strict and required work in one call", func(t *testing.T) {
|
|
err := Unmarshal(data, &Config{}, RejectUnknownFields(true))
|
|
want := `interpres: unknown field "extra" for interpres.Config`
|
|
if err == nil || err.Error() != want {
|
|
t.Errorf("err = %v, want %q", err, want)
|
|
}
|
|
})
|
|
t.Run("UseNumber keeps the literal", func(t *testing.T) {
|
|
var tree map[string]any
|
|
in := []byte("n = 1_000\n")
|
|
if err := Unmarshal(in, &tree, NumbersAsLiterals(true)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, ok := tree["n"].(Number); !ok || got != "1_000" {
|
|
t.Errorf("n = %#v, want Number(\"1_000\")", tree["n"])
|
|
}
|
|
})
|
|
t.Run("the limits apply", func(t *testing.T) {
|
|
var nested strings.Builder
|
|
nested.WriteString("x = ")
|
|
for range 20 {
|
|
nested.WriteString("[")
|
|
}
|
|
nested.WriteString("1")
|
|
for range 20 {
|
|
nested.WriteString("]")
|
|
}
|
|
var tree map[string]any
|
|
if err := Unmarshal([]byte(nested.String()), &tree, MaxNestingDepth(10)); err == nil {
|
|
t.Error("a document over MaxDepth decoded, want an error")
|
|
}
|
|
if err := Unmarshal([]byte("a = 1\n"), &tree, MaxInputSize(2)); err == nil {
|
|
t.Error("a document over MaxInputSize decoded, want an error")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInlineTag(t *testing.T) {
|
|
type Inner struct {
|
|
A int `toml:"a"`
|
|
B int `toml:"b"`
|
|
}
|
|
t.Run("a struct field writes inline", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner,inline"`
|
|
}
|
|
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "inner = {a = 1, b = 2}\n" {
|
|
t.Errorf("output %q", out)
|
|
}
|
|
})
|
|
t.Run("a map field writes inline", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Opts map[string]int `toml:"opts,inline"`
|
|
}
|
|
out, err := Marshal(Cfg{Opts: map[string]int{"x": 1}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "opts = {x = 1}\n" {
|
|
t.Errorf("output %q", out)
|
|
}
|
|
})
|
|
t.Run("a named embedded struct writes inline", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Inner `toml:"inner,inline"`
|
|
}
|
|
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "inner = {a = 1, b = 2}\n" {
|
|
t.Errorf("output %q", out)
|
|
}
|
|
})
|
|
t.Run("an inline field decodes back", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner,inline"`
|
|
}
|
|
var cfg Cfg
|
|
if err := Unmarshal([]byte("inner = {a = 3, b = 4}\n"), &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Inner != (Inner{3, 4}) {
|
|
t.Errorf("decoded %+v", cfg.Inner)
|
|
}
|
|
})
|
|
t.Run("a forced inline of an array of tables is an error", func(t *testing.T) {
|
|
type Item struct {
|
|
N int `toml:"n"`
|
|
}
|
|
type Cfg struct {
|
|
Items []Item `toml:"items,inline"`
|
|
}
|
|
if _, err := Marshal(Cfg{Items: []Item{{1}}}); err == nil {
|
|
t.Error("forced inline of an array of tables succeeded, want an error")
|
|
}
|
|
})
|
|
t.Run("without the tag the header form stands", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Inner Inner `toml:"inner"`
|
|
}
|
|
out, err := Marshal(Cfg{Inner: Inner{1, 2}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(out) != "[inner]\na = 1\nb = 2\n" {
|
|
t.Errorf("output %q", out)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestOmitEmptyJSONSemantics(t *testing.T) {
|
|
type Cfg struct {
|
|
Empty string `toml:"empty,omitempty"`
|
|
Full string `toml:"full,omitempty"`
|
|
Zero int `toml:"zero,omitempty"`
|
|
One int `toml:"one,omitempty"`
|
|
Off bool `toml:"off,omitempty"`
|
|
On bool `toml:"on,omitempty"`
|
|
Nil *string `toml:"nil,omitempty"`
|
|
Set *string `toml:"set,omitempty"`
|
|
Nothing map[string]string `toml:"nothing,omitempty"`
|
|
Somethg map[string]string `toml:"somethg,omitempty"`
|
|
}
|
|
s := "x"
|
|
out, err := Marshal(Cfg{
|
|
Full: "y",
|
|
One: 1,
|
|
On: true,
|
|
Set: &s,
|
|
Somethg: map[string]string{"k": "v"},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "full = \"y\"\none = 1\non = true\nset = \"x\"\n\n[somethg]\nk = \"v\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestEmitFieldComments(t *testing.T) {
|
|
type Cfg struct {
|
|
Host string `toml:"host,comment=The host to dial"`
|
|
Port int `toml:"port,comment=The port to listen on.\nThe default is 8080."`
|
|
User string `toml:"user"`
|
|
}
|
|
cfg := Cfg{Host: "db", Port: 5432, User: "admin"}
|
|
t.Run("off by default", func(t *testing.T) {
|
|
out, err := Marshal(cfg)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "host = \"db\"\nport = 5432\nuser = \"admin\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output:\n%q", out)
|
|
}
|
|
})
|
|
t.Run("on, the comments print above their lines", func(t *testing.T) {
|
|
out, err := Marshal(cfg, EmitFieldComments(true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "# The host to dial\nhost = \"db\"\n" +
|
|
"# The port to listen on.\n# The default is 8080.\nport = 5432\n" +
|
|
"user = \"admin\"\n"
|
|
if string(out) != want {
|
|
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
|
}
|
|
var back Cfg
|
|
if err := Unmarshal(out, &back); err != nil {
|
|
t.Fatalf("the output does not re-parse: %v", err)
|
|
}
|
|
if back != cfg {
|
|
t.Errorf("round trip = %+v", back)
|
|
}
|
|
})
|
|
t.Run("a table header carries its comment", func(t *testing.T) {
|
|
type Inner struct {
|
|
A int `toml:"a,comment=The a"`
|
|
}
|
|
type Nested struct {
|
|
Inner Inner `toml:"inner,comment=The inner table"`
|
|
}
|
|
out, err := Marshal(Nested{Inner: Inner{1}}, EmitFieldComments(true))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := "# The inner table\n[inner]\n# The a\na = 1\n"
|
|
if string(out) != want {
|
|
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
|
}
|
|
})
|
|
}
|