1 // ***********************************************************************
2 // Copyright (c) 2009 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
31 using System.Collections;
32 using System.Collections.Generic;
33 using System.Reflection;
34 using NUnit.Compatibility;
36 namespace NUnit.Framework.Constraints
39 /// NUnitEqualityComparer encapsulates NUnit's handling of
40 /// equality tests between objects.
42 public class NUnitEqualityComparer
44 #region Static and Instance Fields
46 /// If true, all string comparisons will ignore case
48 private bool caseInsensitive;
51 /// If true, arrays will be treated as collections, allowing
52 /// those of different dimensions to be compared
54 private bool compareAsCollection;
57 /// Comparison objects used in comparisons for some constraints.
59 private List<EqualityAdapter> externalComparers = new List<EqualityAdapter>();
62 /// List of points at which a failure occurred.
64 private List<FailurePoint> failurePoints;
66 private static readonly int BUFFER_SIZE = 4096;
72 /// Returns the default NUnitEqualityComparer
74 public static NUnitEqualityComparer Default
76 get { return new NUnitEqualityComparer(); }
79 /// Gets and sets a flag indicating whether case should
80 /// be ignored in determining equality.
82 public bool IgnoreCase
84 get { return caseInsensitive; }
85 set { caseInsensitive = value; }
89 /// Gets and sets a flag indicating that arrays should be
90 /// compared as collections, without regard to their shape.
92 public bool CompareAsCollection
94 get { return compareAsCollection; }
95 set { compareAsCollection = value; }
99 /// Gets the list of external comparers to be used to
100 /// test for equality. They are applied to members of
101 /// collections, in place of NUnit's own logic.
103 public IList<EqualityAdapter> ExternalComparers
105 get { return externalComparers; }
108 // TODO: Define some sort of FailurePoint struct or otherwise
109 // eliminate the type-unsafeness of the current approach
112 /// Gets the list of failure points for the last Match performed.
113 /// The list consists of objects to be interpreted by the caller.
114 /// This generally means that the caller may only make use of
115 /// objects it has placed on the list at a particular depthy.
117 public IList<FailurePoint> FailurePoints
119 get { return failurePoints; }
124 /// Flags the comparer to include <see cref="DateTimeOffset.Offset"/>
125 /// property in comparison of two <see cref="DateTimeOffset"/> values.
128 /// Using this modifier does not allow to use the <see cref="Tolerance"/>
131 public bool WithSameOffset { get; set; }
135 #region Public Methods
137 /// Compares two objects for equality within a tolerance.
139 public bool AreEqual(object x, object y, ref Tolerance tolerance)
141 this.failurePoints = new List<FailurePoint>();
143 if (x == null && y == null)
146 if (x == null || y == null)
149 if (object.ReferenceEquals(x, y))
152 Type xType = x.GetType();
153 Type yType = y.GetType();
155 Type xGenericTypeDefinition = xType.GetTypeInfo().IsGenericType ? xType.GetGenericTypeDefinition() : null;
156 Type yGenericTypeDefinition = yType.GetTypeInfo().IsGenericType ? yType.GetGenericTypeDefinition() : null;
158 EqualityAdapter externalComparer = GetExternalComparer(x, y);
159 if (externalComparer != null)
160 return externalComparer.AreEqual(x, y);
162 if (xType.IsArray && yType.IsArray && !compareAsCollection)
163 return ArraysEqual((Array)x, (Array)y, ref tolerance);
165 if (x is IDictionary && y is IDictionary)
166 return DictionariesEqual((IDictionary)x, (IDictionary)y, ref tolerance);
168 // Issue #70 - EquivalentTo isn't compatible with IgnoreCase for dictionaries
169 if (x is DictionaryEntry && y is DictionaryEntry)
170 return DictionaryEntriesEqual((DictionaryEntry)x, (DictionaryEntry)y, ref tolerance);
172 // IDictionary<,> will eventually try to compare it's key value pairs when using CollectionTally
173 if (xGenericTypeDefinition == typeof(KeyValuePair<,>) &&
174 yGenericTypeDefinition == typeof(KeyValuePair<,>))
176 var keyTolerance = Tolerance.Exact;
177 object xKey = xType.GetProperty("Key").GetValue(x, null);
178 object yKey = yType.GetProperty("Key").GetValue(y, null);
179 object xValue = xType.GetProperty("Value").GetValue(x, null);
180 object yValue = yType.GetProperty("Value").GetValue(y, null);
182 return AreEqual(xKey, yKey, ref keyTolerance) && AreEqual(xValue, yValue, ref tolerance);
185 //if (x is ICollection && y is ICollection)
186 // return CollectionsEqual((ICollection)x, (ICollection)y, ref tolerance);
188 if (x is string && y is string)
189 return StringsEqual((string)x, (string)y);
191 if (x is Stream && y is Stream)
192 return StreamsEqual((Stream)x, (Stream)y);
194 if ( x is char && y is char )
195 return CharsEqual( (char)x, (char)y );
198 if (x is DirectoryInfo && y is DirectoryInfo)
199 return DirectoriesEqual((DirectoryInfo)x, (DirectoryInfo)y);
202 if (Numerics.IsNumericType(x) && Numerics.IsNumericType(y))
203 return Numerics.AreEqual(x, y, ref tolerance);
206 if (x is DateTimeOffset && y is DateTimeOffset)
210 DateTimeOffset xAsOffset = (DateTimeOffset)x;
211 DateTimeOffset yAsOffset = (DateTimeOffset)y;
213 if (tolerance != null && tolerance.Value is TimeSpan)
215 TimeSpan amount = (TimeSpan)tolerance.Value;
216 result = (xAsOffset - yAsOffset).Duration() <= amount;
220 result = xAsOffset == yAsOffset;
223 if (result && WithSameOffset)
225 result = xAsOffset.Offset == yAsOffset.Offset;
232 if (tolerance != null && tolerance.Value is TimeSpan)
234 TimeSpan amount = (TimeSpan)tolerance.Value;
236 if (x is DateTime && y is DateTime)
237 return ((DateTime)x - (DateTime)y).Duration() <= amount;
239 if (x is TimeSpan && y is TimeSpan)
240 return ((TimeSpan)x - (TimeSpan)y).Duration() <= amount;
243 MethodInfo equals = FirstImplementsIEquatableOfSecond(xType, yType);
245 return InvokeFirstIEquatableEqualsSecond(x, y, equals);
246 if (xType != yType && (equals = FirstImplementsIEquatableOfSecond(yType, xType)) != null)
247 return InvokeFirstIEquatableEqualsSecond(y, x, equals);
249 if (x is IEnumerable && y is IEnumerable)
250 return EnumerablesEqual((IEnumerable) x, (IEnumerable) y, ref tolerance);
255 private static MethodInfo FirstImplementsIEquatableOfSecond(Type first, Type second)
257 var pair = new KeyValuePair<Type, MethodInfo>();
259 foreach (var xEquatableArgument in GetEquatableGenericArguments(first))
260 if (xEquatableArgument.Key.IsAssignableFrom(second))
261 if (pair.Key == null || pair.Key.IsAssignableFrom(xEquatableArgument.Key))
262 pair = xEquatableArgument;
267 private static IList<KeyValuePair<Type, MethodInfo>> GetEquatableGenericArguments(Type type)
269 // NOTE: Original implementation used Array.ConvertAll and
270 // Array.FindAll, which don't exist in the compact framework.
271 var genericArgs = new List<KeyValuePair<Type, MethodInfo>>();
273 foreach (Type @interface in type.GetInterfaces())
275 if (@interface.GetTypeInfo().IsGenericType && @interface.GetGenericTypeDefinition().Equals(typeof(IEquatable<>)))
277 genericArgs.Add(new KeyValuePair<Type, MethodInfo>(
278 @interface.GetGenericArguments()[0], @interface.GetMethod("Equals")));
285 private static bool InvokeFirstIEquatableEqualsSecond(object first, object second, MethodInfo equals)
287 return equals != null ? (bool)equals.Invoke(first, new object[] { second }) : false;
292 #region Helper Methods
294 private EqualityAdapter GetExternalComparer(object x, object y)
296 foreach (EqualityAdapter adapter in externalComparers)
297 if (adapter.CanCompare(x, y))
304 /// Helper method to compare two arrays
306 private bool ArraysEqual(Array x, Array y, ref Tolerance tolerance)
313 for (int r = 1; r < rank; r++)
314 if (x.GetLength(r) != y.GetLength(r))
317 return EnumerablesEqual((IEnumerable)x, (IEnumerable)y, ref tolerance);
320 private bool DictionariesEqual(IDictionary x, IDictionary y, ref Tolerance tolerance)
322 if (x.Count != y.Count)
325 CollectionTally tally = new CollectionTally(this, x.Keys);
326 if (!tally.TryRemove(y.Keys) || tally.Count > 0)
329 foreach (object key in x.Keys)
330 if (!AreEqual(x[key], y[key], ref tolerance))
336 private bool DictionaryEntriesEqual(DictionaryEntry x, DictionaryEntry y, ref Tolerance tolerance)
338 var keyTolerance = Tolerance.Exact;
339 return AreEqual(x.Key, y.Key, ref keyTolerance) && AreEqual(x.Value, y.Value, ref tolerance);
342 private bool CollectionsEqual(ICollection x, ICollection y, ref Tolerance tolerance)
344 IEnumerator expectedEnum = null;
345 IEnumerator actualEnum = null;
349 expectedEnum = x.GetEnumerator();
350 actualEnum = y.GetEnumerator();
352 for (count = 0; ; count++)
354 bool expectedHasData = expectedEnum.MoveNext();
355 bool actualHasData = actualEnum.MoveNext();
357 if (!expectedHasData && !actualHasData)
360 if (expectedHasData != actualHasData ||
361 !AreEqual(expectedEnum.Current, actualEnum.Current, ref tolerance))
363 FailurePoint fp = new FailurePoint();
365 fp.ExpectedHasData = expectedHasData;
367 fp.ExpectedValue = expectedEnum.Current;
368 fp.ActualHasData = actualHasData;
370 fp.ActualValue = actualEnum.Current;
371 failurePoints.Insert(0, fp);
378 var expectedDisposable = expectedEnum as IDisposable;
379 if (expectedDisposable != null) expectedDisposable.Dispose();
381 var actualDisposable = actualEnum as IDisposable;
382 if (actualDisposable != null) actualDisposable.Dispose();
387 private bool StringsEqual(string x, string y)
389 string s1 = caseInsensitive ? x.ToLower() : x;
390 string s2 = caseInsensitive ? y.ToLower() : y;
392 return s1.Equals(s2);
395 private bool CharsEqual(char x, char y)
397 char c1 = caseInsensitive ? Char.ToLower(x) : x;
398 char c2 = caseInsensitive ? Char.ToLower(y) : y;
403 private bool EnumerablesEqual(IEnumerable x, IEnumerable y, ref Tolerance tolerance)
405 IEnumerator expectedEnum = null;
406 IEnumerator actualEnum = null;
410 expectedEnum = x.GetEnumerator();
411 actualEnum = y.GetEnumerator();
414 for (count = 0; ; count++)
416 bool expectedHasData = expectedEnum.MoveNext();
417 bool actualHasData = actualEnum.MoveNext();
419 if (!expectedHasData && !actualHasData)
422 if (expectedHasData != actualHasData ||
423 !AreEqual(expectedEnum.Current, actualEnum.Current, ref tolerance))
425 FailurePoint fp = new FailurePoint();
427 fp.ExpectedHasData = expectedHasData;
429 fp.ExpectedValue = expectedEnum.Current;
430 fp.ActualHasData = actualHasData;
432 fp.ActualValue = actualEnum.Current;
433 failurePoints.Insert(0, fp);
440 var expectedDisposable = expectedEnum as IDisposable;
441 if (expectedDisposable != null) expectedDisposable.Dispose();
443 var actualDisposable = actualEnum as IDisposable;
444 if (actualDisposable != null) actualDisposable.Dispose();
451 /// Method to compare two DirectoryInfo objects
453 /// <param name="x">first directory to compare</param>
454 /// <param name="y">second directory to compare</param>
455 /// <returns>true if equivalent, false if not</returns>
456 private static bool DirectoriesEqual(DirectoryInfo x, DirectoryInfo y)
458 // Do quick compares first
459 if (x.Attributes != y.Attributes ||
460 x.CreationTime != y.CreationTime ||
461 x.LastAccessTime != y.LastAccessTime)
466 // TODO: Find a cleaner way to do this
467 return new SamePathConstraint(x.FullName).ApplyTo(y.FullName).IsSuccess;
471 private bool StreamsEqual(Stream x, Stream y)
473 if (x == y) return true;
476 throw new ArgumentException("Stream is not readable", "expected");
478 throw new ArgumentException("Stream is not readable", "actual");
480 throw new ArgumentException("Stream is not seekable", "expected");
482 throw new ArgumentException("Stream is not seekable", "actual");
484 if (x.Length != y.Length) return false;
486 byte[] bufferExpected = new byte[BUFFER_SIZE];
487 byte[] bufferActual = new byte[BUFFER_SIZE];
489 BinaryReader binaryReaderExpected = new BinaryReader(x);
490 BinaryReader binaryReaderActual = new BinaryReader(y);
492 long expectedPosition = x.Position;
493 long actualPosition = y.Position;
497 binaryReaderExpected.BaseStream.Seek(0, SeekOrigin.Begin);
498 binaryReaderActual.BaseStream.Seek(0, SeekOrigin.Begin);
500 for (long readByte = 0; readByte < x.Length; readByte += BUFFER_SIZE)
502 binaryReaderExpected.Read(bufferExpected, 0, BUFFER_SIZE);
503 binaryReaderActual.Read(bufferActual, 0, BUFFER_SIZE);
505 for (int count = 0; count < BUFFER_SIZE; ++count)
507 if (bufferExpected[count] != bufferActual[count])
509 FailurePoint fp = new FailurePoint();
510 fp.Position = readByte + count;
511 fp.ExpectedHasData = true;
512 fp.ExpectedValue = bufferExpected[count];
513 fp.ActualHasData = true;
514 fp.ActualValue = bufferActual[count];
515 failurePoints.Insert(0, fp);
523 x.Position = expectedPosition;
524 y.Position = actualPosition;
532 #region Nested FailurePoint Class
535 /// FailurePoint class represents one point of failure
536 /// in an equality test.
538 public class FailurePoint
541 /// The location of the failure
543 public long Position;
546 /// The expected value
548 public object ExpectedValue;
553 public object ActualValue;
556 /// Indicates whether the expected value is valid
558 public bool ExpectedHasData;
561 /// Indicates whether the actual value is valid
563 public bool ActualHasData;