From e0a08ec1bf4fe434308869cd3840a4dd740e1cac Mon Sep 17 00:00:00 2001
From: Adam Chester <adamchester@gmail.com>
Date: Sun, 19 Jun 2016 09:47:21 +1000
Subject: [PATCH 1/2] Add basic support for LogEventProperty structural
 equality,

The 'LogEventPropertyStructuralEqualityComparer` and `LogEventPropertyValueComparer` currently only support `ScalarValue` and `SequenceValue`. If the comparer encounters a `StructureValue` or `DictionaryValue` it throws `NotImplementedException`.
---
 ...EventPropertyStructuralEqualityComparer.cs | 32 +++++++
 ...PropertyStructuralEqualityComparerTests.cs | 89 +++++++++++++++++++
 .../Support/LogEventPropertyValueComparer.cs  | 52 +++++++++++
 3 files changed, 173 insertions(+)
 create mode 100644 test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparer.cs
 create mode 100644 test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
 create mode 100644 test/Serilog.Tests/Support/LogEventPropertyValueComparer.cs

diff --git a/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparer.cs b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparer.cs
new file mode 100644
index 000000000..e1162ed8f
--- /dev/null
+++ b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparer.cs
@@ -0,0 +1,32 @@
+using System;
+using System.Collections.Generic;
+using Serilog.Events;
+
+namespace Serilog.Tests.Support
+{
+    class LogEventPropertyStructuralEqualityComparer : IEqualityComparer<LogEventProperty>
+    {
+        readonly IEqualityComparer<LogEventPropertyValue> _valueEqualityComparer;
+
+        public LogEventPropertyStructuralEqualityComparer(
+            IEqualityComparer<LogEventPropertyValue> valueEqualityComparer = null)
+        {
+            this._valueEqualityComparer =
+                valueEqualityComparer ?? new LogEventPropertyValueComparer(EqualityComparer<object>.Default);
+        }
+
+        public bool Equals(LogEventProperty x, LogEventProperty y)
+        {
+            if (x == null || y == null)
+                return false; // throw new Exception($"the comparer doesn't support nulls, x={x}, y={y}");
+
+            return x.Name == y.Name
+                   && _valueEqualityComparer.Equals(x.Value, y.Value);
+        }
+
+        public int GetHashCode(LogEventProperty obj)
+        {
+            return 0;
+        }
+    }
+}
diff --git a/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
new file mode 100644
index 000000000..7d79b4520
--- /dev/null
+++ b/test/Serilog.Tests/Support/LogEventPropertyStructuralEqualityComparerTests.cs
@@ -0,0 +1,89 @@
+using System;
+using Serilog.Events;
+using Xunit;
+
+namespace Serilog.Tests.Support
+{
+    public class LogEventPropertyStructuralEqualityComparerTests
+    {
+        [Fact]
+        public void HandlesNullAsNotEqual()
+        {
+            var sut = new LogEventPropertyStructuralEqualityComparer();
+            Assert.False(sut.Equals(null, new LogEventProperty("a", new ScalarValue(null))));
+            Assert.False(sut.Equals(new LogEventProperty("a", new ScalarValue(null)), null));
+            Assert.False(sut.Equals(null, null));
+        }
+
+        [Fact]
+        public void LogEventPropertyStructuralEqualityComparerWorksForSequences()
+        {
+            var intStringDoubleScalarArray =
+                new[] { new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.0), };
+
+            var intStringFloatScalarArray =
+                new[] { new ScalarValue("1"), new ScalarValue(2), new ScalarValue(3.0f), };
+
+            var sequenceOfScalarsIntStringDoubleA = new LogEventProperty("a", new SequenceValue(intStringDoubleScalarArray));
+
+            var sequenceOfScalarsIntStringDoubleAStructurallyEqual = new LogEventProperty("a",
+                new SequenceValue(new[] { new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.0), }));
+
+            var sequenceOfScalarsIntStringDoubleAStructurallyNotEqual = new LogEventProperty("a",
+                new SequenceValue(new [] { new ScalarValue(1), new ScalarValue("2"), new ScalarValue(3.1), }));
+
+            var sequenceOfScalarsIntStringFloatA = new LogEventProperty("a", new ScalarValue(intStringFloatScalarArray));
+
+            var sequenceOfScalarsIntStringDoubleB = new LogEventProperty("b", new SequenceValue(intStringDoubleScalarArray));
+
+            var sut = new LogEventPropertyStructuralEqualityComparer();
+
+            // Structurally equal
+            Assert.True(sut.Equals(sequenceOfScalarsIntStringDoubleA, sequenceOfScalarsIntStringDoubleAStructurallyEqual));
+
+            // Not equal due to having a different property name (but otherwise structurally equal)
+            Assert.False(sut.Equals(sequenceOfScalarsIntStringDoubleA, sequenceOfScalarsIntStringDoubleB));
+            
+            // Structurally not equal because element 3 has a different value
+            Assert.False(sut.Equals(sequenceOfScalarsIntStringDoubleA, sequenceOfScalarsIntStringDoubleAStructurallyNotEqual));
+
+            // Strucrtually not equal because element 3 has a different underlying value and type
+            Assert.False(sut.Equals(sequenceOfScalarsIntStringDoubleA, sequenceOfScalarsIntStringFloatA));
+        }
+
+        [Fact]
+        public void LogEventPropertyStructuralEqualityComparerWorksForScalars()
+        {
+            var scalarStringA = new LogEventProperty("a", new ScalarValue("a"));
+            var scalarStringAStructurallyEqual = new LogEventProperty("a", new ScalarValue("a"));
+
+            var scalarStringB = new LogEventProperty("b", new ScalarValue("b"));
+            var scalarStringBStructurallyNotEqual = new LogEventProperty("b", new ScalarValue("notEqual"));
+
+            var scalarIntA1 = new LogEventProperty("a", new ScalarValue(1));
+            var scalarIntA1StructurallyEqual = new LogEventProperty("a", new ScalarValue(1));
+            var scalarIntA1DiffValueSameType = new LogEventProperty("a", new ScalarValue(0));
+            var scalarIntB1 = new LogEventProperty("b", new ScalarValue(1));
+
+            var guid1 = Guid.NewGuid();
+            var guid2 = Guid.NewGuid();
+            var scalarGuid1 = new LogEventProperty("1", new ScalarValue(guid1));
+            var scalarGuid1StructurallyEqual = new LogEventProperty("1", new ScalarValue(guid1));
+            var scalarGuid1StructurallyNotEqual = new LogEventProperty("1", new ScalarValue("notEqual"));
+            var scalarGuid2 = new LogEventProperty("2", new ScalarValue(guid2));
+
+            var sut = new LogEventPropertyStructuralEqualityComparer();
+
+            Assert.True(sut.Equals(scalarStringA, scalarStringAStructurallyEqual));
+            Assert.True(sut.Equals(scalarIntA1, scalarIntA1StructurallyEqual));
+            Assert.True(sut.Equals(scalarGuid1, scalarGuid1StructurallyEqual));
+
+            Assert.False(sut.Equals(scalarStringB, scalarStringBStructurallyNotEqual));
+            Assert.False(sut.Equals(scalarIntA1, scalarIntB1));
+            Assert.False(sut.Equals(scalarIntA1, scalarIntA1DiffValueSameType));
+
+            Assert.False(sut.Equals(scalarGuid1, scalarGuid2));
+            Assert.False(sut.Equals(scalarGuid1, scalarGuid1StructurallyNotEqual));
+        }
+    }
+}
diff --git a/test/Serilog.Tests/Support/LogEventPropertyValueComparer.cs b/test/Serilog.Tests/Support/LogEventPropertyValueComparer.cs
new file mode 100644
index 000000000..232b1cf96
--- /dev/null
+++ b/test/Serilog.Tests/Support/LogEventPropertyValueComparer.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Serilog.Events;
+
+namespace Serilog.Tests.Support
+{
+    class LogEventPropertyValueComparer : IEqualityComparer<LogEventPropertyValue>
+    {
+        readonly IEqualityComparer<object> _objectEqualityComparer;
+
+        public LogEventPropertyValueComparer(IEqualityComparer<object> objectEqualityComparer = null)
+        {
+            this._objectEqualityComparer = objectEqualityComparer ?? EqualityComparer<object>.Default;
+        }
+
+        public bool Equals(LogEventPropertyValue x, LogEventPropertyValue y)
+        {
+            var scalarX = x as ScalarValue;
+            var scalarY = y as ScalarValue;
+            if (scalarX != null && scalarY != null)
+            {
+                return _objectEqualityComparer.Equals(scalarX.Value, scalarY.Value);
+            }
+
+            var sequenceX = x as SequenceValue;
+            var sequenceY = y as SequenceValue;
+            if (sequenceX != null && sequenceY != null)
+            {
+                return sequenceX.Elements
+                    .SequenceEqual(sequenceY.Elements, this);
+            }
+
+            if (x is StructureValue || y is StructureValue)
+            {
+                throw new NotImplementedException();
+            }
+
+            if (x is DictionaryValue || y is DictionaryValue)
+            {
+                throw new NotImplementedException();
+            }
+
+            return false;
+        }
+
+        public int GetHashCode(LogEventPropertyValue obj)
+        {
+            return 0;
+        }
+    }
+}

From cee46e7fc6b0e92c07f6eb41f4aea734b3a71b2c Mon Sep 17 00:00:00 2001
From: Adam Chester <adamchester@gmail.com>
Date: Sun, 19 Jun 2016 10:00:03 +1000
Subject: [PATCH 2/2] Cover property capturing with better tests,

Only `ScalarValue` and `SequenceValue` are covered.
---
 .../Core/LogEventPropertyCapturingTests.cs    | 112 ++++++++++++++++++
 1 file changed, 112 insertions(+)
 create mode 100644 test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs

diff --git a/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs b/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs
new file mode 100644
index 000000000..a7977cf00
--- /dev/null
+++ b/test/Serilog.Tests/Core/LogEventPropertyCapturingTests.cs
@@ -0,0 +1,112 @@
+using System;
+using System.Linq;
+using System.Collections.Generic;
+using System.IO;
+using Serilog.Core;
+using Serilog.Debugging;
+using Serilog.Events;
+using Serilog.Parameters;
+using Serilog.Parsing;
+using Serilog.Tests.Support;
+using Xunit;
+
+namespace Serilog.Tests.Core
+{
+    public class LogEventPropertyCapturingTests
+    {
+        [Fact]
+        public void CapturingANamedScalarStringWorks()
+        {
+            Assert.Equal(
+                new[] { new LogEventProperty("who", new ScalarValue("world")) },
+                Capture("Hello {who}", "world"),
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void CapturingAPositionalScalarStringWorks()
+        {
+            Assert.Equal(
+                new[] { new LogEventProperty("0", new ScalarValue("world")) },
+                Capture("Hello {0}", "world"),
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void CapturingMixedPositionalAndNamedScalarsWorksUsingNames()
+        {
+            Assert.Equal(new[]
+                {
+                    new LogEventProperty("who", new ScalarValue("worldNamed")),
+                    new LogEventProperty("0", new ScalarValue("worldPositional")),
+                },
+                Capture("Hello {who} {0} {0}", "worldNamed", "worldPositional"),
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void CapturingIntArrayAsScalarSequenceValuesWorks()
+        {
+            const string template = "Hello {sequence}";
+            var templateArguments = new[] { 1, 2, 3, };
+            var expected = new[] {
+                new LogEventProperty("sequence",
+                    new SequenceValue(new[] {
+                        new ScalarValue(1),
+                        new ScalarValue(2),
+                        new ScalarValue(3) })) };
+
+            Assert.Equal(expected, Capture(template, templateArguments),
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void CapturingDestructuredStringArrayAsScalarSequenceValuesWorks()
+        {
+            const string template = "Hello {@sequence}";
+            var templateArguments = new[] { "1", "2", "3", };
+            var expected = new[] {
+                new LogEventProperty("sequence",
+                    new SequenceValue(new[] {
+                        new ScalarValue("1"),
+                        new ScalarValue("2"),
+                        new ScalarValue("3") })) };
+
+            Assert.Equal(expected, Capture(template, new object[] { templateArguments }),
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void WillCaptureProvidedPositionalValuesEvenIfSomeAreMissing()
+        {
+            Assert.Equal(new[]
+                {
+                    new LogEventProperty("0", new ScalarValue(0)),
+                    new LogEventProperty("1", new ScalarValue(1)),
+                },
+                Capture("Hello {3} {2} {1} {0} nothing more", 0, 1), // missing {2} and {3}
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        [Fact]
+        public void WillCaptureProvidedNamedValuesEvenIfSomeAreMissing()
+        {
+            Assert.Equal(new[]
+                {
+                    new LogEventProperty("who", new ScalarValue("who")),
+                    new LogEventProperty("what", new ScalarValue("what")),
+                },
+                Capture("Hello {who} {what} {where}", "who", "what"), // missing "where"
+                new LogEventPropertyStructuralEqualityComparer());
+        }
+
+        static IEnumerable<LogEventProperty> Capture(string messageTemplate, params object[] properties)
+        {
+            var mt = new MessageTemplateParser().Parse(messageTemplate);
+            var binder = new PropertyBinder(
+                new PropertyValueConverter(10, Enumerable.Empty<Type>(), Enumerable.Empty<IDestructuringPolicy>()));
+            return binder.ConstructProperties(mt, properties);
+        }
+
+    }
+}