Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 190 additions & 15 deletions Source/aweXpect.Core/Core/StringDifference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,22 @@ public enum MatchType
/// The expected string is treated as a regex pattern.
/// </summary>
Regex,

/// <summary>
/// The expected string is treated as a prefix.
/// </summary>
/// <remarks>
/// The actual string has to start with expected string.
/// </remarks>
Prefix,

/// <summary>
/// The expected string is treated as a suffix.
/// </summary>
/// <remarks>
/// The actual string has to end with expected string.
/// </remarks>
Suffix,
}

private const string ActualIndicator = " (actual)";
Expand All @@ -56,7 +72,15 @@ public int IndexOfFirstMismatch(MatchType matchType)
return 0;
}

_indexOfFirstMismatch ??= GetIndexOfFirstMismatch(actual, expected, _comparer);
if (matchType is MatchType.Suffix)
{
_indexOfFirstMismatch ??= GetIndexOfFirstMismatchFromEnd(actual, expected, _comparer);
}
else
{
_indexOfFirstMismatch ??= GetIndexOfFirstMismatch(actual, expected, _comparer);
}

return _indexOfFirstMismatch.Value;
}

Expand Down Expand Up @@ -102,7 +126,7 @@ public string ToString(string prefix)
{
MatchType.Wildcard => ToPatternString(MatchType.Wildcard, prefix, actual, expected),
MatchType.Regex => ToPatternString(MatchType.Regex, prefix, actual, expected),
_ => ToEqualityString(prefix, actual, expected, IndexOfFirstMismatch(MatchType.Equality), settings),
_ => ToEqualityString(prefix, actual, expected, IndexOfFirstMismatch(settings?.MatchType ?? MatchType.Equality), settings),
};
}

Expand All @@ -116,13 +140,23 @@ private static string ToEqualityString(string prefix, string actual, string expe

if (indexOfFirstMismatch < 0)
{
if (actual.Length < expected.Length)
{
return $"""
is shorter than the expected length of {expected.Length} and misses the prefix:
"{expected[..^actual.Length].DisplayWhitespace()}"
""";
}

return prefix;
}

int column = settings?.IgnoredTrailingColumns ?? 0;
int indexFromEnd = actual.Length - indexOfFirstMismatch;
StringBuilder sb = new();
int trimStart =
GetStartIndexOfPhraseToShowBeforeTheMismatchingIndex(actual, indexOfFirstMismatch);
int trimStart = settings?.MatchType == MatchType.Suffix
? GetStartIndexOfPhraseToShowBeforeTheMismatchingIndexFromEnd(actual, indexFromEnd)
: GetStartIndexOfPhraseToShowBeforeTheMismatchingIndex(actual, indexOfFirstMismatch);

int whiteSpaceCountBeforeArrow = indexOfFirstMismatch - trimStart + linePrefix.Length;

Expand All @@ -132,7 +166,7 @@ private static string ToEqualityString(string prefix, string actual, string expe
}

string visibleText = actual[trimStart..indexOfFirstMismatch];
whiteSpaceCountBeforeArrow += visibleText.Count(c => c is '\r' or '\n');
whiteSpaceCountBeforeArrow += visibleText.Count(c => c is '\r' or '\n' or '\t');
Comment thread
vbreuss marked this conversation as resolved.

string matchingString = actual[..indexOfFirstMismatch];
int lineNumber = matchingString.Count(c => c == '\n');
Expand All @@ -148,17 +182,42 @@ private static string ToEqualityString(string prefix, string actual, string expe
sb.Append(prefix).Append(" on line ").Append(lineNumber + 1).Append(" and column ")
.Append(column).AppendLine(":");
}
else if (settings?.MatchType == MatchType.Suffix)
{
sb.Append(prefix).Append(" before index ").Append(indexOfFirstMismatch + column).AppendLine(":");
}
else
{
sb.Append(prefix).Append(" at index ").Append(indexOfFirstMismatch + column).AppendLine(":");
}

sb.Append(' ', whiteSpaceCountBeforeArrow).Append(arrowDown).AppendLine(ActualIndicator);
AppendPrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(sb, linePrefix, actual,
trimStart, suffix);
AppendPrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(sb, linePrefix, expected,
trimStart, suffix);
sb.Append(' ', whiteSpaceCountBeforeArrow).Append(arrowUp).Append(GetExpected(settings?.MatchType));
if (settings?.MatchType == MatchType.Suffix)
{
int trimStartExpected =
GetStartIndexOfPhraseToShowBeforeTheMismatchingIndexFromEnd(expected, indexFromEnd);
string actualText = CreatePrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(linePrefix, actual,
trimStart, indexFromEnd, suffix);
string expectedText = CreatePrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(linePrefix, expected,
trimStartExpected, indexFromEnd, suffix);
int actualIndentation = Math.Max(0, expectedText.Length - actualText.Length);
sb.Append(' ', whiteSpaceCountBeforeArrow + actualIndentation).Append(arrowDown)
.AppendLine(ActualIndicator);
sb.Append(' ', actualIndentation);
sb.Append(actualText);
sb.Append(' ', Math.Max(0, actualText.Length - expectedText.Length));
sb.Append(expectedText);
sb.Append(' ', whiteSpaceCountBeforeArrow + actualIndentation).Append(arrowUp)
.Append(GetExpected(settings?.MatchType));
}
else
{
sb.Append(' ', whiteSpaceCountBeforeArrow).Append(arrowDown).AppendLine(ActualIndicator);
AppendPrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(sb, linePrefix, actual,
trimStart, suffix);
AppendPrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(sb, linePrefix, expected,
trimStart, suffix);
sb.Append(' ', whiteSpaceCountBeforeArrow).Append(arrowUp).Append(GetExpected(settings?.MatchType));
}

return sb.ToString();
}
Expand Down Expand Up @@ -215,6 +274,42 @@ private static void AppendPrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(
stringBuilder.AppendLine(suffix);
}

/// <summary>
/// Creates the escaped visible <paramref name="text" /> phrase decorated with ellipsis, with
/// the <paramref name="prefix" /> and the <paramref name="suffix" />.
/// </summary>
/// <remarks>
/// When text phrase starts at <paramref name="indexOfStartingPhrase" /> and with a calculated length omits text
/// on start or end, an ellipsis is added.
/// </remarks>
private static string CreatePrefixAndEscapedPhraseToShowWithEllipsisAndSuffix(string prefix, string text,
int indexOfStartingPhrase, int indexFromEnd, string suffix)
{
StringBuilder? stringBuilder = new();
int indexOfFirstMismatch = text.Length - indexFromEnd;
int minLength = indexOfFirstMismatch + 10 - indexOfStartingPhrase;
int subjectLength = GetLengthOfPhraseToShowOrDefaultLength(text[indexOfStartingPhrase..], minLength);
const char ellipsis = '\u2026';

stringBuilder.Append(prefix);

if (indexOfStartingPhrase > 0)
{
stringBuilder.Append(ellipsis);
}

stringBuilder.Append(text
.Substring(indexOfStartingPhrase, subjectLength).DisplayWhitespace().ToSingleLine());

if (text.Length > indexOfStartingPhrase + subjectLength)
{
stringBuilder.Append(ellipsis);
}

stringBuilder.AppendLine(suffix);
return stringBuilder.ToString();
}

private static int GetIndexOfFirstMismatch(string? actualValue, string? expectedValue,
IEqualityComparer<string> comparer)
{
Expand Down Expand Up @@ -252,17 +347,54 @@ private static int GetIndexOfFirstMismatch(string? actualValue, string? expected
return min;
}

private static int GetIndexOfFirstMismatchFromEnd(string? actualValue, string? expectedValue,
IEqualityComparer<string> comparer)
{
if (comparer.Equals(actualValue, expectedValue))
{
return -1;
}

if (actualValue is null || expectedValue is null)
{
return 0;
}

int maxCommonLength = Math.Min(actualValue.Length, expectedValue.Length);
int min = 0;
int max = maxCommonLength + 1;
while (min < max)
{
int mid = (min + max) / 2;
if (mid == min)
{
break;
}

if (comparer.Equals(actualValue[^mid..], expectedValue[^mid..]))
{
min = mid;
}
else
{
max = mid;
}
}

return actualValue.Length - min - 1;
}

/// <summary>
/// Calculates how many characters to keep in <paramref name="value" />.
/// </summary>
/// <remarks>
/// If a word end is found between 45 and 60 characters, use this word end, otherwise keep 50 characters.
/// </remarks>
private static int GetLengthOfPhraseToShowOrDefaultLength(string value)
private static int GetLengthOfPhraseToShowOrDefaultLength(string value, int? minLength = null)
{
int minLength = Customize.aweXpect.Formatting().MinimumNumberOfCharactersAfterStringDifference.Get();
int defaultLength = minLength + 5;
int maxLength = minLength + 15;
minLength ??= Customize.aweXpect.Formatting().MinimumNumberOfCharactersAfterStringDifference.Get();
int defaultLength = minLength.Value + 5;
int maxLength = minLength.Value + 15;
const int lengthOfWhitespace = 1;

int indexOfWordBoundary = value
Expand Down Expand Up @@ -315,11 +447,54 @@ private static int GetStartIndexOfPhraseToShowBeforeTheMismatchingIndex(string v
return indexOfFirstMismatch - defaultCharactersToKeep;
}

/// <summary>
/// Calculates the start index of the visible segment from <paramref name="value" /> when highlighting the difference
/// at <paramref name="indexFromEnd" /> from the end.
/// </summary>
/// <remarks>
/// Either keep the last 10 characters before <paramref name="indexFromEnd" /> from the end or a word begin (separated
/// by
/// whitespace) between 15 and 5 characters before <paramref name="indexFromEnd" /> from the end.
/// </remarks>
private static int GetStartIndexOfPhraseToShowBeforeTheMismatchingIndexFromEnd(string value,
int indexFromEnd)
{
int minLength = Customize.aweXpect.Formatting().MinimumNumberOfCharactersAfterStringDifference.Get();
int defaultLength = minLength + 5;
int maxLength = minLength + 15;
const int lengthOfWhitespace = 1;
int phraseLengthToCheckForWordBoundary =
maxLength - minLength + lengthOfWhitespace;

int indexOfFirstMismatch = value.Length - indexFromEnd;
if (indexOfFirstMismatch <= defaultLength)
{
return 0;
}

int indexToStartSearchingForWordBoundary =
Math.Max(indexOfFirstMismatch - (maxLength + lengthOfWhitespace), 0);

int indexOfWordBoundary = value
.IndexOf(' ', indexToStartSearchingForWordBoundary,
phraseLengthToCheckForWordBoundary) -
indexToStartSearchingForWordBoundary;

if (indexOfWordBoundary >= 0)
{
return indexToStartSearchingForWordBoundary + indexOfWordBoundary + lengthOfWhitespace;
}

return indexOfFirstMismatch - defaultLength;
}

private static string GetExpected(MatchType? matchType)
=> matchType switch
{
MatchType.Wildcard => " (wildcard pattern)",
MatchType.Regex => " (regex pattern)",
MatchType.Prefix => " (expected prefix)",
MatchType.Suffix => " (expected suffix)",
_ => " (expected)",
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,18 @@ public string GetExtendedFailure(string it, string? actual, string? expected,

string prefix =
$"{it} was {Formatter.Format(actual.TruncateWithEllipsisOnWord(DefaultMaxLength).ToSingleLine())}";
StringDifference stringDifference = new(actual, expected, comparer, settings);
int indexOfFirstMismatch = stringDifference.IndexOfFirstMismatch(StringDifference.MatchType.Equality);
if (indexOfFirstMismatch == 0 && comparer.Equals(actual.TrimStart(), expected))
StringDifference stringDifference = new(actual, expected, comparer,
settings.WithMatchType(StringDifference.MatchType.Prefix));
int indexOfFirstMismatch = stringDifference.IndexOfFirstMismatch(StringDifference.MatchType.Prefix);
if (indexOfFirstMismatch == 0)
{
return
$"{prefix} which has unexpected whitespace (\"{actual.Substring(0, GetIndexOfFirstMatch(actual, expected, comparer)).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the beginning)";
string? trimmedActual = actual.TrimStart();
int commonLength = Math.Min(trimmedActual.Length, expected.Length);
if (comparer.Equals(trimmedActual[..commonLength], expected[..commonLength]))
{
return
$"{prefix} which has unexpected whitespace (\"{actual.Substring(0, GetIndexOfFirstMatch(actual, expected, comparer)).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the beginning)";
}
}

if (indexOfFirstMismatch == 0 && comparer.Equals(actual, expected.TrimStart()))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,24 @@ public string GetExtendedFailure(string it, string? actual, string? expected,
string prefix =
$"{it} was {Formatter.Format(actual.TruncateWithEllipsisOnWord(DefaultMaxLength).ToSingleLine())}";
int minCommonLength = Math.Min(actual.Length, expected.Length);
StringDifference stringDifference = new(actual, expected, comparer, settings);
int indexOfFirstMismatch = stringDifference.IndexOfFirstMismatch(StringDifference.MatchType.Equality);
StringDifference stringDifference = new(actual, expected, comparer,
settings.WithMatchType(StringDifference.MatchType.Suffix));
int indexOfFirstMismatch = stringDifference.IndexOfFirstMismatch(StringDifference.MatchType.Suffix);
if (indexOfFirstMismatch == 0 && comparer.Equals(actual, expected.TrimStart()))
{
return
$"{prefix} which misses some whitespace (\"{expected.Substring(0, GetIndexOfFirstMatch(expected, actual, comparer)).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the beginning)";
}

if (indexOfFirstMismatch == minCommonLength && comparer.Equals(actual.TrimEnd(), expected))
if (indexOfFirstMismatch == actual.Length)
{
return
$"{prefix} which has unexpected whitespace (\"{actual.Substring(indexOfFirstMismatch).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the end)";
string? trimmedActual = actual.TrimEnd();
int commonLength = Math.Min(trimmedActual.Length, expected.Length);
if (comparer.Equals(trimmedActual[^commonLength..], expected[^commonLength..]))
{
return
$"{prefix} which has unexpected whitespace (\"{actual.Substring(trimmedActual.Length).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the end)";
}
}

if (indexOfFirstMismatch == minCommonLength && comparer.Equals(actual, expected.TrimEnd()))
Expand Down
Loading
Loading