Skip to content

Commit

Permalink
Merge pull request #105 from nowsprinting/feature/xml_comparer
Browse files Browse the repository at this point in the history
Add XDocumentComparer and XmlComparer
  • Loading branch information
nowsprinting authored Oct 27, 2024
2 parents ed7ebcf + 653fdb1 commit 84b6839
Show file tree
Hide file tree
Showing 9 changed files with 513 additions and 1 deletion.
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ public class MyTestClass

#### GameObjectNameComparer

`GameObjectNameComparer` is an NUnit test comparer class to compare GameObjects by name.
`GameObjectNameComparer` is a NUnit test comparer class that compares `GameObjects` by name.

Usage:

Expand All @@ -427,6 +427,77 @@ public class GameObjectNameComparerTest
```


#### XDocumentComparer

`XDocumentComparer` is a NUnit test comparer class that loosely compares `XDocument`.

It only compares the attributes and values of each element in the document unordered.
XML declarations and comments are ignored.

Usage:

```csharp
using System.Xml.Linq;
using NUnit.Framework;
using TestHelper.Comparers;

[TestFixture]
public class XDocumentComparerTest
{
[Test]
public void UsingWithEqualTo_Compare()
{
var x = XDocument.Parse(@"<root><child>value1</child><child attribute=""attr"">value2</child></root>");
var y = XDocument.Parse(@"<?xml version=""1.0"" encoding=""utf-8""?>
<root><!-- comment --><child attribute=""attr"">value2</child><!-- comment --><child>value1</child></root>");
// with XML declaration, comments, and different order
Assert.That(x, Is.EqualTo(y).Using(new XDocumentComparer()));
}
}
```


#### XmlComparer

`XmlComparer` is a NUnit test comparer class that loosely compares `string` as XML documents.

It only compares the attributes and values of each element in the document unordered.
XML declarations and comments are ignored, and white spaces, tabs, and newlines before and after the value are ignored.

Usage:

```csharp
using System.Xml.Linq;
using NUnit.Framework;
using TestHelper.Comparers;

[TestFixture]
public class XDocumentComparerTest
{
[Test]
public void UsingWithEqualTo_Compare()
{
var x = @"<root><child>value1</child><child attribute=""attr"">value2</child></root>";
var y = @"<?xml version=""1.0"" encoding=""utf-8""?>
<root>
<!-- comment -->
<child attribute=""attr"">
value2
</child>
<!-- comment -->
<child>
value1
</child>
</root>";
// with new-line, white-space, XML declaration, comments, and different order
Assert.That(x, Is.EqualTo(y).Using(new XmlComparer()));
}
}
```


### Constraints

#### Destroyed
Expand Down
226 changes: 226 additions & 0 deletions Runtime/Comparers/XDocumentComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using System.Xml.Linq;

namespace TestHelper.Comparers
{
/// <summary>
/// Compare XML documents.
///
/// It only compares the attributes and values of each element in the document unordered.
/// XML declarations and comments are ignored.
/// </summary>
public class XDocumentComparer : IComparer<XDocument>
{
/// <inheritdoc/>
public int Compare(XDocument x, XDocument y)
{
if (x == null && y == null)
{
return 0;
}

if (x == null)
{
return -1;
}

if (y == null)
{
return 1;
}

// Note: Declaration is not compared.

var comparisonDictionary = CreateComparisonDictionary(y.Root);
// Note: key is XPath, value is List<XElement>. Any nodes found are deleted one by one.

var current = x.Root;
while (current != null)
{
// Find same element in y.
var foundElement = FindElementAndRemove(current, ref comparisonDictionary);
if (foundElement == null)
{
return -1; // The element exists only in x.
}

current = GetChildOrNextElement(current);
}

if (comparisonDictionary.Any())
{
return 1; // The element exists only in y.
}

return 0;
}

internal static Dictionary<string, List<XElement>> CreateComparisonDictionary(XElement root)
{
var comparisonDictionary = new Dictionary<string, List<XElement>>();
var current = root;
while (current != null)
{
var xPath = GetXPath(current);
if (comparisonDictionary.TryGetValue(xPath, out var elements))
{
elements.Add(current);
}
else
{
comparisonDictionary.Add(xPath, new List<XElement> { current });
}

current = GetChildOrNextElement(current);
}

return comparisonDictionary;
}

private static XElement GetChildOrNextElement(XElement element)
{
if (element.HasElements)
{
return element.Elements().First();
}

var nextNode = element.NextNode;
while (nextNode != null)
{
if (nextNode.NodeType == XmlNodeType.Element)
{
return nextNode as XElement;
}

nextNode = nextNode.NextNode;
}

return null;
}

/// <summary>
/// Find element in comparison dictionary.
/// The entries found are removed from the dictionary.
/// </summary>
/// <returns>XElement if found, Null if not found.</returns>
private static XElement FindElementAndRemove(XElement target, ref Dictionary<string, List<XElement>> dictionary)
{
var xPath = GetXPath(target);
if (!dictionary.TryGetValue(xPath, out var elements))
{
return null;
}

foreach (var element in elements)
{
var compare = Compare(target, element);
if (compare != 0)
{
continue;
}

elements.Remove(element);
if (elements.Count == 0)
{
dictionary.Remove(xPath);
}

return element;
}

return null;
}

/// <summary>
/// Compare two elements not recursively.
/// Do not check child elements.
/// </summary>
private static int Compare(XElement x, XElement y)
{
if (GetXPath(x) != GetXPath(y))
{
return -1;
}

if (x.HasAttributes != y.HasAttributes)
{
return -1;
}

if (x.HasAttributes && y.HasAttributes)
{
var compareAttributes = Compare(x.Attributes(), y.Attributes());
if (compareAttributes != 0)
{
return compareAttributes;
}
}

if (x.HasElements && y.HasElements)
{
return 0;
}

return Compare(x.Value, y.Value);
}

private static string GetXPath(XElement element)
{
var path = new List<string>();
var current = element;
while (current != null)
{
path.Add(current.Name.LocalName);
current = current.Parent;
}

path.Reverse();
return string.Join("/", path);
}

/// <summary>
/// Compare two attribute collections.
/// </summary>
private static int Compare(IEnumerable<XAttribute> x, IEnumerable<XAttribute> y)
{
var comparisonList = y.ToList();

foreach (var xAttribute in x)
{
var yAttribute = comparisonList.FirstOrDefault(attribute => attribute.Name == xAttribute.Name);
if (yAttribute == null)
{
return -1;
}

if (xAttribute.Value != yAttribute.Value)
{
return -1;
}

comparisonList.Remove(yAttribute);
}

if (comparisonList.Any())
{
return 1;
}

return 0;
}

/// <summary>
/// Compare two strings.
/// </summary>
private static int Compare(string x, string y)
{
return string.Compare(x.Trim(), y.Trim(), StringComparison.CurrentCulture);
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Comparers/XDocumentComparer.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions Runtime/Comparers/XmlComparer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2023-2024 Koji Hasegawa.
// This software is released under the MIT License.

using System.Collections.Generic;
using System.Xml.Linq;

namespace TestHelper.Comparers
{
/// <summary>
/// Compare strings as an XML document.
///
/// It only compares the attributes and values of each element in the document unordered.
/// XML declarations and comments are ignored, and white spaces, tabs, and newlines before and after the value are ignored.
/// </summary>
/// <remarks>
/// Internal using <see cref="XDocumentComparer"/> for comparing <see cref="XDocument"/>.
/// </remarks>
public class XmlComparer : IComparer<string>
{
/// <inheritdoc/>
public int Compare(string x, string y)
{
if (x == null && y == null)
{
return 0;
}

if (x == null)
{
return -1;
}

if (y == null)
{
return 1;
}

return new XDocumentComparer().Compare(XDocument.Parse(x), XDocument.Parse(y));
}
}
}
3 changes: 3 additions & 0 deletions Runtime/Comparers/XmlComparer.cs.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 84b6839

Please sign in to comment.