Files
interpres/encode_test.go

680 lines
17 KiB
Go

package interpres
import (
"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 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 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{Base: Base{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 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 }