From 842a167433aee555264aa7bd6f5c9af1d12508ae Mon Sep 17 00:00:00 2001 From: jcamiel Date: Fri, 27 Oct 2023 16:25:20 +0200 Subject: [PATCH 1/2] Add DOM XML parser based on xml-rs. --- Cargo.lock | 1 + packages/hurl/Cargo.toml | 1 + packages/hurl/src/report/junit/mod.rs | 2 +- packages/hurl/src/report/junit/testcase.rs | 6 +- packages/hurl/src/report/junit/xml/mod.rs | 112 ++++++ packages/hurl/src/report/junit/xml/reader.rs | 402 +++++++++++++++++++ packages/hurl/src/report/junit/xml/writer.rs | 219 ++++++++++ 7 files changed, 739 insertions(+), 4 deletions(-) create mode 100644 packages/hurl/src/report/junit/xml/mod.rs create mode 100644 packages/hurl/src/report/junit/xml/reader.rs create mode 100644 packages/hurl/src/report/junit/xml/writer.rs diff --git a/Cargo.lock b/Cargo.lock index c6fc94d4e87..ce26c1eaa0e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -539,6 +539,7 @@ dependencies = [ "url", "uuid", "winres", + "xml-rs", "xmltree", ] diff --git a/packages/hurl/Cargo.toml b/packages/hurl/Cargo.toml index 007b488e20c..3c603efec59 100644 --- a/packages/hurl/Cargo.toml +++ b/packages/hurl/Cargo.toml @@ -41,6 +41,7 @@ serde_json = "1.0.108" sha2 = "0.10.8" url = "2.4.1" xmltree = { version = "0.10.3", features = ["attribute-order"] } +xml-rs = { version = "0.8.19"} lazy_static = "1.4.0" # uuid features: lets you generate random UUIDs and use a faster (but still sufficiently random) RNG uuid = { version = "1.5.0", features = ["v4" , "fast-rng"] } diff --git a/packages/hurl/src/report/junit/mod.rs b/packages/hurl/src/report/junit/mod.rs index 7d2abf61732..d0bf5abc78c 100644 --- a/packages/hurl/src/report/junit/mod.rs +++ b/packages/hurl/src/report/junit/mod.rs @@ -55,7 +55,7 @@ //! ``` //! mod testcase; - +mod xml; use std::fs::File; use indexmap::IndexMap; diff --git a/packages/hurl/src/report/junit/testcase.rs b/packages/hurl/src/report/junit/testcase.rs index dc700bc661c..1a68a62d6ce 100644 --- a/packages/hurl/src/report/junit/testcase.rs +++ b/packages/hurl/src/report/junit/testcase.rs @@ -15,11 +15,11 @@ * limitations under the License. * */ -use indexmap::map::IndexMap; -use xmltree::{Element, XMLNode}; - +use super::xml::XmlDocument; use crate::runner::HurlResult; use crate::util::logger; +use indexmap::map::IndexMap; +use xmltree::{Element, XMLNode}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Testcase { diff --git a/packages/hurl/src/report/junit/xml/mod.rs b/packages/hurl/src/report/junit/xml/mod.rs new file mode 100644 index 00000000000..d955e0d066e --- /dev/null +++ b/packages/hurl/src/report/junit/xml/mod.rs @@ -0,0 +1,112 @@ +/* + * Hurl (https://hurl.dev) + * Copyright (C) 2023 Orange + * + * Licensed 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. + * + */ +mod reader; +mod writer; + +/// An XML element document. +/// +/// This struct provides support for serialization to and from standard XML. +/// This is a lightweight object wrapper around [xml-rs crate](https://github.com/netvl/xml-rs) +/// to simplify XML in-memory tree manipulation. +/// +/// XML namespaces are not supported, as the main usage of this class is +/// to support JUnit report. +/// +pub struct XmlDocument { + pub root: Option, +} + +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum XmlNode { + /// An XML element. + Element(Element), + /// A CDATA section. + CData(String), + /// A comment. + Comment(String), + /// A text content. + Text(String), + /// Processing instruction. + ProcessingInstruction(String, Option), +} + +/// A XML attribute. +#[derive(Clone, Eq, PartialEq, Debug)] +pub struct Attribute { + pub name: String, + pub value: String, +} + +impl Attribute { + fn new(name: &str, value: &str) -> Self { + Attribute { + name: name.to_string(), + value: value.to_string(), + } + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Element { + // We are only using local name, namespaces are not managed + pub name: String, + /// This element's attributes. + pub attrs: Vec, + /// This element's children. + pub children: Vec, +} + +impl Element { + pub fn new(name: &str) -> Element { + Element { + name: name.to_string(), + attrs: Vec::new(), + children: Vec::new(), + } + } + + /// Add a new attribute to `self`. + pub fn attr(mut self, name: &str, value: &str) -> Self { + self.attrs.push(Attribute::new(name, value)); + self + } + + /// Add a new child `element`. + pub fn add_child(mut self, element: Element) -> Self { + self.children.push(XmlNode::Element(element)); + self + } + + /// Add a text content to `self`. + pub fn text(mut self, text: &str) -> Self { + self.children.push(XmlNode::Text(text.to_string())); + self + } + + /// Add a comment `self`. + pub fn comment(mut self, comment: &str) -> Self { + self.children.push(XmlNode::Comment(comment.to_string())); + self + } + + /// Add a CDATA section `self`. + pub fn cdata(mut self, cdata: &str) -> Self { + self.children.push(XmlNode::CData(cdata.to_string())); + self + } +} diff --git a/packages/hurl/src/report/junit/xml/reader.rs b/packages/hurl/src/report/junit/xml/reader.rs new file mode 100644 index 00000000000..d8335b61dfe --- /dev/null +++ b/packages/hurl/src/report/junit/xml/reader.rs @@ -0,0 +1,402 @@ +/* + * Hurl (https://hurl.dev) + * Copyright (C) 2023 Orange + * + * Licensed 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. + * + */ +use crate::report::junit::xml::reader::ParserError::{GenericError, InvalidXml}; +use crate::report::junit::xml::{Attribute, Element, XmlDocument, XmlNode}; +use xml::attribute::OwnedAttribute; +use xml::name::OwnedName; +use xml::reader::{EventReader, XmlEvent}; + +/// Errors raised when deserializing a buffer. +#[derive(Clone, Eq, PartialEq, Debug)] +pub enum ParserError { + InvalidXml(String), + GenericError(String), +} + +/// Deserializes a XML document from [`std::io::Read`] source. The XML +/// document returned is a in-memory tree representation of the whole document. +impl XmlDocument { + /// Convenient associated method to read and parse a XML string `source`. + pub fn parse_str(source: &str) -> Result { + let bytes = source.as_bytes(); + XmlDocument::parse(bytes) + } + + /// Read a XML `source` and parses it to a [`XmlDocument`]. + pub fn parse(source: R) -> Result + where + R: std::io::Read, + { + let mut reader = EventReader::new(source); + let mut initialized = false; + let mut root: Option = None; + loop { + match reader.next() { + Ok(XmlEvent::StartDocument { .. }) => initialized = true, + Ok(XmlEvent::EndDocument) => { + if !initialized { + return Err(InvalidXml("Invalid end of document".to_string())); + } + return Ok(XmlDocument { root }); + } + Ok(XmlEvent::ProcessingInstruction { .. }) => {} + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + // At this point of the parsing, we must have an initialized document + // and no root. + if !initialized || root.is_some() { + return Err(InvalidXml("Invalid start of document".to_string())); + } + let element = Element::try_parse(&name, &attributes, &mut reader)?; + root = Some(element); + } + Ok(XmlEvent::EndElement { .. }) => { + return Err(InvalidXml("Invalid end of element".to_string())) + } + Ok(XmlEvent::CData(_)) => {} + Ok(XmlEvent::Comment(_)) => {} + Ok(XmlEvent::Characters(_)) => {} + Ok(XmlEvent::Whitespace(_)) => {} + Err(e) => return Err(GenericError(e.to_string())), + } + } + } +} + +impl Element { + fn try_parse( + name: &OwnedName, + attributes: &[OwnedAttribute], + reader: &mut EventReader, + ) -> Result { + let mut element = Element::new(&name.local_name); + element.attrs = attributes + .iter() + .map(|a| Attribute::new(&a.name.to_string(), &a.value)) + .collect(); + + loop { + match reader.next() { + Ok(XmlEvent::StartDocument { .. }) => { + return Err(InvalidXml("Invalid start of document".to_string())) + } + Ok(XmlEvent::EndDocument) => { + return Err(InvalidXml("Invalid stop of document".to_string())) + } + Ok(XmlEvent::ProcessingInstruction { name, data }) => { + let child = XmlNode::ProcessingInstruction(name, data); + element.children.push(child); + } + Ok(XmlEvent::StartElement { + name, attributes, .. + }) => { + let child = Element::try_parse(&name, &attributes, reader)?; + element.children.push(XmlNode::Element(child)); + } + Ok(XmlEvent::EndElement { name, .. }) => { + return if element.name == name.local_name { + Ok(element) + } else { + Err(InvalidXml(format!("Bag closing element {name}"))) + } + } + Ok(XmlEvent::CData(value)) => { + let child = XmlNode::CData(value); + element.children.push(child); + } + Ok(XmlEvent::Comment(value)) => { + let child = XmlNode::Comment(value); + element.children.push(child); + } + Ok(XmlEvent::Characters(value)) => { + let child = XmlNode::Text(value); + element.children.push(child); + } + Ok(XmlEvent::Whitespace(_)) => {} + Err(e) => return Err(GenericError(e.to_string())), + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::report::junit::xml::{Element, XmlDocument}; + + #[test] + fn read_xml_0_succeed() { + let xml = r#" + + + + +"#; + let doc = XmlDocument::parse_str(xml).unwrap(); + assert_eq!( + doc.root.unwrap(), + Element::new("names") + .add_child( + Element::new("name") + .attr("first", "bob") + .attr("last", "jones") + ) + .add_child( + Element::new("name") + .attr("first", "elizabeth") + .attr("last", "smith") + ) + ); + } + + #[test] + fn read_xml_1_succeed() { + let xml = r#" + + + + + + + + + Some <java> class + + + Another "java" class + + + Weird 'XML' config + + + + + + + + + + JavaScript & program + + + Cascading style sheet: © - ҉ + + + + + "#; + + let doc = XmlDocument::parse_str(xml).unwrap(); + assert_eq!( + doc.root.unwrap(), + Element::new("project") + .attr("name", "project-name") + .add_child( + Element::new("libraries") + .add_child( + Element::new("library") + .attr("groupId", "org.example") + .attr("artifactId", "") + .attr("version", "0.1") + ) + .add_child( + Element::new("library") + .attr("groupId", "com.example") + .attr("artifactId", "\"cool-lib&") + .attr("version", "999") + ) + ) + .add_child( + Element::new("module") + .attr("name", "module-1") + .add_child( + Element::new("files") + .add_child( + Element::new("file") + .attr("name", "somefile.java") + .attr("type", "java") + .text("\n Some class\n ") + ) + .add_child( + Element::new("file") + .attr("name", "another_file.java") + .attr("type", "java") + .text("\n Another \"java\" class\n ") + ) + .add_child( + Element::new("file") + .attr("name", "config.xml") + .attr("type", "xml") + .text("\n Weird 'XML' config\n ") + ) + ) + .add_child( + Element::new("libraries") + .add_child(Element::new("library") + .attr("groupId", "junit") + .attr("artifactId", "junit") + .attr("version", "1.9.5") + ) + ) + ) + .add_child( + Element::new("module") + .attr("name", "module-2") + .add_child( + Element::new("files") + .add_child( + Element::new("file") + .attr("name", "program.js") + .attr("type", "javascript") + .text("\n JavaScript & program\n ") + ) + .add_child( + Element::new("file") + .attr("name", "style.css") + .attr("type", "css") + .text("\n Cascading style sheet: © - \u{489}\n ") + ) + ) + ) + ); + } + + #[test] + fn read_xml_with_namespaces() { + let xml = r#" + + + Name + Another name + 0.3 + 0.2 + 0.1 + 0.01 + header 1 value + + Some bigger value + + +"#; + + let doc = XmlDocument::parse_str(xml).unwrap(); + assert_eq!( + doc.root.unwrap(), + Element::new("data").add_child( + Element::new("datum") + .attr("id", "34") + .add_child(Element::new("name").text("Name")) + .add_child(Element::new("name").text("Another name")) + .add_child(Element::new("arg").text("0.3")) + .add_child(Element::new("arg").text("0.2")) + .add_child(Element::new("arg").text("0.1")) + .add_child(Element::new("arg").text("0.01")) + .add_child( + Element::new("header") + .attr("name", "Header-1") + .text("header 1 value") + ) + .add_child( + Element::new("header") + .attr("name", "Header-2") + .text("\n Some bigger value\n ") + ) + ) + ); + } + + #[test] + fn read_junit_xml() { + let xml = r#"Assert status code + --> /tmp/b.hurl:4:6 + | + 4 | HTTP 300 + | ^^^ actual value is <200> + |Assert failure + --> /tmp/c.hurl:6:0 + | + 6 | xpath "normalize-space(//title)" == "Hello World!" + | actual: string <Hurl - Run and Test HTTP Requests> + | expected: string <Hello World!> + |"#; + let doc = XmlDocument::parse_str(xml).unwrap(); + assert_eq!( + doc.root.unwrap(), + Element::new("testsuites") + .add_child( + Element::new("testsuite") + .attr("tests", "3") + .attr("errors", "0") + .attr("failures", "1") + .add_child( + Element::new("testcase") + .attr("id", "/tmp/a.hurl") + .attr("name", "/tmp/a.hurl") + .attr("time", "0.438") + ) + .add_child( + Element::new("testcase") + .attr("id", "/tmp/b.hurl") + .attr("name", "/tmp/b.hurl") + .attr("time", "0.234") + .add_child(Element::new("failure").text( + r#"Assert status code + --> /tmp/b.hurl:4:6 + | + 4 | HTTP 300 + | ^^^ actual value is <200> + |"# + )) + ) + .add_child( + Element::new("testcase") + .attr("id", "/tmp/c.hurl") + .attr("name", "/tmp/c.hurl") + .attr("time", "0.236") + ) + ) + .add_child( + Element::new("testsuite") + .attr("tests", "2") + .attr("errors", "0") + .attr("failures", "1") + .add_child( + Element::new("testcase") + .attr("id", "/tmp/a.hurl") + .attr("name", "/tmp/a.hurl") + .attr("time", "0.370") + ) + .add_child( + Element::new("testcase") + .attr("id", "/tmp/c.hurl") + .attr("name", "/tmp/c.hurl") + .attr("time", "0.247") + .add_child(Element::new("failure").text( + r#"Assert failure + --> /tmp/c.hurl:6:0 + | + 6 | xpath "normalize-space(//title)" == "Hello World!" + | actual: string + | expected: string + |"# + )) + ) + ) + ) + } +} diff --git a/packages/hurl/src/report/junit/xml/writer.rs b/packages/hurl/src/report/junit/xml/writer.rs new file mode 100644 index 00000000000..47383ad89cf --- /dev/null +++ b/packages/hurl/src/report/junit/xml/writer.rs @@ -0,0 +1,219 @@ +/* + * Hurl (https://hurl.dev) + * Copyright (C) 2023 Orange + * + * Licensed 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. + * + */ +use crate::report::junit::xml::writer::WriterError::GenericError; +use crate::report::junit::xml::{Element, XmlDocument, XmlNode}; +use std::borrow::Cow; +use std::string::FromUtf8Error; +use xml::attribute::Attribute; +use xml::name::Name; +use xml::namespace::Namespace; +use xml::writer::{Error, XmlEvent}; +use xml::EventWriter; + +/// Errors raised when serializing an XML document. +#[derive(Debug)] +pub enum WriterError { + Io(std::io::Error), + FromUtf8Error(FromUtf8Error), + GenericError(String), +} + +impl From for WriterError { + fn from(value: Error) -> Self { + match value { + Error::Io(error) => WriterError::Io(error), + Error::DocumentStartAlreadyEmitted + | Error::LastElementNameNotAvailable + | Error::EndElementNameIsNotEqualToLastStartElementName + | Error::EndElementNameIsNotSpecified => GenericError(value.to_string()), + } + } +} + +impl From for WriterError { + fn from(value: FromUtf8Error) -> Self { + WriterError::FromUtf8Error(value) + } +} + +impl XmlDocument { + /// Convenient method to seralize an XML document to a string. + pub fn write_string(&self) -> Result { + let buffer = vec![]; + let buffer = self.write(buffer)?; + let str = String::from_utf8(buffer)?; + Ok(str) + } + + /// Serializes an XML document to a `buffer`. + pub fn write(&self, buffer: W) -> Result + where + W: std::io::Write, + { + let mut writer = EventWriter::new(buffer); + if let Some(root) = &self.root { + root.write(&mut writer)?; + } + Ok(writer.into_inner()) + } +} + +impl XmlNode { + fn write(&self, writer: &mut EventWriter) -> Result<(), Error> + where + W: std::io::Write, + { + match self { + XmlNode::Element(elem) => elem.write(writer)?, + XmlNode::CData(cdata) => writer.write(XmlEvent::CData(cdata))?, + XmlNode::Comment(comment) => writer.write(XmlEvent::Comment(comment))?, + XmlNode::Text(text) => writer.write(XmlEvent::Characters(text))?, + XmlNode::ProcessingInstruction(name, data) => match data { + Some(string) => writer.write(XmlEvent::ProcessingInstruction { + name, + data: Some(string), + })?, + None => writer.write(XmlEvent::ProcessingInstruction { name, data: None })?, + }, + } + Ok(()) + } +} + +impl Element { + fn write(&self, writer: &mut EventWriter) -> Result<(), Error> + where + W: std::io::Write, + { + let name = Name::local(&self.name); + let attributes = self + .attrs + .iter() + .map(|attr| Attribute { + name: Name::local(&attr.name), + value: &attr.value, + }) + .collect(); + + // TODO: manage namespaces + let empty_ns = Namespace::empty(); + writer.write(XmlEvent::StartElement { + name, + attributes: Cow::Owned(attributes), + namespace: Cow::Owned(empty_ns), + })?; + + for child in self.children.iter() { + child.write(writer)?; + } + + writer.write(XmlEvent::EndElement { name: Some(name) })?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use crate::report::junit::xml::{Element, XmlDocument}; + + #[test] + fn write_xml_0() { + let root = Element::new("catalog") + .add_child( + Element::new("book") + .attr("id", "bk101") + .add_child( + Element::new("author") + .text("Gambardella, Matthew") + ) + .add_child( + Element::new("title") + .text("XML Developer's Guide") + ) + .add_child( + Element::new("genre") + .text("Computer") + ) + .add_child( + Element::new("price") + .text("44.95") + ) + .add_child( + Element::new("publish_date") + .text("2000-10-01") + ) + .add_child( + Element::new("description") + .text("An in-depth look at creating applications with XML.") + ) + ) + .add_child( + Element::new("book") + .attr("id", "bk102") + .add_child( + Element::new("author") + .text("Ralls, Kim") + ) + .add_child( + Element::new("title") + .text("Midnight Rain") + ) + .add_child( + Element::new("genre") + .text("Fantasy") + ) + .add_child( + Element::new("price") + .text("5.95") + ) + .add_child( + Element::new("publish_date") + .text("2000-12-16") + ) + .add_child( + Element::new("description") + .text("A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.") + ) + ) + ; + let doc = XmlDocument { root: Some(root) }; + assert_eq!( + doc.write_string().unwrap(), + "\ + \ + \ + Gambardella, Matthew\ + XML Developer's Guide\ + Computer\ + 44.95\ + 2000-10-01\ + An in-depth look at creating applications with XML.\ + \ + \ + Ralls, Kim\ + Midnight Rain\ + Fantasy\ + 5.95\ + 2000-12-16\ + A former architect battles corporate zombies, an evil sorceress, and her own childhood to become queen of the world.\ + \ + " + ); + } +} From c72f04fa0e4639455c4510ba451e89dc7fd33be9 Mon Sep 17 00:00:00 2001 From: jcamiel Date: Wed, 1 Nov 2023 20:23:49 +0100 Subject: [PATCH 2/2] Remove xmltree/indexmap dependency. Due to xmltree re-exposing an older version of indexmap, we couldnt' upgrade to the latest version of indemap. xmltree is a tree in-memory representation of an XML document that we use for JUnit export. As xmltree is a thin layer above xml-rs, we re-implement a thin tree in-memory XML document using xml-rs directly and remove xmltree/indexmap dependency. --- Cargo.lock | 30 +------ bin/update_crates.sh | 4 +- integration/tests_ok/junit.out.pattern | 2 +- packages/hurl/Cargo.toml | 2 - packages/hurl/src/report/junit/mod.rs | 68 ++++++---------- packages/hurl/src/report/junit/testcase.rs | 86 ++++++-------------- packages/hurl/src/report/junit/xml/mod.rs | 6 ++ packages/hurl/src/report/junit/xml/reader.rs | 1 + packages/hurl/src/report/junit/xml/writer.rs | 7 +- 9 files changed, 66 insertions(+), 140 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ce26c1eaa0e..06c0e74f73a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -474,12 +474,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.13.2" @@ -525,7 +519,6 @@ dependencies = [ "hex", "hex-literal", "hurl_core", - "indexmap", "lazy_static", "libflate", "libxml", @@ -540,7 +533,6 @@ dependencies = [ "uuid", "winres", "xml-rs", - "xmltree", ] [[package]] @@ -597,16 +589,6 @@ dependencies = [ "unicode-normalization", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "is-terminal" version = "0.4.9" @@ -665,7 +647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be5f52fb8c451576ec6b79d3f4deb327398bc05bbdbd99021a6e77a4c855d524" dependencies = [ "core2", - "hashbrown 0.13.2", + "hashbrown", "rle-decode-fast", ] @@ -1362,16 +1344,6 @@ version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a" -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "indexmap", - "xml-rs", -] - [[package]] name = "zerocopy" version = "0.7.21" diff --git a/bin/update_crates.sh b/bin/update_crates.sh index 97c7e706044..48ad136ceca 100755 --- a/bin/update_crates.sh +++ b/bin/update_crates.sh @@ -116,9 +116,7 @@ main() { check_args "${arg}" updated_count=0 - # xmltree-rs crate v0.10.3 doesn't build with the latest indexmap crates - # see - blacklisted="indexmap" + blacklisted="" # update toml for package in packages/*; do diff --git a/integration/tests_ok/junit.out.pattern b/integration/tests_ok/junit.out.pattern index 3be6fc6c1df..c75c4b3f70d 100644 --- a/integration/tests_ok/junit.out.pattern +++ b/integration/tests_ok/junit.out.pattern @@ -1,4 +1,4 @@ -Assert body value +Assert body value --> tests_ok/test.2.hurl:8:1 | 8 | `Goodbye World!` diff --git a/packages/hurl/Cargo.toml b/packages/hurl/Cargo.toml index 3c603efec59..2b238474c16 100644 --- a/packages/hurl/Cargo.toml +++ b/packages/hurl/Cargo.toml @@ -30,7 +30,6 @@ glob = "0.3.1" hex = "0.4.3" hex-literal = "0.4.1" hurl_core = { version = "4.2.0-SNAPSHOT", path = "../hurl_core" } -indexmap = "1.9.3" libflate = "2.0.0" libxml = "0.3.3" md5 = "0.7.0" @@ -40,7 +39,6 @@ serde = "1.0.190" serde_json = "1.0.108" sha2 = "0.10.8" url = "2.4.1" -xmltree = { version = "0.10.3", features = ["attribute-order"] } xml-rs = { version = "0.8.19"} lazy_static = "1.4.0" # uuid features: lets you generate random UUIDs and use a faster (but still sufficiently random) RNG diff --git a/packages/hurl/src/report/junit/mod.rs b/packages/hurl/src/report/junit/mod.rs index d0bf5abc78c..8e7f6c73d84 100644 --- a/packages/hurl/src/report/junit/mod.rs +++ b/packages/hurl/src/report/junit/mod.rs @@ -58,20 +58,16 @@ mod testcase; mod xml; use std::fs::File; -use indexmap::IndexMap; -use xmltree::{Element, XMLNode}; - +use crate::report::junit::xml::{Element, XmlDocument}; use crate::report::Error; pub use testcase::Testcase; /// Creates a JUnit from a list of `testcases`. pub fn write_report(filename: &str, testcases: &[Testcase]) -> Result<(), Error> { - let mut testsuites = vec![]; - // If there is an existing JUnit report, we parses it to insert a new testsuite. let path = std::path::Path::new(&filename); - if path.exists() { - let s = match std::fs::read_to_string(path) { + let mut root = if path.exists() { + let file = match File::open(path) { Ok(s) => s, Err(why) => { return Err(Error { @@ -79,24 +75,16 @@ pub fn write_report(filename: &str, testcases: &[Testcase]) -> Result<(), Error> }); } }; - let root = Element::parse(s.as_bytes()).unwrap(); - for child in root.children { - if let XMLNode::Element(_) = child.clone() { - testsuites.push(child.clone()); - } - } - } + let doc = XmlDocument::parse(file).unwrap(); + doc.root.unwrap() + } else { + Element::new("testsuites") + }; let testsuite = create_testsuite(testcases); - testsuites.push(XMLNode::Element(testsuite)); - let report = Element { - name: "testsuites".to_string(), - prefix: None, - namespace: None, - namespaces: None, - attributes: IndexMap::new(), - children: testsuites, - }; + root = root.add_child(testsuite); + + let doc = XmlDocument::new(root); let file = match File::create(filename) { Ok(f) => f, Err(e) => { @@ -105,7 +93,7 @@ pub fn write_report(filename: &str, testcases: &[Testcase]) -> Result<(), Error> }); } }; - match report.write(file) { + match doc.write(file) { Ok(_) => Ok(()), Err(e) => Err(Error { message: format!("Failed to produce Junit report: {e:?}"), @@ -115,7 +103,6 @@ pub fn write_report(filename: &str, testcases: &[Testcase]) -> Result<(), Error> /// Returns a testsuite as a XML object, from a list of `testcases`. fn create_testsuite(testcases: &[Testcase]) -> Element { - let mut attrs = IndexMap::new(); let mut tests = 0; let mut errors = 0; let mut failures = 0; @@ -126,26 +113,21 @@ fn create_testsuite(testcases: &[Testcase]) -> Element { failures += cases.get_fail_count(); } - attrs.insert("tests".to_string(), tests.to_string()); - attrs.insert("errors".to_string(), errors.to_string()); - attrs.insert("failures".to_string(), failures.to_string()); + let mut element = Element::new("testsuite") + .attr("tests", &tests.to_string()) + .attr("errors", &errors.to_string()) + .attr("failures", &failures.to_string()); - let children = testcases - .iter() - .map(|t| XMLNode::Element(t.to_xml())) - .collect(); - Element { - name: "testsuite".to_string(), - prefix: None, - namespace: None, - namespaces: None, - attributes: attrs, - children, + for testcase in testcases.iter() { + let child = testcase.to_xml(); + element = element.add_child(child); } + element } #[cfg(test)] mod tests { + use crate::report::junit::xml::XmlDocument; use crate::report::junit::{create_testsuite, Testcase}; use crate::runner::{EntryResult, Error, HurlResult, RunnerError}; use hurl_core::ast::SourceInfo; @@ -214,11 +196,11 @@ mod tests { let tc = Testcase::from(&res, content, filename); testcases.push(tc); - let mut buffer = Vec::new(); - create_testsuite(&testcases).write(&mut buffer).unwrap(); + let suite = create_testsuite(&testcases); + let doc = XmlDocument::new(suite); assert_eq!( - std::str::from_utf8(&buffer).unwrap(), - "\ + doc.to_string().unwrap(), + "\ \ \ \ diff --git a/packages/hurl/src/report/junit/testcase.rs b/packages/hurl/src/report/junit/testcase.rs index 1a68a62d6ce..d4331c7b619 100644 --- a/packages/hurl/src/report/junit/testcase.rs +++ b/packages/hurl/src/report/junit/testcase.rs @@ -15,11 +15,9 @@ * limitations under the License. * */ -use super::xml::XmlDocument; +use crate::report::junit::xml::Element; use crate::runner::HurlResult; use crate::util::logger; -use indexmap::map::IndexMap; -use xmltree::{Element, XMLNode}; #[derive(Clone, Debug, PartialEq, Eq)] pub struct Testcase { @@ -58,44 +56,21 @@ impl Testcase { /// Serializes this testcase to XML. pub fn to_xml(&self) -> Element { - let name = "testcase".to_string(); - let mut attributes = IndexMap::new(); - attributes.insert("id".to_string(), self.id.clone()); - attributes.insert("name".to_string(), self.name.clone()); let time_in_seconds = format!("{:.3}", self.time_in_ms as f64 / 1000.0); - attributes.insert("time".to_string(), time_in_seconds); - - let mut children = vec![]; - for message in self.failures.clone() { - let element = Element { - prefix: None, - namespace: None, - namespaces: None, - name: "failure".to_string(), - attributes: IndexMap::new(), - children: vec![XMLNode::Text(message)], - }; - children.push(XMLNode::Element(element)); - } - for message in self.errors.clone() { - let element = Element { - prefix: None, - namespace: None, - namespaces: None, - name: "error".to_string(), - attributes: IndexMap::new(), - children: vec![XMLNode::Text(message)], - }; - children.push(XMLNode::Element(element)); + + let mut element = Element::new("testcase") + .attr("id", &self.id) + .attr("name", &self.name) + .attr("time", &time_in_seconds); + + for failure in self.failures.iter() { + element = element.add_child(Element::new("failure").text(failure)) } - Element { - name, - prefix: None, - namespace: None, - namespaces: None, - attributes, - children, + + for error in self.errors.iter() { + element = element.add_child(Element::new("error").text(error)) } + element } pub fn get_error_count(&self) -> usize { @@ -112,6 +87,7 @@ mod test { use hurl_core::ast::SourceInfo; use crate::report::junit::testcase::Testcase; + use crate::report::junit::xml::XmlDocument; use crate::runner::{EntryResult, Error, HurlResult, RunnerError}; #[test] @@ -124,16 +100,13 @@ mod test { timestamp: 1, }; - let mut buffer = Vec::new(); let content = ""; let filename = "test.hurl"; - Testcase::from(&hurl_result, content, filename) - .to_xml() - .write(&mut buffer) - .unwrap(); + let element = Testcase::from(&hurl_result, content, filename).to_xml(); + let doc = XmlDocument::new(element); assert_eq!( - std::str::from_utf8(&buffer).unwrap(), - r#""# + doc.to_string().unwrap(), + r#""# ); } @@ -164,14 +137,12 @@ HTTP/1.0 200 cookies: vec![], timestamp: 1, }; - let mut buffer = Vec::new(); - Testcase::from(&hurl_result, content, filename) - .to_xml() - .write(&mut buffer) - .unwrap(); + + let element = Testcase::from(&hurl_result, content, filename).to_xml(); + let doc = XmlDocument::new(element); assert_eq!( - std::str::from_utf8(&buffer).unwrap(), - r#"Assert status code + doc.to_string().unwrap(), + r#"Assert status code --> test.hurl:2:10 | 2 | HTTP/1.0 200 @@ -205,14 +176,11 @@ HTTP/1.0 200 cookies: vec![], timestamp: 1, }; - let mut buffer = Vec::new(); - Testcase::from(&hurl_result, content, filename) - .to_xml() - .write(&mut buffer) - .unwrap(); + let element = Testcase::from(&hurl_result, content, filename).to_xml(); + let doc = XmlDocument::new(element); assert_eq!( - std::str::from_utf8(&buffer).unwrap(), - r#"HTTP connection + doc.to_string().unwrap(), + r#"HTTP connection --> test.hurl:1:5 | 1 | GET http://unknown diff --git a/packages/hurl/src/report/junit/xml/mod.rs b/packages/hurl/src/report/junit/xml/mod.rs index d955e0d066e..28f2b0e45fb 100644 --- a/packages/hurl/src/report/junit/xml/mod.rs +++ b/packages/hurl/src/report/junit/xml/mod.rs @@ -31,6 +31,12 @@ pub struct XmlDocument { pub root: Option, } +impl XmlDocument { + pub fn new(root: Element) -> XmlDocument { + XmlDocument { root: Some(root) } + } +} + #[derive(Clone, Eq, PartialEq, Debug)] pub enum XmlNode { /// An XML element. diff --git a/packages/hurl/src/report/junit/xml/reader.rs b/packages/hurl/src/report/junit/xml/reader.rs index d8335b61dfe..99a59fe6bc6 100644 --- a/packages/hurl/src/report/junit/xml/reader.rs +++ b/packages/hurl/src/report/junit/xml/reader.rs @@ -32,6 +32,7 @@ pub enum ParserError { /// document returned is a in-memory tree representation of the whole document. impl XmlDocument { /// Convenient associated method to read and parse a XML string `source`. + #[allow(dead_code)] pub fn parse_str(source: &str) -> Result { let bytes = source.as_bytes(); XmlDocument::parse(bytes) diff --git a/packages/hurl/src/report/junit/xml/writer.rs b/packages/hurl/src/report/junit/xml/writer.rs index 47383ad89cf..304df60af7f 100644 --- a/packages/hurl/src/report/junit/xml/writer.rs +++ b/packages/hurl/src/report/junit/xml/writer.rs @@ -52,8 +52,9 @@ impl From for WriterError { } impl XmlDocument { - /// Convenient method to seralize an XML document to a string. - pub fn write_string(&self) -> Result { + /// Convenient method to serialize an XML document to a string. + #[allow(dead_code)] + pub fn to_string(&self) -> Result { let buffer = vec![]; let buffer = self.write(buffer)?; let str = String::from_utf8(buffer)?; @@ -194,7 +195,7 @@ mod tests { ; let doc = XmlDocument { root: Some(root) }; assert_eq!( - doc.write_string().unwrap(), + doc.to_string().unwrap(), "\ \ \