// **************************************************************** // 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.Text; using System.Collections; using System.Globalization; using NUnit.Framework.Constraints; namespace NUnit.Framework { /// /// TextMessageWriter writes constraint descriptions and messages /// in displayable form as a text stream. It tailors the display /// of individual message components to form the standard message /// format of NUnit assertion failure messages. /// public class TextMessageWriter : MessageWriter { #region Message Formats and Constants private static readonly int DEFAULT_LINE_LENGTH = 78; // Prefixes used in all failure messages. All must be the same // length, which is held in the PrefixLength field. Should not // contain any tabs or newline characters. /// /// Prefix used for the expected value line of a message /// public static readonly string Pfx_Expected = " Expected: "; /// /// Prefix used for the actual value line of a message /// public static readonly string Pfx_Actual = " But was: "; /// /// Length of a message prefix /// public static readonly int PrefixLength = Pfx_Expected.Length; private static readonly string Fmt_Connector = " {0} "; private static readonly string Fmt_Predicate = "{0} "; //private static readonly string Fmt_Label = "{0}"; private static readonly string Fmt_Modifier = ", {0}"; private static readonly string Fmt_Null = "null"; private static readonly string Fmt_EmptyString = ""; private static readonly string Fmt_EmptyCollection = ""; private static readonly string Fmt_String = "\"{0}\""; private static readonly string Fmt_Char = "'{0}'"; private static readonly string Fmt_DateTime = "yyyy-MM-dd HH:mm:ss.fff"; private static readonly string Fmt_ValueType = "{0}"; private static readonly string Fmt_Default = "<{0}>"; #endregion private int maxLineLength = DEFAULT_LINE_LENGTH; #region Constructors /// /// Construct a TextMessageWriter /// public TextMessageWriter() { } /// /// Construct a TextMessageWriter, specifying a user message /// and optional formatting arguments. /// /// /// public TextMessageWriter(string userMessage, params object[] args) { if ( userMessage != null && userMessage != string.Empty) this.WriteMessageLine(userMessage, args); } #endregion #region Properties /// /// Gets or sets the maximum line length for this writer /// public override int MaxLineLength { get { return maxLineLength; } set { maxLineLength = value; } } #endregion #region Public Methods - High Level /// /// Method to write single line message with optional args, usually /// written to precede the general failure message, at a givel /// indentation level. /// /// The indentation level of the message /// The message to be written /// Any arguments used in formatting the message public override void WriteMessageLine(int level, string message, params object[] args) { if (message != null) { while (level-- >= 0) Write(" "); if (args != null && args.Length > 0) message = string.Format(message, args); WriteLine(message); } } /// /// Display Expected and Actual lines for a constraint. This /// is called by MessageWriter's default implementation of /// WriteMessageTo and provides the generic two-line display. /// /// The constraint that failed public override void DisplayDifferences(Constraint constraint) { WriteExpectedLine(constraint); WriteActualLine(constraint); } /// /// Display Expected and Actual lines for given values. This /// method may be called by constraints that need more control over /// the display of actual and expected values than is provided /// by the default implementation. /// /// The expected value /// The actual value causing the failure public override void DisplayDifferences(object expected, object actual) { WriteExpectedLine(expected); WriteActualLine(actual); } /// /// Display Expected and Actual lines for given values, including /// a tolerance value on the expected line. /// /// The expected value /// The actual value causing the failure /// The tolerance within which the test was made public override void DisplayDifferences(object expected, object actual, object tolerance) { WriteExpectedLine(expected, tolerance); WriteActualLine(actual); } /// /// Display the expected and actual string values on separate lines. /// If the mismatch parameter is >=0, an additional line is displayed /// line containing a caret that points to the mismatch point. /// /// The expected string value /// The actual string value /// The point at which the strings don't match or -1 /// If true, case is ignored in string comparisons /// If true, clip the strings to fit the max line length public override void DisplayStringDifferences(string expected, string actual, int mismatch, bool ignoreCase, bool clipping) { // Maximum string we can display without truncating int maxDisplayLength = MaxLineLength - PrefixLength // Allow for prefix - 2; // 2 quotation marks if ( clipping ) MsgUtils.ClipExpectedAndActual(ref expected, ref actual, maxDisplayLength, mismatch); expected = MsgUtils.ConvertWhitespace(expected); actual = MsgUtils.ConvertWhitespace(actual); // The mismatch position may have changed due to clipping or white space conversion mismatch = MsgUtils.FindMismatchPosition(expected, actual, 0, ignoreCase); Write( Pfx_Expected ); WriteExpectedValue( expected ); if ( ignoreCase ) WriteModifier( "ignoring case" ); WriteLine(); WriteActualLine( actual ); //DisplayDifferences(expected, actual); if (mismatch >= 0) WriteCaretLine(mismatch); } #endregion #region Public Methods - Low Level /// /// Writes the text for a connector. /// /// The connector. public override void WriteConnector(string connector) { Write(Fmt_Connector, connector); } /// /// Writes the text for a predicate. /// /// The predicate. public override void WritePredicate(string predicate) { Write(Fmt_Predicate, predicate); } //public override void WriteLabel(string label) //{ // Write(Fmt_Label, label); //} /// /// Write the text for a modifier. /// /// The modifier. public override void WriteModifier(string modifier) { Write(Fmt_Modifier, modifier); } /// /// Writes the text for an expected value. /// /// The expected value. public override void WriteExpectedValue(object expected) { WriteValue(expected); } /// /// Writes the text for an actual value. /// /// The actual value. public override void WriteActualValue(object actual) { WriteValue(actual); } /// /// Writes the text for a generalized value. /// /// The value. public override void WriteValue(object val) { if (val == null) Write(Fmt_Null); else if (val.GetType().IsArray) WriteArray((Array)val); else if (val is ICollection) WriteCollectionElements((ICollection)val, 0, 10); else if (val is string) WriteString((string)val); else if (val is char) WriteChar((char)val); else if (val is double) WriteDouble((double)val); else if (val is float) WriteFloat((float)val); else if (val is decimal) WriteDecimal((decimal)val); else if (val is DateTime) WriteDateTime((DateTime)val); else if (val.GetType().IsValueType) Write(Fmt_ValueType, val); else Write(Fmt_Default, val); } /// /// Writes the text for a collection value, /// starting at a particular point, to a max length /// /// The collection containing elements to write. /// The starting point of the elements to write /// The maximum number of elements to write public override void WriteCollectionElements(ICollection collection, int start, int max) { if ( collection.Count == 0 ) { Write(Fmt_EmptyCollection); return; } int count = 0; int index = 0; Write("< "); foreach (object obj in collection) { if ( index++ >= start ) { if (count > 0) Write(", "); WriteValue(obj); if ( ++count >= max ) break; } } if ( index < collection.Count ) Write("..."); Write(" >"); } private void WriteArray(Array array) { if ( array.Length == 0 ) { Write( Fmt_EmptyCollection ); return; } int rank = array.Rank; int[] products = new int[rank]; for (int product = 1, r = rank; --r >= 0; ) products[r] = product *= array.GetLength(r); int count = 0; foreach (object obj in array) { if (count > 0) Write(", "); bool startSegment = false; for (int r = 0; r < rank; r++) { startSegment = startSegment || count % products[r] == 0; if (startSegment) Write("< "); } WriteValue(obj); ++count; bool nextSegment = false; for (int r = 0; r < rank; r++) { nextSegment = nextSegment || count % products[r] == 0; if (nextSegment) Write(" >"); } } } private void WriteString(string s) { if (s == string.Empty) Write(Fmt_EmptyString); else Write(Fmt_String, s); } private void WriteChar(char c) { Write(Fmt_Char, c); } private void WriteDouble(double d) { if (double.IsNaN(d) || double.IsInfinity(d)) Write(d); else { string s = d.ToString("G17", CultureInfo.InvariantCulture); if (s.IndexOf('.') > 0) Write(s + "d"); else Write(s + ".0d"); } } private void WriteFloat(float f) { if (float.IsNaN(f) || float.IsInfinity(f)) Write(f); else { string s = f.ToString("G9", CultureInfo.InvariantCulture); if (s.IndexOf('.') > 0) Write(s + "f"); else Write(s + ".0f"); } } private void WriteDecimal(Decimal d) { Write(d.ToString("G29", CultureInfo.InvariantCulture) + "m"); } private void WriteDateTime(DateTime dt) { Write(dt.ToString(Fmt_DateTime, CultureInfo.InvariantCulture)); } #endregion #region Helper Methods /// /// Write the generic 'Expected' line for a constraint /// /// The constraint that failed private void WriteExpectedLine(Constraint constraint) { Write(Pfx_Expected); constraint.WriteDescriptionTo(this); WriteLine(); } /// /// Write the generic 'Expected' line for a given value /// /// The expected value private void WriteExpectedLine(object expected) { WriteExpectedLine(expected, null); } /// /// Write the generic 'Expected' line for a given value /// and tolerance. /// /// The expected value /// The tolerance within which the test was made private void WriteExpectedLine(object expected, object tolerance) { Write(Pfx_Expected); WriteExpectedValue(expected); if (tolerance != null) { WriteConnector("+/-"); WriteExpectedValue(tolerance); } WriteLine(); } /// /// Write the generic 'Actual' line for a constraint /// /// The constraint for which the actual value is to be written private void WriteActualLine(Constraint constraint) { Write(Pfx_Actual); constraint.WriteActualValueTo(this); WriteLine(); } /// /// Write the generic 'Actual' line for a given value /// /// The actual value causing a failure private void WriteActualLine(object actual) { Write(Pfx_Actual); WriteActualValue(actual); WriteLine(); } private void WriteCaretLine(int mismatch) { // We subtract 2 for the initial 2 blanks and add back 1 for the initial quote WriteLine(" {0}^", new string('-', PrefixLength + mismatch - 2 + 1)); } #endregion } }