/src/NUnit/framework/Constraints/EqualConstraint.cs
C# | 510 lines | 303 code | 56 blank | 151 comment | 46 complexity | dc4dddf6cd4d86229be538817788ead4 MD5 | raw file
1// **************************************************************** 2// Copyright 2007, Charlie Poole 3// This is free software licensed under the NUnit license. You may 4// obtain a copy of the license at http://nunit.org 5// **************************************************************** 6 7using System; 8using System.IO; 9using System.Collections; 10#if NET_2_0 11using System.Collections.Generic; 12#endif 13 14namespace NUnit.Framework.Constraints 15{ 16 /// <summary> 17 /// EqualConstraint is able to compare an actual value with the 18 /// expected value provided in its constructor. Two objects are 19 /// considered equal if both are null, or if both have the same 20 /// value. NUnit has special semantics for some object types. 21 /// </summary> 22 public class EqualConstraint : Constraint 23 { 24 #region Static and Instance Fields 25 26 private readonly object expected; 27 28 /// <summary> 29 /// If true, strings in error messages will be clipped 30 /// </summary> 31 private bool clipStrings = true; 32 33 /// <summary> 34 /// NUnitEqualityComparer used to test equality. 35 /// </summary> 36 private NUnitEqualityComparer comparer = new NUnitEqualityComparer(); 37 38 #region Message Strings 39 private static readonly string StringsDiffer_1 = 40 "String lengths are both {0}. Strings differ at index {1}."; 41 private static readonly string StringsDiffer_2 = 42 "Expected string length {0} but was {1}. Strings differ at index {2}."; 43 private static readonly string StreamsDiffer_1 = 44 "Stream lengths are both {0}. Streams differ at offset {1}."; 45 private static readonly string StreamsDiffer_2 = 46 "Expected Stream length {0} but was {1}.";// Streams differ at offset {2}."; 47 private static readonly string CollectionType_1 = 48 "Expected and actual are both {0}"; 49 private static readonly string CollectionType_2 = 50 "Expected is {0}, actual is {1}"; 51 private static readonly string ValuesDiffer_1 = 52 "Values differ at index {0}"; 53 private static readonly string ValuesDiffer_2 = 54 "Values differ at expected index {0}, actual index {1}"; 55 56 #endregion 57 58 #endregion 59 60 #region Constructor 61 /// <summary> 62 /// Initializes a new instance of the <see cref="EqualConstraint"/> class. 63 /// </summary> 64 /// <param name="expected">The expected value.</param> 65 public EqualConstraint(object expected) : base(expected) 66 { 67 this.expected = expected; 68 } 69 #endregion 70 71 #region Constraint Modifiers 72 /// <summary> 73 /// Flag the constraint to ignore case and return self. 74 /// </summary> 75 public EqualConstraint IgnoreCase 76 { 77 get 78 { 79 comparer.IgnoreCase = true; 80 return this; 81 } 82 } 83 84 /// <summary> 85 /// Flag the constraint to suppress string clipping 86 /// and return self. 87 /// </summary> 88 public EqualConstraint NoClip 89 { 90 get 91 { 92 clipStrings = false; 93 return this; 94 } 95 } 96 97 /// <summary> 98 /// Flag the constraint to compare arrays as collections 99 /// and return self. 100 /// </summary> 101 public EqualConstraint AsCollection 102 { 103 get 104 { 105 comparer.CompareAsCollection = true; 106 return this; 107 } 108 } 109 110 /// <summary> 111 /// Flag the constraint to use a tolerance when determining equality. 112 /// </summary> 113 /// <param name="amount">Tolerance value to be used</param> 114 /// <returns>Self.</returns> 115 public EqualConstraint Within(object amount) 116 { 117 if (!comparer.Tolerance.IsEmpty) 118 throw new InvalidOperationException("Within modifier may appear only once in a constraint expression"); 119 120 comparer.Tolerance = new Tolerance(amount); 121 return this; 122 } 123 124 /// <summary> 125 /// Switches the .Within() modifier to interpret its tolerance as 126 /// a distance in representable values (see remarks). 127 /// </summary> 128 /// <returns>Self.</returns> 129 /// <remarks> 130 /// Ulp stands for "unit in the last place" and describes the minimum 131 /// amount a given value can change. For any integers, an ulp is 1 whole 132 /// digit. For floating point values, the accuracy of which is better 133 /// for smaller numbers and worse for larger numbers, an ulp depends 134 /// on the size of the number. Using ulps for comparison of floating 135 /// point results instead of fixed tolerances is safer because it will 136 /// automatically compensate for the added inaccuracy of larger numbers. 137 /// </remarks> 138 public EqualConstraint Ulps 139 { 140 get 141 { 142 comparer.Tolerance = comparer.Tolerance.Ulps; 143 return this; 144 } 145 } 146 147 /// <summary> 148 /// Switches the .Within() modifier to interpret its tolerance as 149 /// a percentage that the actual values is allowed to deviate from 150 /// the expected value. 151 /// </summary> 152 /// <returns>Self</returns> 153 public EqualConstraint Percent 154 { 155 get 156 { 157 comparer.Tolerance = comparer.Tolerance.Percent; 158 return this; 159 } 160 } 161 162 /// <summary> 163 /// Causes the tolerance to be interpreted as a TimeSpan in days. 164 /// </summary> 165 /// <returns>Self</returns> 166 public EqualConstraint Days 167 { 168 get 169 { 170 comparer.Tolerance = comparer.Tolerance.Days; 171 return this; 172 } 173 } 174 175 /// <summary> 176 /// Causes the tolerance to be interpreted as a TimeSpan in hours. 177 /// </summary> 178 /// <returns>Self</returns> 179 public EqualConstraint Hours 180 { 181 get 182 { 183 comparer.Tolerance = comparer.Tolerance.Hours; 184 return this; 185 } 186 } 187 188 /// <summary> 189 /// Causes the tolerance to be interpreted as a TimeSpan in minutes. 190 /// </summary> 191 /// <returns>Self</returns> 192 public EqualConstraint Minutes 193 { 194 get 195 { 196 comparer.Tolerance = comparer.Tolerance.Minutes; 197 return this; 198 } 199 } 200 201 /// <summary> 202 /// Causes the tolerance to be interpreted as a TimeSpan in seconds. 203 /// </summary> 204 /// <returns>Self</returns> 205 public EqualConstraint Seconds 206 { 207 get 208 { 209 comparer.Tolerance = comparer.Tolerance.Seconds; 210 return this; 211 } 212 } 213 214 /// <summary> 215 /// Causes the tolerance to be interpreted as a TimeSpan in milliseconds. 216 /// </summary> 217 /// <returns>Self</returns> 218 public EqualConstraint Milliseconds 219 { 220 get 221 { 222 comparer.Tolerance = comparer.Tolerance.Milliseconds; 223 return this; 224 } 225 } 226 227 /// <summary> 228 /// Causes the tolerance to be interpreted as a TimeSpan in clock ticks. 229 /// </summary> 230 /// <returns>Self</returns> 231 public EqualConstraint Ticks 232 { 233 get 234 { 235 comparer.Tolerance = comparer.Tolerance.Ticks; 236 return this; 237 } 238 } 239 240 /// <summary> 241 /// Flag the constraint to use the supplied IComparer object. 242 /// </summary> 243 /// <param name="comparer">The IComparer object to use.</param> 244 /// <returns>Self.</returns> 245 [Obsolete("Replace with 'Using'")] 246 public EqualConstraint Comparer(IComparer comparer) 247 { 248 return Using(comparer); 249 } 250 251 /// <summary> 252 /// Flag the constraint to use the supplied IComparer object. 253 /// </summary> 254 /// <param name="comparer">The IComparer object to use.</param> 255 /// <returns>Self.</returns> 256 public EqualConstraint Using(IComparer comparer) 257 { 258 this.comparer.ExternalComparer = EqualityAdapter.For(comparer); 259 return this; 260 } 261 262#if NET_2_0 263 /// <summary> 264 /// Flag the constraint to use the supplied IComparer object. 265 /// </summary> 266 /// <param name="comparer">The IComparer object to use.</param> 267 /// <returns>Self.</returns> 268 public EqualConstraint Using<T>(IComparer<T> comparer) 269 { 270 this.comparer.ExternalComparer = EqualityAdapter.For( comparer ); 271 return this; 272 } 273 274 /// <summary> 275 /// Flag the constraint to use the supplied Comparison object. 276 /// </summary> 277 /// <param name="comparer">The IComparer object to use.</param> 278 /// <returns>Self.</returns> 279 public EqualConstraint Using<T>(Comparison<T> comparer) 280 { 281 this.comparer.ExternalComparer = EqualityAdapter.For( comparer ); 282 return this; 283 } 284 285 /// <summary> 286 /// Flag the constraint to use the supplied IEqualityComparer object. 287 /// </summary> 288 /// <param name="comparer">The IComparer object to use.</param> 289 /// <returns>Self.</returns> 290 public EqualConstraint Using(IEqualityComparer comparer) 291 { 292 this.comparer.ExternalComparer = EqualityAdapter.For(comparer); 293 return this; 294 } 295 296 /// <summary> 297 /// Flag the constraint to use the supplied IEqualityComparer object. 298 /// </summary> 299 /// <param name="comparer">The IComparer object to use.</param> 300 /// <returns>Self.</returns> 301 public EqualConstraint Using<T>(IEqualityComparer<T> comparer) 302 { 303 this.comparer.ExternalComparer = EqualityAdapter.For(comparer); 304 return this; 305 } 306#endif 307 #endregion 308 309 #region Public Methods 310 /// <summary> 311 /// Test whether the constraint is satisfied by a given value 312 /// </summary> 313 /// <param name="actual">The value to be tested</param> 314 /// <returns>True for success, false for failure</returns> 315 public override bool Matches(object actual) 316 { 317 this.actual = actual; 318 319 return comparer.ObjectsEqual(expected, actual); 320 } 321 322 /// <summary> 323 /// Write a failure message. Overridden to provide custom 324 /// failure messages for EqualConstraint. 325 /// </summary> 326 /// <param name="writer">The MessageWriter to write to</param> 327 public override void WriteMessageTo(MessageWriter writer) 328 { 329 DisplayDifferences(writer, expected, actual, 0); 330 } 331 332 333 /// <summary> 334 /// Write description of this constraint 335 /// </summary> 336 /// <param name="writer">The MessageWriter to write to</param> 337 public override void WriteDescriptionTo(MessageWriter writer) 338 { 339 writer.WriteExpectedValue( expected ); 340 341 if (comparer.Tolerance != null && !comparer.Tolerance.IsEmpty) 342 { 343 writer.WriteConnector("+/-"); 344 writer.WriteExpectedValue(comparer.Tolerance.Value); 345 } 346 347 if ( comparer.IgnoreCase ) 348 writer.WriteModifier("ignoring case"); 349 } 350 351 private void DisplayDifferences(MessageWriter writer, object expected, object actual, int depth) 352 { 353 if (expected is string && actual is string) 354 DisplayStringDifferences(writer, (string)expected, (string)actual); 355 else if (expected is ICollection && actual is ICollection) 356 DisplayCollectionDifferences(writer, (ICollection)expected, (ICollection)actual, depth); 357 else if (expected is Stream && actual is Stream) 358 DisplayStreamDifferences(writer, (Stream)expected, (Stream)actual, depth); 359 else if (comparer.Tolerance != null) 360 writer.DisplayDifferences(expected, actual, comparer.Tolerance); 361 else 362 writer.DisplayDifferences(expected, actual); 363 } 364 #endregion 365 366 #region DisplayStringDifferences 367 private void DisplayStringDifferences(MessageWriter writer, string expected, string actual) 368 { 369 int mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, comparer.IgnoreCase); 370 371 if (expected.Length == actual.Length) 372 writer.WriteMessageLine(StringsDiffer_1, expected.Length, mismatch); 373 else 374 writer.WriteMessageLine(StringsDiffer_2, expected.Length, actual.Length, mismatch); 375 376 writer.DisplayStringDifferences(expected, actual, mismatch, comparer.IgnoreCase, clipStrings); 377 } 378 #endregion 379 380 #region DisplayStreamDifferences 381 private void DisplayStreamDifferences(MessageWriter writer, Stream expected, Stream actual, int depth) 382 { 383 if ( expected.Length == actual.Length ) 384 { 385 long offset = (long)comparer.FailurePoints[depth]; 386 writer.WriteMessageLine(StreamsDiffer_1, expected.Length, offset); 387 } 388 else 389 writer.WriteMessageLine(StreamsDiffer_2, expected.Length, actual.Length); 390 } 391 #endregion 392 393 #region DisplayCollectionDifferences 394 /// <summary> 395 /// Display the failure information for two collections that did not match. 396 /// </summary> 397 /// <param name="writer">The MessageWriter on which to display</param> 398 /// <param name="expected">The expected collection.</param> 399 /// <param name="actual">The actual collection</param> 400 /// <param name="depth">The depth of this failure in a set of nested collections</param> 401 private void DisplayCollectionDifferences(MessageWriter writer, ICollection expected, ICollection actual, int depth) 402 { 403 int failurePoint = comparer.FailurePoints.Count > depth ? (int)comparer.FailurePoints[depth] : -1; 404 405 DisplayCollectionTypesAndSizes(writer, expected, actual, depth); 406 407 if (failurePoint >= 0) 408 { 409 DisplayFailurePoint(writer, expected, actual, failurePoint, depth); 410 if (failurePoint < expected.Count && failurePoint < actual.Count) 411 DisplayDifferences( 412 writer, 413 GetValueFromCollection(expected, failurePoint), 414 GetValueFromCollection(actual, failurePoint), 415 ++depth); 416 else if (expected.Count < actual.Count) 417 { 418 writer.Write( " Extra: " ); 419 writer.WriteCollectionElements( actual, failurePoint, 3 ); 420 } 421 else 422 { 423 writer.Write( " Missing: " ); 424 writer.WriteCollectionElements( expected, failurePoint, 3 ); 425 } 426 } 427 } 428 429 /// <summary> 430 /// Displays a single line showing the types and sizes of the expected 431 /// and actual collections or arrays. If both are identical, the value is 432 /// only shown once. 433 /// </summary> 434 /// <param name="writer">The MessageWriter on which to display</param> 435 /// <param name="expected">The expected collection or array</param> 436 /// <param name="actual">The actual collection or array</param> 437 /// <param name="indent">The indentation level for the message line</param> 438 private void DisplayCollectionTypesAndSizes(MessageWriter writer, ICollection expected, ICollection actual, int indent) 439 { 440 string sExpected = MsgUtils.GetTypeRepresentation(expected); 441 if (!(expected is Array)) 442 sExpected += string.Format(" with {0} elements", expected.Count); 443 444 string sActual = MsgUtils.GetTypeRepresentation(actual); 445 if (!(actual is Array)) 446 sActual += string.Format(" with {0} elements", actual.Count); 447 448 if (sExpected == sActual) 449 writer.WriteMessageLine(indent, CollectionType_1, sExpected); 450 else 451 writer.WriteMessageLine(indent, CollectionType_2, sExpected, sActual); 452 } 453 454 /// <summary> 455 /// Displays a single line showing the point in the expected and actual 456 /// arrays at which the comparison failed. If the arrays have different 457 /// structures or dimensions, both values are shown. 458 /// </summary> 459 /// <param name="writer">The MessageWriter on which to display</param> 460 /// <param name="expected">The expected array</param> 461 /// <param name="actual">The actual array</param> 462 /// <param name="failurePoint">Index of the failure point in the underlying collections</param> 463 /// <param name="indent">The indentation level for the message line</param> 464 private void DisplayFailurePoint(MessageWriter writer, ICollection expected, ICollection actual, int failurePoint, int indent) 465 { 466 Array expectedArray = expected as Array; 467 Array actualArray = actual as Array; 468 469 int expectedRank = expectedArray != null ? expectedArray.Rank : 1; 470 int actualRank = actualArray != null ? actualArray.Rank : 1; 471 472 bool useOneIndex = expectedRank == actualRank; 473 474 if (expectedArray != null && actualArray != null) 475 for (int r = 1; r < expectedRank && useOneIndex; r++) 476 if (expectedArray.GetLength(r) != actualArray.GetLength(r)) 477 useOneIndex = false; 478 479 int[] expectedIndices = MsgUtils.GetArrayIndicesFromCollectionIndex(expected, failurePoint); 480 if (useOneIndex) 481 { 482 writer.WriteMessageLine(indent, ValuesDiffer_1, MsgUtils.GetArrayIndicesAsString(expectedIndices)); 483 } 484 else 485 { 486 int[] actualIndices = MsgUtils.GetArrayIndicesFromCollectionIndex(actual, failurePoint); 487 writer.WriteMessageLine(indent, ValuesDiffer_2, 488 MsgUtils.GetArrayIndicesAsString(expectedIndices), MsgUtils.GetArrayIndicesAsString(actualIndices)); 489 } 490 } 491 492 private static object GetValueFromCollection(ICollection collection, int index) 493 { 494 Array array = collection as Array; 495 496 if (array != null && array.Rank > 1) 497 return array.GetValue(MsgUtils.GetArrayIndicesFromCollectionIndex(array, index)); 498 499 if (collection is IList) 500 return ((IList)collection)[index]; 501 502 foreach (object obj in collection) 503 if (--index < 0) 504 return obj; 505 506 return null; 507 } 508 #endregion 509 } 510}