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/?p=license&r=2.4 5 // **************************************************************** 6 7 using System; 8 using System.IO; 9 using System.Text; 10 using System.Collections; 11 using System.Globalization; 12 using NUnit.Framework.Constraints; 13 14 namespace NUnit.Framework 15 { 16 /// <summary> 17 /// TextMessageWriter writes constraint descriptions and messages 18 /// in displayable form as a text stream. It tailors the display 19 /// of individual message components to form the standard message 20 /// format of NUnit assertion failure messages. 21 /// </summary> 22 public class TextMessageWriter : MessageWriter 23 { 24 #region Message Formats and Constants 25 private static readonly int DEFAULT_LINE_LENGTH = 78; 26 27 // Prefixes used in all failure messages. All must be the same 28 // length, which is held in the PrefixLength field. Should not 29 // contain any tabs or newline characters. 30 /// <summary> 31 /// Prefix used for the expected value line of a message 32 /// </summary> 33 public static readonly string Pfx_Expected = " Expected: "; 34 /// <summary> 35 /// Prefix used for the actual value line of a message 36 /// </summary> 37 public static readonly string Pfx_Actual = " But was: "; 38 /// <summary> 39 /// Length of a message prefix 40 /// </summary> 41 public static readonly int PrefixLength = Pfx_Expected.Length; 42 43 private static readonly string Fmt_Connector = " {0} "; 44 private static readonly string Fmt_Predicate = "{0} "; 45 //private static readonly string Fmt_Label = "{0}"; 46 private static readonly string Fmt_Modifier = ", {0}"; 47 48 private static readonly string Fmt_Null = "null"; 49 private static readonly string Fmt_EmptyString = "<string.Empty>"; 50 private static readonly string Fmt_EmptyCollection = "<empty>"; 51 52 private static readonly string Fmt_String = "\"{0}\""; 53 private static readonly string Fmt_Char = "'{0}'"; 54 private static readonly string Fmt_DateTime = "yyyy-MM-dd HH:mm:ss.fff"; 55 private static readonly string Fmt_ValueType = "{0}"; 56 private static readonly string Fmt_Default = "<{0}>"; 57 #endregion 58 59 private int maxLineLength = DEFAULT_LINE_LENGTH; 60 61 #region Constructors 62 /// <summary> 63 /// Construct a TextMessageWriter 64 /// </summary> TextMessageWriter()65 public TextMessageWriter() { } 66 67 /// <summary> 68 /// Construct a TextMessageWriter, specifying a user message 69 /// and optional formatting arguments. 70 /// </summary> 71 /// <param name="userMessage"></param> 72 /// <param name="args"></param> TextMessageWriter(string userMessage, params object[] args)73 public TextMessageWriter(string userMessage, params object[] args) 74 { 75 if ( userMessage != null && userMessage != string.Empty) 76 this.WriteMessageLine(userMessage, args); 77 } 78 #endregion 79 80 #region Properties 81 /// <summary> 82 /// Gets or sets the maximum line length for this writer 83 /// </summary> 84 public override int MaxLineLength 85 { 86 get { return maxLineLength; } 87 set { maxLineLength = value; } 88 } 89 #endregion 90 91 #region Public Methods - High Level 92 /// <summary> 93 /// Method to write single line message with optional args, usually 94 /// written to precede the general failure message, at a givel 95 /// indentation level. 96 /// </summary> 97 /// <param name="level">The indentation level of the message</param> 98 /// <param name="message">The message to be written</param> 99 /// <param name="args">Any arguments used in formatting the message</param> WriteMessageLine(int level, string message, params object[] args)100 public override void WriteMessageLine(int level, string message, params object[] args) 101 { 102 if (message != null) 103 { 104 while (level-- >= 0) Write(" "); 105 106 if (args != null && args.Length > 0) 107 message = string.Format(message, args); 108 109 WriteLine(message); 110 } 111 } 112 113 /// <summary> 114 /// Display Expected and Actual lines for a constraint. This 115 /// is called by MessageWriter's default implementation of 116 /// WriteMessageTo and provides the generic two-line display. 117 /// </summary> 118 /// <param name="constraint">The constraint that failed</param> DisplayDifferences(Constraint constraint)119 public override void DisplayDifferences(Constraint constraint) 120 { 121 WriteExpectedLine(constraint); 122 WriteActualLine(constraint); 123 } 124 125 /// <summary> 126 /// Display Expected and Actual lines for given values. This 127 /// method may be called by constraints that need more control over 128 /// the display of actual and expected values than is provided 129 /// by the default implementation. 130 /// </summary> 131 /// <param name="expected">The expected value</param> 132 /// <param name="actual">The actual value causing the failure</param> DisplayDifferences(object expected, object actual)133 public override void DisplayDifferences(object expected, object actual) 134 { 135 WriteExpectedLine(expected); 136 WriteActualLine(actual); 137 } 138 139 /// <summary> 140 /// Display Expected and Actual lines for given values, including 141 /// a tolerance value on the expected line. 142 /// </summary> 143 /// <param name="expected">The expected value</param> 144 /// <param name="actual">The actual value causing the failure</param> 145 /// <param name="tolerance">The tolerance within which the test was made</param> DisplayDifferences(object expected, object actual, object tolerance)146 public override void DisplayDifferences(object expected, object actual, object tolerance) 147 { 148 WriteExpectedLine(expected, tolerance); 149 WriteActualLine(actual); 150 } 151 152 /// <summary> 153 /// Display the expected and actual string values on separate lines. 154 /// If the mismatch parameter is >=0, an additional line is displayed 155 /// line containing a caret that points to the mismatch point. 156 /// </summary> 157 /// <param name="expected">The expected string value</param> 158 /// <param name="actual">The actual string value</param> 159 /// <param name="mismatch">The point at which the strings don't match or -1</param> 160 /// <param name="ignoreCase">If true, case is ignored in string comparisons</param> 161 /// <param name="clipping">If true, clip the strings to fit the max line length</param> DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping)162 public override void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping) 163 { 164 // Maximum string we can display without truncating 165 int maxDisplayLength = MaxLineLength 166 - PrefixLength // Allow for prefix 167 - 2; // 2 quotation marks 168 169 if ( clipping ) 170 MsgUtils.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, mismatch); 171 172 expected = MsgUtils.ConvertWhitespace(expected); 173 actual = MsgUtils.ConvertWhitespace(actual); 174 175 // The mismatch position may have changed due to clipping or white space conversion 176 mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, ignoreCase); 177 178 Write( Pfx_Expected ); 179 WriteExpectedValue( expected ); 180 if ( ignoreCase ) 181 WriteModifier( "ignoring case" ); 182 WriteLine(); 183 WriteActualLine( actual ); 184 //DisplayDifferences(expected, actual); 185 if (mismatch >= 0) 186 WriteCaretLine(mismatch); 187 } 188 #endregion 189 190 #region Public Methods - Low Level 191 /// <summary> 192 /// Writes the text for a connector. 193 /// </summary> 194 /// <param name="connector">The connector.</param> WriteConnector(string connector)195 public override void WriteConnector(string connector) 196 { 197 Write(Fmt_Connector, connector); 198 } 199 200 /// <summary> 201 /// Writes the text for a predicate. 202 /// </summary> 203 /// <param name="predicate">The predicate.</param> WritePredicate(string predicate)204 public override void WritePredicate(string predicate) 205 { 206 Write(Fmt_Predicate, predicate); 207 } 208 209 //public override void WriteLabel(string label) 210 //{ 211 // Write(Fmt_Label, label); 212 //} 213 214 /// <summary> 215 /// Write the text for a modifier. 216 /// </summary> 217 /// <param name="modifier">The modifier.</param> WriteModifier(string modifier)218 public override void WriteModifier(string modifier) 219 { 220 Write(Fmt_Modifier, modifier); 221 } 222 223 224 /// <summary> 225 /// Writes the text for an expected value. 226 /// </summary> 227 /// <param name="expected">The expected value.</param> WriteExpectedValue(object expected)228 public override void WriteExpectedValue(object expected) 229 { 230 WriteValue(expected); 231 } 232 233 /// <summary> 234 /// Writes the text for an actual value. 235 /// </summary> 236 /// <param name="actual">The actual value.</param> WriteActualValue(object actual)237 public override void WriteActualValue(object actual) 238 { 239 WriteValue(actual); 240 } 241 242 /// <summary> 243 /// Writes the text for a generalized value. 244 /// </summary> 245 /// <param name="val">The value.</param> WriteValue(object val)246 public override void WriteValue(object val) 247 { 248 if (val == null) 249 Write(Fmt_Null); 250 else if (val.GetType().IsArray) 251 WriteArray((Array)val); 252 else if (val is ICollection) 253 WriteCollectionElements((ICollection)val, 0, 10); 254 else if (val is string) 255 WriteString((string)val); 256 else if (val is char) 257 WriteChar((char)val); 258 else if (val is double) 259 WriteDouble((double)val); 260 else if (val is float) 261 WriteFloat((float)val); 262 else if (val is decimal) 263 WriteDecimal((decimal)val); 264 else if (val is DateTime) 265 WriteDateTime((DateTime)val); 266 else if (val.GetType().IsValueType) 267 Write(Fmt_ValueType, val); 268 else 269 Write(Fmt_Default, val); 270 } 271 272 /// <summary> 273 /// Writes the text for a collection value, 274 /// starting at a particular point, to a max length 275 /// </summary> 276 /// <param name="collection">The collection containing elements to write.</param> 277 /// <param name="start">The starting point of the elements to write</param> 278 /// <param name="max">The maximum number of elements to write</param> WriteCollectionElements(ICollection collection, int start, int max)279 public override void WriteCollectionElements(ICollection collection, int start, int max) 280 { 281 if ( collection.Count == 0 ) 282 { 283 Write(Fmt_EmptyCollection); 284 return; 285 } 286 287 int count = 0; 288 int index = 0; 289 Write("< "); 290 291 foreach (object obj in collection) 292 { 293 if ( index++ >= start ) 294 { 295 if (count > 0) 296 Write(", "); 297 WriteValue(obj); 298 if ( ++count >= max ) 299 break; 300 } 301 } 302 303 if ( index < collection.Count ) 304 Write("..."); 305 306 Write(" >"); 307 } 308 WriteArray(Array array)309 private void WriteArray(Array array) 310 { 311 if ( array.Length == 0 ) 312 { 313 Write( Fmt_EmptyCollection ); 314 return; 315 } 316 317 int rank = array.Rank; 318 int[] products = new int[rank]; 319 320 for (int product = 1, r = rank; --r >= 0; ) 321 products[r] = product *= array.GetLength(r); 322 323 int count = 0; 324 foreach (object obj in array) 325 { 326 if (count > 0) 327 Write(", "); 328 329 bool startSegment = false; 330 for (int r = 0; r < rank; r++) 331 { 332 startSegment = startSegment || count % products[r] == 0; 333 if (startSegment) Write("< "); 334 } 335 336 WriteValue(obj); 337 338 ++count; 339 340 bool nextSegment = false; 341 for (int r = 0; r < rank; r++) 342 { 343 nextSegment = nextSegment || count % products[r] == 0; 344 if (nextSegment) Write(" >"); 345 } 346 } 347 } 348 WriteString(string s)349 private void WriteString(string s) 350 { 351 if (s == string.Empty) 352 Write(Fmt_EmptyString); 353 else 354 Write(Fmt_String, s); 355 } 356 WriteChar(char c)357 private void WriteChar(char c) 358 { 359 Write(Fmt_Char, c); 360 } 361 WriteDouble(double d)362 private void WriteDouble(double d) 363 { 364 365 if (double.IsNaN(d) || double.IsInfinity(d)) 366 Write(d); 367 else 368 { 369 string s = d.ToString("G17", CultureInfo.InvariantCulture); 370 371 if (s.IndexOf('.') > 0) 372 Write(s + "d"); 373 else 374 Write(s + ".0d"); 375 } 376 } 377 WriteFloat(float f)378 private void WriteFloat(float f) 379 { 380 if (float.IsNaN(f) || float.IsInfinity(f)) 381 Write(f); 382 else 383 { 384 string s = f.ToString("G9", CultureInfo.InvariantCulture); 385 386 if (s.IndexOf('.') > 0) 387 Write(s + "f"); 388 else 389 Write(s + ".0f"); 390 } 391 } 392 WriteDecimal(Decimal d)393 private void WriteDecimal(Decimal d) 394 { 395 Write(d.ToString("G29", CultureInfo.InvariantCulture) + "m"); 396 } 397 WriteDateTime(DateTime dt)398 private void WriteDateTime(DateTime dt) 399 { 400 Write(dt.ToString(Fmt_DateTime, CultureInfo.InvariantCulture)); 401 } 402 #endregion 403 404 #region Helper Methods 405 /// <summary> 406 /// Write the generic 'Expected' line for a constraint 407 /// </summary> 408 /// <param name="constraint">The constraint that failed</param> WriteExpectedLine(Constraint constraint)409 private void WriteExpectedLine(Constraint constraint) 410 { 411 Write(Pfx_Expected); 412 constraint.WriteDescriptionTo(this); 413 WriteLine(); 414 } 415 416 /// <summary> 417 /// Write the generic 'Expected' line for a given value 418 /// </summary> 419 /// <param name="expected">The expected value</param> WriteExpectedLine(object expected)420 private void WriteExpectedLine(object expected) 421 { 422 WriteExpectedLine(expected, null); 423 } 424 425 /// <summary> 426 /// Write the generic 'Expected' line for a given value 427 /// and tolerance. 428 /// </summary> 429 /// <param name="expected">The expected value</param> 430 /// <param name="tolerance">The tolerance within which the test was made</param> WriteExpectedLine(object expected, object tolerance)431 private void WriteExpectedLine(object expected, object tolerance) 432 { 433 Write(Pfx_Expected); 434 WriteExpectedValue(expected); 435 436 if (tolerance != null) 437 { 438 WriteConnector("+/-"); 439 WriteExpectedValue(tolerance); 440 } 441 442 WriteLine(); 443 } 444 445 /// <summary> 446 /// Write the generic 'Actual' line for a constraint 447 /// </summary> 448 /// <param name="constraint">The constraint for which the actual value is to be written</param> WriteActualLine(Constraint constraint)449 private void WriteActualLine(Constraint constraint) 450 { 451 Write(Pfx_Actual); 452 constraint.WriteActualValueTo(this); 453 WriteLine(); 454 } 455 456 /// <summary> 457 /// Write the generic 'Actual' line for a given value 458 /// </summary> 459 /// <param name="actual">The actual value causing a failure</param> WriteActualLine(object actual)460 private void WriteActualLine(object actual) 461 { 462 Write(Pfx_Actual); 463 WriteActualValue(actual); 464 WriteLine(); 465 } 466 WriteCaretLine(int mismatch)467 private void WriteCaretLine(int mismatch) 468 { 469 // We subtract 2 for the initial 2 blanks and add back 1 for the initial quote 470 WriteLine(" {0}^", new string('-', PrefixLength + mismatch - 2 + 1)); 471 } 472 #endregion 473 } 474 } 475