blob: fe8c657183016fd76f5c848f1a99c7f59e6e01d7 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.handler.dataimport;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.junit.Test;
/**
* <p> Test for XPathRecordReader </p>
*
*
* @since solr 1.3
*/
public class TestXPathRecordReader extends AbstractDataImportHandlerTestCase {
@Test
public void testBasic() {
String xml="<root>\n"
+ " <b><c>Hello C1</c>\n"
+ " <c>Hello C1</c>\n"
+ " </b>\n"
+ " <b><c>Hello C2</c>\n"
+ " </b>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/b");
rr.addField("c", "/root/b/c", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertEquals(2, ((List) l.get(0).get("c")).size());
assertEquals(1, ((List) l.get(1).get("c")).size());
}
@Test
public void testAttributes() {
String xml="<root>\n"
+ " <b a=\"x0\" b=\"y0\" />\n"
+ " <b a=\"x1\" b=\"y1\" />\n"
+ " <b a=\"x2\" b=\"y2\" />\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/b");
rr.addField("a", "/root/b/@a", false);
rr.addField("b", "/root/b/@b", false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(3, l.size());
assertEquals("x0", l.get(0).get("a"));
assertEquals("x1", l.get(1).get("a"));
assertEquals("x2", l.get(2).get("a"));
assertEquals("y0", l.get(0).get("b"));
assertEquals("y1", l.get(1).get("b"));
assertEquals("y2", l.get(2).get("b"));
}
@Test
public void testAttrInRoot(){
String xml="<r>\n" +
"<merchantProduct id=\"814636051\" mid=\"189973\">\n" +
" <in_stock type=\"stock-4\" />\n" +
" <condition type=\"cond-0\" />\n" +
" <price>301.46</price>\n" +
" </merchantProduct>\n" +
"<merchantProduct id=\"814636052\" mid=\"189974\">\n" +
" <in_stock type=\"stock-5\" />\n" +
" <condition type=\"cond-1\" />\n" +
" <price>302.46</price>\n" +
" </merchantProduct>\n" +
"\n" +
"</r>";
XPathRecordReader rr = new XPathRecordReader("/r/merchantProduct");
rr.addField("id", "/r/merchantProduct/@id", false);
rr.addField("mid", "/r/merchantProduct/@mid", false);
rr.addField("price", "/r/merchantProduct/price", false);
rr.addField("conditionType", "/r/merchantProduct/condition/@type", false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
Map<String, Object> m = l.get(0);
assertEquals("814636051", m.get("id"));
assertEquals("189973", m.get("mid"));
assertEquals("301.46", m.get("price"));
assertEquals("cond-0", m.get("conditionType"));
m = l.get(1);
assertEquals("814636052", m.get("id"));
assertEquals("189974", m.get("mid"));
assertEquals("302.46", m.get("price"));
assertEquals("cond-1", m.get("conditionType"));
}
@Test
public void testAttributes2Level() {
String xml="<root>\n"
+ "<a>\n <b a=\"x0\" b=\"y0\" />\n"
+ " <b a=\"x1\" b=\"y1\" />\n"
+ " <b a=\"x2\" b=\"y2\" />\n"
+ " </a>"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a/b");
rr.addField("a", "/root/a/b/@a", false);
rr.addField("b", "/root/a/b/@b", false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(3, l.size());
assertEquals("x0", l.get(0).get("a"));
assertEquals("y1", l.get(1).get("b"));
}
@Test
public void testAttributes2LevelHetero() {
String xml="<root>\n"
+ "<a>\n <b a=\"x0\" b=\"y0\" />\n"
+ " <b a=\"x1\" b=\"y1\" />\n"
+ " <b a=\"x2\" b=\"y2\" />\n"
+ " </a>"
+ "<x>\n <b a=\"x4\" b=\"y4\" />\n"
+ " <b a=\"x5\" b=\"y5\" />\n"
+ " <b a=\"x6\" b=\"y6\" />\n"
+ " </x>"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a | /root/x");
rr.addField("a", "/root/a/b/@a", false);
rr.addField("b", "/root/a/b/@b", false);
rr.addField("a", "/root/x/b/@a", false);
rr.addField("b", "/root/x/b/@b", false);
final List<Map<String, Object>> a = new ArrayList<>();
final List<Map<String, Object>> x = new ArrayList<>();
rr.streamRecords(new StringReader(xml), (record, xpath) -> {
if (record == null) return;
if (xpath.equals("/root/a")) a.add(record);
if (xpath.equals("/root/x")) x.add(record);
});
assertEquals(1, a.size());
assertEquals(1, x.size());
}
@Test
public void testAttributes2LevelMissingAttrVal() {
String xml="<root>\n"
+ "<a>\n <b a=\"x0\" b=\"y0\" />\n"
+ " <b a=\"x1\" b=\"y1\" />\n"
+ " </a>"
+ "<a>\n <b a=\"x3\" />\n"
+ " <b b=\"y4\" />\n"
+ " </a>"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("a", "/root/a/b/@a", true);
rr.addField("b", "/root/a/b/@b", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertNull(((List) l.get(1).get("a")).get(1));
assertNull(((List) l.get(1).get("b")).get(0));
}
@Test
public void testElems2LevelMissing() {
String xml="<root>\n"
+ "\t<a>\n"
+ "\t <b>\n\t <x>x0</x>\n"
+ "\t <y>y0</y>\n"
+ "\t </b>\n"
+ "\t <b>\n\t <x>x1</x>\n"
+ "\t <y>y1</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "\t<a>\n"
+ "\t <b>\n\t <x>x3</x>\n\t </b>\n"
+ "\t <b>\n\t <y>y4</y>\n\t </b>\n"
+ "\t </a>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("a", "/root/a/b/x", true);
rr.addField("b", "/root/a/b/y", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertNull(((List) l.get(1).get("a")).get(1));
assertNull(((List) l.get(1).get("b")).get(0));
}
@Test
public void testElems2LevelEmpty() {
String xml="<root>\n"
+ "\t<a>\n"
+ "\t <b>\n\t <x>x0</x>\n"
+ "\t <y>y0</y>\n"
+ "\t </b>\n"
+ "\t <b>\n\t <x></x>\n" // empty
+ "\t <y>y1</y>\n"
+ "\t </b>\n"
+ "\t</a>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("a", "/root/a/b/x", true);
rr.addField("b", "/root/a/b/y", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
assertEquals("x0",((List) l.get(0).get("a")).get(0));
assertEquals("y0",((List) l.get(0).get("b")).get(0));
assertEquals("",((List) l.get(0).get("a")).get(1));
assertEquals("y1",((List) l.get(0).get("b")).get(1));
}
@Test
public void testMixedContent() {
String xml = "<xhtml:p xmlns:xhtml=\"http://xhtml.com/\" >This text is \n" +
" <xhtml:b>bold</xhtml:b> and this text is \n" +
" <xhtml:u>underlined</xhtml:u>!\n" +
"</xhtml:p>";
XPathRecordReader rr = new XPathRecordReader("/p");
rr.addField("p", "/p", true);
rr.addField("b", "/p/b", true);
rr.addField("u", "/p/u", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
Map<String, Object> row = l.get(0);
assertEquals("bold", ((List) row.get("b")).get(0));
assertEquals("underlined", ((List) row.get("u")).get(0));
String p = (String) ((List) row.get("p")).get(0);
assertTrue(p.contains("This text is"));
assertTrue(p.contains("and this text is"));
assertTrue(p.contains("!"));
// Should not contain content from child elements
assertFalse(p.contains("bold"));
}
@Test
public void testMixedContentFlattened() {
String xml = "<xhtml:p xmlns:xhtml=\"http://xhtml.com/\" >This text is \n" +
" <xhtml:b>bold</xhtml:b> and this text is \n" +
" <xhtml:u>underlined</xhtml:u>!\n" +
"</xhtml:p>";
XPathRecordReader rr = new XPathRecordReader("/p");
rr.addField("p", "/p", false, XPathRecordReader.FLATTEN);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
Map<String, Object> row = l.get(0);
assertEquals("This text is \n" +
" bold and this text is \n" +
" underlined!", ((String)row.get("p")).trim() );
}
@Test
public void testElems2LevelWithAttrib() {
String xml = "<root>\n\t<a>\n\t <b k=\"x\">\n"
+ "\t <x>x0</x>\n"
+ "\t <y></y>\n" // empty
+ "\t </b>\n"
+ "\t <b k=\"y\">\n"
+ "\t <x></x>\n" // empty
+ "\t <y>y1</y>\n"
+ "\t </b>\n"
+ "\t <b k=\"z\">\n"
+ "\t <x>x2</x>\n"
+ "\t <y>y2</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "\t <a>\n\t <b>\n"
+ "\t <x>x3</x>\n"
+ "\t </b>\n"
+ "\t <b>\n"
+ "\t <y>y4</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("x", "/root/a/b[@k]/x", true);
rr.addField("y", "/root/a/b[@k]/y", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertEquals(3, ((List) l.get(0).get("x")).size());
assertEquals(3, ((List) l.get(0).get("y")).size());
assertEquals("x0", ((List) l.get(0).get("x")).get(0));
assertEquals("", ((List) l.get(0).get("y")).get(0));
assertEquals("", ((List) l.get(0).get("x")).get(1));
assertEquals("y1", ((List) l.get(0).get("y")).get(1));
assertEquals("x2", ((List) l.get(0).get("x")).get(2));
assertEquals("y2", ((List) l.get(0).get("y")).get(2));
assertEquals(0, l.get(1).size());
}
@Test
public void testElems2LevelWithAttribMultiple() {
String xml="<root>\n"
+ "\t<a>\n\t <b k=\"x\" m=\"n\" >\n"
+ "\t <x>x0</x>\n"
+ "\t <y>y0</y>\n"
+ "\t </b>\n"
+ "\t <b k=\"y\" m=\"p\">\n"
+ "\t <x>x1</x>\n"
+ "\t <y>y1</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "\t<a>\n\t <b k=\"x\">\n"
+ "\t <x>x3</x>\n"
+ "\t </b>\n"
+ "\t <b m=\"n\">\n"
+ "\t <y>y4</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("x", "/root/a/b[@k][@m='n']/x", true);
rr.addField("y", "/root/a/b[@k][@m='n']/y", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertEquals(1, ((List) l.get(0).get("x")).size());
assertEquals(1, ((List) l.get(0).get("y")).size());
assertEquals(0, l.get(1).size());
}
@Test
public void testElems2LevelWithAttribVal() {
String xml="<root>\n\t<a>\n <b k=\"x\">\n"
+ "\t <x>x0</x>\n"
+ "\t <y>y0</y>\n"
+ "\t </b>\n"
+ "\t <b k=\"y\">\n"
+ "\t <x>x1</x>\n"
+ "\t <y>y1</y>\n"
+ "\t </b>\n"
+ "\t </a>\n"
+ "\t <a>\n <b><x>x3</x></b>\n"
+ "\t <b><y>y4</y></b>\n"
+ "\t</a>\n" + "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/a");
rr.addField("x", "/root/a/b[@k='x']/x", true);
rr.addField("y", "/root/a/b[@k='x']/y", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(2, l.size());
assertEquals(1, ((List) l.get(0).get("x")).size());
assertEquals(1, ((List) l.get(0).get("y")).size());
assertEquals(0, l.get(1).size());
}
@Test
public void testAttribValWithSlash() {
String xml = "<root><b>\n" +
" <a x=\"a/b\" h=\"hello-A\"/> \n" +
"</b></root>";
XPathRecordReader rr = new XPathRecordReader("/root/b");
rr.addField("x", "/root/b/a[@x='a/b']/@h", false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
Map<String, Object> m = l.get(0);
assertEquals("hello-A", m.get("x"));
}
@Test
public void testUnsupportedXPaths() {
RuntimeException ex = expectThrows(RuntimeException.class, () -> new XPathRecordReader("//b"));
assertEquals("forEach cannot start with '//': //b", ex.getMessage());
XPathRecordReader rr = new XPathRecordReader("/anyd/contenido");
ex = expectThrows(RuntimeException.class, () -> rr.addField("bold", "b", false));
assertEquals("xpath must start with '/' : b", ex.getMessage());
}
@Test
public void testAny_decendent_from_root() {
XPathRecordReader rr = new XPathRecordReader("/anyd/contenido");
rr.addField("descdend", "//boo", true);
rr.addField("inr_descd","//boo/i", false);
rr.addField("cont", "/anyd/contenido", false);
rr.addField("id", "/anyd/contenido/@id", false);
rr.addField("status", "/anyd/status", false);
rr.addField("title", "/anyd/contenido/titulo", false,XPathRecordReader.FLATTEN);
rr.addField("resume", "/anyd/contenido/resumen",false);
rr.addField("text", "/anyd/contenido/texto", false);
String xml="<anyd>\n"
+ " this <boo>top level</boo> is ignored because it is external to the forEach\n"
+ " <status>as is <boo>this element</boo></status>\n"
+ " <contenido id=\"10097\" idioma=\"cat\">\n"
+ " This one is <boo>not ignored as it's</boo> inside a forEach\n"
+ " <antetitulo><i> big <boo>antler</boo></i></antetitulo>\n"
+ " <titulo> My <i>flattened <boo>title</boo></i> </titulo>\n"
+ " <resumen> My summary <i>skip this!</i> </resumen>\n"
+ " <texto> <boo>Within the body of</boo>My text</texto>\n"
+ " <p>Access <boo>inner <i>sub clauses</i> as well</boo></p>\n"
+ " </contenido>\n"
+ "</anyd>";
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
Map<String, Object> m = l.get(0);
assertEquals("This one is inside a forEach", m.get("cont").toString().trim());
assertEquals("10097" ,m.get("id"));
assertEquals("My flattened title" ,m.get("title").toString().trim());
assertEquals("My summary" ,m.get("resume").toString().trim());
assertEquals("My text" ,m.get("text").toString().trim());
assertEquals("not ignored as it's",(String) ((List) m.get("descdend")).get(0) );
assertEquals("antler" ,(String) ((List) m.get("descdend")).get(1) );
assertEquals("Within the body of" ,(String) ((List) m.get("descdend")).get(2) );
assertEquals("inner as well" ,(String) ((List) m.get("descdend")).get(3) );
assertEquals("sub clauses" ,m.get("inr_descd").toString().trim());
}
@Test
public void testAny_decendent_of_a_child1() {
XPathRecordReader rr = new XPathRecordReader("/anycd");
rr.addField("descdend", "/anycd//boo", true);
// same test string as above but checking to see if *all* //boo's are collected
String xml="<anycd>\n"
+ " this <boo>top level</boo> is ignored because it is external to the forEach\n"
+ " <status>as is <boo>this element</boo></status>\n"
+ " <contenido id=\"10097\" idioma=\"cat\">\n"
+ " This one is <boo>not ignored as it's</boo> inside a forEach\n"
+ " <antetitulo><i> big <boo>antler</boo></i></antetitulo>\n"
+ " <titulo> My <i>flattened <boo>title</boo></i> </titulo>\n"
+ " <resumen> My summary <i>skip this!</i> </resumen>\n"
+ " <texto> <boo>Within the body of</boo>My text</texto>\n"
+ " <p>Access <boo>inner <i>sub clauses</i> as well</boo></p>\n"
+ " </contenido>\n"
+ "</anycd>";
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
Map<String, Object> m = l.get(0);
assertEquals("top level" ,(String) ((List) m.get("descdend")).get(0) );
assertEquals("this element" ,(String) ((List) m.get("descdend")).get(1) );
assertEquals("not ignored as it's",(String) ((List) m.get("descdend")).get(2) );
assertEquals("antler" ,(String) ((List) m.get("descdend")).get(3) );
assertEquals("title" ,(String) ((List) m.get("descdend")).get(4) );
assertEquals("Within the body of" ,(String) ((List) m.get("descdend")).get(5) );
assertEquals("inner as well" ,(String) ((List) m.get("descdend")).get(6) );
}
@Test
public void testAny_decendent_of_a_child2() {
XPathRecordReader rr = new XPathRecordReader("/anycd");
rr.addField("descdend", "/anycd/contenido//boo", true);
// same test string as above but checking to see if *some* //boo's are collected
String xml="<anycd>\n"
+ " this <boo>top level</boo> is ignored because it is external to the forEach\n"
+ " <status>as is <boo>this element</boo></status>\n"
+ " <contenido id=\"10097\" idioma=\"cat\">\n"
+ " This one is <boo>not ignored as it's</boo> inside a forEach\n"
+ " <antetitulo><i> big <boo>antler</boo></i></antetitulo>\n"
+ " <titulo> My <i>flattened <boo>title</boo></i> </titulo>\n"
+ " <resumen> My summary <i>skip this!</i> </resumen>\n"
+ " <texto> <boo>Within the body of</boo>My text</texto>\n"
+ " <p>Access <boo>inner <i>sub clauses</i> as well</boo></p>\n"
+ " </contenido>\n"
+ "</anycd>";
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
Map<String, Object> m = l.get(0);
assertEquals("not ignored as it's",((List) m.get("descdend")).get(0) );
assertEquals("antler" ,((List) m.get("descdend")).get(1) );
assertEquals("title" ,((List) m.get("descdend")).get(2) );
assertEquals("Within the body of" ,((List) m.get("descdend")).get(3) );
assertEquals("inner as well" ,((List) m.get("descdend")).get(4) );
}
@Test
public void testAnother() {
String xml="<root>\n"
+ " <contenido id=\"10097\" idioma=\"cat\">\n"
+ " <antetitulo></antetitulo>\n"
+ " <titulo> This is my title </titulo>\n"
+ " <resumen> This is my summary </resumen>\n"
+ " <texto> This is the body of my text </texto>\n"
+ " </contenido>\n"
+ "</root>";
XPathRecordReader rr = new XPathRecordReader("/root/contenido");
rr.addField("id", "/root/contenido/@id", false);
rr.addField("title", "/root/contenido/titulo", false);
rr.addField("resume","/root/contenido/resumen",false);
rr.addField("text", "/root/contenido/texto", false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals(1, l.size());
Map<String, Object> m = l.get(0);
assertEquals("10097", m.get("id"));
assertEquals("This is my title", m.get("title").toString().trim());
assertEquals("This is my summary", m.get("resume").toString().trim());
assertEquals("This is the body of my text", m.get("text").toString()
.trim());
}
@Test
public void testSameForEachAndXpath(){
String xml="<root>\n" +
" <cat>\n" +
" <name>hello</name>\n" +
" </cat>\n" +
" <item name=\"item name\"/>\n" +
"</root>";
XPathRecordReader rr = new XPathRecordReader("/root/cat/name");
rr.addField("catName", "/root/cat/name",false);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
assertEquals("hello",l.get(0).get("catName"));
}
@Test
@SuppressWarnings({"unchecked"})
public void testPutNullTest(){
String xml = "<root>\n" +
" <i>\n" +
" <x>\n" +
" <a>A.1.1</a>\n" +
" <b>B.1.1</b>\n" +
" </x>\n" +
" <x>\n" +
" <b>B.1.2</b>\n" +
" <c>C.1.2</c>\n" +
" </x>\n" +
" </i>\n" +
" <i>\n" +
" <x>\n" +
" <a>A.2.1</a>\n" +
" <c>C.2.1</c>\n" +
" </x>\n" +
" <x>\n" +
" <b>B.2.2</b>\n" +
" <c>C.2.2</c>\n" +
" </x>\n" +
" </i>\n" +
"</root>";
XPathRecordReader rr = new XPathRecordReader("/root/i");
rr.addField("a", "/root/i/x/a", true);
rr.addField("b", "/root/i/x/b", true);
rr.addField("c", "/root/i/x/c", true);
List<Map<String, Object>> l = rr.getAllRecords(new StringReader(xml));
Map<String, Object> map = l.get(0);
List<String> a = (List<String>) map.get("a");
List<String> b = (List<String>) map.get("b");
List<String> c = (List<String>) map.get("c");
assertEquals("A.1.1",a.get(0));
assertEquals("B.1.1",b.get(0));
assertNull(c.get(0));
assertNull(a.get(1));
assertEquals("B.1.2",b.get(1));
assertEquals("C.1.2",c.get(1));
map = l.get(1);
a = (List<String>) map.get("a");
b = (List<String>) map.get("b");
c = (List<String>) map.get("c");
assertEquals("A.2.1",a.get(0));
assertNull(b.get(0));
assertEquals("C.2.1",c.get(0));
assertNull(a.get(1));
assertEquals("B.2.2",b.get(1));
assertEquals("C.2.2",c.get(1));
}
@Test
public void testError(){
String malformedXml = "<root>\n" +
" <node>\n" +
" <id>1</id>\n" +
" <desc>test1</desc>\n" +
" </node>\n" +
" <node>\n" +
" <id>2</id>\n" +
" <desc>test2</desc>\n" +
" </node>\n" +
" <node>\n" +
" <id/>3</id>\n" + // invalid XML
" <desc>test3</desc>\n" +
" </node>\n" +
"</root>";
XPathRecordReader rr = new XPathRecordReader("/root/node");
rr.addField("id", "/root/node/id", true);
rr.addField("desc", "/root/node/desc", true);
RuntimeException e = expectThrows(RuntimeException.class, () -> rr.getAllRecords(new StringReader(malformedXml)));
assertTrue(e.getMessage().contains("Unexpected close tag </id>"));
}
}