1 // ***********************************************************************
2 // Copyright (c) 2012-2015 Charlie Poole
4 // Permission is hereby granted, free of charge, to any person obtaining
5 // a copy of this software and associated documentation files (the
6 // "Software"), to deal in the Software without restriction, including
7 // without limitation the rights to use, copy, modify, merge, publish,
8 // distribute, sublicense, and/or sell copies of the Software, and to
9 // permit persons to whom the Software is furnished to do so, subject to
10 // the following conditions:
12 // The above copyright notice and this permission notice shall be
13 // included in all copies or substantial portions of the Software.
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 // ***********************************************************************
25 #define NUNIT_FRAMEWORK
30 using System.Text.RegularExpressions;
33 #if PORTABLE || SILVERLIGHT
34 using System.Xml.Linq;
37 namespace NUnit.Framework.Interfaces
40 /// TNode represents a single node in the XML representation
41 /// of a Test or TestResult. It replaces System.Xml.XmlNode and
42 /// System.Xml.Linq.XElement, providing a minimal set of methods
43 /// for operating on the XML in a platform-independent manner.
50 /// Constructs a new instance of TNode
52 /// <param name="name">The name of the node</param>
53 public TNode(string name)
56 Attributes = new AttributeDictionary();
57 ChildNodes = new NodeList();
61 /// Constructs a new instance of TNode with a value
63 /// <param name="name">The name of the node</param>
64 /// <param name="value">The text content of the node</param>
65 public TNode(string name, string value) : this(name, value, false) { }
68 /// Constructs a new instance of TNode with a value
70 /// <param name="name">The name of the node</param>
71 /// <param name="value">The text content of the node</param>
72 /// <param name="valueIsCDATA">Flag indicating whether to use CDATA when writing the text</param>
73 public TNode(string name, string value, bool valueIsCDATA)
77 ValueIsCDATA = valueIsCDATA;
85 /// Gets the name of the node
87 public string Name { get; private set; }
90 /// Gets the value of the node
92 public string Value { get; set; }
95 /// Gets a flag indicating whether the value should be output using CDATA.
97 public bool ValueIsCDATA { get; private set; }
100 /// Gets the dictionary of attributes
102 public AttributeDictionary Attributes { get; private set; }
105 /// Gets a list of child nodes
107 public NodeList ChildNodes { get; private set; }
110 /// Gets the first ChildNode
112 public TNode FirstChild
114 get { return ChildNodes.Count == 0 ? null : ChildNodes[0]; }
118 /// Gets the XML representation of this node.
120 public string OuterXml
124 var stringWriter = new System.IO.StringWriter();
125 var settings = new XmlWriterSettings();
126 settings.ConformanceLevel = ConformanceLevel.Fragment;
128 using (XmlWriter xmlWriter = XmlWriter.Create(stringWriter, settings))
133 return stringWriter.ToString();
139 #region Static Methods
142 /// Create a TNode from it's XML text representation
144 /// <param name="xmlText">The XML text to be parsed</param>
145 /// <returns>A TNode</returns>
146 public static TNode FromXml(string xmlText)
148 #if PORTABLE || SILVERLIGHT
149 return FromXml(XElement.Parse(xmlText));
151 var doc = new XmlDocument();
152 doc.LoadXml(xmlText);
153 return FromXml(doc.FirstChild);
159 #region Instance Methods
162 /// Adds a new element as a child of the current node and returns it.
164 /// <param name="name">The element name.</param>
165 /// <returns>The newly created child element</returns>
166 public TNode AddElement(string name)
168 TNode childResult = new TNode(name);
169 ChildNodes.Add(childResult);
174 /// Adds a new element with a value as a child of the current node and returns it.
176 /// <param name="name">The element name</param>
177 /// <param name="value">The text content of the new element</param>
178 /// <returns>The newly created child element</returns>
179 public TNode AddElement(string name, string value)
181 TNode childResult = new TNode(name, EscapeInvalidXmlCharacters(value));
182 ChildNodes.Add(childResult);
187 /// Adds a new element with a value as a child of the current node and returns it.
188 /// The value will be output using a CDATA section.
190 /// <param name="name">The element name</param>
191 /// <param name="value">The text content of the new element</param>
192 /// <returns>The newly created child element</returns>
193 public TNode AddElementWithCDATA(string name, string value)
195 TNode childResult = new TNode(name, EscapeInvalidXmlCharacters(value), true);
196 ChildNodes.Add(childResult);
201 /// Adds an attribute with a specified name and value to the XmlNode.
203 /// <param name="name">The name of the attribute.</param>
204 /// <param name="value">The value of the attribute.</param>
205 public void AddAttribute(string name, string value)
207 Attributes.Add(name, EscapeInvalidXmlCharacters(value));
211 /// Finds a single descendant of this node matching an xpath
212 /// specification. The format of the specification is
213 /// limited to what is needed by NUnit and its tests.
215 /// <param name="xpath"></param>
216 /// <returns></returns>
217 public TNode SelectSingleNode(string xpath)
219 NodeList nodes = SelectNodes(xpath);
221 return nodes.Count > 0
227 /// Finds all descendants of this node matching an xpath
228 /// specification. The format of the specification is
229 /// limited to what is needed by NUnit and its tests.
231 public NodeList SelectNodes(string xpath)
233 NodeList nodeList = new NodeList();
236 return ApplySelection(nodeList, xpath);
240 /// Writes the XML representation of the node to an XmlWriter
242 /// <param name="writer"></param>
243 public void WriteTo(XmlWriter writer)
245 writer.WriteStartElement(Name);
247 foreach (string name in Attributes.Keys)
248 writer.WriteAttributeString(name, Attributes[name]);
252 WriteCDataTo(writer);
254 writer.WriteString(Value);
256 foreach (TNode node in ChildNodes)
257 node.WriteTo(writer);
259 writer.WriteEndElement();
264 #region Helper Methods
266 #if PORTABLE || SILVERLIGHT
267 private static TNode FromXml(XElement xElement)
269 TNode tNode = new TNode(xElement.Name.ToString(), xElement.Value);
271 foreach (var attr in xElement.Attributes())
272 tNode.AddAttribute(attr.Name.ToString(), attr.Value);
274 foreach (var child in xElement.Elements())
275 tNode.ChildNodes.Add(FromXml(child));
280 private static TNode FromXml(XmlNode xmlNode)
282 TNode tNode = new TNode(xmlNode.Name, xmlNode.InnerText);
284 foreach (XmlAttribute attr in xmlNode.Attributes)
285 tNode.AddAttribute(attr.Name, attr.Value);
287 foreach (XmlNode child in xmlNode.ChildNodes)
288 if (child.NodeType == XmlNodeType.Element)
289 tNode.ChildNodes.Add(FromXml(child));
295 private static NodeList ApplySelection(NodeList nodeList, string xpath)
297 Guard.ArgumentNotNullOrEmpty(xpath, "xpath");
299 throw new ArgumentException("XPath expressions starting with '/' are not supported", "xpath");
300 if (xpath.IndexOf("//") >= 0)
301 throw new ArgumentException("XPath expressions with '//' are not supported", "xpath");
306 int slash = xpath.IndexOf('/');
309 head = xpath.Substring(0, slash);
310 tail = xpath.Substring(slash + 1);
313 NodeList resultNodes = new NodeList();
314 NodeFilter filter = new NodeFilter(head);
316 foreach(TNode node in nodeList)
317 foreach (TNode childNode in node.ChildNodes)
318 if (filter.Pass(childNode))
319 resultNodes.Add(childNode);
322 ? ApplySelection(resultNodes, tail)
326 private static readonly Regex InvalidXmlCharactersRegex = new Regex("[^\u0009\u000a\u000d\u0020-\ufffd]|([\ud800-\udbff](?![\udc00-\udfff]))|((?<![\ud800-\udbff])[\udc00-\udfff])");
327 private static string EscapeInvalidXmlCharacters(string str)
329 if (str == null) return null;
331 // Based on the XML spec http://www.w3.org/TR/xml/#charsets
332 // For detailed explanation of the regex see http://mnaoumov.wordpress.com/2014/06/15/escaping-invalid-xml-unicode-characters/
334 return InvalidXmlCharactersRegex.Replace(str, match => CharToUnicodeSequence(match.Value[0]));
337 private static string CharToUnicodeSequence(char symbol)
339 return string.Format("\\u{0}", ((int)symbol).ToString("x4"));
342 private void WriteCDataTo(XmlWriter writer)
349 int illegal = text.IndexOf("]]>", start);
352 writer.WriteCData(text.Substring(start, illegal - start + 2));
354 if (start >= text.Length)
359 writer.WriteCData(text.Substring(start));
361 writer.WriteCData(text);
366 #region Nested NodeFilter class
370 private string _nodeName;
371 private string _propName;
372 private string _propValue;
374 public NodeFilter(string xpath)
378 int lbrack = xpath.IndexOf('[');
381 if (!xpath.EndsWith("]"))
382 throw new ArgumentException("Invalid property expression", "xpath");
384 _nodeName = xpath.Substring(0, lbrack);
385 string filter = xpath.Substring(lbrack+1, xpath.Length - lbrack - 2);
387 int equals = filter.IndexOf('=');
388 if (equals < 0 || filter[0] != '@')
389 throw new ArgumentException("Invalid property expression", "xpath");
391 _propName = filter.Substring(1, equals - 1).Trim();
392 _propValue = filter.Substring(equals + 1).Trim(new char[] { ' ', '"', '\'' });
396 public bool Pass(TNode node)
398 if (node.Name != _nodeName)
401 if (_propName == null)
404 return node.Attributes[_propName] == _propValue;
412 /// Class used to represent a list of XmlResults
414 public class NodeList : System.Collections.Generic.List<TNode>
419 /// Class used to represent the attributes of a node
421 public class AttributeDictionary : System.Collections.Generic.Dictionary<string, string>
424 /// Gets or sets the value associated with the specified key.
425 /// Overridden to return null if attribute is not found.
427 /// <param name="key">The key.</param>
428 /// <returns>Value of the attribute or null</returns>
429 public new string this[string key]
435 if (TryGetValue(key, out value))