#region Copyright (c) 2002-2003, James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole, Philip A. Craig, Douglas de la Torre /************************************************************************************ ' ' Copyright 2002-2003 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov, Charlie Poole ' Copyright 2000-2002 Philip A. Craig ' Copyright 2001 Douglas de la Torre ' ' This software is provided 'as-is', without any express or implied warranty. In no ' event will the authors be held liable for any damages arising from the use of this ' software. ' ' Permission is granted to anyone to use this software for any purpose, including ' commercial applications, and to alter it and redistribute it freely, subject to the ' following restrictions: ' ' 1. The origin of this software must not be misrepresented; you must not claim that ' you wrote the original software. If you use this software in a product, an ' acknowledgment (see the following) in the product documentation is required. ' ' Portions Copyright 2002 James W. Newkirk, Michael C. Two, Alexei A. Vorontsov ' Copyright 2000-2002 Philip A. Craig, or Copyright 2001 Douglas de la Torre ' ' 2. Altered source versions must be plainly marked as such, and must not be ' misrepresented as being the original software. ' ' 3. This notice may not be removed or altered from any source distribution. ' '***********************************************************************************/ #endregion using System; using System.Text; namespace NUnit.Framework { /// /// Summary description for AssertionFailureMessage. /// public class AssertionFailureMessage { /// /// Protected constructor, used since this class is only used via /// static methods /// protected AssertionFailureMessage() {} /// /// Number of characters before a highlighted position before /// clipping will occur. Clipped text is replaced with an /// elipses "..." /// static protected int PreClipLength { get { return 35; } } /// /// Number of characters after a highlighted position before /// clipping will occur. Clipped text is replaced with an /// elipses "..." /// static protected int PostClipLength { get { return 35; } } /// /// Called to test if the position will cause clipping /// to occur in the early part of a string. /// /// /// static private bool IsPreClipped( int iPosition ) { if( iPosition > PreClipLength ) { return true; } return false; } /// /// Called to test if the position will cause clipping /// to occur in the later part of a string past the /// specified position. /// /// /// /// static private bool IsPostClipped( string sString, int iPosition ) { if( sString.Length - iPosition > PostClipLength ) { return true; } return false; } /// /// Property called to insert newline characters into a string /// static private string NewLine { get { return "\r\n\t"; } } /// /// 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( null == sString || 0 == sString.Length ) { return ""; } return BuildBefore( sString, iPosition ) + BuildAfter( sString, iPosition ); } /// /// Clips the string before the specified position, and appends /// ellipses (...) to show that clipping has occurred /// /// /// /// static protected string PreClip( string sString, int iPosition ) { return "..." + sString.Substring( iPosition - PreClipLength, PreClipLength ); } /// /// Clips the string after the specified position, and appends /// ellipses (...) to show that clipping has occurred /// /// /// /// static protected string PostClip( string sString, int iPosition ) { return sString.Substring( iPosition, PostClipLength ) + "..."; } /// /// Builds the first half of a string, limiting the number of /// characters before the position, and removing newline /// characters. If the leading string is truncated, the /// ellipses (...) characters are appened. /// /// /// /// static private string BuildBefore( string sString, int iPosition ) { if( IsPreClipped(iPosition) ) { return PreClip( sString, iPosition ); } return sString.Substring( 0, iPosition ); } /// /// Builds the last half of a string, limiting the number of /// characters after the position, and removing newline /// characters. If the string is truncated, the /// ellipses (...) characters are appened. /// /// /// /// static private string BuildAfter( string sString, int iPosition ) { if( IsPostClipped(sString, iPosition) ) { return PostClip( sString, iPosition ); } return sString.Substring( iPosition ); } /// /// Text that is rendered for the expected value /// /// static protected string ExpectedText() { return "expected:<"; } /// /// Text rendered for the actual value. This text should /// be the same length as the Expected text, so leading /// spaces should pad this string to ensure they match. /// /// static protected string ButWasText() { return " but was:<"; } /// /// Raw line that communicates the expected value, and the actual value /// /// /// /// static protected void AppendExpectedAndActual( StringBuilder sbOutput, Object expected, Object actual ) { sbOutput.Append( NewLine ); sbOutput.Append( ExpectedText() ); sbOutput.Append( DisplayString( expected ) ); sbOutput.Append( ">" ); sbOutput.Append( NewLine ); sbOutput.Append( ButWasText() ); sbOutput.Append( DisplayString( actual ) ); sbOutput.Append( ">" ); } /// /// Display an object as a string /// /// /// static protected string DisplayString( object obj ) { if ( obj == null ) return "(null)"; else if ( obj is string ) return Quoted( (string)obj ); else return obj.ToString(); } /// /// Quote a string /// /// /// static protected string Quoted( string text ) { return string.Format( "\"{0}\"", text ); } /// /// Draws a marker under the expected/actual strings that highlights /// where in the string a mismatch occurred. /// /// /// static protected void AppendPositionMarker( StringBuilder sbOutput, int iPosition ) { sbOutput.Append( new String( '-', ButWasText().Length + 1 ) ); if( iPosition > 0 ) { sbOutput.Append( new string( '-', iPosition ) ); } sbOutput.Append( "^" ); } /// /// Tests two objects to determine if they are strings. /// /// /// /// static protected bool InputsAreStrings( Object expected, Object actual ) { if( null != expected && null != actual && expected is string && actual is string ) { return true; } return false; } /// /// Tests if two strings are different lengths. /// /// /// /// True if string lengths are different static protected bool LengthsDifferent( string sExpected, string sActual ) { if( sExpected.Length != sActual.Length ) { return true; } return false; } /// /// Tests if two arrays are different lengths. /// /// /// /// True if array lengths are different static protected bool LengthsDifferent( object[] expected, object[] actual ) { if( expected.Length != actual.Length ) { return true; } return false; } /// /// Used to construct a message when the lengths of two strings are /// different. Also includes the strings themselves, to allow them /// to be compared visually. /// /// /// /// static protected void BuildLengthsDifferentMessage( StringBuilder sbOutput, string sExpected, string sActual ) { BuildContentDifferentMessage( sbOutput, sExpected, sActual ); } /// /// Reports the length of two strings that are different lengths /// /// /// /// static protected void BuildStringLengthDifferentReport( StringBuilder sbOutput, string sExpected, string sActual ) { sbOutput.Append( "String lengths differ. Expected length=" ); sbOutput.Append( sExpected.Length ); sbOutput.Append( ", but was length=" ); sbOutput.Append( sActual.Length ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); } /// /// Reports the length of two strings that are the same length /// /// /// /// static protected void BuildStringLengthSameReport( StringBuilder sbOutput, string sExpected, string sActual ) { sbOutput.Append( "String lengths are both " ); sbOutput.Append( sExpected.Length ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); } /// /// Reports whether the string lengths are the same or different, and /// what the string lengths are. /// /// /// /// static protected void BuildStringLengthReport( StringBuilder sbOutput, string sExpected, string sActual ) { if( sExpected.Length != sActual.Length ) { BuildStringLengthDifferentReport( sbOutput, sExpected, sActual ); } else { BuildStringLengthSameReport( sbOutput, sExpected, sActual ); } } /// /// Reports the length of two arrays that are different lengths /// /// /// /// static protected void BuildArrayLengthDifferentReport( StringBuilder sbOutput, Array expected, Array actual ) { sbOutput.Append( "Array lengths differ. Expected length=" ); sbOutput.Append( expected.Length ); sbOutput.Append( ", but was length=" ); sbOutput.Append( actual.Length ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); } /// /// Reports the length of two arrays that are the same length /// /// /// /// static protected void BuildArrayLengthSameReport( StringBuilder sbOutput, Array expected, Array actual ) { sbOutput.Append( "Array lengths are both " ); sbOutput.Append( expected.Length ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); } /// /// Reports whether the array lengths are the same or different, and /// what the array lengths are. /// /// /// /// static protected void BuildArrayLengthReport( StringBuilder sbOutput, Array expected, Array actual ) { if( expected.Length != actual.Length ) { BuildArrayLengthDifferentReport( sbOutput, expected, actual ); } else { BuildArrayLengthSameReport( sbOutput, expected, actual ); } } /// /// /// /// /// /// /// static private void BuildContentDifferentAtPosition( StringBuilder sbOutput, string sExpected, string sActual, int iPosition ) { BuildStringLengthReport( sbOutput, sExpected, sActual ); sbOutput.Append( "Strings differ at index " ); sbOutput.Append( iPosition ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); // // Clips the strings, then turns any hidden whitespace into visible // characters // string sClippedExpected = ConvertWhitespace(ClipAroundPosition( sExpected, iPosition )); string sClippedActual = ConvertWhitespace(ClipAroundPosition( sActual, iPosition )); AppendExpectedAndActual( sbOutput, sClippedExpected, sClippedActual ); sbOutput.Append( NewLine ); // Add a line showing where they differ. If the string lengths are // different, they start differing just past the length of the // shorter string AppendPositionMarker( sbOutput, FindMismatchPosition( sClippedExpected, sClippedActual, 0 ) ); sbOutput.Append( NewLine ); } /// /// 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( "\r", "\\r" ); sInput = sInput.Replace( "\n", "\\n" ); sInput = sInput.Replace( "\t", "\\t" ); } return sInput; } /// /// 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 /// 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. /// /// /// /// static protected void BuildContentDifferentMessage( StringBuilder sbOutput, string sExpected, string sActual ) { // // If they mismatch at a specified position, report the // difference. // int iMismatch = FindMismatchPosition( sExpected, sActual, 0 ); if( -1 != iMismatch ) { BuildContentDifferentAtPosition( sbOutput, sExpected, sActual, iMismatch ); return; } // // If the lengths differ, but they match up to the length, // show the difference just past the length of the shorter // string // if( sExpected.Length != sActual.Length ) { BuildContentDifferentAtPosition( sbOutput, sExpected, sActual, Math.Min(sExpected.Length, sActual.Length) ); } } /// /// Called to append a message when the input strings are different. /// A different message is rendered when the lengths are mismatched, /// and when the lengths match but content is mismatched. /// /// /// /// static private void BuildStringsDifferentMessage( StringBuilder sbOutput, string expected, string actual ) { sbOutput.Append( NewLine ); if( LengthsDifferent( expected, actual ) ) { BuildLengthsDifferentMessage( sbOutput, expected, actual ); } else { BuildContentDifferentMessage( sbOutput, expected, actual ); } } /// /// Called to append a message when the input arrays are different. /// A different message is rendered when the lengths are mismatched, /// and when the lengths match but content is mismatched. /// /// /// /// static private void BuildArraysDifferentMessage( StringBuilder sbOutput, int index, Array expected, Array actual ) { sbOutput.Append( NewLine ); BuildArrayLengthReport( sbOutput, expected, actual ); sbOutput.Append( "Arrays differ at index " ); sbOutput.Append( index ); sbOutput.Append( "." ); sbOutput.Append( NewLine ); if ( index < expected.Length && index < actual.Length ) { if( InputsAreStrings( expected.GetValue(index), actual.GetValue(index) ) ) { BuildStringsDifferentMessage( sbOutput, (string)expected.GetValue(index), (string)actual.GetValue(index) ); } else { AppendExpectedAndActual( sbOutput, expected.GetValue(index), actual.GetValue(index) ); } } else if( expected.Length < actual.Length ) { sbOutput.Append( NewLine ); sbOutput.Append( " extra:<" ); DisplayElements( sbOutput, actual, index, 3 ); sbOutput.Append( ">" ); } else { sbOutput.Append( NewLine ); sbOutput.Append( " missing:<" ); DisplayElements( sbOutput, expected, index, 3 ); sbOutput.Append( ">" ); } return; } static private void DisplayElements( StringBuilder sbOutput, Array array, int index, int max ) { for( int i = 0; i < max; i++ ) { sbOutput.Append( DisplayString( array.GetValue(index++) ) ); if ( index >= array.Length ) return; sbOutput.Append( "," ); } sbOutput.Append( "..." ); } /// /// Used to create a StringBuilder that is used for constructing /// the output message when text is different. Handles initialization /// when a message is provided. If message is null, an empty /// StringBuilder is returned. /// /// /// static protected StringBuilder CreateStringBuilder( string message, params object[] args ) { StringBuilder sbOutput; if (message != null) { if ( args != null && args.Length > 0 ) sbOutput = new StringBuilder( string.Format( message, args ) ); else sbOutput = new StringBuilder( message ); } else { sbOutput = new StringBuilder(); } return sbOutput; } /// /// Called to create a message 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. /// /// /// /// /// /// static public string FormatMessageForFailNotEquals(Object expected, Object actual, string message, params object[] args) { StringBuilder sbOutput = CreateStringBuilder( message, args ); if( null != message ) { if( message.Length > 0 ) { sbOutput.Append( " " ); } } if( InputsAreStrings( expected, actual ) ) { BuildStringsDifferentMessage( sbOutput, (string)expected, (string)actual ); } else { AppendExpectedAndActual( sbOutput, expected, actual ); } return sbOutput.ToString(); } /// /// Called to create a message when two arrays are not equal. /// /// /// /// /// static public string FormatMessageForFailArraysNotEqual(int index, Array expected, Array actual, string message, params object[] args) { StringBuilder sbOutput = CreateStringBuilder( message, args ); if( null != message ) { if( message.Length > 0 ) { sbOutput.Append( " " ); } } BuildArraysDifferentMessage( sbOutput, index, expected, actual ); return sbOutput.ToString(); } } }