question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Inconsistency between StringAssertions.StartWith and String.StartsWith

See original GitHub issue

Description

StringAssertions.StartWith throws an ArgumentException when asserting that a string starts with the empty string.

This makes it so that the assertions in one of the tips are not equivalent when expectedPrefix is empty.

Complete minimal example reproducing the issue

string actual = "abc";
string expectedPrefix = "";

actual.StartsWith(expectedPrefix).Should().BeTrue();
actual.Should().StartWith(expectedPrefix);

Expected behavior:

Both assertions to pass.

Actual behavior:

The first assertion passes, but the second throws an ArgumentException with message

Cannot compare start of string with empty string. (Parameter ‘expected’)

Versions

  • Fluent Assertions: 5.10.3
  • .NET runtime: .NET Core 3.1

Additional Information

StringAssertions.EndWith and String.EndsWith are inconsistent in the same way.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
dennisdoomencommented, Oct 23, 2020

Isn’t this the only one that makes sense. The rest will only become impossible to explain.

Hence, my point above. Let’s just stick to changing StartWith/EndWith/NotStartWith/NotEndWith

0reactions
Overlongcommented, Oct 21, 2020

Another point that speaks in favor of this change in StringAssertions.StartWith/EndWith is that it is consistent with SelfReferencingCollectionAssertions.StartWith/EndWith, which do allow an empty collection as prefix/suffix and always pass (as far as I can tell).

  • every string starts/ends with the empty string {StartWith/EndWith}(EquivalentOf)

I agree.

  • no string does not “not start/ends” with the empty string {NotStartWith/NotEndWith}(EquivalentOf)

I think you meant ‘every string’ rather than ‘no string’ (or equivalently, ‘no string does not “not start/end” with the empty string’), right? I agree with this change, because then the assertions StartWith and NotStartWith are complementary (for all prefixes) in the sense that one passes and the other fails.

I’m not sure about whether we should also loosen up containment. Every string contains the empty string as "".Contains("") is true.

This makes sense to me. In the same way, StringAssertions.NotContain("") should always fail.

But for the Contain(string expected, OccurrenceConstraint occurrenceConstraint it gets weirder as one could argue both that the number of occurrences are:

  • twice, the empty prefix and the empty suffix
  • infinite, 1 for the empty prefix + the number of the occurrences for the remaining string, but since the remaining string hasn’t changed, therefore an infinite amount of occurrences.

Counting the number of occurrences does indeed seem tricky, especially when occurrences are not allowed to overlap (which is the convention used as far as I can tell). The recursive definition that the justification for your second option uses seems clean (certainly for non-empty strings), but dealing with infinity might be problematic.

Two other options that might be worth considering are:

  1. Disallowing the empty string in the OccurrenceConstraint overload by keeping the ArgumentException.
  2. Letting the empty string occur in any string s exactly s.Length + 1 times.

The second option might seem strange, but it can be justified by counting occurrences that are allowed to overlap and then noting that occurrences of the empty string cannot overlap, so the number of occurrences of the empty string should not change when we disallow overlaps.

A reasonable definition of the number of occurrences of a pattern p in a string s when allowing overlaps could be the number of indices i (between 0 and s.Length, inclusive) for which the substring s[i..] starting at index i starts with p. If we agree that every string starts with the empty string, then plugging in p = "" gives s.Length + 1 matches.

Neat properties of the s.Length + 1 option are that

  • for every string s, we have that s occurs exactly once in itself, and that
  • there is no need to deal with infinity,

but it does not seem very intuitive.

Read more comments on GitHub >

github_iconTop Results From Across the Web

c# - Inconsistent string.StartsWith on different platforms
1 Answer 1 ... This is a known difference between Windows and Linux/Unix: on Unix platforms, nulls have no "weight". The behavior of...
Read more >
Huge performance difference in string.StartsWith and ...
Very simple code: using System; using System.Diagnostics; namespace ConsoleApplication { public class Program { public static void ...
Read more >
StringAssert.StartsWith Method
Tests whether the specified string begins with the specified substring and throws an exception if the test string does not start with the...
Read more >
Python String | startswith() method with Examples
Python startswith() method returns either True or False. It returns True if the string starts with the prefix, otherwise False.
Read more >
Java.String.startsWith()
The method startsWith() is a convenience method that checks whether a String starts with another String. We can also pass the index of...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found