1265 lines
33 KiB
Go
1265 lines
33 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"
|
|
"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)
|
|
}
|
|
if _, err := NewEncoder().MarshalContext(ctx, C{A: 1}); !errors.Is(err, context.Canceled) {
|
|
t.Fatalf("Encoder.MarshalContext returned %v, want context.Canceled", err)
|
|
}
|
|
}
|
|
|
|
func TestEncoderGroupByKindDefault(t *testing.T) {
|
|
// NewEncoder must default to GroupByKind=true 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 := NewEncoder().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 TestEncoderGroupByKindFalsePreservesOrder(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 := NewEncoder().GroupByKind(false).Marshal(in)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
// With GroupByKind(false) 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 TestEncoderGroupByKindTrueDefaultOrder(t *testing.T) {
|
|
// The default (GroupByKind=true) 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 := NewEncoder().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 := NewEncoder().OmitEmptyArrays().Marshal(Cfg{
|
|
Tags: []string{"a", "b"},
|
|
Secrets: []string{},
|
|
})
|
|
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 := NewEncoder().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 := NewEncoder().OmitEmptyArrays().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 TestEncoderUseLiteralMultiline(t *testing.T) {
|
|
type Cfg struct {
|
|
Long string `toml:"long"`
|
|
}
|
|
long := strings.Repeat("a", 50) + "\nline two\nline three"
|
|
out, err := NewEncoder().UseLiteralMultiline(20).Marshal(Cfg{Long: long})
|
|
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 TestEncoderUseLiteralMultilineBelowThreshold(t *testing.T) {
|
|
// A multi-line value shorter than the threshold must remain escaped.
|
|
type Cfg struct {
|
|
Short string `toml:"short"`
|
|
}
|
|
out, err := NewEncoder().UseLiteralMultiline(1000).Marshal(Cfg{Short: "one\ntwo"})
|
|
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 TestEncoderUseLiteralMultilineThresholdZero(t *testing.T) {
|
|
// UseLiteralMultiline(0) disables the literal form entirely.
|
|
type Cfg struct {
|
|
S string `toml:"s"`
|
|
}
|
|
out, err := NewEncoder().UseLiteralMultiline(0).Marshal(Cfg{S: "a\nb\nc\nd"})
|
|
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 := NewEncoder().UseLiteralMultiline(5).Marshal(map[string]any{"s": c.in})
|
|
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 := Parse(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:00: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)
|
|
}
|
|
}
|
|
|
|
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: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 := NewEncoder().
|
|
GroupByKind(false).
|
|
OmitEmptyArrays().
|
|
UseLiteralMultiline(50).
|
|
Marshal(Cfg{S: "short", I: Inner{V: long}})
|
|
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:00:00Z\nlocal = 2026-06-26T07:32:00\nday = 2026-06-26\nclock = 07:32:00\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 := Parse([]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 := Parse(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)
|
|
}
|
|
}
|
|
|
|
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:32:00Z, {t = 1979-05-27T07:32:00}]\n"
|
|
if string(out) != want {
|
|
t.Fatalf("output mismatch:\ngot: %q\nwant: %q", out, want)
|
|
}
|
|
}
|
|
|
|
func TestMarshalArrayOfTablesStaysHeaderForm(t *testing.T) {
|
|
tree, err := Parse([]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 := Parse(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:00: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 := Parse(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
|
|
`)
|
|
tree1, err := Parse(src)
|
|
if err != nil {
|
|
t.Fatalf("parse src: %v", err)
|
|
}
|
|
out, err := Marshal(tree1)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
tree2, err := Parse(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")
|
|
}
|
|
}
|
|
|
|
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) {
|
|
ldt := LocalDateTime{Time: time.Date(1979, 5, 27, 7, 32, 0, 0, time.UTC)}
|
|
if got := ldt.String(); got != "1979-05-27T07:32:00" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00", 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.000000500" {
|
|
t.Errorf("LocalDateTime.String() = %q, want 1979-05-27T07:32:00.000000500", 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:00" {
|
|
t.Errorf("LocalTime.String() = %q, want 07:32:00", 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 := NewEncoder().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 != "server.port" {
|
|
t.Fatalf("Path = %q, want %q", ee.Path, "server.port")
|
|
}
|
|
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 != "port" {
|
|
t.Fatalf("Path = %q, want %q", ee.Path, "port")
|
|
}
|
|
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 != "items[0]" {
|
|
t.Fatalf("Path = %q, want %q", ee.Path, "items[0]")
|
|
}
|
|
}
|