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 (
|
2026-09-19 19:36:24 +02:00
|
|
|
"errors"
|
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"
|
2026-09-17 23:11:38 +02:00
|
|
|
"strings"
|
2026-08-19 09:47:00 +02:00
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestParseScalars(t *testing.T) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
if _, ok := tree["a"].(OffsetDateTime); !ok {
|
|
|
|
|
t.Errorf("a is %T, want OffsetDateTime", tree["a"])
|
2026-08-19 09:47:00 +02:00
|
|
|
}
|
2026-09-19 19:36:24 +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 := NewDecoder().DisallowUnknownFields().Decode(data, &strict)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("expected error for unknown field, got nil")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:11:38 +02:00
|
|
|
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 := NewDecoder().DisallowUnknownFields().Decode(data, &c)
|
|
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
_, 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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
_, 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",
|
|
|
|
|
} {
|
2026-09-20 10:40:57 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 21:29:12 +02:00
|
|
|
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",
|
|
|
|
|
} {
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMap([]byte("v = " + tok + "\n")); err == nil {
|
2026-09-17 21:29:12 +02:00
|
|
|
t.Errorf("%q: expected an error, got none", tok)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestParseAcceptsOffsetBounds(t *testing.T) {
|
2026-09-20 10:40:57 +02:00
|
|
|
tree, err := ParseMap([]byte("a = 1979-05-27T07:32:00+23:59\nb = 1979-05-27T07:32:00-23:59\n"))
|
2026-09-17 21:29:12 +02:00
|
|
|
if err != nil {
|
|
|
|
|
t.Fatalf("parse: %v", err)
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
a := tree["a"].(OffsetDateTime)
|
2026-09-17 21:29:12 +02:00
|
|
|
if _, offset := a.Zone(); offset != 23*3600+59*60 {
|
|
|
|
|
t.Fatalf("a offset = %d, want %d", offset, 23*3600+59*60)
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
b := tree["b"].(OffsetDateTime)
|
2026-09-17 21:29:12 +02:00
|
|
|
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,
|
2026-09-17 23:05:27 +02:00
|
|
|
"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 {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
_, 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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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",
|
2026-09-17 23:04:49 +02:00
|
|
|
// 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 {
|
2026-09-20 10:40:57 +02:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-17 23:05:06 +02:00
|
|
|
// 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 {
|
2026-09-20 10:40:57 +02:00
|
|
|
tree, err := ParseMap([]byte(doc))
|
2026-09-17 23:05:06 +02:00
|
|
|
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",
|
|
|
|
|
} {
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMap([]byte(doc)); err == nil {
|
2026-09-17 23:05:06 +02:00
|
|
|
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 {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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)
|
|
|
|
|
}
|
2026-09-19 12:18:18 +02:00
|
|
|
// 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
|
|
|
}
|
2026-09-19 12:18:18 +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
|
|
|
}
|
2026-09-19 19:36:24 +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)
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
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.
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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"`} {
|
2026-09-20 10:40:57 +02:00
|
|
|
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) {
|
2026-09-20 10:40:57 +02:00
|
|
|
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.
|
2026-09-20 10:40:57 +02:00
|
|
|
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.
|
2026-09-20 10:40:57 +02:00
|
|
|
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",
|
|
|
|
|
} {
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMap([]byte(doc)); err == nil {
|
2026-09-17 22:06:26 +02:00
|
|
|
t.Errorf("%s: expected an error, got none", name)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 19:36:24 +02:00
|
|
|
|
|
|
|
|
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")
|
|
|
|
|
}
|
2026-09-20 10:40:57 +02:00
|
|
|
if _, err := ParseMap(deep(100)); err != nil {
|
2026-09-19 19:36:24 +02:00
|
|
|
t.Fatalf("a document well inside the limit: %v", err)
|
|
|
|
|
}
|
2026-09-20 10:40:57 +02:00
|
|
|
_, err := ParseMap(deep(maxNestingDepth + 1))
|
2026-09-19 19:36:24 +02:00
|
|
|
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
|
|
|
|
|
}
|