// **************************************************************** // This is free software licensed under the NUnit license. You // may obtain a copy of the license as well as information regarding // copyright ownership at http://nunit.org/?p=license&r=2.4. // **************************************************************** using System; using System.Text; using System.IO; using System.Collections; namespace NUnit.Framework { /// /// AssertionFailureMessage encapsulates a failure message /// issued as a result of an Assert failure. /// [Obsolete( "Use MessageWriter for new work" )] public class AssertionFailureMessage : StringWriter { #region Static Constants /// /// Number of characters before a highlighted position before /// clipping will occur. Clipped text is replaced with an /// elipsis "..." /// static public readonly int PreClipLength = 35; /// /// Number of characters after a highlighted position before /// clipping will occur. Clipped text is replaced with an /// elipsis "..." /// static public readonly int PostClipLength = 35; /// /// Prefix used to start an expected value line. /// Must be same length as actualPrefix. /// static protected readonly string expectedPrefix = "expected:"; /// /// Prefix used to start an actual value line. /// Must be same length as expectedPrefix. /// static protected readonly string actualPrefix = " but was:"; static private readonly string expectedAndActualFmt = "\t{0} {1}"; static private readonly string diffStringLengthsFmt = "\tString lengths differ. Expected length={0}, but was length={1}."; static private readonly string sameStringLengthsFmt = "\tString lengths are both {0}."; static private readonly string diffArrayLengthsFmt = "Array lengths differ. Expected length={0}, but was length={1}."; static private readonly string sameArrayLengthsFmt = "Array lengths are both {0}."; static private readonly string stringsDifferAtIndexFmt = "\tStrings differ at index {0}."; static private readonly string arraysDifferAtIndexFmt = "Arrays differ at index {0}."; #endregion #region Constructors /// /// Construct an AssertionFailureMessage with a message /// and optional arguments. /// /// /// public AssertionFailureMessage( string message, params object[] args ) { if ( message != null && message != string.Empty ) if ( args != null ) WriteLine( message, args ); else WriteLine( message ); } /// /// Construct an empty AssertionFailureMessage /// public AssertionFailureMessage() : this( null, null ) { } #endregion /// /// Add an expected value line to the message containing /// the text provided as an argument. /// /// Text describing what was expected. public void WriteExpectedLine( string text ) { WriteLine( string.Format( expectedAndActualFmt, expectedPrefix, text ) ); } /// /// Add an actual value line to the message containing /// the text provided as an argument. /// /// Text describing the actual value. public void WriteActualLine( string text ) { WriteLine( string.Format( expectedAndActualFmt, actualPrefix, text ) ); } /// /// Add an expected value line to the message containing /// a string representation of the object provided. /// /// An object representing the expected value public void DisplayExpectedValue( object expected ) { WriteExpectedLine( FormatObjectForDisplay( expected ) ); } /// /// Add an expected value line to the message containing a double /// and the tolerance used in making the comparison. /// /// The expected value /// The tolerance specified in the Assert public void DisplayExpectedValue( double expected, double tolerance ) { WriteExpectedLine( FormatObjectForDisplay( expected ) + " +/- " + tolerance.ToString() ); } /// /// Add an actual value line to the message containing /// a string representation of the object provided. /// /// An object representing what was actually found public void DisplayActualValue( object actual ) { WriteActualLine( FormatObjectForDisplay( actual ) ); } /// /// Display two lines that communicate the expected value, and the actual value /// /// The expected value /// The actual value found public void DisplayExpectedAndActual( Object expected, Object actual ) { DisplayExpectedValue( expected ); DisplayActualValue( actual ); } /// /// Display two lines that communicate the expected value, the actual value and /// the tolerance used in comparing two doubles. /// /// The expected value /// The actual value found /// The tolerance specified in the Assert public void DisplayExpectedAndActual( double expected, double actual, double tolerance ) { DisplayExpectedValue( expected, tolerance ); DisplayActualValue( actual ); } /// /// Draws a marker under the expected/actual strings that highlights /// where in the string a mismatch occurred. /// /// The position of the mismatch public void DisplayPositionMarker( int iPosition ) { WriteLine( "\t{0}^", new String( '-', expectedPrefix.Length + iPosition + 3 ) ); } /// /// Reports whether the string lengths are the same or different, and /// what the string lengths are. /// /// The expected string /// The actual string value protected void BuildStringLengthReport( string sExpected, string sActual ) { if( sExpected.Length != sActual.Length ) WriteLine( diffStringLengthsFmt, sExpected.Length, sActual.Length ); else WriteLine( sameStringLengthsFmt, sExpected.Length ); } /// /// Called to create additional message lines when two objects have been /// found to be unequal. If the inputs are strings, a special message is /// rendered that can help track down where the strings are different, /// based on differences in length, or differences in content. /// /// If the inputs are not strings, the ToString method of the objects /// is used to show what is different about them. /// /// The expected value /// The actual value /// True if a case-insensitive comparison is being performed public void DisplayDifferences( object expected, object actual, bool caseInsensitive ) { if( InputsAreStrings( expected, actual ) ) { DisplayStringDifferences( (string)expected, (string)actual, caseInsensitive ); } else { DisplayExpectedAndActual( expected, actual ); } } /// /// Called to create additional message lines when two doubles have been /// found to be unequal, within the specified tolerance. /// public void DisplayDifferencesWithTolerance( double expected, double actual, double tolerance ) { DisplayExpectedAndActual( expected, actual, tolerance ); } /// /// Constructs a message that can be displayed when the content of two /// strings are different, but the string lengths are the same. The /// message will clip the strings to a reasonable length, centered /// around the first position where they are mismatched, and draw /// a line marking the position of the difference to make comparison /// quicker. /// /// The expected string value /// The actual string value /// True if a case-insensitive comparison is being performed protected void DisplayStringDifferences( string sExpected, string sActual, bool caseInsensitive ) { // // If they mismatch at a specified position, report the // difference. // int iPosition = caseInsensitive ? FindMismatchPosition( sExpected.ToLower(), sActual.ToLower(), 0 ) : FindMismatchPosition( sExpected, sActual, 0 ); // // If the lengths differ, but they match up to the length, // show the difference just past the length of the shorter // string // if( iPosition == -1 ) iPosition = Math.Min( sExpected.Length, sActual.Length ); BuildStringLengthReport( sExpected, sActual ); WriteLine( stringsDifferAtIndexFmt, iPosition ); // // Clips the strings, then turns any hidden whitespace into visible // characters // string sClippedExpected = ConvertWhitespace(ClipAroundPosition( sExpected, iPosition )); string sClippedActual = ConvertWhitespace(ClipAroundPosition( sActual, iPosition )); DisplayExpectedAndActual( sClippedExpected, sClippedActual ); // Add a line showing where they differ. If the string lengths are // different, they start differing just past the length of the // shorter string DisplayPositionMarker( caseInsensitive ? FindMismatchPosition( sClippedExpected.ToLower(), sClippedActual.ToLower(), 0 ) : FindMismatchPosition( sClippedExpected, sClippedActual, 0 ) ); } /// /// Display a standard message showing the differences found between /// two arrays that were expected to be equal. /// /// The expected array value /// The actual array value /// The index at which a difference was found public void DisplayArrayDifferences( Array expected, Array actual, int index ) { if( expected.Length != actual.Length ) WriteLine( diffArrayLengthsFmt, expected.Length, actual.Length ); else WriteLine( sameArrayLengthsFmt, expected.Length ); WriteLine( arraysDifferAtIndexFmt, index ); if ( index < expected.Length && index < actual.Length ) { DisplayDifferences( GetValueFromCollection(expected, index ), GetValueFromCollection(actual, index), false ); } else if( expected.Length < actual.Length ) DisplayListElements( " extra:", actual, index, 3 ); else DisplayListElements( " missing:", expected, index, 3 ); } /// /// Display a standard message showing the differences found between /// two collections that were expected to be equal. /// /// The expected collection value /// The actual collection value /// The index at which a difference was found // NOTE: This is a temporary method for use until the code from NUnitLite // is integrated into NUnit. public void DisplayCollectionDifferences( ICollection expected, ICollection actual, int index ) { if( expected.Count != actual.Count ) WriteLine( diffArrayLengthsFmt, expected.Count, actual.Count ); else WriteLine( sameArrayLengthsFmt, expected.Count ); WriteLine( arraysDifferAtIndexFmt, index ); if ( index < expected.Count && index < actual.Count ) { DisplayDifferences( GetValueFromCollection(expected, index ), GetValueFromCollection(actual, index), false ); } // else if( expected.Count < actual.Count ) // DisplayListElements( " extra:", actual, index, 3 ); // else // DisplayListElements( " missing:", expected, index, 3 ); } private static object GetValueFromCollection(ICollection collection, int index) { Array array = collection as Array; if (array != null && array.Rank > 1) return array.GetValue(GetArrayIndicesFromCollectionIndex(array, index)); if (collection is IList) return ((IList)collection)[index]; foreach (object obj in collection) if (--index < 0) return obj; return null; } /// /// Get an array of indices representing the point in a collection or /// array corresponding to a single int index into the collection. /// /// The collection to which the indices apply /// Index in the collection /// Array of indices private static int[] GetArrayIndicesFromCollectionIndex(ICollection collection, int index) { Array array = collection as Array; int rank = array == null ? 1 : array.Rank; int[] result = new int[rank]; for (int r = array.Rank; --r > 0; ) { int l = array.GetLength(r); result[r] = index % l; index /= l; } result[0] = index; return result; } /// /// Displays elements from a list on a line /// /// Text to prefix the line with /// The list of items to display /// The index in the list of the first element to display /// The maximum number of elements to display public void DisplayListElements( string label, IList list, int index, int max ) { Write( "{0}<", label ); if ( list == null ) Write( "null" ); else if ( list.Count == 0 ) Write( "empty" ); else { for( int i = 0; i < max && index < list.Count; i++ ) { Write( FormatObjectForDisplay( list[index++] ) ); if ( index < list.Count ) Write( "," ); } if ( index < list.Count ) Write( "..." ); } WriteLine( ">" ); } #region Static Methods /// /// Formats an object for display in a message line /// /// The object to be displayed /// static public string FormatObjectForDisplay( object obj ) { if ( obj == null ) return "<(null)>"; else if ( obj is string ) return string.Format( "<\"{0}\">", obj ); else if ( obj is double ) return string.Format( "<{0}>", ((double)obj).ToString( "G17" ) ); else if ( obj is float ) return string.Format( "<{0}>", ((float)obj).ToString( "G9" ) ); else return string.Format( "<{0}>", obj ); } /// /// Tests two objects to determine if they are strings. /// /// /// /// static protected bool InputsAreStrings( Object expected, Object actual ) { return expected != null && actual != null && expected is string && actual is string; } /// /// Renders up to M characters before, and up to N characters after /// the specified index position. If leading or trailing text is /// clipped, and elipses "..." is added where the missing text would /// be. /// /// Clips strings to limit previous or post newline characters, /// since these mess up the comparison /// /// /// /// static protected string ClipAroundPosition( string sString, int iPosition ) { if( sString == null || sString.Length == 0 ) return ""; bool preClip = iPosition > PreClipLength; bool postClip = iPosition + PostClipLength < sString.Length; int start = preClip ? iPosition - PreClipLength : 0; int length = postClip ? iPosition + PostClipLength - start : sString.Length - start; if ( start + length > iPosition + PostClipLength ) length = iPosition + PostClipLength - start; StringBuilder sb = new StringBuilder(); if ( preClip ) sb.Append("..."); sb.Append( sString.Substring( start, length ) ); if ( postClip ) sb.Append("..."); return sb.ToString(); } /// /// Shows the position two strings start to differ. Comparison /// starts at the start index. /// /// /// /// /// -1 if no mismatch found, or the index where mismatch found static private int FindMismatchPosition( string sExpected, string sActual, int iStart ) { int iLength = Math.Min( sExpected.Length, sActual.Length ); for( int i=iStart; i /// Turns CR, LF, or TAB into visual indicator to preserve visual marker /// position. This is done by replacing the '\r' into '\\' and 'r' /// characters, and the '\n' into '\\' and 'n' characters, and '\t' into /// '\\' and 't' characters. /// /// Thus the single character becomes two characters for display. /// /// /// static protected string ConvertWhitespace( string sInput ) { if( null != sInput ) { sInput = sInput.Replace( "\\", "\\\\" ); sInput = sInput.Replace( "\r", "\\r" ); sInput = sInput.Replace( "\n", "\\n" ); sInput = sInput.Replace( "\t", "\\t" ); } return sInput; } #endregion } }