XML-PugiXML
view release on metacpan or search on metacpan
t/01-basic.t view on Meta::CPAN
is($node->text, 'deep', 'can navigate deep nesting');
}
# Test long attribute value
{
my $long_val = 'x' x 10000;
my $doc = XML::PugiXML->new;
$doc->load_string("<root attr=\"$long_val\"/>");
is(length($doc->root->attr('attr')->value), 10000, 'long attr value preserved');
}
# Test many children
{
my $xml = '<root>' . ('<item/>' x 1000) . '</root>';
my $doc = XML::PugiXML->new;
ok($doc->load_string($xml), 'many children parse');
my @children = $doc->root->children;
is(scalar @children, 1000, 'got all 1000 children');
}
# Test XPath returning empty
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><item/></root>');
my $node = $doc->select_node('//nonexistent');
ok(!defined $node || !$node->valid, 'xpath no match returns undef or invalid');
my @nodes = $doc->select_nodes('//nonexistent');
is(scalar @nodes, 0, 'xpath no matches returns empty list');
}
# Test node modification after XPath
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><b/><c/></root>');
my @nodes = $doc->select_nodes('//a | //b | //c');
is(scalar @nodes, 3, 'xpath union works');
$nodes[0]->set_text('modified');
is($nodes[0]->text, 'modified', 'can modify xpath result');
}
# Test whitespace handling
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root> text with spaces </root>');
is($doc->root->text, ' text with spaces ', 'whitespace preserved in text');
}
# Test special characters in names
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root xmlns:ns="http://example.com"><ns:item ns:attr="val"/></root>');
my $item = $doc->root->first_child;
is($item->name, 'ns:item', 'namespaced element name');
is($item->attr('ns:attr')->value, 'val', 'namespaced attr value');
}
#--------------------------------------------------
# CDATA and Comment tests
#--------------------------------------------------
# Test append_cdata
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
my $cdata = $doc->root->append_cdata('<script>alert(1)</script>');
ok($cdata, 'append_cdata returns node');
is($cdata->type, 4, 'CDATA node type is 4');
like($doc->to_string, qr/<!\[CDATA\[<script>alert\(1\)<\/script>\]\]>/, 'CDATA in output');
}
# Test append_comment
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
my $comment = $doc->root->append_comment('This is a comment');
ok($comment, 'append_comment returns node');
is($comment->type, 5, 'comment node type is 5');
like($doc->to_string, qr/<!--This is a comment-->/, 'comment in output');
}
# Test node type
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root>text</root>');
is($doc->root->type, 2, 'element node type is 2');
# Text node (PCDATA)
my $text = $doc->root->first_child;
is($text->type, 3, 'text node type is 3');
}
# Test reading existing CDATA
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><![CDATA[<raw>&data</raw>]]></root>');
is($doc->root->text, '<raw>&data</raw>', 'CDATA content read correctly');
my $cdata_node = $doc->root->first_child;
is($cdata_node->type, 4, 'existing CDATA node type is 4');
}
#--------------------------------------------------
# New functionality tests
#--------------------------------------------------
# Test prepend_child
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><b/><c/></root>');
my $a = $doc->root->prepend_child('a');
ok($a, 'prepend_child returns node');
is($doc->root->first_child->name, 'a', 'prepend_child inserts at beginning');
}
# Test insert_child_before
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><c/></root>');
my $c = $doc->root->child('c');
my $b = $doc->root->insert_child_before('b', $c);
ok($b, 'insert_child_before returns node');
my @children = grep { $_->type == 2 } $doc->root->children;
is($children[1]->name, 'b', 'insert_child_before at correct position');
}
# Test insert_child_after
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><c/></root>');
my $a = $doc->root->child('a');
my $b = $doc->root->insert_child_after('b', $a);
ok($b, 'insert_child_after returns node');
my @children = grep { $_->type == 2 } $doc->root->children;
is($children[1]->name, 'b', 'insert_child_after at correct position');
}
# Test prepend_attr
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root b="2" c="3"/>');
my $a = $doc->root->prepend_attr('a');
$a->set_value('1');
my @attrs = $doc->root->attrs;
is($attrs[0]->name, 'a', 'prepend_attr inserts at beginning');
}
# Test path()
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><parent><child id="1"/></parent></root>');
my $child = $doc->root->child('parent')->child('child');
is($child->path, '/root/parent/child', 'path returns correct XPath');
is($child->path('.'), '.root.parent.child', 'path with custom delimiter');
}
# Test find_child_by_attribute
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><item id="1"/><item id="2"/><item id="3"/></root>');
t/01-basic.t view on Meta::CPAN
# Test PARSE_ESCAPES (enabled by default, test explicit use)
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root>ABC</root>', XML::PugiXML::PARSE_ESCAPES());
is($doc->root->text, 'ABC', 'PARSE_ESCAPES decodes numeric entities');
}
# Test PARSE_EOL (normalizes line endings)
{
my $doc = XML::PugiXML->new;
$doc->load_string("<root>line1\r\nline2</root>", XML::PugiXML::PARSE_EOL());
my $text = $doc->root->text;
unlike($text, qr/\r/, 'PARSE_EOL normalizes CRLF to LF');
}
#--------------------------------------------------
# Test gaps: error paths and edge cases
#--------------------------------------------------
# doc->child with nonexistent name
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
ok(!defined $doc->child('nonexistent'), 'doc->child nonexistent returns undef');
}
# load_file failure sets $@
{
my $doc = XML::PugiXML->new;
$doc->load_file('/nonexistent/path.xml');
like($@, qr/parse error/i, 'load_file failure sets $@ with error');
}
# load_string $@ includes offset
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><bad');
like($@, qr/offset \d+/, 'parse error $@ includes offset');
}
# set_value on element node returns false
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
ok(!$doc->root->set_value('x'), 'set_value on element returns false');
}
# set_name on text node returns false
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root>text</root>');
my $text_node = $doc->root->first_child;
is($text_node->type, 3, 'got text node');
ok(!$text_node->set_name('x'), 'set_name on text node returns false');
}
# value() on CDATA node
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><![CDATA[data]]></root>');
my $cdata = $doc->root->first_child;
is($cdata->value, 'data', 'value() on CDATA node returns content');
}
# previous_sibling with name filter no match
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><b/><c/></root>');
my $c = $doc->root->last_child;
ok(!defined $c->previous_sibling('z'), 'previous_sibling(name) no match returns undef');
}
# children with name that matches nothing
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><b/></root>');
my @none = $doc->root->children('z');
is(scalar @none, 0, 'children(nonexistent) returns empty list');
}
# attrs on node with no attributes
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
my @a = $doc->root->attrs;
is(scalar @a, 0, 'attrs on no-attribute node returns empty list');
}
# Node::select_nodes with invalid XPath
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
eval { $doc->root->select_nodes('///bad'); };
ok($@, 'invalid xpath on node select_nodes throws');
}
# evaluate_node with no match
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/></root>');
my $xpath = $doc->compile_xpath('//nonexistent');
my $result = $xpath->evaluate_node($doc->root);
ok(!defined $result, 'evaluate_node no match returns undef');
}
# evaluate_nodes with no match
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/></root>');
my $xpath = $doc->compile_xpath('//nonexistent');
my @results = $xpath->evaluate_nodes($doc->root);
is(scalar @results, 0, 'evaluate_nodes no match returns empty list');
}
# PARSE_COMMENTS functional test
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><!--hello--></root>', XML::PugiXML::PARSE_COMMENTS());
my @comments = grep { $_->type == 5 } $doc->root->children;
is(scalar @comments, 1, 'PARSE_COMMENTS makes comments accessible');
is($comments[0]->value, 'hello', 'PARSE_COMMENTS comment content');
}
# PARSE_CDATA functional test
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><![CDATA[raw]]></root>', XML::PugiXML::PARSE_CDATA());
my @cdata = grep { $_->type == 4 } $doc->root->children;
is(scalar @cdata, 1, 'PARSE_CDATA makes CDATA nodes accessible');
is($cdata[0]->value, 'raw', 'PARSE_CDATA content');
}
# save_file clears $@ on success
{
my ($fh, $tmp) = tempfile(SUFFIX => '.xml', UNLINK => 1);
close $fh;
my $doc = XML::PugiXML->new;
$doc->load_string('<bad>'); # sets $@
ok($@, '$@ set before save_file');
$doc->load_string('<root/>');
$doc->save_file($tmp);
is($@, '', 'save_file clears $@ on success');
unlink $tmp;
}
#--------------------------------------------------
# Stale handle detection (generation counter)
#--------------------------------------------------
# Test stale node after reset()
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><item>hello</item></root>');
my $node = $doc->root->child('item');
is($node->text, 'hello', 'node works before reset');
$doc->reset;
eval { $node->text; };
like($@, qr/Stale node handle/, 'accessing node after reset() croaks');
eval { $node->name; };
like($@, qr/Stale node handle/, 'accessing name after reset() croaks');
ok(!$node->valid, 'stale node valid() returns false');
}
# Test stale attr after reset()
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root id="1"/>');
my $attr = $doc->root->attr('id');
is($attr->value, '1', 'attr works before reset');
$doc->reset;
eval { $attr->value; };
like($@, qr/Stale attribute handle/, 'accessing attr after reset() croaks');
ok(!$attr->valid, 'stale attr valid() returns false');
}
# Test stale node after load_string()
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><old/></root>');
my $old_node = $doc->root;
$doc->load_string('<newroot/>');
t/01-basic.t view on Meta::CPAN
my $attr = $doc->select_node('//@x');
isa_ok($attr, 'XML::PugiXML::Attr', 'XPath returns Attr');
my $elem = $attr->element;
is($elem->name, 'a', 'element() from XPath attr returns correct parent');
}
# element() from attrs() list
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root foo="1" bar="2"/>');
my @attrs = $doc->root->attrs;
is($attrs[0]->element->name, 'root', 'element() from attrs() list');
}
# element() from set_attr
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
my $attr = $doc->root->set_attr('key', 'val');
is($attr->element->name, 'root', 'element() from set_attr');
}
# Test UTF-8 flagged input works normally
{
my $doc = XML::PugiXML->new;
my $utf8_xml = "<root>café</root>";
utf8::upgrade($utf8_xml);
ok(utf8::is_utf8($utf8_xml), 'test string is UTF-8 flagged');
ok($doc->load_string($utf8_xml), 'load_string accepts UTF-8 flagged string');
like($doc->root->text, qr/caf/, 'UTF-8 text preserved');
}
# Test set_text with non-ASCII
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root/>');
my $text = "Ãnïcödé";
utf8::upgrade($text);
$doc->root->set_text($text);
like($doc->root->text, qr/n.c/, 'set_text with UTF-8 works');
}
#--------------------------------------------------
# NODE_* type constants
#--------------------------------------------------
{
is(XML::PugiXML::NODE_NULL(), 0, 'NODE_NULL');
is(XML::PugiXML::NODE_DOCUMENT(), 1, 'NODE_DOCUMENT');
is(XML::PugiXML::NODE_ELEMENT(), 2, 'NODE_ELEMENT');
is(XML::PugiXML::NODE_PCDATA(), 3, 'NODE_PCDATA');
is(XML::PugiXML::NODE_CDATA(), 4, 'NODE_CDATA');
is(XML::PugiXML::NODE_COMMENT(), 5, 'NODE_COMMENT');
is(XML::PugiXML::NODE_PI(), 6, 'NODE_PI');
is(XML::PugiXML::NODE_DECLARATION(), 7, 'NODE_DECLARATION');
is(XML::PugiXML::NODE_DOCTYPE(), 8, 'NODE_DOCTYPE');
my $doc = XML::PugiXML->new;
$doc->load_string('<root>text</root>');
is($doc->root->type, XML::PugiXML::NODE_ELEMENT(), 'element type matches constant');
is($doc->root->first_child->type, XML::PugiXML::NODE_PCDATA(), 'pcdata type matches constant');
}
#--------------------------------------------------
# New format/parse constants
#--------------------------------------------------
{
ok(defined XML::PugiXML::FORMAT_INDENT_ATTRIBUTES(), 'FORMAT_INDENT_ATTRIBUTES defined');
ok(defined XML::PugiXML::PARSE_WS_PCDATA_SINGLE(), 'PARSE_WS_PCDATA_SINGLE defined');
my $doc = XML::PugiXML->new;
$doc->load_string('<root a="1" b="2"/>');
my $out = $doc->to_string(" ", XML::PugiXML::FORMAT_INDENT()
| XML::PugiXML::FORMAT_INDENT_ATTRIBUTES());
like($out, qr/\n.*a="1"/, 'FORMAT_INDENT_ATTRIBUTES wraps attrs');
}
#--------------------------------------------------
# Attr::set_name
#--------------------------------------------------
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root old="v"/>');
my $attr = $doc->root->attr('old');
ok($attr->set_name('renamed'), 'set_name returns true');
is($attr->name, 'renamed', 'attr name updated');
is($doc->root->attr('renamed')->value, 'v', 'value preserved after rename');
ok(!defined $doc->root->attr('old'), 'old name no longer present');
}
#--------------------------------------------------
# Stale-handle checks on secondary node arguments
#--------------------------------------------------
{
my $doc = XML::PugiXML->new;
$doc->load_string('<root><a/><b/></root>');
my $a = $doc->root->child('a');
my $b = $doc->root->child('b');
$doc->reset;
$doc->load_string('<root><x/></root>');
my $root = $doc->root;
eval { $root->insert_child_before('y', $a); };
like($@, qr/Stale/, 'insert_child_before croaks on stale ref');
eval { $root->insert_child_after('y', $a); };
like($@, qr/Stale/, 'insert_child_after croaks on stale ref');
eval { $root->remove_child($a); };
like($@, qr/Stale/, 'remove_child croaks on stale arg');
eval { $root->append_copy($a); };
like($@, qr/Stale/, 'append_copy croaks on stale source');
eval { $root->prepend_copy($a); };
like($@, qr/Stale/, 'prepend_copy croaks on stale source');
eval { $root->insert_copy_before($a, $b); };
like($@, qr/Stale/, 'insert_copy_before croaks on stale arg');
eval { $root->insert_copy_after($a, $b); };
( run in 0.946 second using v1.01-cache-2.11-cpan-995e09ba956 )