// Copyright (c) 2026 Petr BalvĂ­n (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) } }