Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion Pipeline/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ partial class Build : NukeBuild
/// <para />
/// Afterward, you can update the package reference in `Directory.Packages.props` and reset this flag.
/// </summary>
readonly BuildScope BuildScope = BuildScope.Default;
readonly BuildScope BuildScope = BuildScope.CoreOnly;

[Parameter("Github Token")] readonly string GithubToken;

Expand Down
170 changes: 152 additions & 18 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,7 @@ public int IndexOfFirstMismatch(MatchType matchType)
return 0;
}

_indexOfFirstMismatch ??= GetIndexOfFirstMismatch(actual, expected, _comparer);
_indexOfFirstMismatch ??= GetIndexOfFirstMismatch(actual, expected, _comparer, fromEnd: matchType is MatchType.Suffix);
return _indexOfFirstMismatch.Value;
}

Expand Down Expand Up @@ -102,7 +118,8 @@ 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 +133,24 @@ private static string ToEqualityString(string prefix, string actual, string expe

if (indexOfFirstMismatch < 0)
{
if (settings?.MatchType == MatchType.Suffix &&
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 +160,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 +176,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,8 +268,44 @@ 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)
IEqualityComparer<string> comparer, bool fromEnd = false)
{
if (comparer.Equals(actualValue, expectedValue))
{
Expand All @@ -239,7 +328,9 @@ private static int GetIndexOfFirstMismatch(string? actualValue, string? expected
break;
}

if (comparer.Equals(actualValue[..mid], expectedValue[..mid]))
if (fromEnd
? comparer.Equals(actualValue[^mid..], expectedValue[^mid..])
: comparer.Equals(actualValue[..mid], expectedValue[..mid]))
{
min = mid;
}
Expand All @@ -249,7 +340,7 @@ private static int GetIndexOfFirstMismatch(string? actualValue, string? expected
}
}

return min;
return fromEnd ? actualValue.Length - min - 1 : min;
}

/// <summary>
Expand All @@ -258,11 +349,11 @@ private static int GetIndexOfFirstMismatch(string? actualValue, string? expected
/// <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 +406,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