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 is4
// compiled DIRECTLY into this test binary (see CMakeLists) so its coverage is measured here — the5
// 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"15
namespace xml = cheatah::parsers::xml;17
namespace {19
// The id of the document element (first top-level element) for convenience.20
int doc_elem(const xml::Document& d, const char* tag) {21
return xml::find(d, xml::root(d), tag);22
}24
// cppcheck-suppress syntaxError // cppcheck's tokenizer mishandles the embedded " in the raw string25
TEST(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 attribute35
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");39
}41
TEST(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 value48
EXPECT_EQ(xml::attr(d, e, "d"), "spaced"); // whitespace around '='49
// self-closing element has no children50
EXPECT_TRUE(xml::children(d, e).empty());51
}53
TEST(ParsersXml, EntityDecodingAllForms) {54
// the 5 predefined + decimal + hex numeric across the UTF-8 size ranges55
auto d = xml::parse("<t>&<>"' A © 中 😀</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 too61
auto d2 = xml::parse(R"(<t v="a&b"/>)");62
EXPECT_EQ(xml::attr(d2, doc_elem(d2, "t"), "v"), "a&b");63
}65
TEST(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 &#; z; � � "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("z;"), std::string::npos); // invalid digit inside a numeric ref75
EXPECT_NE(got.find("�"), std::string::npos);76
EXPECT_NE(got.find("�"), std::string::npos);77
EXPECT_NE(got.find("&really"), std::string::npos);78
}80
TEST(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 subtree84
const int b = xml::find(d, p, "b");85
EXPECT_EQ(xml::text(d, b), "BC");86
}88
TEST(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 only92
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 match96
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 self98
// children() includes the trailing text node99
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);102
}104
TEST(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/text110
EXPECT_EQ(xml::text(d, doc), "<raw> & ");111
}113
TEST(ParsersXml, IgnorableVsSignificantWhitespace) {114
// whitespace between top-level nodes is dropped; whitespace inside an element is kept115
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);124
}126
TEST(ParsersXml, LenientOnMalformedInput) {127
// none of these may crash; each returns a usable (possibly empty) document128
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 text131
xml::parse("<unterminated attr=\"x"); // tag runs to EOF132
xml::parse("<!-- comment never closed"); // comment to EOF133
xml::parse("<![CDATA[ never closed"); // CDATA to EOF134
xml::parse("<a><b></c></a>"); // mismatched close (stray </c>)135
xml::parse("</stray>"); // close with no open element136
xml::parse("<a/>trailing"); // self-close then trailing text137
auto d = xml::parse("<a><b>deep</b>"); // missing </a>: still navigable138
EXPECT_EQ(xml::tag(d, doc_elem(d, "a")), "a");139
SUCCEED();140
}142
TEST(ParsersXml, OutOfRangeAndTypeMismatchedIdsAreSafe) {143
auto d = xml::parse("<a>x</a>");144
// invalid ids never read out of bounds145
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), "");160
}162
} // namespace