Skip to content

Commit 13003ea

Browse files
authored
feat: include missing or superfluous whitespace in message (#109)
When two strings only differ in whitespace or length, the difference is now included in the failure message. - Whitespace is escaped so that it is displayed as `\t`, `\n` or `\r` - The displayed string is limited to 100 characters *fixes #106*
1 parent 7b23d21 commit 13003ea

File tree

3 files changed

+134
-30
lines changed

3 files changed

+134
-30
lines changed

Source/Testably.Expectations/Core/Helpers/StringExtensions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ namespace Testably.Expectations.Core.Helpers;
55

66
internal static class StringExtensions
77
{
8+
[return: NotNullIfNotNull(nameof(value))]
9+
public static string? DisplayWhitespace(this string? value)
10+
{
11+
return value?.Replace("\n", "\\n").Replace("\r", "\\r").Replace("\t", "\\t");
12+
}
13+
814
[return: NotNullIfNotNull(nameof(value))]
915
public static string? Indent(this string? value, string indentation = " ",
1016
bool indentFirstLine = true)

Source/Testably.Expectations/Options/StringMatcher.cs

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -169,44 +169,43 @@ public string GetExtendedFailure(string? actual, string? pattern, bool ignoreCas
169169
if (stringDifference.IndexOfFirstMismatch == 0 &&
170170
comparer.Equals(actual.TrimStart(), pattern))
171171
{
172-
return $"{prefix} which has unexpected whitespace at the beginning";
172+
return
173+
$"{prefix} which has unexpected whitespace (\"{actual.Substring(0, GetIndexOfFirstMatch(actual, pattern, comparer)).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the beginning)";
173174
}
174175

175176
if (stringDifference.IndexOfFirstMismatch == 0 &&
176177
comparer.Equals(actual, pattern.TrimStart()))
177178
{
178-
return $"{prefix} which misses some whitespace at the beginning";
179+
return
180+
$"{prefix} which misses some whitespace (\"{pattern.Substring(0, GetIndexOfFirstMatch(pattern, actual, comparer)).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the beginning)";
179181
}
180182

181183
if (stringDifference.IndexOfFirstMismatch == minCommonLength &&
182184
comparer.Equals(actual.TrimEnd(), pattern))
183185
{
184-
return $"{prefix} which has unexpected whitespace at the end";
186+
return
187+
$"{prefix} which has unexpected whitespace (\"{actual.Substring(stringDifference.IndexOfFirstMismatch).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the end)";
185188
}
186189

187190
if (stringDifference.IndexOfFirstMismatch == minCommonLength &&
188191
comparer.Equals(actual, pattern.TrimEnd()))
189192
{
190-
return $"{prefix} which misses some whitespace at the end";
191-
}
192-
193-
if (comparer.Equals(actual.Trim(), pattern.Trim()))
194-
{
195-
return $"{prefix} which differs in whitespace";
193+
return
194+
$"{prefix} which misses some whitespace (\"{pattern.Substring(stringDifference.IndexOfFirstMismatch).DisplayWhitespace().TruncateWithEllipsis(100)}\" at the end)";
196195
}
197196

198197
if (actual.Length < pattern.Length &&
199198
stringDifference.IndexOfFirstMismatch == actual.Length)
200199
{
201200
return
202-
$"{prefix} with a length of {actual.Length} which is shorter than the expected length of {pattern.Length}";
201+
$"{prefix} with a length of {actual.Length} which is shorter than the expected length of {pattern.Length} and misses:{Environment.NewLine} \"{pattern.Substring(actual.Length).TruncateWithEllipsis(100)}\"";
203202
}
204203

205204
if (actual.Length > pattern.Length &&
206205
stringDifference.IndexOfFirstMismatch == pattern.Length)
207206
{
208207
return
209-
$"{prefix} with a length of {actual.Length} which is longer than the expected length of {pattern.Length}";
208+
$"{prefix} with a length of {actual.Length} which is longer than the expected length of {pattern.Length} and has superfluous:{Environment.NewLine} \"{actual.Substring(pattern.Length).TruncateWithEllipsis(100)}\"";
210209
}
211210

212211
return $"{prefix} which {new StringDifference(actual, pattern, comparer)}";
@@ -229,6 +228,21 @@ public bool Matches(string? value, string? pattern, bool ignoreCase,
229228
}
230229

231230
#endregion
231+
232+
private int GetIndexOfFirstMatch(string stringWithLeadingWhitespace, string value,
233+
IEqualityComparer<string> comparer)
234+
{
235+
for (int i = 0; i <= stringWithLeadingWhitespace.Length - value.Length; i++)
236+
{
237+
if (comparer.Equals(
238+
stringWithLeadingWhitespace.Substring(i, value.Length), value))
239+
{
240+
return i;
241+
}
242+
}
243+
244+
return 0;
245+
}
232246
}
233247

234248
private sealed class RegexMatchType : IMatchType

Tests/Testably.Expectations.Tests/Core/StringMatcherTests.cs renamed to Tests/Testably.Expectations.Tests/Options/StringMatcherTests.cs

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Testably.Expectations.Tests.Core;
1+
namespace Testably.Expectations.Tests.Options;
22

33
public class StringMatcherTests
44
{
@@ -86,9 +86,11 @@ at Expect.That(subject).Should().Be(expected)
8686
}
8787

8888
[Theory]
89-
[InlineData("SomeVeryLongDummyTextWithMore_ThisLongTextIsUsedToCheckADifferenceAtTheEnd after 40 + 5 characters")]
89+
[InlineData(
90+
"SomeVeryLongDummyTextWithMore_ThisLongTextIsUsedToCheckADifferenceAtTheEnd after 40 + 5 characters")]
9091
// ReSharper disable once StringLiteralTypo
91-
[InlineData("SomeVeryLongDummyTextWithMore_ThisLongTextIsUsedToCheckADifferen after 40 + 15 characters")]
92+
[InlineData(
93+
"SomeVeryLongDummyTextWithMore_ThisLongTextIsUsedToCheckADifferen after 40 + 15 characters")]
9294
public async Task
9395
ShouldLookForAWordBoundaryBetween45And60CharactersAfterTheMismatchingIndexToHighlightTheMismatch(
9496
string expected)
@@ -161,8 +163,10 @@ async Task Act()
161163
[Fact]
162164
public async Task WhenStringsAreLong_ShouldHighlightTheDifferences()
163165
{
164-
string subject = "this is a long text that differs in between two words and has lot of text afterwards";
165-
string expected = "this is a long text which differs in between two words and has lot of text afterwards";
166+
string subject =
167+
"this is a long text that differs in between two words and has lot of text afterwards";
168+
string expected =
169+
"this is a long text which differs in between two words and has lot of text afterwards";
166170

167171
async Task Act()
168172
=> await That(subject).Should().Be(expected);
@@ -205,20 +209,23 @@ async Task Act()
205209
=> await That(subject).Should().Be(expected);
206210

207211
await That(Act).Should().ThrowException()
208-
.WithMessage("*misses some whitespace at the beginning*").AsWildcard();
212+
.WithMessage("*misses some whitespace (\" \" at the beginning)*").AsWildcard();
209213
}
210214

211215
[Fact]
212216
public async Task
213-
WhenTheExpectedStringHasTrailingAndLeadingWhitespace_ShouldFailWithDescriptiveMessage()
217+
WhenTheExpectedStringHasLeadingWhitespace_ShouldLimitDisplayedWhitespaceTo100Characters()
214218
{
219+
const int maxCharacters = 100;
215220
string subject = "ABC";
216-
string expected = "\tABC\t";
221+
string expected = new string(' ', maxCharacters) + " ABC";
217222

218223
async Task Act()
219224
=> await That(subject).Should().Be(expected);
220225

221-
await That(Act).Should().ThrowException().WithMessage("*differs in whitespace*")
226+
await That(Act).Should().ThrowException()
227+
.WithMessage(
228+
$"*misses some whitespace (\"{new string(' ', maxCharacters)}\" at the beginning)*")
222229
.AsWildcard();
223230
}
224231

@@ -233,7 +240,24 @@ async Task Act()
233240
=> await That(subject).Should().Be(expected);
234241

235242
await That(Act).Should().ThrowException()
236-
.WithMessage("*misses some whitespace at the end*").AsWildcard();
243+
.WithMessage("*misses some whitespace (\" \" at the end)*").AsWildcard();
244+
}
245+
246+
[Fact]
247+
public async Task
248+
WhenTheExpectedStringHasTrailingWhitespace_ShouldLimitDisplayedWhitespaceTo100Characters()
249+
{
250+
const int maxCharacters = 100;
251+
string subject = "ABC";
252+
string expected = "ABC " + new string(' ', maxCharacters);
253+
254+
async Task Act()
255+
=> await That(subject).Should().Be(expected);
256+
257+
await That(Act).Should().ThrowException()
258+
.WithMessage(
259+
$"*misses some whitespace (\"{new string(' ', maxCharacters)}\" at the end)*")
260+
.AsWildcard();
237261
}
238262

239263
[Fact]
@@ -247,7 +271,8 @@ async Task Act()
247271

248272
await That(Act).Should()
249273
.ThrowException()
250-
.WithMessage("*length of 2 which is longer than the expected length of 0*")
274+
.WithMessage(
275+
"*length of 2 which is longer than the expected length of 0 and has superfluous*\"AB\"*")
251276
.AsWildcard();
252277
}
253278

@@ -263,7 +288,26 @@ async Task Act()
263288

264289
await That(Act).Should()
265290
.ThrowException()
266-
.WithMessage("*length of 2 which is shorter than the expected length of 3*")
291+
.WithMessage(
292+
"*length of 2 which is shorter than the expected length of 3 and misses*\"C\"*")
293+
.AsWildcard();
294+
}
295+
296+
[Fact]
297+
public async Task
298+
WhenTheExpectedStringIsLongerThanTheSubjectString_ShouldLimitDisplayedDifferenceTo100Characters()
299+
{
300+
const int maxCharacters = 100;
301+
string subject = "AB";
302+
string expected = "ABX" + new string('X', maxCharacters);
303+
304+
async Task Act()
305+
=> await That(subject).Should().Be(expected);
306+
307+
await That(Act).Should()
308+
.ThrowException()
309+
.WithMessage(
310+
$"*length of 2 which is shorter than the expected length of 103 and misses*\"{new string('X', maxCharacters)}\"*")
267311
.AsWildcard();
268312
}
269313

@@ -292,7 +336,26 @@ async Task Act()
292336

293337
await That(Act).Should()
294338
.ThrowException()
295-
.WithMessage("*length of 3 which is longer than the expected length of 2*")
339+
.WithMessage(
340+
"*length of 3 which is longer than the expected length of 2 and has superfluous*\"C\"*")
341+
.AsWildcard();
342+
}
343+
344+
[Fact]
345+
public async Task
346+
WhenTheExpectedStringIsShorterThanTheSubjectString_ShouldLimitDisplayedDifferenceTo100Characters()
347+
{
348+
const int maxCharacters = 100;
349+
string subject = "ABX" + new string('X', maxCharacters);
350+
string expected = "AB";
351+
352+
async Task Act()
353+
=> await That(subject).Should().Be(expected);
354+
355+
await That(Act).Should()
356+
.ThrowException()
357+
.WithMessage(
358+
$"*length of 103 which is longer than the expected length of 2 and has superfluous*\"{new string('X', maxCharacters)}\"*")
296359
.AsWildcard();
297360
}
298361

@@ -319,20 +382,23 @@ async Task Act()
319382
=> await That(subject).Should().Be(expected);
320383

321384
await That(Act).Should().ThrowException()
322-
.WithMessage("*unexpected whitespace at the beginning*").AsWildcard();
385+
.WithMessage("*unexpected whitespace (\"\\t \" at the beginning)*").AsWildcard();
323386
}
324387

325388
[Fact]
326389
public async Task
327-
WhenTheSubjectHasTrailingAndLeadingWhitespace_ShouldFailWithDescriptiveMessage()
390+
WhenTheSubjectHasLeadingWhitespace_ShouldLimitDisplayedWhitespaceTo100Characters()
328391
{
329-
string subject = " ABC\t";
392+
const int maxCharacters = 100;
393+
string subject = new string(' ', maxCharacters) + " ABC";
330394
string expected = "ABC";
331395

332396
async Task Act()
333397
=> await That(subject).Should().Be(expected);
334398

335-
await That(Act).Should().ThrowException().WithMessage("*differs in whitespace*")
399+
await That(Act).Should().ThrowException()
400+
.WithMessage(
401+
$"*unexpected whitespace (\"{new string(' ', maxCharacters)}\" at the beginning)*")
336402
.AsWildcard();
337403
}
338404

@@ -346,7 +412,24 @@ async Task Act()
346412
=> await That(subject).Should().Be(expected);
347413

348414
await That(Act).Should().ThrowException()
349-
.WithMessage("*unexpected whitespace at the end*").AsWildcard();
415+
.WithMessage("*unexpected whitespace (\"\\t\" at the end)*").AsWildcard();
416+
}
417+
418+
[Fact]
419+
public async Task
420+
WhenTheSubjectHasTrailingWhitespace_ShouldLimitDisplayedWhitespaceTo100Characters()
421+
{
422+
const int maxCharacters = 100;
423+
string subject = "ABC " + new string(' ', maxCharacters);
424+
string expected = "ABC";
425+
426+
async Task Act()
427+
=> await That(subject).Should().Be(expected);
428+
429+
await That(Act).Should().ThrowException()
430+
.WithMessage(
431+
$"*unexpected whitespace (\"{new string(' ', maxCharacters)}\" at the end)*")
432+
.AsWildcard();
350433
}
351434

352435
[Fact]
@@ -360,7 +443,8 @@ async Task Act()
360443

361444
await That(Act).Should()
362445
.ThrowException()
363-
.WithMessage("*length of 0 which is shorter than the expected length of 2*")
446+
.WithMessage(
447+
"*length of 0 which is shorter than the expected length of 2 and misses*\"AB\"*")
364448
.AsWildcard();
365449
}
366450

0 commit comments

Comments
 (0)