From 31289b673619bc899a14dec4043b645a4703ed73 Mon Sep 17 00:00:00 2001 From: Mads Gram Date: Sun, 9 Jul 2023 21:31:32 +0200 Subject: [PATCH] Introduce equality comparers for OpenXmlElement (#1476) --- .../Equality/OpenXmlElementComparers.cs | 28 ++ .../OpenXmlElementEqualityComparer.cs | 269 ++++++++++++++++ .../Equality/OpenXmlElementEqualityOptions.cs | 31 ++ .../MarkupCompatibilityAttributes.cs | 45 ++- .../EqualityBenchmark.cs | 116 +++++++ .../OpenXmlElementEqualityTest.cs | 298 ++++++++++++++++++ 6 files changed, 786 insertions(+), 1 deletion(-) create mode 100644 src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementComparers.cs create mode 100644 src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityComparer.cs create mode 100644 src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityOptions.cs create mode 100644 test/DocumentFormat.OpenXml.Benchmarks/EqualityBenchmark.cs create mode 100644 test/DocumentFormat.OpenXml.Tests/OpenXmlElementEqualityTest.cs diff --git a/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementComparers.cs b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementComparers.cs new file mode 100644 index 000000000..96feeb666 --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementComparers.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using System.Collections.Generic; + +namespace DocumentFormat.OpenXml +{ + /// + /// Equality comparer for determining value equality for . + /// + public static class OpenXmlElementComparers + { + /// + /// Gets the default equality comparer. + /// + public static IEqualityComparer Default { get; } = Create(new OpenXmlElementEqualityOptions()); + + /// + /// Creates a based on the given options./> + /// + /// The options defining equality. + /// + public static IEqualityComparer Create(OpenXmlElementEqualityOptions openXmlElementEqualityOptions) + { + return new OpenXmlElementEqualityComparer(openXmlElementEqualityOptions); + } + } +} diff --git a/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityComparer.cs b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityComparer.cs new file mode 100644 index 000000000..85cab150b --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityComparer.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Framework; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace DocumentFormat.OpenXml +{ + internal sealed class OpenXmlElementEqualityComparer : IEqualityComparer + { + /// + /// Gets the options regulating how equality is defined. + /// + internal OpenXmlElementEqualityOptions Options { get; } + + internal OpenXmlElementEqualityComparer(OpenXmlElementEqualityOptions options) + { + this.Options = options; + } + + /// + /// Determines equality for two given . + /// + /// First object. + /// Second object. + /// + public bool Equals(OpenXmlElement? x, OpenXmlElement? y) + { + if (ReferenceEquals(x, y)) + { + return true; + } + + if (x == null || y == null) + { + return false; + } + + if (x.GetType() != y.GetType()) + { + return false; + } + + if (!this.CheckAndEquateSpecialOptions(x, y)) + { + return false; + } + + if (x.HasChildren != y.HasChildren) + { + return false; + } + + if (!OpenXmlElementEqualityComparer.PrefixAndQNameEqual(x, y, this.Options)) + { + return false; + } + + if (x is OpenXmlLeafTextElement) + { + if (!string.Equals(x.InnerText, y.InnerText, StringComparison.Ordinal)) + { + return false; + } + } + + if (x.HasChildren) + { + // DEVNOTE: Do not refactor this to use "simpler" for construct. + // The indexer on ChildElement walks the linked list for each operation, + // not maintaining state so it turns into a O(n^2) operation. + OpenXmlElementList.Enumerator tChilds = x.ChildElements.GetEnumerator(); + OpenXmlElementList.Enumerator oChilds = y.ChildElements.GetEnumerator(); + + int e1 = 0, e2 = 0; + while (OpenXmlElementEqualityComparer.MoveNextAndTrackCount(ref tChilds, ref oChilds, ref e1, ref e2)) + { + if (!this.Equals(tChilds.Current, oChilds.Current)) + { + return false; + } + } + + // Different amount of children. + if (e1 != e2) + { + return false; + } + } + + for (int i = 0; i < x.ParsedState.Attributes.Length; i++) + { + var tAttr = x.ParsedState.Attributes[i]; + var oAttr = y.ParsedState.Attributes[i]; + + if ((tAttr.Value == null && oAttr.Value != null) || (tAttr.Value != null && oAttr.Value == null)) + { + return false; + } + + if (tAttr.Value == null) + { + continue; + } + + if (!tAttr.Value.Equals(oAttr.Value)) + { + return false; + } + } + + return true; + } + + /// + /// Handles checking of all options that changes the behaviour of equality based on options in . + /// + private bool CheckAndEquateSpecialOptions(OpenXmlElement x, OpenXmlElement y) + { + if (!this.Options.RequireParsed) + { + if (!x.XmlParsed && !y.XmlParsed) + { + return string.Equals(x.RawOuterXml, y.RawOuterXml, StringComparison.Ordinal); + } + } + + x.MakeSureParsed(); + y.MakeSureParsed(); + + if (this.Options.IncludeExtendedAttributes) + { + if (x.ExtendedAttributes == null != (y.ExtendedAttributes == null)) + { + return false; + } + + if (x.ExtendedAttributes != null && y.ExtendedAttributes != null) + { + if (x.ExtendedAttributes.Count() != y.ExtendedAttributes.Count()) + { + return false; + } + + for (int i = 0; i < x.ExtendedAttributes.Count(); i++) + { + if (!x.ExtendedAttributes.ElementAt(i).Equals(y.ExtendedAttributes.ElementAt(i))) + { + return false; + } + } + } + } + + if (this.Options.IncludeMCAttributes) + { + if (x.MCAttributes == null != (y.MCAttributes == null) || (x.MCAttributes != null && !x.MCAttributes.Equals(y.MCAttributes))) + { + return false; + } + } + + return true; + } + + /// + /// Calculates a hashcode based on the given object. + /// + /// The object to get a hashcode for. + /// + public int GetHashCode([DisallowNull] OpenXmlElement obj) + { + if (obj == null) + { + return 0; + } + + var hc = default(HashCode); + + if (this.Options.IncludeMCAttributes) + { + hc.Add(obj.MCAttributes); + } + + for (int i = 0; i < obj.ParsedState.Attributes.Length; i++) + { + if (obj.ParsedState.Attributes[i].Value != null) + { + hc.Add(obj.ParsedState.Attributes[i].Value); + } + } + + if (this.Options.IncludeExtendedAttributes) + { + foreach (OpenXmlAttribute attr in obj.ExtendedAttributes) + { + hc.Add(attr); + } + } + + if (obj.HasChildren) + { + foreach (OpenXmlElement child in obj.ChildElements) + { + hc.Add(child); + } + } + + return hc.ToHashCode(); + } + + private static bool PrefixAndQNameEqual(OpenXmlElement x, OpenXmlElement y, OpenXmlElementEqualityOptions options) + { + OpenXmlQualifiedName tQName = x.ParsedState.Metadata.QName; + OpenXmlQualifiedName oQName = y.ParsedState.Metadata.QName; + + if (!tQName.Equals(oQName)) + { + return false; + } + + if (options.SkipPrefixComparison) + { + return true; + } + + string turi = tQName.Namespace.Uri; + string ouri = oQName.Namespace.Uri; + + var tPrefix = x.LookupPrefixLocal(ouri); + var oPrefix = y.LookupPrefixLocal(ouri); + + if (string.IsNullOrEmpty(tPrefix)) + { + tPrefix = x.Features.GetNamespaceResolver().LookupPrefix(turi); + } + + if (string.IsNullOrEmpty(oPrefix)) + { + oPrefix = y.Features.GetNamespaceResolver().LookupPrefix(ouri); + } + + return string.Equals(tPrefix, oPrefix, StringComparison.Ordinal); + } + + private static bool MoveNextAndTrackCount(ref OpenXmlElementList.Enumerator e1, ref OpenXmlElementList.Enumerator e2, ref int e1ctr, ref int e2ctr) + { + if (e1.MoveNext()) + { + e1ctr++; + if (e2.MoveNext()) + { + e2ctr++; + return true; + } + } + else if (e2.MoveNext()) + { + e2ctr++; + } + + return false; + } + } +} diff --git a/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityOptions.cs b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityOptions.cs new file mode 100644 index 000000000..241b994ec --- /dev/null +++ b/src/DocumentFormat.OpenXml.Framework/Equality/OpenXmlElementEqualityOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +namespace DocumentFormat.OpenXml +{ + /// + /// Options defining the behaviour of equality for . + /// + public sealed class OpenXmlElementEqualityOptions + { + /// + /// Gets or sets a value indicating whether extended attributes should be considered when determining equality. + /// + public bool IncludeExtendedAttributes { get; set; } = true; + + /// + /// Gets or sets a value indicating whether mC attributes should be considered when determining equality. + /// + public bool IncludeMCAttributes { get; set; } = true; + + /// + /// Gets or sets a value indicating whether namespace should alone be used when comparing idenity of elements, skipping prefix lookup to improve performance. + /// + public bool SkipPrefixComparison { get; set; } + + /// + /// Gets or sets a value indicating whether elements must be parsed which ensures order of schema is used instead of input ordering. + /// + public bool RequireParsed { get; set; } + } +} diff --git a/src/DocumentFormat.OpenXml.Framework/MarkupCompatibilityAttributes.cs b/src/DocumentFormat.OpenXml.Framework/MarkupCompatibilityAttributes.cs index 80061119a..f4f0648d7 100644 --- a/src/DocumentFormat.OpenXml.Framework/MarkupCompatibilityAttributes.cs +++ b/src/DocumentFormat.OpenXml.Framework/MarkupCompatibilityAttributes.cs @@ -1,12 +1,15 @@ // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using DocumentFormat.OpenXml.Framework; +using System; + namespace DocumentFormat.OpenXml { /// /// Defines the Markup Compatibility Attributes. /// - public class MarkupCompatibilityAttributes + public class MarkupCompatibilityAttributes : IEquatable { /// /// Gets or sets a whitespace-delimited list of prefixes, where each @@ -42,5 +45,45 @@ public class MarkupCompatibilityAttributes /// a set of namespace names. /// public StringValue? MustUnderstand { get; set; } + + /// + public override bool Equals(object? obj) + { + return this.Equals(obj as MarkupCompatibilityAttributes); + } + + /// + public bool Equals(MarkupCompatibilityAttributes? other) + { + if (other == null) + { + return false; + } + + if (object.ReferenceEquals(this, other)) + { + return true; + } + + return Equals(this.Ignorable, other.Ignorable) + && Equals(this.ProcessContent, other.ProcessContent) + && Equals(this.PreserveElements, other.PreserveElements) + && Equals(this.PreserveAttributes, other.PreserveAttributes) + && Equals(this.MustUnderstand, other.MustUnderstand); + } + + /// + public override int GetHashCode() + { + var hc = default(HashCode); + + hc.Add(this.Ignorable); + hc.Add(this.ProcessContent); + hc.Add(this.PreserveElements); + hc.Add(this.PreserveAttributes); + hc.Add(this.MustUnderstand); + + return hc.ToHashCode(); + } } } diff --git a/test/DocumentFormat.OpenXml.Benchmarks/EqualityBenchmark.cs b/test/DocumentFormat.OpenXml.Benchmarks/EqualityBenchmark.cs new file mode 100644 index 000000000..ca8e53893 --- /dev/null +++ b/test/DocumentFormat.OpenXml.Benchmarks/EqualityBenchmark.cs @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using BenchmarkDotNet.Attributes; + +using DocumentFormat.OpenXml.Drawing; +using DocumentFormat.OpenXml.Spreadsheet; + +using System; +using System.Collections.Generic; + +namespace DocumentFormat.OpenXml.Benchmarks +{ + public class EqualityBenchmark + { + private const string FontXml = ""; + + private readonly IEqualityComparer _defaultEqualityComparer = OpenXmlElementComparers.Default; + private readonly IEqualityComparer _skipPrefixComparer = OpenXmlElementComparers.Create(new OpenXmlElementEqualityOptions() { SkipPrefixComparison = true }); + + private OpenXmlElement _largeElement; + private OpenXmlElement _largeElement2; + + private OpenXmlElement _smallElementUnParsed; + private OpenXmlElement _smallElement2UnParsed; + + private OpenXmlElement _smallElementParsed; + private OpenXmlElement _smallElement2Parsed; + + [GlobalSetup] + public void Setup() + { + var rand = new Random(1234567); + + var textBody = new TextBody(); + var textBody2 = new TextBody(); + for (int i = 0; i < 200; i++) + { + var paragraph = new Paragraph(); + var paragraph2 = new Paragraph(); + for (int j = 0; j < 100; j++) + { + string txt = new string('a', rand.Next(0, 10)); + paragraph.AppendChild(new Drawing.Run() { Text = new Drawing.Text(txt) }); + paragraph2.AppendChild(new Drawing.Run() { Text = new Drawing.Text(txt) }); + + paragraph.AppendChild(new Drawing.Field() { Id = new StringValue(txt) }); + paragraph2.AppendChild(new Drawing.Field() { Id = new StringValue(txt) }); + } + + textBody.AppendChild(paragraph); + textBody2.AppendChild(paragraph2); + } + + _largeElement = textBody; + _largeElement2 = textBody2; + + _smallElementUnParsed = new Font(FontXml); + _smallElement2UnParsed = new Font(FontXml); + + _smallElementParsed = new Font(FontXml); + _smallElementParsed.MakeSureParsed(); + + _smallElement2Parsed = new Font(FontXml); + _smallElement2Parsed.MakeSureParsed(); + } + + [Benchmark] + public bool OuterXmlLargeElement() + { + return _largeElement.OuterXml.Equals(_largeElement2.OuterXml); + } + + [Benchmark] + public bool EqualsLargeElement() + { + return _defaultEqualityComparer.Equals(_largeElement, _largeElement2); + } + + [Benchmark] + public bool EqualsLargeElementSkipPrefix() + { + return _skipPrefixComparer.Equals(_largeElement, _largeElement2); + } + + [Benchmark] + public bool UnparsedOuterXml() + { + return _smallElementUnParsed.OuterXml.Equals(_smallElement2UnParsed.OuterXml); + } + + [Benchmark] + public bool UnparsedEquals() + { + return _defaultEqualityComparer.Equals(_smallElementUnParsed, _smallElement2UnParsed); + } + + [Benchmark] + public bool SmallOuterXml() + { + return _smallElementParsed.OuterXml.Equals(_smallElement2Parsed.OuterXml); + } + + [Benchmark] + public bool SmallEquals() + { + return _defaultEqualityComparer.Equals(_smallElementParsed, _smallElement2Parsed); + } + + [Benchmark] + public bool SmallEqualsSkipPrefix() + { + return _skipPrefixComparer.Equals(_smallElementParsed, _smallElement2Parsed); + } + } +} diff --git a/test/DocumentFormat.OpenXml.Tests/OpenXmlElementEqualityTest.cs b/test/DocumentFormat.OpenXml.Tests/OpenXmlElementEqualityTest.cs new file mode 100644 index 000000000..01413ecdd --- /dev/null +++ b/test/DocumentFormat.OpenXml.Tests/OpenXmlElementEqualityTest.cs @@ -0,0 +1,298 @@ +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE file in the project root for full license information. + +using DocumentFormat.OpenXml.Wordprocessing; + +using System.Collections.Generic; + +using Xunit; + +namespace DocumentFormat.OpenXml.Tests +{ + public class OpenXmlElementEqualityTest + { + [Fact] + public void NullTest() + { + string paragraphOuterXmlrsidP001 = "Run Text.Run 2."; + Paragraph para = new Paragraph(paragraphOuterXmlrsidP001); + +#pragma warning disable xUnit2003 // Do not use equality check to test for null value + Assert.NotEqual(null, para, OpenXmlElementComparers.Default); +#pragma warning restore xUnit2003 // Do not use equality check to test for null value + } + + [Fact] + public void ReferenceEqualsTest() + { + // C:\src\Open-XML-SDK\data\schemas\schemas_openxmlformats_org_wordprocessingml_2006_main.json :: 13537 + string paragraphOuterXmlrsidP001 = "Run Text.Run 2."; + Paragraph para = new Paragraph(paragraphOuterXmlrsidP001); + + Assert.Equal(para, para, OpenXmlElementComparers.Default); + } + + [Fact] + public void AttributeTest() + { + // C:\src\Open-XML-SDK\data\schemas\schemas_openxmlformats_org_wordprocessingml_2006_main.json :: 13537 + string paragraphOuterXmlrsidP001 = "Run Text.Run 2."; + string paragraphOuterXmlrsidP002 = "Run Text.Run 2."; + Paragraph para = new Paragraph(paragraphOuterXmlrsidP001); + Paragraph paraDifferentAttributeValue = new Paragraph(paragraphOuterXmlrsidP002); + + Assert.Equal(para, para, OpenXmlElementComparers.Default); + + // Different attribute values should not be the same. + Assert.NotEqual(para, paraDifferentAttributeValue, OpenXmlElementComparers.Default); + + para.MakeSureParsed(); + paraDifferentAttributeValue.MakeSureParsed(); + + // Different attribute values should not be the same. + Assert.NotEqual(para, paraDifferentAttributeValue, OpenXmlElementComparers.Default); + } + + /// + /// Test that shows extended attributes are considered in equality. + /// + [Fact] + public void ExtendedAttributeTest() + { + // C:\src\Open-XML-SDK\data\schemas\schemas_openxmlformats_org_wordprocessingml_2006_main.json :: 13537 + string paragraphWithFooExtendedAttribute = "Run Text.Run 2."; + string paragraphWithFooExtendedAttributeDifferent = "Run Text.Run 2."; + string paragraphWithBarExtendedAttribute = "Run Text.Run 2."; + string paragraphWithBarNoExtendedAttribute = "Run Text.Run 2."; + Paragraph para = new Paragraph(paragraphWithFooExtendedAttribute); + Paragraph para2 = new Paragraph(paragraphWithFooExtendedAttribute); + Paragraph paraFooExtendedAttribute = new Paragraph(paragraphWithFooExtendedAttributeDifferent); + Paragraph paraBarExtendedAttribute = new Paragraph(paragraphWithBarExtendedAttribute); + Paragraph paraNoExtendedAttribute = new Paragraph(paragraphWithBarNoExtendedAttribute); + + Assert.Equal(para, para, OpenXmlElementComparers.Default); // Ref equality should always be equal. + Assert.Equal(para, para2, OpenXmlElementComparers.Default); // Identity equality. + + para.MakeSureParsed(); + para2.MakeSureParsed(); + paraFooExtendedAttribute.MakeSureParsed(); + paraBarExtendedAttribute.MakeSureParsed(); + paraNoExtendedAttribute.MakeSureParsed(); + + // Extended attributes with same attribute name but different value. + Assert.NotEqual(para, paraFooExtendedAttribute, OpenXmlElementComparers.Default); + + IEqualityComparer comparerWithoutExtendedAttributes = OpenXmlElementComparers.Create(new OpenXmlElementEqualityOptions() { IncludeExtendedAttributes = false }); + + // Unless extended attributes are not considered. + Assert.Equal(para, paraFooExtendedAttribute, comparerWithoutExtendedAttributes); + + // Extended attribute with different attribute name. + Assert.NotEqual(para, paraBarExtendedAttribute, OpenXmlElementComparers.Default); + + // Unless extended attributes are not considered. + Assert.Equal(para, paraBarExtendedAttribute, comparerWithoutExtendedAttributes); + + // With different amount of extended attributes. + Assert.NotEqual(para, paraNoExtendedAttribute, OpenXmlElementComparers.Default); + + // Unless extended attributes are not considered. + Assert.Equal(para, paraNoExtendedAttribute, comparerWithoutExtendedAttributes); + } + + [Fact] + public void DifferentChildValueTest() + { + // C:\src\Open-XML-SDK\data\schemas\schemas_openxmlformats_org_wordprocessingml_2006_main.json :: 13537 + string paraChildRun1Xml = "Run Text.Run 1."; + string paraChildRun2Xml = "Run Text.Run 2."; + Paragraph paraChildRun1 = new Paragraph(paraChildRun1Xml); + Paragraph paraChildRun2 = new Paragraph(paraChildRun2Xml); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + + paraChildRun1.MakeSureParsed(); + paraChildRun2.MakeSureParsed(); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + } + + /// + /// Test that documents the specified order of children matters for unparsed equality checks, but not parse equality checks. + /// + [Fact] + public void ChildrenOrderingSameButDifferentNamespace() + { + string paraXml1 = ""; + string paraXml2 = ""; + Paragraph para1 = new Paragraph(paraXml1); + Paragraph para2 = new Paragraph(paraXml2); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + para1.MakeSureParsed(); + para2.MakeSureParsed(); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + } + + /// + /// Test that documents the specified order of children isn't guaranteed for equality. + /// + [Fact] + public void ChildrenOrderingDifferentChildren() + { + string paraXml1 = ""; + string paraXml2 = ""; + Paragraph para1 = new Paragraph(paraXml1); + Paragraph para2 = new Paragraph(paraXml2); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + para1.MakeSureParsed(); + para2.MakeSureParsed(); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + } + + [Fact] + public void DifferentAmountOfChildrenTest() + { + string paraChildRun1Xml = ""; + string paraChildRun2Xml = ""; + Paragraph paraChildRun1 = new Paragraph(paraChildRun1Xml); + Paragraph paraChildRun2 = new Paragraph(paraChildRun2Xml); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + + paraChildRun1.MakeSureParsed(); + paraChildRun2.MakeSureParsed(); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + } + + /// + /// Test that shows namespaces is considered for equality. + /// + [Fact] + public void NamespaceDifferenceTest() + { + // C:\src\Open-XML-SDK\data\schemas\schemas_openxmlformats_org_wordprocessingml_2006_main.json :: 13537 + string paraChildRun1Xml = "Run Text.Run 1."; + string paraChildRun2Xml = "Run Text.Run 1."; + Paragraph paraChildRun1 = new Paragraph(paraChildRun1Xml); + Paragraph paraChildRun2 = new Paragraph(paraChildRun2Xml); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + + paraChildRun1.MakeSureParsed(); + paraChildRun2.MakeSureParsed(); + + Assert.NotEqual(paraChildRun1, paraChildRun2, OpenXmlElementComparers.Default); + } + + /// + /// Test that documents the specified order of attributes matters for unparsed equality checks, but not parse equality checks. + /// + [Fact] + public void AttributeOrderingTest() + { + string paraXml1 = ""; + string paraXml2 = ""; + Paragraph para1 = new Paragraph(paraXml1); + Paragraph para2 = new Paragraph(paraXml2); + + // While unparsed, there is no guarantees for ordering on attributes fpr equality. + // The order in the input XML doc is used. + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + para1.MakeSureParsed(); + para2.MakeSureParsed(); + + // After parsing the order from schema is used. + Assert.Equal(para1, para2, OpenXmlElementComparers.Default); + } + + /// + /// Test that documents the specified order of extended attributes matters for equality. + /// + [Fact] + public void ExtendedAttributeOrderingTest() + { + string paraChildRun1Xml = ""; + string paraChildRun2Xml = ""; + Paragraph para1 = new Paragraph(paraChildRun1Xml); + Paragraph para2 = new Paragraph(paraChildRun2Xml); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + // Order is still important after parsing, since schema doesn't define order for extended attributes. + para1.MakeSureParsed(); + para2.MakeSureParsed(); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + } + + /// + /// Test that shows Markup Compatibility Attributes are accounted for for equality. + /// + [Fact] + public void MarkupAttributeTest() + { + string xml = ""; + Paragraph para1 = new Paragraph(xml); + Paragraph para2 = new Paragraph(xml); + + Assert.True(OpenXmlElementComparers.Default.Equals(para1, para2)); + + para2.SetPreserveElements("*"); + + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + para1.SetPreserveAttributes("*"); + + // Test that different MC attributes are not equal. + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + + // Unless it is selected that it shouldn't be considered in equality tests. + Assert.Equal(para1, para1, OpenXmlElementComparers.Create(new OpenXmlElementEqualityOptions() { IncludeMCAttributes = false })); + } + + /// + /// Test that shows will triggering parsing. + /// + [Fact] + public void IgnoreParseTest() + { + string paraXml1 = ""; + string paraXml2 = ""; + Paragraph para1 = new Paragraph(paraXml1); + Paragraph para2 = new Paragraph(paraXml2); + + // While unparsed, there is no guarantees for ordering on attributes fpr equality. + // The order in the input XML doc is used. Use Comparer to ensure parsed. + Assert.NotEqual(para1, para2, OpenXmlElementComparers.Default); + Assert.Equal(para1, para2, OpenXmlElementComparers.Create(new OpenXmlElementEqualityOptions() { RequireParsed = true })); + } + + /// + /// Test that documents that GetHashCode is stable, allowing for GetHashCode/Equals to be used in dictionary/hashset. + /// + [Fact] + public void GetHashCodeEqualityTest() + { + var hs = new HashSet(OpenXmlElementComparers.Default); + + string paraXml = "Run Text.Run 1."; + Paragraph para1 = new Paragraph(paraXml); + Paragraph para2 = new Paragraph(paraXml); + + para1.MakeSureParsed(); + para2.MakeSureParsed(); + + hs.Add(para1); + + Assert.Contains(para2, hs); + } + } +}