315 lines
8.3 KiB
Go
315 lines
8.3 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 TestMarshalRejectsDocument(t *testing.T) {
|
||
|
|
// A Document is not a value to marshal: its order and comments would be
|
||
|
|
// dropped, and a struct walk would silently write nothing at all.
|
||
|
|
doc, err := Parse([]byte("a = 1\n"))
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("parse: %v", err)
|
||
|
|
}
|
||
|
|
if _, err := Marshal(doc); err == nil {
|
||
|
|
t.Fatal("expected an error for a Document")
|
||
|
|
} else if !strings.Contains(err.Error(), "Map()") {
|
||
|
|
t.Errorf("err = %v, want it to point at Map()", err)
|
||
|
|
}
|
||
|
|
if _, err := Marshal(*doc); err == nil {
|
||
|
|
t.Fatal("expected an error for a Document value")
|
||
|
|
}
|
||
|
|
// The tree marshals, which is the way through.
|
||
|
|
out, err := Marshal(doc.Map())
|
||
|
|
if err != nil {
|
||
|
|
t.Fatalf("marshal of the tree: %v", err)
|
||
|
|
}
|
||
|
|
if want := "a = 1\n"; string(out) != want {
|
||
|
|
t.Errorf("output = %q, want %q", out, want)
|
||
|
|
}
|
||
|
|
}
|