Files
interpres/interpres_test.go
T

1006 lines
27 KiB
Go
Raw Normal View History

2026-08-19 09:47:00 +02:00
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package interpres
import (
"errors"
"fmt"
2026-08-19 09:47:00 +02:00
"math"
2026-09-21 23:51:58 +02:00
"os"
"path/filepath"
2026-09-22 00:36:10 +02:00
"reflect"
"slices"
"strings"
2026-08-19 09:47:00 +02:00
"testing"
"time"
)
func TestParseScalars(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
title = "interpres"
count = 42
ratio = 3.14
enabled = true
disabled = false
hexv = 0xFF
octv = 0o755
binv = 0b1010
grouped = 1_000_000
neg = -17
expv = 1e3
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
cases := map[string]any{
"title": "interpres",
"count": int64(42),
"ratio": 3.14,
"enabled": true,
"disabled": false,
"hexv": int64(255),
"octv": int64(493),
"binv": int64(10),
"grouped": int64(1000000),
"neg": int64(-17),
"expv": 1000.0,
}
for k, want := range cases {
if got := tree[k]; got != want {
t.Errorf("%s = %#v (%T), want %#v (%T)", k, got, got, want, want)
}
}
}
func TestParseInfNan(t *testing.T) {
tree, err := ParseMap([]byte("pos = inf\nneg = -inf\nbad = nan\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if v := tree["pos"].(float64); !math.IsInf(v, 1) {
t.Errorf("pos = %v, want +Inf", v)
}
if v := tree["neg"].(float64); !math.IsInf(v, -1) {
t.Errorf("neg = %v, want -Inf", v)
}
if v := tree["bad"].(float64); !math.IsNaN(v) {
t.Errorf("bad = %v, want NaN", v)
}
}
func TestParseStrings(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
basic = "a\tb\nc"
literal = 'C:\path\no\escape'
quote = "say \"hi\""
unicode = "\u00e9"
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["basic"] != "a\tb\nc" {
t.Errorf("basic = %q", tree["basic"])
}
if tree["literal"] != `C:\path\no\escape` {
t.Errorf("literal = %q", tree["literal"])
}
if tree["quote"] != `say "hi"` {
t.Errorf("quote = %q", tree["quote"])
}
if tree["unicode"] != "é" {
t.Errorf("unicode = %q", tree["unicode"])
}
}
func TestParseMultilineString(t *testing.T) {
tree, err := ParseMap([]byte("text = \"\"\"\nfirst\nsecond\"\"\"\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["text"] != "first\nsecond" {
t.Errorf("text = %q, want %q", tree["text"], "first\nsecond")
}
}
func TestParseMultilineLineEndingBackslash(t *testing.T) {
tree, err := ParseMap([]byte("text = \"\"\"\\\n one \\\n two\"\"\"\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["text"] != "one two" {
t.Errorf("text = %q, want %q", tree["text"], "one two")
}
}
func TestParseTablesAndDottedKeys(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
owner.name = "Petr"
[server]
host = "localhost"
port = 9090
[server.tls]
enabled = true
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
server := tree["server"].(map[string]any)
if server["host"] != "localhost" || server["port"] != int64(9090) {
t.Errorf("server = %#v", server)
}
tls := server["tls"].(map[string]any)
if tls["enabled"] != true {
t.Errorf("tls = %#v", tls)
}
owner := tree["owner"].(map[string]any)
if owner["name"] != "Petr" {
t.Errorf("owner = %#v", owner)
}
}
func TestParseArrayOfTables(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
[[forms]]
name = "contact"
[[forms]]
name = "feedback"
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
forms := tree["forms"].([]map[string]any)
if len(forms) != 2 {
t.Fatalf("len(forms) = %d, want 2", len(forms))
}
if forms[0]["name"] != "contact" || forms[1]["name"] != "feedback" {
t.Errorf("forms = %#v", forms)
}
}
func TestParseArraysAndInlineTables(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
ports = [80, 443]
mixed = [
"a",
"b",
]
point = { x = 1, y = 2 }
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
ports := tree["ports"].([]any)
if len(ports) != 2 || ports[0] != int64(80) || ports[1] != int64(443) {
t.Errorf("ports = %#v", ports)
}
mixed := tree["mixed"].([]any)
if len(mixed) != 2 || mixed[0] != "a" || mixed[1] != "b" {
t.Errorf("mixed = %#v", mixed)
}
point := tree["point"].(map[string]any)
if point["x"] != int64(1) || point["y"] != int64(2) {
t.Errorf("point = %#v", point)
}
}
func TestParseDateTime(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
offset = 1979-05-27T07:32:00Z
local = 1979-05-27T07:32:00
day = 1979-05-27
clock = 07:32:00
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if off, ok := tree["offset"].(OffsetDateTime); !ok || off.Year() != 1979 || off.Hour() != 7 {
2026-08-19 09:47:00 +02:00
t.Errorf("offset = %#v (%T)", tree["offset"], tree["offset"])
}
if ldt, ok := tree["local"].(LocalDateTime); !ok || ldt.Year() != 1979 || ldt.Hour() != 7 {
t.Errorf("local = %#v (%T)", tree["local"], tree["local"])
}
if d, ok := tree["day"].(LocalDate); !ok || d.Month() != time.May || d.Day() != 27 {
t.Errorf("day = %#v (%T)", tree["day"], tree["day"])
}
if clk, ok := tree["clock"].(LocalTime); !ok || clk.Hour() != 7 || clk.Minute() != 32 {
t.Errorf("clock = %#v (%T)", tree["clock"], tree["clock"])
}
}
func TestDateTimeFormats(t *testing.T) {
tree, err := ParseMap([]byte("a = 1987-07-05 17:45:00Z\nb = 1987-07-05t17:45:00z\nc = 1977-12-21T10:32:00.555\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if _, ok := tree["a"].(OffsetDateTime); !ok {
t.Errorf("a is %T, want OffsetDateTime", tree["a"])
2026-08-19 09:47:00 +02:00
}
if _, ok := tree["b"].(OffsetDateTime); !ok {
t.Errorf("b is %T, want OffsetDateTime", tree["b"])
2026-08-19 09:47:00 +02:00
}
if _, ok := tree["c"].(LocalDateTime); !ok {
t.Errorf("c is %T, want LocalDateTime", tree["c"])
}
}
func TestUnmarshalDateTime(t *testing.T) {
type Doc struct {
Created time.Time `toml:"created"`
Day LocalDate `toml:"day"`
}
var d Doc
if err := Unmarshal([]byte("created = 2026-06-20T10:00:00Z\nday = 2026-06-20\n"), &d); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if d.Created.Year() != 2026 || d.Created.Hour() != 10 {
t.Errorf("Created = %v", d.Created)
}
if d.Day.Year() != 2026 || d.Day.Month() != time.June || d.Day.Day() != 20 {
t.Errorf("Day = %v", d.Day)
}
}
func TestUnmarshalStruct(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"`
Origins []string `toml:"allowed_origins"`
}
type Config struct {
Port int `toml:"port"`
Forms []Form `toml:"forms"`
}
data := []byte(`
port = 8080
[[forms]]
name = "contact"
allowed_origins = ["https://example.com"]
[forms.smtp]
host = "smtp.example.com"
port = 587
`)
var cfg Config
if err := Unmarshal(data, &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.Port != 8080 {
t.Errorf("Port = %d", cfg.Port)
}
if len(cfg.Forms) != 1 {
t.Fatalf("len(Forms) = %d", len(cfg.Forms))
}
f := cfg.Forms[0]
if f.Name != "contact" || f.SMTP.Host != "smtp.example.com" || f.SMTP.Port != 587 {
t.Errorf("form = %#v", f)
}
if len(f.Origins) != 1 || f.Origins[0] != "https://example.com" {
t.Errorf("origins = %#v", f.Origins)
}
}
func TestUnmarshalCaseInsensitiveAndUntagged(t *testing.T) {
type Config struct {
Title string
Count int
}
var cfg Config
if err := Unmarshal([]byte("title = \"x\"\ncount = 3\n"), &cfg); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if cfg.Title != "x" || cfg.Count != 3 {
t.Errorf("cfg = %#v", cfg)
}
}
func TestDisallowUnknownFields(t *testing.T) {
type C struct {
Known string `toml:"known"`
}
data := []byte("known = \"x\"\nbogus = 1\n")
var lenient C
if err := Unmarshal(data, &lenient); err != nil {
t.Fatalf("lenient unmarshal: %v", err)
}
var strict C
err := Unmarshal(data, &strict, RejectUnknownFields(true))
2026-08-19 09:47:00 +02:00
if err == nil {
t.Fatal("expected error for unknown field, got nil")
}
}
func TestDisallowUnknownFieldsReportsSmallestKey(t *testing.T) {
// Map iteration order is random, so the reported key must be chosen
// deterministically: the smallest unknown key, whichever order the map
// iterates in.
type C struct {
Known string `toml:"known"`
}
data := []byte("known = \"x\"\nzeta = 1\nalpha = 2\nmu = 3\n")
for range 20 {
var c C
err := Unmarshal(data, &c, RejectUnknownFields(true))
if err == nil {
t.Fatal("expected error for unknown fields")
}
if !strings.Contains(err.Error(), `unknown field "alpha"`) {
t.Fatalf("err = %v, want the smallest unknown key alpha", err)
}
}
}
2026-08-19 09:47:00 +02:00
func TestSkippedFieldTag(t *testing.T) {
type C struct {
Keep string `toml:"keep"`
Skip string `toml:"-"`
}
var c C
if err := Unmarshal([]byte("keep = \"y\"\n"), &c); err != nil {
t.Fatalf("unmarshal: %v", err)
}
if c.Keep != "y" || c.Skip != "" {
t.Errorf("c = %#v", c)
}
}
func TestSyntaxErrorReportsLine(t *testing.T) {
_, err := ParseMap([]byte("a = 1\nb = \nc = 3\n"))
2026-08-19 09:47:00 +02:00
if err == nil {
t.Fatal("expected a syntax error")
}
se, ok := err.(*SyntaxError)
if !ok {
t.Fatalf("error is %T, want *SyntaxError", err)
}
if se.Line != 2 {
t.Errorf("Line = %d, want 2", se.Line)
}
}
func TestComments(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
# a leading comment
key = "value" # trailing comment
# another
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["key"] != "value" {
t.Errorf("key = %q", tree["key"])
}
}
func TestDuplicateKeyRejected(t *testing.T) {
_, err := ParseMap([]byte("a = 1\na = 2\n"))
2026-08-19 09:47:00 +02:00
if err == nil {
t.Fatal("expected duplicate key error")
}
}
func TestRejectsInvalidNumbers(t *testing.T) {
for _, tok := range []string{
"01", "-01", "00",
"1__0", "_1", "1_", "0x_1", "1_.0",
"1.", ".5", "1.2.3", "1.e2",
2026-09-17 21:29:07 +02:00
"1e", "1e+", "1e-", "0.0E", "0.0e", "1.5e+",
2026-08-19 09:47:00 +02:00
"0x", "0o", "0b", "0b2", "0o8", "0xG",
"+0x1",
} {
if _, err := ParseMap([]byte("v = " + tok + "\n")); err == nil {
2026-08-19 09:47:00 +02:00
t.Errorf("%q: expected an error, got none", tok)
}
}
}
func TestParseRejectsOffsetOutOfRange(t *testing.T) {
for _, tok := range []string{
"1979-05-27T07:32:00+00:60",
"1979-05-27T07:32:00-00:99",
"1979-05-27T07:32:00+24:00",
"1979-05-27T07:32:00+99:99",
} {
if _, err := ParseMap([]byte("v = " + tok + "\n")); err == nil {
t.Errorf("%q: expected an error, got none", tok)
}
}
}
func TestParseAcceptsOffsetBounds(t *testing.T) {
tree, err := ParseMap([]byte("a = 1979-05-27T07:32:00+23:59\nb = 1979-05-27T07:32:00-23:59\n"))
if err != nil {
t.Fatalf("parse: %v", err)
}
a := tree["a"].(OffsetDateTime)
if _, offset := a.Zone(); offset != 23*3600+59*60 {
t.Fatalf("a offset = %d, want %d", offset, 23*3600+59*60)
}
b := tree["b"].(OffsetDateTime)
if _, offset := b.Zone(); offset != -(23*3600 + 59*60) {
t.Fatalf("b offset = %d", offset)
}
}
2026-08-19 09:47:00 +02:00
func TestAcceptsNumberEdgeCases(t *testing.T) {
cases := map[string]any{
"0": int64(0),
"-0": int64(0),
"+99": int64(99),
"1_000": int64(1000),
"0xDEAD_BEEF": int64(0xDEADBEEF),
"0o755": int64(493),
"0b1010": int64(10),
"0.0": 0.0,
"3.14": 3.14,
"6.022e23": 6.022e23,
"1e10": 1e10,
"1e0": 1.0,
"1e06": 1e6,
"0e00": 0.0,
"2E-3": 2e-3,
2026-08-19 09:47:00 +02:00
"-2.5E-3": -2.5e-3,
}
for tok, want := range cases {
tree, err := ParseMap([]byte("v = " + tok + "\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Errorf("%q: %v", tok, err)
continue
}
if got := tree["v"]; got != want {
t.Errorf("%q = %#v (%T), want %#v", tok, got, got, want)
}
}
}
func TestRejectsTableRedefinition(t *testing.T) {
_, err := ParseMap([]byte("[a]\nx = 1\n\n[a]\ny = 2\n"))
2026-08-19 09:47:00 +02:00
if err == nil {
t.Fatal("expected a table-redefinition error")
}
}
func TestAllowsImplicitThenExplicitTable(t *testing.T) {
tree, err := ParseMap([]byte("[a.b]\nx = 1\n\n[a]\ny = 2\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
a := tree["a"].(map[string]any)
if a["y"] != int64(2) {
t.Errorf("a.y = %#v", a["y"])
}
if b := a["b"].(map[string]any); b["x"] != int64(1) {
t.Errorf("a.b.x = %#v", b["x"])
}
}
func TestRejectsControlCharInString(t *testing.T) {
if _, err := ParseMap([]byte("v = \"a\x01b\"\n")); err == nil {
2026-08-19 09:47:00 +02:00
t.Fatal("expected a control-character error")
}
}
func TestAllowsEscapedControlChar(t *testing.T) {
tree, err := ParseMap([]byte(`v = "\u0000"`))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["v"] != "\x00" {
t.Errorf("v = %q", tree["v"])
}
}
func TestMultilineQuotesAtDelimiter(t *testing.T) {
tree, err := ParseMap([]byte("a = '''''two quotes'''''\n"))
2026-08-19 09:47:00 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
if tree["a"] != "''two quotes''" {
t.Errorf("a = %q", tree["a"])
}
}
func TestRejectsInlineTableExtension(t *testing.T) {
cases := map[string]string{
"by header": "a = { b = 1 }\n[a.c]\nx = 2\n",
"by dotted key": "a = { b = 1 }\na.c = 2\n",
"header over it": "a = { b = 1 }\n[a]\nx = 2\n",
// The frozen check must cover the intermediate steps of an array-of-tables
// header, not only the leaf: [[a.b.c]] walks through a and a.b.
"by nested array header": "a = { b = {} }\n[[a.b.c]]\nx = 2\n",
2026-08-19 09:47:00 +02:00
}
for name, doc := range cases {
if _, err := ParseMap([]byte(doc)); err == nil {
2026-08-19 09:47:00 +02:00
t.Errorf("%s: expected an inline-table extension error", name)
}
}
}
// A new element of an array of tables starts a fresh scope: sub-table headers,
// nested arrays of tables, and dotted-key paths recorded for the previous
// element must not block the same paths in the next one.
func TestArrayOfTablesFreshScopePerElement(t *testing.T) {
cases := map[string]string{
"nested array of tables": "[[a]]\n[[a.b]]\nx = 1\n[[a]]\n[a.b]\ny = 2\n",
"dotted key": "[[a]]\nb.c = 1\n[[a]]\n[a.b]\nd = 2\n",
}
for name, doc := range cases {
tree, err := ParseMap([]byte(doc))
if err != nil {
t.Errorf("%s: %v", name, err)
continue
}
elements := tree["a"].([]map[string]any)
if len(elements) != 2 {
t.Errorf("%s: len(a) = %d, want 2", name, len(elements))
}
}
// Within one element the redefinition rules keep applying.
for name, doc := range map[string]string{
"header over dotted in one element": "[[a]]\nb.c = 1\n[a.b]\nd = 2\n",
"table over nested array": "[[a]]\n[[a.b]]\n[a.b]\nx = 1\n",
} {
if _, err := ParseMap([]byte(doc)); err == nil {
t.Errorf("%s: expected an error, got none", name)
}
}
}
2026-08-19 09:47:00 +02:00
func TestRejectsSpecInvalid(t *testing.T) {
cases := map[string]string{
"single-digit hour": "a = 2023-10-01T1:32:00Z\n",
"inline duplicate key": "a = { b = 1, b = 2 }\n",
"inline dotted overwrite": "a = { b = 1, b.c = 2 }\n",
"dotted over header": "[a.b]\nx = 1\n[a]\nb.y = 2\n",
"table over array": "[[t]]\n[t]\n",
"truncated datetime": "a = 2026-01-02T\n",
2026-09-17 22:06:26 +02:00
// "datetime no seconds" moved to the acceptance tests: TOML 1.1
// makes the seconds optional.
2026-08-19 09:47:00 +02:00
}
for name, doc := range cases {
if _, err := ParseMap([]byte(doc)); err == nil {
2026-08-19 09:47:00 +02:00
t.Errorf("%s: expected an error", name)
}
}
}
func TestArrayOfTablesPerElementSubtable(t *testing.T) {
tree, err := ParseMap([]byte(`
2026-08-19 09:47:00 +02:00
[[forms]]
name = "a"
[forms.smtp]
host = "h1"
[[forms]]
name = "b"
[forms.smtp]
host = "h2"
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
forms := tree["forms"].([]map[string]any)
if len(forms) != 2 {
t.Fatalf("len(forms) = %d", len(forms))
}
if h := forms[0]["smtp"].(map[string]any)["host"]; h != "h1" {
t.Errorf("forms[0].smtp.host = %v", h)
}
if h := forms[1]["smtp"].(map[string]any)["host"]; h != "h2" {
t.Errorf("forms[1].smtp.host = %v", h)
}
}
2026-09-17 22:06:26 +02:00
// --- TOML 1.1 --------------------------------------------------------------
func TestParseAcceptsNoSecondsDatetimes(t *testing.T) {
tree, err := ParseMap([]byte(`t = 13:37
2026-09-17 22:06:26 +02:00
dt = 1979-05-27T07:32
odt1 = 1979-05-27 07:32Z
odt2 = 1979-05-27 07:32-07:00
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
// A value written without seconds comes back without them: the seconds are
// only written when the value carries them.
if got := tree["t"].(LocalTime).String(); got != "13:37" {
t.Errorf("t = %q, want %q", got, "13:37")
2026-09-17 22:06:26 +02:00
}
if got := tree["dt"].(LocalDateTime).String(); got != "1979-05-27T07:32" {
t.Errorf("dt = %q, want %q", got, "1979-05-27T07:32")
2026-09-17 22:06:26 +02:00
}
if got := tree["odt1"].(OffsetDateTime).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00Z" {
2026-09-17 22:06:26 +02:00
t.Errorf("odt1 = %q", got)
}
if got := tree["odt2"].(OffsetDateTime).Format(time.RFC3339Nano); got != "1979-05-27T07:32:00-07:00" {
2026-09-17 22:06:26 +02:00
t.Errorf("odt2 = %q", got)
}
// The fraction still requires the seconds it belongs to.
if _, err := ParseMap([]byte("a = 07:32.5\n")); err == nil {
2026-09-17 22:06:26 +02:00
t.Error("07:32.5: expected an error, got none")
}
}
func TestParseAcceptsEscapeAndHexEscapes(t *testing.T) {
tree, err := ParseMap([]byte(`esc = "\e"
2026-09-17 22:06:26 +02:00
hex = "\x20\x7f\xf8"
nul = "\x00"
multi = """\x68\x65"""
lit = '\x20'
`))
if err != nil {
t.Fatalf("parse: %v", err)
}
if got := tree["esc"].(string); got != "\x1b" {
t.Errorf("esc = %q, want the escape character", got)
}
if got := tree["hex"].(string); got != " \x7f\u00f8" {
t.Errorf("hex = %q", got)
}
if got := tree["nul"].(string); got != "\x00" {
t.Errorf("nul = %q", got)
}
if got := tree["multi"].(string); got != "he" {
t.Errorf("multi = %q", got)
}
// A literal string carries the sequence verbatim.
if got := tree["lit"].(string); got != `\x20` {
t.Errorf("lit = %q, want the verbatim sequence", got)
}
// Two digits exactly; a short or non-hex escape is an error.
for _, doc := range []string{`a = "\x4"`, `a = "\x"`, `a = "\xgg"`} {
if _, err := ParseMap([]byte(doc)); err == nil {
2026-09-17 22:06:26 +02:00
t.Errorf("%s: expected an error, got none", doc)
}
}
}
func TestParseAcceptsMultilineInlineTables(t *testing.T) {
tree, err := ParseMap([]byte("tbl = {\n\thello = \"world\",\n\tarr = [1,\n\t\t2,\n\t],\n\tsub = {\n\t\tk = 1,\n\t},\n\tbare = 2}\n"))
2026-09-17 22:06:26 +02:00
if err != nil {
t.Fatalf("parse: %v", err)
}
tbl := tree["tbl"].(map[string]any)
if tbl["hello"] != "world" || tbl["bare"] != int64(2) {
t.Fatalf("tbl = %#v", tbl)
}
if arr := tbl["arr"].([]any); len(arr) != 2 {
t.Errorf("arr = %#v", tbl["arr"])
}
if sub := tbl["sub"].(map[string]any); sub["k"] != int64(1) {
t.Errorf("sub = %#v", tbl["sub"])
}
// Comments inside the table, and a trailing comma at both depths.
tree, err = ParseMap([]byte("m = { # one\n\t# two\n\ta = 1, # three\n\t# four\n}\n"))
2026-09-17 22:06:26 +02:00
if err != nil {
t.Fatalf("parse with comments: %v", err)
}
if m := tree["m"].(map[string]any); m["a"] != int64(1) {
t.Errorf("m = %#v", m)
}
// The old single-line shapes keep working, with and without the comma.
if _, err := ParseMap([]byte("a = { b = 1, c = 2 }\n")); err != nil {
2026-09-17 22:06:26 +02:00
t.Errorf("single line: %v", err)
}
// Still rejected: two commas, a missing value, and an unclosed table.
for name, doc := range map[string]string{
"double comma": "a = { b = 1,, c = 2 }\n",
"missing value": "a = {\n\tb =\n}\n",
"unterminated": "a = { b = 1,\n",
} {
if _, err := ParseMap([]byte(doc)); err == nil {
2026-09-17 22:06:26 +02:00
t.Errorf("%s: expected an error, got none", name)
}
}
}
func TestParseNestingLimit(t *testing.T) {
// The parser is a recursive descent, so a document that nests without bound
// is rejected instead of exhausting the stack.
deep := func(n int) []byte {
return []byte("v = " + strings.Repeat("[", n) + strings.Repeat("]", n) + "\n")
}
if _, err := ParseMap(deep(100)); err != nil {
t.Fatalf("a document well inside the limit: %v", err)
}
_, err := ParseMap(deep(maxNestingDepth + 1))
if err == nil {
t.Fatal("expected a nesting error")
}
var se *SyntaxError
if !errors.As(err, &se) {
t.Fatalf("expected a *SyntaxError, got %T: %v", err, err)
}
if !strings.Contains(se.Msg, "nesting") {
t.Errorf("Msg = %q, want it to name the nesting limit", se.Msg)
}
}
2026-09-21 23:51:58 +02:00
func TestParseFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "config.toml")
if err := os.WriteFile(path, []byte("port = 8080\n"), 0o644); err != nil {
t.Fatal(err)
}
doc, err := ParseFile(path)
if err != nil {
t.Fatal(err)
}
if got := doc.Map()["port"]; got != int64(8080) {
t.Errorf("port = %v, want 8080", got)
}
_, err = ParseFile(filepath.Join(t.TempDir(), "missing.toml"))
if err == nil || !strings.Contains(err.Error(), "missing.toml") {
t.Errorf("read error = %v, want it to name the file", err)
}
bad := filepath.Join(t.TempDir(), "broken.toml")
if err := os.WriteFile(bad, []byte("port =\n"), 0o644); err != nil {
t.Fatal(err)
}
_, err = ParseFile(bad)
if err == nil || !strings.Contains(err.Error(), "broken.toml") {
t.Errorf("parse error = %v, want it to name the file", err)
}
s, ok := errors.AsType[*SyntaxError](err)
if !ok || s.Line != 1 {
t.Errorf("parse error = %v, want a SyntaxError with line 1 inside", err)
}
}
func TestValid(t *testing.T) {
if err := Valid([]byte("a = 1\n[t]\nb = 2\n")); err != nil {
t.Errorf("Valid(valid) = %v, want nil", err)
}
err := Valid([]byte("a = \n"))
if err == nil {
t.Fatal("Valid(invalid) = nil, want an error")
}
if _, ok := errors.AsType[*SyntaxError](err); !ok {
t.Errorf("Valid(invalid) = %v, want a SyntaxError", err)
}
}
2026-09-22 00:36:10 +02:00
func TestParseAsAndNewSchema(t *testing.T) {
type Config struct {
Host string `toml:"host"`
Port int `toml:"port"`
}
cfg, err := ParseAs[Config]([]byte("host = \"db\"\nport = 5432\n"))
if err != nil {
t.Fatal(err)
}
if cfg.Host != "db" || cfg.Port != 5432 {
t.Errorf("decoded %+v", cfg)
}
if _, err := ParseAs[Config]([]byte("port =\n")); err == nil {
t.Error("ParseAs(invalid) succeeded, want an error and the zero value")
}
NewSchema[Config]()
if _, ok := structSchemaCache.Load(reflect.TypeFor[Config]()); !ok {
t.Error("NewSchema left no schema in the cache")
}
NewSchema[map[string]any]() // must not panic
}
func TestZeroOffsetRoundTrip(t *testing.T) {
// A document may write a zero offset as +00:00; the tree must hold the
// same value after a round trip, because the written form is "Z" either
// way.
src := []byte("a = 1979-05-27T07:32:00+00:00\n")
tree, err := ParseMap(src)
if err != nil {
t.Fatal(err)
}
out, err := Marshal(tree)
if err != nil {
t.Fatal(err)
}
re, err := ParseMap(out)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tree, re) {
t.Errorf("round trip changed the tree: %#v vs %#v", tree, re)
}
if got := tree["a"].(OffsetDateTime).String(); got != "1979-05-27T07:32Z" {
t.Errorf("a = %q, want 1979-05-27T07:32Z", got)
}
}
func TestStatements(t *testing.T) {
src := strings.NewReader(`title = "demo"
port = 8080
[server]
host = "127.0.0.1"
[[items]]
name = "a"
[[items]]
name = "b"
`)
var lines []string
for stmt, err := range Statements(src) {
if err != nil {
t.Fatal(err)
}
switch {
case stmt.Index >= 0:
lines = append(lines, fmt.Sprintf("%s #%d", stmt.Key, stmt.Index))
case stmt.Table != nil:
lines = append(lines, fmt.Sprintf("[%s] %v", stmt.Key, stmt.Table.Keys()))
default:
lines = append(lines, fmt.Sprintf("%s = %v", stmt.Key, stmt.Value))
}
}
want := []string{
`title = demo`,
`port = 8080`,
`[server] [host]`,
`items #0`,
`items #1`,
}
if !slices.Equal(lines, want) {
t.Errorf("statements =\n%v\nwant:\n%v", lines, want)
}
t.Run("breaking stops the iteration", func(t *testing.T) {
src := strings.NewReader("a = 1\nb = 2\nc = 3\n")
count := 0
for range Statements(src) {
count++
break
}
if count != 1 {
t.Errorf("iterated %d statements after break, want 1", count)
}
})
t.Run("a parse error arrives as the second value", func(t *testing.T) {
for stmt, err := range Statements(strings.NewReader("broken =\n")) {
if err == nil {
t.Fatalf("statement %+v without an error", stmt)
}
if _, ok := errors.AsType[*SyntaxError](err); !ok {
t.Errorf("err = %v, want a SyntaxError", err)
}
break
}
})
}
func TestParseCRLFDocument(t *testing.T) {
tree, err := ParseMap([]byte("a = 1\r\nb = 2\r\n[t]\r\nc = \"x\"\r\n"))
if err != nil {
t.Fatal(err)
}
if tree["a"] != int64(1) || tree["b"] != int64(2) {
t.Errorf("tree = %v", tree)
}
}
// TestParseUnicodeEscapeBoundaries pins the scalar-value checks of \u and \U:
// a surrogate, a value past U+10FFFF, and a sign are all rejected, and the
// greatest scalar value parses.
func TestParseUnicodeEscapeBoundaries(t *testing.T) {
bad := []struct {
name string
in string
}{
{"high surrogate", `a = "\ud800"`},
{"low surrogate", `a = "\udfff"`},
{"past the greatest scalar", `a = "\U00110000"`},
{"signed short escape", `a = "\u+041"`},
{"negative long escape", `a = "\U-0000001"`},
}
for _, tt := range bad {
t.Run(tt.name, func(t *testing.T) {
_, err := Parse([]byte(tt.in))
if err == nil {
t.Fatalf("Parse accepted %q", tt.in)
}
})
}
tree, err := ParseMap([]byte("a = \"\\U0010FFFF\""))
if err != nil {
t.Fatalf("ParseMap: %v", err)
}
if tree["a"] != "􏿿" {
t.Errorf("a = %q", tree["a"])
}
}
// TestParseRejectsOutOfRangeDateTimes pins that a token shaped like a
// date-time with a component out of range is rejected as a date-time, not
// left to the number decoder's complaint.
func TestParseRejectsOutOfRangeDateTimes(t *testing.T) {
bad := []struct {
name string
in string
}{
{"hour 24", "a = 1979-05-27T24:00:00Z"},
{"minute 60", "a = 1979-05-27T07:60:00Z"},
{"second 60", "a = 1979-05-27T07:32:60Z"},
{"month 13", "a = 1979-13-27T07:32:00Z"},
{"day 32", "a = 1979-05-32T07:32:00Z"},
{"february the thirtieth", "a = 1979-02-30"},
}
for _, tt := range bad {
t.Run(tt.name, func(t *testing.T) {
_, err := ParseMap([]byte(tt.in))
if err == nil {
t.Fatalf("ParseMap accepted %q", tt.in)
}
if !strings.Contains(err.Error(), "invalid date-time") {
t.Errorf("err = %v, want the date-time complaint", err)
}
})
}
}
// TestParseMultilineStringEdges pins the carriage-return and delimiter rules
// of multi-line strings: a bare CR right after the opening delimiter is the
// bare-CR error, a CRLF pair is the trimmed newline, and a CRLF inside the
// content survives.
func TestParseMultilineStringEdges(t *testing.T) {
_, err := ParseMap([]byte("a = \"\"\"\rX\"\"\""))
if err == nil || !strings.Contains(err.Error(), "bare carriage return") {
t.Errorf("err = %v, want the bare-CR error after the delimiter", err)
}
tree, err := ParseMap([]byte("a = \"\"\"\r\nX\r\nY\"\"\""))
if err != nil {
t.Fatalf("ParseMap: %v", err)
}
if tree["a"] != "X\r\nY" {
t.Errorf("a = %q, want the CRLF pairs preserved", tree["a"])
}
}
// TestParseMultilineBasicDelimiterRuns pins that up to two extra quotes
// before the closing delimiter of a basic multi-line string are content, and
// more than five are the error.
func TestParseMultilineBasicDelimiterRuns(t *testing.T) {
tree, err := ParseMap([]byte("a = \"\"\"end\"\"\"\""))
if err != nil {
t.Fatalf("ParseMap: %v", err)
}
if tree["a"] != `end"` {
t.Errorf("a = %q", tree["a"])
}
_, err = ParseMap([]byte("a = \"\"\"end\"\"\"\"\"\"\""))
if err == nil || !strings.Contains(err.Error(), "too many") {
t.Errorf("err = %v, want the too-many-delimiters error", err)
}
}
// TestParseLineEndingBackslashEdges pins the line-ending backslash at the
// very end of the input and before a bare CR.
func TestParseLineEndingBackslashEdges(t *testing.T) {
bad := []string{
"a = \"\"\"x \\\\",
"a = \"\"\"x \\\\\rZ\"\"\"",
}
for _, in := range bad {
if _, err := ParseMap([]byte(in)); err == nil {
t.Errorf("ParseMap accepted %q", in)
}
}
}