419 lines
11 KiB
Go
419 lines
11 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package interpres
|
|
|
|
import (
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// mustEntry returns the entry a table must have, and fails the test when it
|
|
// does not.
|
|
func mustEntry(t *testing.T, tbl *Table, key string) *Entry {
|
|
t.Helper()
|
|
e, ok := tbl.Get(key)
|
|
if !ok {
|
|
t.Fatalf("%q is missing from the table", key)
|
|
}
|
|
return e
|
|
}
|
|
|
|
func TestDocumentKeepsKeyOrder(t *testing.T) {
|
|
doc, err := Parse([]byte(`
|
|
b = 1
|
|
a = 2
|
|
inline = {y = 1, x = 2}
|
|
|
|
[table]
|
|
z = 3
|
|
m = 4
|
|
`))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
// The root's keys come back in written order, not sorted.
|
|
if got := doc.Root().Keys(); !slices.Equal(got, []string{"b", "a", "inline", "table"}) {
|
|
t.Errorf("root keys = %v, want [b a inline table]", got)
|
|
}
|
|
|
|
// So do the keys of an inline table, which the map shape loses.
|
|
inline, ok := doc.Root().Get("inline")
|
|
if !ok {
|
|
t.Fatal("inline is missing from the root")
|
|
}
|
|
if !inline.Inline() {
|
|
t.Error("inline is not marked inline")
|
|
}
|
|
if got := inline.Table().Keys(); !slices.Equal(got, []string{"y", "x"}) {
|
|
t.Errorf("inline keys = %v, want [y x]", got)
|
|
}
|
|
|
|
// And the keys of a table written under a header, which is not inline.
|
|
tbl, ok := doc.Root().Get("table")
|
|
if !ok {
|
|
t.Fatal("table is missing from the root")
|
|
}
|
|
if tbl.Inline() {
|
|
t.Error("table is marked inline")
|
|
}
|
|
if got := tbl.Table().Keys(); !slices.Equal(got, []string{"z", "m"}) {
|
|
t.Errorf("table keys = %v, want [z m]", got)
|
|
}
|
|
}
|
|
|
|
func TestDocumentValuesAreTheTree(t *testing.T) {
|
|
doc, err := Parse([]byte("n = 7\ns = \"x\"\n\n[t]\nk = true\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if got := mustEntry(t, doc.Root(), "n").Value(); got != int64(7) {
|
|
t.Errorf("n = %#v, want int64(7)", got)
|
|
}
|
|
tbl := mustEntry(t, doc.Root(), "t").Table()
|
|
if got := mustEntry(t, tbl, "k").Value(); got != true {
|
|
t.Errorf("t.k = %#v, want true", got)
|
|
}
|
|
// Map gives the tree ParseMap would have returned, the same values.
|
|
tree := doc.Map()
|
|
if tree["n"] != int64(7) || tree["s"] != "x" {
|
|
t.Errorf("Map = %#v", tree)
|
|
}
|
|
if tree["t"].(map[string]any)["k"] != true {
|
|
t.Errorf("Map[t] = %#v", tree["t"])
|
|
}
|
|
if tbl.Values()["k"] != true {
|
|
t.Errorf("t.Values() = %#v", tbl.Values())
|
|
}
|
|
}
|
|
|
|
func TestDocumentComments(t *testing.T) {
|
|
doc, err := Parse([]byte(`# above b
|
|
b = 1 # trailing b
|
|
|
|
# above the table
|
|
[table] # trailing table
|
|
# above m
|
|
m = 2
|
|
|
|
# footer
|
|
`))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
b, ok := doc.Root().Get("b")
|
|
if !ok {
|
|
t.Fatal("b is missing")
|
|
}
|
|
if got := b.Comments(); !slices.Equal(got, []string{"above b"}) {
|
|
t.Errorf("b comments = %q, want [above b]", got)
|
|
}
|
|
if got := b.Trailing(); got != "trailing b" {
|
|
t.Errorf("b trailing = %q, want \"trailing b\"", got)
|
|
}
|
|
|
|
tbl, ok := doc.Root().Get("table")
|
|
if !ok {
|
|
t.Fatal("table is missing")
|
|
}
|
|
// A [header] line introduces the table, so the comments around it belong
|
|
// to the table node; the entry that names it stays bare.
|
|
if got := tbl.Table().Comments(); !slices.Equal(got, []string{"above the table"}) {
|
|
t.Errorf("table comments = %q, want [above the table]", got)
|
|
}
|
|
if got := tbl.Table().Trailing(); got != "trailing table" {
|
|
t.Errorf("table trailing = %q, want \"trailing table\"", got)
|
|
}
|
|
if got := tbl.Comments(); got != nil {
|
|
t.Errorf("entry comments = %q, want none", got)
|
|
}
|
|
m, ok := tbl.Table().Get("m")
|
|
if !ok {
|
|
t.Fatal("table.m is missing")
|
|
}
|
|
if got := m.Comments(); !slices.Equal(got, []string{"above m"}) {
|
|
t.Errorf("m comments = %q, want [above m]", got)
|
|
}
|
|
|
|
if got := doc.Footer(); !slices.Equal(got, []string{"footer"}) {
|
|
t.Errorf("footer = %q, want [footer]", got)
|
|
}
|
|
}
|
|
|
|
func TestDocumentCommentsAreWritable(t *testing.T) {
|
|
doc, err := Parse([]byte("# above\nk = 1\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
entry, ok := doc.Root().Get("k")
|
|
if !ok {
|
|
t.Fatal("k is missing")
|
|
}
|
|
entry.SetComments([]string{"first", "second"})
|
|
entry.SetTrailing("beside")
|
|
if got := entry.Comments(); !slices.Equal(got, []string{"first", "second"}) {
|
|
t.Errorf("comments = %q", got)
|
|
}
|
|
if got := entry.Trailing(); got != "beside" {
|
|
t.Errorf("trailing = %q", got)
|
|
}
|
|
|
|
tbl := doc.Root()
|
|
tbl.SetComments([]string{"above the root"})
|
|
if got := tbl.Comments(); !slices.Equal(got, []string{"above the root"}) {
|
|
t.Errorf("root comments = %q", got)
|
|
}
|
|
doc.SetFooter([]string{"end"})
|
|
if got := doc.Footer(); !slices.Equal(got, []string{"end"}) {
|
|
t.Errorf("footer = %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDocumentArrayOfTables(t *testing.T) {
|
|
doc, err := Parse([]byte(`# first element
|
|
[[item]]
|
|
a = 1
|
|
|
|
[[item]]
|
|
b = 2 # beside b
|
|
`))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
entry, ok := doc.Root().Get("item")
|
|
if !ok {
|
|
t.Fatal("item is missing")
|
|
}
|
|
elems := entry.Elements()
|
|
if len(elems) != 2 {
|
|
t.Fatalf("elements = %d, want 2", len(elems))
|
|
}
|
|
if got := elems[0].Keys(); !slices.Equal(got, []string{"a"}) {
|
|
t.Errorf("first element keys = %v, want [a]", got)
|
|
}
|
|
if got := elems[0].Comments(); !slices.Equal(got, []string{"first element"}) {
|
|
t.Errorf("first element comments = %q", got)
|
|
}
|
|
if got := elems[1].Keys(); !slices.Equal(got, []string{"b"}) {
|
|
t.Errorf("second element keys = %v, want [b]", got)
|
|
}
|
|
if got := mustEntry(t, elems[1], "b").Trailing(); got != "beside b" {
|
|
t.Errorf("b trailing = %q, want \"beside b\"", got)
|
|
}
|
|
// The value keeps the map shape the decoder reads.
|
|
if _, ok := entry.Value().([]map[string]any); !ok {
|
|
t.Errorf("item value = %#v, want []map[string]any", entry.Value())
|
|
}
|
|
}
|
|
|
|
func TestDocumentDottedKeysAndValueArrays(t *testing.T) {
|
|
doc, err := Parse([]byte("a.b.c = 1\narr = [1, {x = 1}]\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
|
|
// A dotted key builds tables, and they are not inline ones.
|
|
a, ok := doc.Root().Get("a")
|
|
if !ok {
|
|
t.Fatal("a is missing")
|
|
}
|
|
if a.Inline() {
|
|
t.Error("a is marked inline")
|
|
}
|
|
b, ok := a.Table().Get("b")
|
|
if !ok {
|
|
t.Fatal("a.b is missing")
|
|
}
|
|
if b.Inline() {
|
|
t.Error("a.b is marked inline")
|
|
}
|
|
if got := b.Table().Keys(); !slices.Equal(got, []string{"c"}) {
|
|
t.Errorf("a.b keys = %v, want [c]", got)
|
|
}
|
|
|
|
// An inline table inside a value array keeps its node in the elements
|
|
// slice; the scalar before it has none.
|
|
arr, ok := doc.Root().Get("arr")
|
|
if !ok {
|
|
t.Fatal("arr is missing")
|
|
}
|
|
elems := arr.Elements()
|
|
if len(elems) != 2 {
|
|
t.Fatalf("elements = %d, want 2", len(elems))
|
|
}
|
|
if elems[0] != nil {
|
|
t.Errorf("elements[0] = %#v, want nil for a scalar", elems[0])
|
|
}
|
|
if got := elems[1].Keys(); !slices.Equal(got, []string{"x"}) {
|
|
t.Errorf("elements[1] keys = %v, want [x]", got)
|
|
}
|
|
}
|
|
|
|
func TestDocumentWithoutStatements(t *testing.T) {
|
|
doc, err := Parse([]byte("# only a comment\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if got := doc.Root().Keys(); len(got) != 0 {
|
|
t.Errorf("keys = %v, want none", got)
|
|
}
|
|
if got := doc.Footer(); !slices.Equal(got, []string{"only a comment"}) {
|
|
t.Errorf("footer = %q, want [only a comment]", got)
|
|
}
|
|
|
|
empty, err := Parse(nil)
|
|
if err != nil {
|
|
t.Fatalf("parse of nothing: %v", err)
|
|
}
|
|
if len(empty.Root().Keys()) != 0 || len(empty.Footer()) != 0 {
|
|
t.Errorf("empty document = %v / %q", empty.Root().Keys(), empty.Footer())
|
|
}
|
|
}
|
|
|
|
func TestParseMapIsTheValueTree(t *testing.T) {
|
|
// ParseMap is the path that does not build a document, and it gives the
|
|
// tree the decoder reads.
|
|
tree, err := ParseMap([]byte("a = 1\n\n[t]\nb = \"x\"\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if tree["a"] != int64(1) {
|
|
t.Errorf("a = %#v", tree["a"])
|
|
}
|
|
if tree["t"].(map[string]any)["b"] != "x" {
|
|
t.Errorf("t = %#v", tree["t"])
|
|
}
|
|
}
|
|
|
|
func TestMarshalDocument(t *testing.T) {
|
|
// A Document writes back: the keys in written order, the comments above
|
|
// the lines and headers they belonged to, and inline tables inline again.
|
|
doc, err := Parse([]byte("# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\n\ninline = { n = 1 }\n"))
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
out, err := Marshal(doc)
|
|
if err != nil {
|
|
t.Fatalf("marshal of a Document: %v", err)
|
|
}
|
|
want := "# leading\na = 1 # trailing\n\n[t]\nb = \"x\"\ninline = {n = 1}\n"
|
|
if string(out) != want {
|
|
t.Errorf("output:\n%q\nwant:\n%q", out, want)
|
|
}
|
|
// The written document parses back to the same values.
|
|
re, err := Parse(out)
|
|
if err != nil {
|
|
t.Fatalf("re-parse: %v", err)
|
|
}
|
|
if got := re.Map()["a"]; got != int64(1) {
|
|
t.Errorf("a = %#v", got)
|
|
}
|
|
if _, err := Marshal(*doc); err != nil {
|
|
t.Errorf("marshal of a Document value: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestDocumentEditPipeline(t *testing.T) {
|
|
doc, err := Parse([]byte("host = \"db\"\nport = 5432\n\n# The cache section\ntimeout = 1.5\n"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Run("typed getters", func(t *testing.T) {
|
|
if s, ok := doc.GetString("host"); !ok || s != "db" {
|
|
t.Errorf("host = %q, %v", s, ok)
|
|
}
|
|
if i, ok := doc.GetInt("port"); !ok || i != 5432 {
|
|
t.Errorf("port = %d, %v", i, ok)
|
|
}
|
|
if f, ok := doc.GetFloat("timeout"); !ok || f != 1.5 {
|
|
t.Errorf("timeout = %g, %v", f, ok)
|
|
}
|
|
if _, ok := doc.GetBool("host"); ok {
|
|
t.Error("host claimed as bool")
|
|
}
|
|
})
|
|
t.Run("set keeps the position and the comments", func(t *testing.T) {
|
|
doc.Set("port", int64(9090))
|
|
if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout"}) {
|
|
t.Fatalf("keys = %v", got)
|
|
}
|
|
out, err := Marshal(doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "port = 9090") {
|
|
t.Errorf("output missing the new value:\n%s", out)
|
|
}
|
|
})
|
|
t.Run("a new key joins the end", func(t *testing.T) {
|
|
doc.Set("lang", "cs")
|
|
if got := doc.Root().Keys(); !slices.Equal(got, []string{"host", "port", "timeout", "lang"}) {
|
|
t.Fatalf("keys = %v", got)
|
|
}
|
|
})
|
|
t.Run("a set table keeps an order of its own", func(t *testing.T) {
|
|
sub := map[string]any{"z": int64(1), "a": int64(2)}
|
|
doc.Set("cache", sub)
|
|
out, err := Marshal(doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "[cache]\na = 2\nz = 1\n") {
|
|
t.Errorf("output missing the new table in order:\n%s", out)
|
|
}
|
|
})
|
|
t.Run("delete removes the key", func(t *testing.T) {
|
|
doc.Delete("lang")
|
|
if _, ok := doc.Get("lang"); ok {
|
|
t.Fatal("lang survived Delete")
|
|
}
|
|
out, err := Marshal(doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(out), "lang") {
|
|
t.Errorf("output still names lang:\n%s", out)
|
|
}
|
|
})
|
|
t.Run("UnmarshalDocument decodes without reparsing", func(t *testing.T) {
|
|
type Cfg struct {
|
|
Host string `toml:"host"`
|
|
Port int `toml:"port"`
|
|
}
|
|
var cfg Cfg
|
|
if err := UnmarshalDocument(doc, &cfg); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if cfg.Host != "db" || cfg.Port != 9090 {
|
|
t.Errorf("decoded %+v", cfg)
|
|
}
|
|
})
|
|
t.Run("comments survive the round trip", func(t *testing.T) {
|
|
src := "# header comment\n[a]\n# key comment\nb = 2\n"
|
|
doc, err := Parse([]byte(src))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := Marshal(doc)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, want := range []string{"# header comment", "[a]", "# key comment", "b = 2"} {
|
|
if !strings.Contains(string(out), want) {
|
|
t.Errorf("output missing %q:\n%s", want, out)
|
|
}
|
|
}
|
|
})
|
|
t.Run("a nil document refuses to decode", func(t *testing.T) {
|
|
var cfg struct {
|
|
A int `toml:"a"`
|
|
}
|
|
if err := UnmarshalDocument(nil, &cfg); err == nil {
|
|
t.Error("UnmarshalDocument(nil) succeeded, want an error")
|
|
}
|
|
})
|
|
}
|