Files
interpres/interpres_test.go

509 lines
12 KiB
Go

package interpres
import (
"math"
"testing"
"time"
)
func TestParseScalars(t *testing.T) {
tree, err := Parse([]byte(`
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 := Parse([]byte("pos = inf\nneg = -inf\nbad = nan\n"))
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 := Parse([]byte(`
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 := Parse([]byte("text = \"\"\"\nfirst\nsecond\"\"\"\n"))
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 := Parse([]byte("text = \"\"\"\\\n one \\\n two\"\"\"\n"))
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 := Parse([]byte(`
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 := Parse([]byte(`
[[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 := Parse([]byte(`
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 := Parse([]byte(`
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"].(time.Time); !ok || off.Year() != 1979 || off.Hour() != 7 {
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 := Parse([]byte("a = 1987-07-05 17:45:00Z\nb = 1987-07-05t17:45:00z\nc = 1977-12-21T10:32:00.555\n"))
if err != nil {
t.Fatalf("parse: %v", err)
}
if _, ok := tree["a"].(time.Time); !ok {
t.Errorf("a is %T, want time.Time", tree["a"])
}
if _, ok := tree["b"].(time.Time); !ok {
t.Errorf("b is %T, want time.Time", tree["b"])
}
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 := NewDecoder().DisallowUnknownFields().Decode(data, &strict)
if err == nil {
t.Fatal("expected error for unknown field, got nil")
}
}
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 := Parse([]byte("a = 1\nb = \nc = 3\n"))
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 := Parse([]byte(`
# 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 := Parse([]byte("a = 1\na = 2\n"))
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",
"0x", "0o", "0b", "0b2", "0o8", "0xG",
"+0x1",
} {
if _, err := Parse([]byte("v = " + tok + "\n")); err == nil {
t.Errorf("%q: expected an error, got none", tok)
}
}
}
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,
"-2.5E-3": -2.5e-3,
}
for tok, want := range cases {
tree, err := Parse([]byte("v = " + tok + "\n"))
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 := Parse([]byte("[a]\nx = 1\n\n[a]\ny = 2\n"))
if err == nil {
t.Fatal("expected a table-redefinition error")
}
}
func TestAllowsImplicitThenExplicitTable(t *testing.T) {
tree, err := Parse([]byte("[a.b]\nx = 1\n\n[a]\ny = 2\n"))
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 := Parse([]byte("v = \"a\x01b\"\n")); err == nil {
t.Fatal("expected a control-character error")
}
}
func TestAllowsEscapedControlChar(t *testing.T) {
tree, err := Parse([]byte(`v = "\u0000"`))
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 := Parse([]byte("a = '''''two quotes'''''\n"))
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",
}
for name, doc := range cases {
if _, err := Parse([]byte(doc)); err == nil {
t.Errorf("%s: expected an inline-table extension error", name)
}
}
}
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",
}
for name, doc := range cases {
if _, err := Parse([]byte(doc)); err == nil {
t.Errorf("%s: expected an error", name)
}
}
}
func TestArrayOfTablesPerElementSubtable(t *testing.T) {
tree, err := Parse([]byte(`
[[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)
}
}