1 // ***********************************************************************
2 // Copyright (c) 2012 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.Globalization;
33 using NUnit.Framework.Internal;
35 namespace NUnit.Framework.Constraints
38 /// Custom value formatter function
40 /// <param name="val">The value</param>
41 /// <returns></returns>
42 public delegate string ValueFormatter(object val);
45 /// Custom value formatter factory function
47 /// <param name="next">The next formatter function</param>
48 /// <returns>ValueFormatter</returns>
49 /// <remarks>If the given formatter is unable to handle a certain format, it must call the next formatter in the chain</remarks>
50 public delegate ValueFormatter ValueFormatterFactory(ValueFormatter next);
53 /// Static methods used in creating messages
55 internal static class MsgUtils
58 /// Static string used when strings are clipped
60 private const string ELLIPSIS = "...";
63 /// Formatting strings used for expected and actual _values
65 private static readonly string Fmt_Null = "null";
66 private static readonly string Fmt_EmptyString = "<string.Empty>";
67 private static readonly string Fmt_EmptyCollection = "<empty>";
69 private static readonly string Fmt_String = "\"{0}\"";
70 private static readonly string Fmt_Char = "'{0}'";
71 private static readonly string Fmt_DateTime = "yyyy-MM-dd HH:mm:ss.fff";
73 private static readonly string Fmt_DateTimeOffset = "yyyy-MM-dd HH:mm:ss.fffzzz";
75 private static readonly string Fmt_ValueType = "{0}";
76 private static readonly string Fmt_Default = "<{0}>";
79 /// Current head of chain of value formatters. Public for testing.
81 public static ValueFormatter DefaultValueFormatter { get; set; }
85 // Initialize formatter to default for values of indeterminate type.
86 DefaultValueFormatter = val => string.Format(Fmt_Default, val);
88 AddFormatter(next => val => val is ValueType ? string.Format(Fmt_ValueType, val) : next(val));
90 AddFormatter(next => val => val is DateTime ? FormatDateTime((DateTime)val) : next(val));
93 AddFormatter(next => val => val is DateTimeOffset ? FormatDateTimeOffset ((DateTimeOffset)val) : next (val));
96 AddFormatter(next => val => val is decimal ? FormatDecimal((decimal)val) : next(val));
98 AddFormatter(next => val => val is float ? FormatFloat((float)val) : next(val));
100 AddFormatter(next => val => val is double ? FormatDouble((double)val) : next(val));
102 AddFormatter(next => val => val is char ? string.Format(Fmt_Char, val) : next(val));
104 AddFormatter(next => val => val is IEnumerable ? FormatCollection((IEnumerable)val, 0, 10) : next(val));
106 AddFormatter(next => val => val is string ? FormatString((string)val) : next(val));
108 AddFormatter(next => val => val.GetType().IsArray ? FormatArray((Array)val) : next(val));
111 AddFormatter(next => val =>
113 var vi = val as System.Reflection.MethodInfo;
114 return (vi != null && vi.IsGenericMethodDefinition)
115 ? string.Format(Fmt_Default, vi.Name + "<>")
122 /// Add a formatter to the chain of responsibility.
124 /// <param name="formatterFactory"></param>
125 public static void AddFormatter(ValueFormatterFactory formatterFactory)
127 DefaultValueFormatter = formatterFactory(DefaultValueFormatter);
131 /// Formats text to represent a generalized value.
133 /// <param name="val">The value</param>
134 /// <returns>The formatted text</returns>
135 public static string FormatValue(object val)
140 var context = TestExecutionContext.CurrentContext;
143 return context.CurrentValueFormatter(val);
145 return DefaultValueFormatter(val);
149 /// Formats text for a collection value,
150 /// starting at a particular point, to a max length
152 /// <param name="collection">The collection containing elements to write.</param>
153 /// <param name="start">The starting point of the elements to write</param>
154 /// <param name="max">The maximum number of elements to write</param>
155 public static string FormatCollection(IEnumerable collection, long start, int max)
159 System.Text.StringBuilder sb = new System.Text.StringBuilder();
161 foreach (object obj in collection)
163 if (index++ >= start)
167 sb.Append(count == 1 ? "< " : ", ");
168 sb.Append(FormatValue(obj));
173 return Fmt_EmptyCollection;
180 return sb.ToString();
183 private static string FormatArray(Array array)
185 if (array.Length == 0)
186 return Fmt_EmptyCollection;
188 int rank = array.Rank;
189 int[] products = new int[rank];
191 for (int product = 1, r = rank; --r >= 0; )
192 products[r] = product *= array.GetLength(r);
195 System.Text.StringBuilder sb = new System.Text.StringBuilder();
196 foreach (object obj in array)
201 bool startSegment = false;
202 for (int r = 0; r < rank; r++)
204 startSegment = startSegment || count % products[r] == 0;
205 if (startSegment) sb.Append("< ");
208 sb.Append(FormatValue(obj));
212 bool nextSegment = false;
213 for (int r = 0; r < rank; r++)
215 nextSegment = nextSegment || count % products[r] == 0;
216 if (nextSegment) sb.Append(" >");
220 return sb.ToString();
223 private static string FormatString(string s)
225 return s == string.Empty
227 : string.Format(Fmt_String, s);
230 private static string FormatDouble(double d)
233 if (double.IsNaN(d) || double.IsInfinity(d))
237 string s = d.ToString("G17", CultureInfo.InvariantCulture);
239 if (s.IndexOf('.') > 0)
246 private static string FormatFloat(float f)
248 if (float.IsNaN(f) || float.IsInfinity(f))
252 string s = f.ToString("G9", CultureInfo.InvariantCulture);
254 if (s.IndexOf('.') > 0)
261 private static string FormatDecimal(Decimal d)
263 return d.ToString("G29", CultureInfo.InvariantCulture) + "m";
266 private static string FormatDateTime(DateTime dt)
268 return dt.ToString(Fmt_DateTime, CultureInfo.InvariantCulture);
272 private static string FormatDateTimeOffset(DateTimeOffset dto)
274 return dto.ToString(Fmt_DateTimeOffset, CultureInfo.InvariantCulture);
279 /// Returns the representation of a type as used in NUnitLite.
280 /// This is the same as Type.ToString() except for arrays,
281 /// which are displayed with their declared sizes.
283 /// <param name="obj"></param>
284 /// <returns></returns>
285 public static string GetTypeRepresentation(object obj)
287 Array array = obj as Array;
289 return string.Format("<{0}>", obj.GetType());
291 StringBuilder sb = new StringBuilder();
292 Type elementType = array.GetType();
294 while (elementType.IsArray)
296 elementType = elementType.GetElementType();
299 sb.Append(elementType.ToString());
301 for (int r = 0; r < array.Rank; r++)
303 if (r > 0) sb.Append(',');
304 sb.Append(array.GetLength(r));
311 return string.Format("<{0}>", sb.ToString());
314 /// Converts any control characters in a string
315 /// to their escaped representation.
317 /// <param name="s">The string to be converted</param>
318 /// <returns>The converted string</returns>
319 public static string EscapeControlChars(string s)
323 StringBuilder sb = new StringBuilder();
325 foreach (char c in s)
330 // sb.Append("\\\'");
333 // sb.Append("\\\"");
366 sb.Append(string.Format("\\x{0:X4}", (int)c));
382 /// Return the a string representation for a set of indices into an array
384 /// <param name="indices">Array of indices for which a string is needed</param>
385 public static string GetArrayIndicesAsString(int[] indices)
387 StringBuilder sb = new StringBuilder();
389 for (int r = 0; r < indices.Length; r++)
391 if (r > 0) sb.Append(',');
392 sb.Append(indices[r].ToString());
395 return sb.ToString();
399 /// Get an array of indices representing the point in a collection or
400 /// array corresponding to a single int index into the collection.
402 /// <param name="collection">The collection to which the indices apply</param>
403 /// <param name="index">Index in the collection</param>
404 /// <returns>Array of indices</returns>
405 public static int[] GetArrayIndicesFromCollectionIndex(IEnumerable collection, long index)
407 Array array = collection as Array;
409 int rank = array == null ? 1 : array.Rank;
410 int[] result = new int[rank];
412 for (int r = rank; --r > 0; )
414 int l = array.GetLength(r);
415 result[r] = (int)index % l;
419 result[0] = (int)index;
424 /// Clip a string to a given length, starting at a particular offset, returning the clipped
425 /// string with ellipses representing the removed parts
427 /// <param name="s">The string to be clipped</param>
428 /// <param name="maxStringLength">The maximum permitted length of the result string</param>
429 /// <param name="clipStart">The point at which to start clipping</param>
430 /// <returns>The clipped string</returns>
431 public static string ClipString(string s, int maxStringLength, int clipStart)
433 int clipLength = maxStringLength;
434 StringBuilder sb = new StringBuilder();
438 clipLength -= ELLIPSIS.Length;
442 if (s.Length - clipStart > clipLength)
444 clipLength -= ELLIPSIS.Length;
445 sb.Append(s.Substring(clipStart, clipLength));
448 else if (clipStart > 0)
449 sb.Append(s.Substring(clipStart));
453 return sb.ToString();
457 /// Clip the expected and actual strings in a coordinated fashion,
458 /// so that they may be displayed together.
460 /// <param name="expected"></param>
461 /// <param name="actual"></param>
462 /// <param name="maxDisplayLength"></param>
463 /// <param name="mismatch"></param>
464 public static void ClipExpectedAndActual(ref string expected, ref string actual, int maxDisplayLength, int mismatch)
466 // Case 1: Both strings fit on line
467 int maxStringLength = Math.Max(expected.Length, actual.Length);
468 if (maxStringLength <= maxDisplayLength)
471 // Case 2: Assume that the tail of each string fits on line
472 int clipLength = maxDisplayLength - ELLIPSIS.Length;
473 int clipStart = maxStringLength - clipLength;
475 // Case 3: If it doesn't, center the mismatch position
476 if (clipStart > mismatch)
477 clipStart = Math.Max(0, mismatch - clipLength / 2);
479 expected = ClipString(expected, maxDisplayLength, clipStart);
480 actual = ClipString(actual, maxDisplayLength, clipStart);
484 /// Shows the position two strings start to differ. Comparison
485 /// starts at the start index.
487 /// <param name="expected">The expected string</param>
488 /// <param name="actual">The actual string</param>
489 /// <param name="istart">The index in the strings at which comparison should start</param>
490 /// <param name="ignoreCase">Boolean indicating whether case should be ignored</param>
491 /// <returns>-1 if no mismatch found, or the index where mismatch found</returns>
492 static public int FindMismatchPosition(string expected, string actual, int istart, bool ignoreCase)
494 int length = Math.Min(expected.Length, actual.Length);
496 string s1 = ignoreCase ? expected.ToLower() : expected;
497 string s2 = ignoreCase ? actual.ToLower() : actual;
499 for (int i = istart; i < length; i++)
506 // Strings have same content up to the length of the shorter string.
507 // Mismatch occurs because string lengths are different, so show
508 // that they start differing where the shortest string ends
510 if (expected.Length != actual.Length)
514 // Same strings : We shouldn't get here