a575963da9
Former-commit-id: da6be194a6b1221998fc28233f2503bd61dd9d14
394 lines
15 KiB
C#
394 lines
15 KiB
C#
// ****************************************************************
|
|
// Copyright 2007, Charlie Poole
|
|
// This is free software licensed under the NUnit license. You may
|
|
// obtain a copy of the license at http://nunit.org/?p=license&r=2.4
|
|
// ****************************************************************
|
|
|
|
using System;
|
|
using System.IO;
|
|
using System.Collections;
|
|
|
|
namespace NUnit.Framework.Constraints
|
|
{
|
|
/// <summary>
|
|
/// EqualConstraint is able to compare an actual value with the
|
|
/// expected value provided in its constructor.
|
|
/// </summary>
|
|
public class EqualConstraint : Constraint
|
|
{
|
|
private static IDictionary constraintHelpers = new Hashtable();
|
|
|
|
private readonly object expected;
|
|
|
|
private ArrayList failurePoints;
|
|
|
|
private static readonly string StringsDiffer_1 =
|
|
"String lengths are both {0}. Strings differ at index {1}.";
|
|
private static readonly string StringsDiffer_2 =
|
|
"Expected string length {0} but was {1}. Strings differ at index {2}.";
|
|
private static readonly string StreamsDiffer_1 =
|
|
"Stream lengths are both {0}. Streams differ at offset {1}.";
|
|
private static readonly string StreamsDiffer_2 =
|
|
"Expected Stream length {0} but was {1}.";// Streams differ at offset {2}.";
|
|
private static readonly string CollectionType_1 =
|
|
"Expected and actual are both {0}";
|
|
private static readonly string CollectionType_2 =
|
|
"Expected is {0}, actual is {1}";
|
|
private static readonly string ValuesDiffer_1 =
|
|
"Values differ at index {0}";
|
|
private static readonly string ValuesDiffer_2 =
|
|
"Values differ at expected index {0}, actual index {1}";
|
|
|
|
private static readonly int BUFFER_SIZE = 4096;
|
|
|
|
#region Constructor
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="EqualConstraint"/> class.
|
|
/// </summary>
|
|
/// <param name="expected">The expected value.</param>
|
|
public EqualConstraint(object expected)
|
|
{
|
|
this.expected = expected;
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
/// <summary>
|
|
/// Test whether the constraint is satisfied by a given value
|
|
/// </summary>
|
|
/// <param name="actual">The value to be tested</param>
|
|
/// <returns>True for success, false for failure</returns>
|
|
public override bool Matches(object actual)
|
|
{
|
|
this.actual = actual;
|
|
this.failurePoints = new ArrayList();
|
|
|
|
return ObjectsEqual( expected, actual );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a failure message. Overridden to provide custom
|
|
/// failure messages for EqualConstraint.
|
|
/// </summary>
|
|
/// <param name="writer">The MessageWriter to write to</param>
|
|
public override void WriteMessageTo(MessageWriter writer)
|
|
{
|
|
DisplayDifferences(writer, expected, actual, 0);
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Write description of this constraint
|
|
/// </summary>
|
|
/// <param name="writer">The MessageWriter to write to</param>
|
|
public override void WriteDescriptionTo(MessageWriter writer)
|
|
{
|
|
writer.WriteExpectedValue( expected );
|
|
|
|
if ( tolerance != null )
|
|
{
|
|
writer.WriteConnector("+/-");
|
|
writer.WriteExpectedValue(tolerance);
|
|
}
|
|
|
|
if ( this.caseInsensitive )
|
|
writer.WriteModifier("ignoring case");
|
|
}
|
|
|
|
private void DisplayDifferences(MessageWriter writer, object expected, object actual, int depth)
|
|
{
|
|
if (expected is string && actual is string)
|
|
DisplayStringDifferences(writer, (string)expected, (string)actual);
|
|
else if (expected is ICollection && actual is ICollection)
|
|
DisplayCollectionDifferences(writer, (ICollection)expected, (ICollection)actual, depth);
|
|
else if (expected is Stream && actual is Stream)
|
|
DisplayStreamDifferences(writer, (Stream)expected, (Stream)actual, depth);
|
|
else if ( tolerance != null )
|
|
writer.DisplayDifferences( expected, actual, tolerance );
|
|
else
|
|
writer.DisplayDifferences(expected, actual);
|
|
}
|
|
#endregion
|
|
|
|
#region ObjectsEqual
|
|
private bool ObjectsEqual(object expected, object actual)
|
|
{
|
|
if (expected == null && actual == null)
|
|
return true;
|
|
|
|
if (expected == null || actual == null)
|
|
return false;
|
|
|
|
Type expectedType = expected.GetType();
|
|
Type actualType = actual.GetType();
|
|
|
|
if (expectedType.IsArray && actualType.IsArray && !compareAsCollection)
|
|
return ArraysEqual((Array)expected, (Array)actual);
|
|
|
|
if (expected is ICollection && actual is ICollection)
|
|
return CollectionsEqual((ICollection)expected, (ICollection)actual);
|
|
|
|
if (expected is Stream && actual is Stream)
|
|
return StreamsEqual((Stream)expected, (Stream)actual);
|
|
|
|
if (compareWith != null)
|
|
return compareWith.Compare( expected, actual ) == 0;
|
|
|
|
if (expected is DirectoryInfo && actual is DirectoryInfo)
|
|
return DirectoriesEqual((DirectoryInfo)expected, (DirectoryInfo)actual);
|
|
|
|
if (Numerics.IsNumericType(expected) && Numerics.IsNumericType(actual))
|
|
{
|
|
return Numerics.AreEqual(expected, actual, ref tolerance);
|
|
}
|
|
|
|
if (expected is string && actual is string)
|
|
{
|
|
return StringsEqual( (string) expected, (string)actual );
|
|
}
|
|
|
|
if (expected is DateTime && actual is DateTime && tolerance is TimeSpan)
|
|
{
|
|
return ((DateTime)expected - (DateTime)actual).Duration() <= (TimeSpan)tolerance;
|
|
}
|
|
|
|
return expected.Equals(actual);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Helper method to compare two arrays
|
|
/// </summary>
|
|
protected virtual bool ArraysEqual(Array expected, Array actual)
|
|
{
|
|
int rank = expected.Rank;
|
|
|
|
if (rank != actual.Rank)
|
|
return false;
|
|
|
|
for (int r = 1; r < rank; r++)
|
|
if (expected.GetLength(r) != actual.GetLength(r))
|
|
return false;
|
|
|
|
return CollectionsEqual((ICollection)expected, (ICollection)actual);
|
|
}
|
|
|
|
private bool CollectionsEqual(ICollection expected, ICollection actual)
|
|
{
|
|
IEnumerator expectedEnum = expected.GetEnumerator();
|
|
IEnumerator actualEnum = actual.GetEnumerator();
|
|
|
|
int count;
|
|
for (count = 0; expectedEnum.MoveNext() && actualEnum.MoveNext(); count++)
|
|
{
|
|
if (!ObjectsEqual(expectedEnum.Current, actualEnum.Current))
|
|
break;
|
|
}
|
|
|
|
if (count == expected.Count && count == actual.Count)
|
|
return true;
|
|
|
|
failurePoints.Insert(0, count);
|
|
return false;
|
|
}
|
|
|
|
private bool StreamsEqual( Stream expected, Stream actual )
|
|
{
|
|
if (expected.Length != actual.Length) return false;
|
|
|
|
byte[] bufferExpected = new byte[BUFFER_SIZE];
|
|
byte[] bufferActual = new byte[BUFFER_SIZE];
|
|
|
|
BinaryReader binaryReaderExpected = new BinaryReader(expected);
|
|
BinaryReader binaryReaderActual = new BinaryReader(actual);
|
|
|
|
binaryReaderExpected.BaseStream.Seek(0, SeekOrigin.Begin);
|
|
binaryReaderActual.BaseStream.Seek(0, SeekOrigin.Begin);
|
|
|
|
for(long readByte = 0; readByte < expected.Length; readByte += BUFFER_SIZE )
|
|
{
|
|
binaryReaderExpected.Read(bufferExpected, 0, BUFFER_SIZE);
|
|
binaryReaderActual.Read(bufferActual, 0, BUFFER_SIZE);
|
|
|
|
for (int count=0; count < BUFFER_SIZE; ++count)
|
|
{
|
|
if (bufferExpected[count] != bufferActual[count])
|
|
{
|
|
failurePoints.Insert( 0, readByte + count );
|
|
//FailureMessage.WriteLine("\tIndex : {0}", readByte + count);
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private bool StringsEqual( string expected, string actual )
|
|
{
|
|
string s1 = caseInsensitive ? expected.ToLower() : expected;
|
|
string s2 = caseInsensitive ? actual.ToLower() : actual;
|
|
|
|
return s1.Equals( s2 );
|
|
}
|
|
|
|
/// <summary>
|
|
/// Method to compare two DirectoryInfo objects
|
|
/// </summary>
|
|
/// <param name="expected">first directory to compare</param>
|
|
/// <param name="actual">second directory to compare</param>
|
|
/// <returns>true if equivalent, false if not</returns>
|
|
private bool DirectoriesEqual(DirectoryInfo expected, DirectoryInfo actual)
|
|
{
|
|
return expected.Attributes == actual.Attributes
|
|
&& expected.CreationTime == actual.CreationTime
|
|
&& expected.FullName == actual.FullName
|
|
&& expected.LastAccessTime == actual.LastAccessTime;
|
|
}
|
|
#endregion
|
|
|
|
#region DisplayStringDifferences
|
|
private void DisplayStringDifferences(MessageWriter writer, string expected, string actual)
|
|
{
|
|
int mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, this.caseInsensitive);
|
|
|
|
if (expected.Length == actual.Length)
|
|
writer.WriteMessageLine(StringsDiffer_1, expected.Length, mismatch);
|
|
else
|
|
writer.WriteMessageLine(StringsDiffer_2, expected.Length, actual.Length, mismatch);
|
|
|
|
writer.DisplayStringDifferences(expected, actual, mismatch, caseInsensitive, clipStrings);
|
|
}
|
|
#endregion
|
|
|
|
#region DisplayStreamDifferences
|
|
private void DisplayStreamDifferences(MessageWriter writer, Stream expected, Stream actual, int depth)
|
|
{
|
|
if ( expected.Length == actual.Length )
|
|
{
|
|
long offset = (long)failurePoints[depth];
|
|
writer.WriteMessageLine(StreamsDiffer_1, expected.Length, offset);
|
|
}
|
|
else
|
|
writer.WriteMessageLine(StreamsDiffer_2, expected.Length, actual.Length);
|
|
}
|
|
#endregion
|
|
|
|
#region DisplayCollectionDifferences
|
|
/// <summary>
|
|
/// Display the failure information for two collections that did not match.
|
|
/// </summary>
|
|
/// <param name="writer">The MessageWriter on which to display</param>
|
|
/// <param name="expected">The expected collection.</param>
|
|
/// <param name="actual">The actual collection</param>
|
|
/// <param name="depth">The depth of this failure in a set of nested collections</param>
|
|
private void DisplayCollectionDifferences(MessageWriter writer, ICollection expected, ICollection actual, int depth)
|
|
{
|
|
int failurePoint = failurePoints.Count > depth ? (int)failurePoints[depth] : -1;
|
|
|
|
DisplayCollectionTypesAndSizes(writer, expected, actual, depth);
|
|
|
|
if (failurePoint >= 0)
|
|
{
|
|
DisplayFailurePoint(writer, expected, actual, failurePoint, depth);
|
|
if (failurePoint < expected.Count && failurePoint < actual.Count)
|
|
DisplayDifferences(
|
|
writer,
|
|
GetValueFromCollection(expected, failurePoint),
|
|
GetValueFromCollection(actual, failurePoint),
|
|
++depth);
|
|
else if (expected.Count < actual.Count)
|
|
{
|
|
writer.Write( " Extra: " );
|
|
writer.WriteCollectionElements( actual, failurePoint, 3 );
|
|
}
|
|
else
|
|
{
|
|
writer.Write( " Missing: " );
|
|
writer.WriteCollectionElements( expected, failurePoint, 3 );
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays a single line showing the types and sizes of the expected
|
|
/// and actual collections or arrays. If both are identical, the value is
|
|
/// only shown once.
|
|
/// </summary>
|
|
/// <param name="writer">The MessageWriter on which to display</param>
|
|
/// <param name="expected">The expected collection or array</param>
|
|
/// <param name="actual">The actual collection or array</param>
|
|
/// <param name="indent">The indentation level for the message line</param>
|
|
private void DisplayCollectionTypesAndSizes(MessageWriter writer, ICollection expected, ICollection actual, int indent)
|
|
{
|
|
string sExpected = MsgUtils.GetTypeRepresentation(expected);
|
|
if (!(expected is Array))
|
|
sExpected += string.Format(" with {0} elements", expected.Count);
|
|
|
|
string sActual = MsgUtils.GetTypeRepresentation(actual);
|
|
if (!(actual is Array))
|
|
sActual += string.Format(" with {0} elements", actual.Count);
|
|
|
|
if (sExpected == sActual)
|
|
writer.WriteMessageLine(indent, CollectionType_1, sExpected);
|
|
else
|
|
writer.WriteMessageLine(indent, CollectionType_2, sExpected, sActual);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Displays a single line showing the point in the expected and actual
|
|
/// arrays at which the comparison failed. If the arrays have different
|
|
/// structures or dimensions, both values are shown.
|
|
/// </summary>
|
|
/// <param name="writer">The MessageWriter on which to display</param>
|
|
/// <param name="expected">The expected array</param>
|
|
/// <param name="actual">The actual array</param>
|
|
/// <param name="failurePoint">Index of the failure point in the underlying collections</param>
|
|
/// <param name="indent">The indentation level for the message line</param>
|
|
private void DisplayFailurePoint(MessageWriter writer, ICollection expected, ICollection actual, int failurePoint, int indent)
|
|
{
|
|
Array expectedArray = expected as Array;
|
|
Array actualArray = actual as Array;
|
|
|
|
int expectedRank = expectedArray != null ? expectedArray.Rank : 1;
|
|
int actualRank = actualArray != null ? actualArray.Rank : 1;
|
|
|
|
bool useOneIndex = expectedRank == actualRank;
|
|
|
|
if (expectedArray != null && actualArray != null)
|
|
for (int r = 1; r < expectedRank && useOneIndex; r++)
|
|
if (expectedArray.GetLength(r) != actualArray.GetLength(r))
|
|
useOneIndex = false;
|
|
|
|
int[] expectedIndices = MsgUtils.GetArrayIndicesFromCollectionIndex(expected, failurePoint);
|
|
if (useOneIndex)
|
|
{
|
|
writer.WriteMessageLine(indent, ValuesDiffer_1, MsgUtils.GetArrayIndicesAsString(expectedIndices));
|
|
}
|
|
else
|
|
{
|
|
int[] actualIndices = MsgUtils.GetArrayIndicesFromCollectionIndex(actual, failurePoint);
|
|
writer.WriteMessageLine(indent, ValuesDiffer_2,
|
|
MsgUtils.GetArrayIndicesAsString(expectedIndices), MsgUtils.GetArrayIndicesAsString(actualIndices));
|
|
}
|
|
}
|
|
|
|
private static object GetValueFromCollection(ICollection collection, int index)
|
|
{
|
|
Array array = collection as Array;
|
|
|
|
if (array != null && array.Rank > 1)
|
|
return array.GetValue(MsgUtils.GetArrayIndicesFromCollectionIndex(array, index));
|
|
|
|
if (collection is IList)
|
|
return ((IList)collection)[index];
|
|
|
|
foreach (object obj in collection)
|
|
if (--index < 0)
|
|
return obj;
|
|
|
|
return null;
|
|
}
|
|
#endregion
|
|
}
|
|
}
|