// SPDX-License-Identifier: GPL-3.0-or-later package sexp import "testing" func mustParse(t *testing.T, src string) []*Node { t.Helper() nodes, err := Parse("t.conf", []byte(src)) if err != nil { t.Fatalf("Parse(%q): %v", src, err) } return nodes } // walk calls fn for every node, depth first. func walk(nodes []*Node, fn func(*Node)) { for _, n := range nodes { fn(n) walk(n.Children, fn) } } func TestAtoms(t *testing.T) { tests := []struct { src string kind Kind text string }{ {`pdf`, Symbol, "pdf"}, {`30d`, Symbol, "30d"}, {`>=`, Symbol, ">="}, {`"acme ltd"`, String, "acme ltd"}, {`"say \"hi\""`, String, `say "hi"`}, {`"\bacme\b"`, String, `\bacme\b`}, {`"a\\b"`, String, `a\b`}, {`"a\\\\b"`, String, `a\\b`}, {"\"two\nlines\"", String, "two\nlines"}, {`"spółka"`, String, "spółka"}, {`""`, String, ""}, } for _, tt := range tests { nodes := mustParse(t, tt.src) if len(nodes) != 1 || nodes[0].Kind != tt.kind || nodes[0].Text != tt.text { t.Errorf("Parse(%q) = %+v, want one %v %q", tt.src, nodes, tt.kind, tt.text) } } } func TestListStructure(t *testing.T) { nodes := mustParse(t, `(when (type pdf) (content "x" "y"))`) if len(nodes) != 1 { t.Fatalf("got %d nodes", len(nodes)) } w := nodes[0] if w.Kind != List || w.Head() != "when" || len(w.Args()) != 2 { t.Fatalf("when = %+v", w) } c := w.Args()[1] if c.Head() != "content" || len(c.Args()) != 2 || c.Args()[1].Text != "y" { t.Fatalf("content = %+v", c) } if got := mustParse(t, `("x" y)`)[0].Head(); got != "" { t.Errorf("Head of a list starting with a string = %q, want empty", got) } } func TestPositions(t *testing.T) { src := "; comment\n(path \"~/d\")\n (ignore \"*.part\")\n" nodes := mustParse(t, src) if len(nodes) != 2 { t.Fatalf("got %d nodes", len(nodes)) } checks := []struct { name string got, want Pos }{ {"path start", nodes[0].Pos, Pos{Offset: 10, Line: 2, Col: 1}}, {"path end", nodes[0].End, Pos{Offset: 22, Line: 2, Col: 13}}, {"string start", nodes[0].Args()[0].Pos, Pos{Offset: 16, Line: 2, Col: 7}}, {"string end", nodes[0].Args()[0].End, Pos{Offset: 21, Line: 2, Col: 12}}, {"ignore start", nodes[1].Pos, Pos{Offset: 25, Line: 3, Col: 3}}, } for _, c := range checks { if c.got != c.want { t.Errorf("%s = %+v, want %+v", c.name, c.got, c.want) } } } func TestColumnsCountCharacters(t *testing.T) { x := mustParse(t, `("ł" x)`)[0].Children[1] if want := (Pos{Offset: 6, Line: 1, Col: 6}); x.Pos != want { t.Fatalf("x at %+v, want %+v", x.Pos, want) } } // TestBOMIsSkipped is item G: a leading UTF-8 byte-order mark must not // become a visible symbol, and byte offsets after it must stay offsets into // the original source. func TestBOMIsSkipped(t *testing.T) { src := "\xEF\xBB\xBF(a)" nodes := mustParse(t, src) if len(nodes) != 1 { t.Fatalf("got %d nodes, want 1", len(nodes)) } n := nodes[0] want := Pos{Offset: 3, Line: 1, Col: 1} if n.Pos != want { t.Errorf("Pos = %+v, want %+v", n.Pos, want) } if n.End.Offset != 6 { t.Errorf("End.Offset = %d, want 6", n.End.Offset) } } func TestListOffsetsPointAtParens(t *testing.T) { src := "(rule \"a\"\n (when (or (type pdf) (name \"x\")))\n (stop)) (b)" walk(mustParse(t, src), func(n *Node) { if n.Kind == List && (src[n.Pos.Offset] != '(' || src[n.End.Offset-1] != ')') { t.Errorf("list %s spans %d..%d", n, n.Pos.Offset, n.End.Offset) } }) } func TestComments(t *testing.T) { nodes := mustParse(t, "(a ; x ) y\n b) ; c") if len(nodes) != 1 || len(nodes[0].Children) != 2 || nodes[0].Children[1].Text != "b" { t.Fatalf("got %+v, want (a b)", nodes) } if n := mustParse(t, ""); n != nil { t.Errorf("empty source gave %+v", n) } if n := mustParse(t, "; only a comment\n"); n != nil { t.Errorf("comment-only source gave %+v", n) } } func TestNodeString(t *testing.T) { tests := map[string]string{ `(rule "acme" (when x) (stop))`: `(rule "acme" ...)`, `(stop)`: `(stop)`, `(type pdf docx odt)`: `(type pdf ...)`, `(a b)`: `(a b)`, `()`: `()`, `((a) b)`: `(...)`, `sym`: `sym`, `"a\"b"`: `"a\"b"`, } for src, want := range tests { if got := mustParse(t, src)[0].String(); got != want { t.Errorf("String of %s = %s, want %s", src, got, want) } } } func TestQuoteRoundTrip(t *testing.T) { for _, v := range []string{"plain", `a"b`, `a\b`, `\bacme\b`, `x\"y`, "~/My Files"} { n := mustParse(t, Quote(v)) if len(n) != 1 || n[0].Kind != String || n[0].Text != v { t.Errorf("Quote(%q) = %s does not read back", v, Quote(v)) } } } func TestErrors(t *testing.T) { tests := []struct{ src, want string }{ {`(a (b)`, `t.conf:1:1: "(" never closed: (a ...)`}, {"(rule \"acme\"\n (when (type pdf)", `t.conf:1:1: "(" never closed: (rule "acme" ...)`}, {`a)`, `t.conf:1:2: unexpected ")"`}, {"(a\n \"abc", `t.conf:2:3: string never closed`}, {"ok \xff", `t.conf:1:4: invalid UTF-8`}, } for _, tt := range tests { _, err := Parse("t.conf", []byte(tt.src)) if err == nil || err.Error() != tt.want { t.Errorf("Parse(%q) error = %v, want %s", tt.src, err, tt.want) } if _, ok := err.(*Error); err != nil && !ok { t.Errorf("Parse(%q) error is %T, want *Error", tt.src, err) } } }