cheatah
Source

stdlib/tests/parsers_xml_test.cpp

1// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).
2// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.
3// In-process unit tests for the `parsers.xml` slab-DOM reader (parsers/xml/xml.cpp). xml.cpp is
4// compiled DIRECTLY into this test binary (see CMakeLists) so its coverage is measured here — the
5// tests below exercise every parse branch (elements/attrs/text/entities/CDATA/comments/PI/DOCTYPE,
6// self-closing, and the lenient malformed-input paths) and every navigation function.
8#include <string>
9#include <vector>
11#include <gtest/gtest.h>
13#include "xml/xml.hpp"
15namespace xml = cheatah::parsers::xml;
17namespace {
19// The id of the document element (first top-level element) for convenience.
20int doc_elem(const xml::Document& d, const char* tag) {
21 return xml::find(d, xml::root(d), tag);
24// cppcheck-suppress syntaxError // cppcheck's tokenizer mishandles the embedded " in the raw string
25TEST(ParsersXml, ParsesElementsAttrsAndText) {
26 auto d = xml::parse(R"(<doc><item id="1" flag>hello</item></doc>)");
27 const int doc = doc_elem(d, "doc");
28 ASSERT_GE(doc, 0);
29 EXPECT_TRUE(xml::is_element(d, doc));
30 EXPECT_EQ(xml::tag(d, doc), "doc");
31 const int item = xml::find(d, doc, "item");
32 ASSERT_GE(item, 0);
33 EXPECT_EQ(xml::attr(d, item, "id"), "1");
34 EXPECT_TRUE(xml::has_attr(d, item, "flag")); // valueless attribute
35 EXPECT_EQ(xml::attr(d, item, "flag"), "");
36 EXPECT_FALSE(xml::has_attr(d, item, "missing"));
37 EXPECT_EQ(xml::attr(d, item, "missing"), "");
38 EXPECT_EQ(xml::text(d, item), "hello");
41TEST(ParsersXml, AttributeQuotingForms) {
42 auto d = xml::parse(R"(<e a="dq" b='sq' c=bare d = "spaced" />)");
43 const int e = doc_elem(d, "e");
44 ASSERT_GE(e, 0);
45 EXPECT_EQ(xml::attr(d, e, "a"), "dq");
46 EXPECT_EQ(xml::attr(d, e, "b"), "sq");
47 EXPECT_EQ(xml::attr(d, e, "c"), "bare"); // unquoted value
48 EXPECT_EQ(xml::attr(d, e, "d"), "spaced"); // whitespace around '='
49 // self-closing element has no children
50 EXPECT_TRUE(xml::children(d, e).empty());
53TEST(ParsersXml, EntityDecodingAllForms) {
54 // the 5 predefined + decimal + hex numeric across the UTF-8 size ranges
55 auto d = xml::parse("<t>&amp;&lt;&gt;&quot;&apos; &#65; &#169; &#x4E2D; &#x1F600;</t>");
56 const int t = doc_elem(d, "t");
57 ASSERT_GE(t, 0);
58 EXPECT_EQ(xml::text(d, t),
59 std::string("&<>\"' ") + "A" + " \xC2\xA9" + " \xE4\xB8\xAD" + " \xF0\x9F\x98\x80");
60 // attribute values are decoded too
61 auto d2 = xml::parse(R"(<t v="a&amp;b"/>)");
62 EXPECT_EQ(xml::attr(d2, doc_elem(d2, "t"), "v"), "a&b");
65TEST(ParsersXml, UnknownAndMalformedReferencesKeptVerbatim) {
66 // unknown named entity, bare '&', numeric with no digits, out-of-range, zero, and a '&'
67 // whose ';' is past the 32-char window are all left as-is.
68 auto d = xml::parse("<t>&bogus; A & B &#; &#12z; &#x110000; &#0; "
69 "&reallyreallyreallyreallyreallylongname;</t>");
70 const std::string got = xml::text(d, doc_elem(d, "t"));
71 EXPECT_NE(got.find("&bogus;"), std::string::npos);
72 EXPECT_NE(got.find("A & B"), std::string::npos);
73 EXPECT_NE(got.find("&#;"), std::string::npos);
74 EXPECT_NE(got.find("&#12z;"), std::string::npos); // invalid digit inside a numeric ref
75 EXPECT_NE(got.find("&#x110000;"), std::string::npos);
76 EXPECT_NE(got.find("&#0;"), std::string::npos);
77 EXPECT_NE(got.find("&really"), std::string::npos);
80TEST(ParsersXml, NestedTextIsCollectedRecursively) {
81 auto d = xml::parse("<p>a<b>B<i>C</i></b>d</p>");
82 const int p = doc_elem(d, "p");
83 EXPECT_EQ(xml::text(d, p), "aBCd"); // whole subtree
84 const int b = xml::find(d, p, "b");
85 EXPECT_EQ(xml::text(d, b), "BC");
88TEST(ParsersXml, FindFindallIterChildren) {
89 auto d = xml::parse("<r><x/><x/><y><x/></y>text</r>");
90 const int r = doc_elem(d, "r");
91 EXPECT_EQ(xml::findall(d, r, "x").size(), 2u); // direct children only
92 EXPECT_EQ(xml::find(d, r, "y"), *(&xml::findall(d, r, "y")[0]));
93 EXPECT_EQ(xml::find(d, r, "nope"), -1);
94 EXPECT_TRUE(xml::findall(d, r, "nope").empty());
95 // iter() walks the whole subtree, in document order, including a self match
96 EXPECT_EQ(xml::iter(d, r, "x").size(), 3u); // two direct + one under <y>
97 EXPECT_EQ(xml::iter(d, r, "r").size(), 1u); // matches self
98 // children() includes the trailing text node
99 bool saw_text = false;
100 for (int c : xml::children(d, r)) if (!xml::is_element(d, c)) saw_text = true;
101 EXPECT_TRUE(saw_text);
104TEST(ParsersXml, CommentsCdataPiPrologDoctype) {
105 auto d = xml::parse("<?xml version=\"1.0\"?>\n<!DOCTYPE doc>\n"
106 "<doc><!-- a comment --><![CDATA[<raw> & ]]></doc>");
107 const int doc = doc_elem(d, "doc");
108 ASSERT_GE(doc, 0);
109 // CDATA is literal (not entity-decoded), comment produces no node/text
110 EXPECT_EQ(xml::text(d, doc), "<raw> & ");
113TEST(ParsersXml, IgnorableVsSignificantWhitespace) {
114 // whitespace between top-level nodes is dropped; whitespace inside an element is kept
115 auto d = xml::parse(" <a> </a> ");
116 const int a = doc_elem(d, "a");
117 ASSERT_GE(a, 0);
118 EXPECT_EQ(xml::text(d, a), " ");
119 // no stray text node at the document root (only the <a> element)
120 int elems = 0;
121 for (int c : xml::children(d, xml::root(d))) if (xml::is_element(d, c)) ++elems;
122 EXPECT_EQ(elems, 1);
123 EXPECT_EQ(xml::children(d, xml::root(d)).size(), 1u);
126TEST(ParsersXml, LenientOnMalformedInput) {
127 // none of these may crash; each returns a usable (possibly empty) document
128 EXPECT_EQ(xml::children(xml::parse(""), 0).size(), 0u);
129 xml::parse("plain text with no tags");
130 xml::parse("a < b, 3 < 4"); // bare '<' as text
131 xml::parse("<unterminated attr=\"x"); // tag runs to EOF
132 xml::parse("<!-- comment never closed"); // comment to EOF
133 xml::parse("<![CDATA[ never closed"); // CDATA to EOF
134 xml::parse("<a><b></c></a>"); // mismatched close (stray </c>)
135 xml::parse("</stray>"); // close with no open element
136 xml::parse("<a/>trailing"); // self-close then trailing text
137 auto d = xml::parse("<a><b>deep</b>"); // missing </a>: still navigable
138 EXPECT_EQ(xml::tag(d, doc_elem(d, "a")), "a");
139 SUCCEED();
142TEST(ParsersXml, OutOfRangeAndTypeMismatchedIdsAreSafe) {
143 auto d = xml::parse("<a>x</a>");
144 // invalid ids never read out of bounds
145 EXPECT_FALSE(xml::is_element(d, -1));
146 EXPECT_FALSE(xml::is_element(d, 9999));
147 EXPECT_EQ(xml::tag(d, -1), "");
148 EXPECT_EQ(xml::attr(d, 9999, "z"), "");
149 EXPECT_FALSE(xml::has_attr(d, 9999, "z"));
150 EXPECT_EQ(xml::text(d, -1), "");
151 EXPECT_TRUE(xml::children(d, -1).empty());
152 EXPECT_EQ(xml::find(d, -1, "z"), -1);
153 EXPECT_TRUE(xml::findall(d, -1, "z").empty());
154 EXPECT_TRUE(xml::iter(d, -1, "z").empty());
155 // tag() on a text node returns ""
156 const int a = doc_elem(d, "a");
157 const int textnode = xml::children(d, a)[0];
158 EXPECT_FALSE(xml::is_element(d, textnode));
159 EXPECT_EQ(xml::tag(d, textnode), "");
162} // namespace