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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); Fix various LastIndexOf bugs when dealing with zero-length target substrings by GrabYourPitchforks · Pull Request #34616 · dotnet/runtime · GitHub
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
32 changes: 15 additions & 17 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line numberDiff line numberDiff line change
Expand Up@@ -3379,7 +3379,6 @@ public static void IndexOfSequenceLengthOneValue_Char()
Assert.Equal(2, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
Comment thread
GrabYourPitchforks marked this conversation as resolved.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3396,7 +3395,6 @@ public static void IndexOfSequenceLengthOneValueAtVeryEnd_Char()
Assert.Equal(5, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand All@@ -3413,7 +3411,6 @@ public static void IndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
Assert.Equal(-1, index);
Assert.Equal(index, s1.IndexOf(s2, StringComparison.Ordinal));

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.IndexOf(value);
Expand DownExpand Up@@ -3887,18 +3884,23 @@ public static void LastIndexOf_AllSubstrings(string s, string value, int startIn

if (value.Length == 0)
{
int expectedIndex = s.Length > 0 ? s.Length - 1 : 0;
int expectedStartIndex = startIndex == s.Length ? startIndex - 1 : startIndex;
int expectedStartIndex = startIndex;
if (s.Length == 0 && (startIndex == -1 || startIndex == 0))
expectedStartIndex = (value.Length == 0) ? 0 : -1;
Assert.Equal(expectedIndex, s.LastIndexOf(value, comparison));
expectedStartIndex = 0; // empty string occurs at beginning of search space
if (s.Length > 0 && startIndex < s.Length)
expectedStartIndex = startIndex + 1; // empty string occurs just after the last char included in the search space

Assert.Equal(s.Length, s.LastIndexOf(value, comparison));
Assert.Equal(expectedStartIndex, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(expectedIndex, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Assert.Equal(s.Length, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
return;
}

if (s.Length == 0)
{
// unit test shouldn't have passed a weightless string to this routine
Assert.NotEqual(value, string.Empty, StringComparer.FromComparison(comparison));

Assert.Equal(-1, s.LastIndexOf(value, comparison));
Assert.Equal(-1, s.LastIndexOf(value, startIndex, comparison));
Assert.Equal(-1, s.AsSpan().LastIndexOf(value.AsSpan(), comparison));
Expand DownExpand Up@@ -4068,8 +4070,8 @@ public static void LastIndexOf_TurkishI_EnglishUSCulture()
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not related to your changes: IMHO this test (IndexOf_AllSubstrings) is very complex and is trying to test too many test cases at the same time. It would be better to split it into few smaller tests with self-describing names

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it's a bit of a bear. Do you have recommendations for how it might naturally be split?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the two if blocks could become separate tests: value.Length == 0, s.Length == 0


[Theory]
[InlineData("foo", 2)]
[InlineData("hello", 4)]
[InlineData("foo", 3)]
[InlineData("hello", 5)]
[InlineData("", 0)]
public static void LastIndexOf_EmptyString(string s, int expected)
{
Expand DownExpand Up@@ -4325,13 +4327,13 @@ public static void LastIndexOfSequenceZeroLengthValue_Char()
string s1 = "0172377457778667789";
string s2 = string.Empty;
int index = s1.LastIndexOf(s2);
Assert.Equal(s1.Length - 1, index);
Assert.Equal(s1.Length, index);

// A zero-length value is always "found" at the start of the span.
// A zero-length value is always "found" at the end of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Assert.Equal(0, index);
Assert.Equal(span.Length, index);
}

[Fact]
Expand All@@ -4356,7 +4358,6 @@ public static void LastIndexOfSequenceLengthOneValue_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(2, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4371,7 +4372,6 @@ public static void LastIndexOfSequenceLengthOneValueAtVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4386,7 +4386,6 @@ public static void LastIndexOfSequenceLengthOneValueMultipleTimes_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(5, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand All@@ -4401,7 +4400,6 @@ public static void LastIndexOfSequenceLengthOneValueJustPasttVeryEnd_Char()
int index = s1.LastIndexOf(s2);
Assert.Equal(-1, index);

// A zero-length value is always "found" at the start of the span.
ReadOnlySpan<char> span = s1.AsSpan();
ReadOnlySpan<char> value = s2.AsSpan();
index = span.LastIndexOf(value);
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,7 @@ public class CompareInfoLastIndexOfTests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 2 };
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 };
Expand All@@ -30,8 +30,8 @@ public static IEnumerable<object[]> LastIndexOf_TestData()
yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 };

yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 4 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 };
yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 };

// OrdinalIgnoreCase
yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 };
Expand DownExpand Up@@ -157,6 +157,32 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
// Use LastIndexOf(string, string, int, int, CompareOptions)
Assert.Equal(expected, compareInfo.LastIndexOf(source, value, startIndex, count, options));

// Fixup offsets so that we can call the span-based APIs.

ReadOnlySpan<char> sourceSpan;
int adjustmentFactor; // number of chars to add to retured index from span-based APIs

if (startIndex == source.Length - 1 && count == source.Length)
{
// This idiom means "read the whole span"
sourceSpan = source;
adjustmentFactor = 0;
}
else if (startIndex == source.Length)
{
// Account for possible off-by-one at the call site
sourceSpan = source.AsSpan()[^(Math.Max(0, count - 1))..];
adjustmentFactor = source.Length - sourceSpan.Length;
}
else
{
// Bump 'startIndex' by 1, then go back 'count' chars
sourceSpan = source.AsSpan()[..(startIndex + 1)][^count..];
adjustmentFactor = startIndex + 1 - count;
}

if (expected < 0) { adjustmentFactor = 0; } // don't modify "not found" (-1) return values

if ((compareInfo == s_invariantCompare) && ((options == CompareOptions.None) || (options == CompareOptions.IgnoreCase)))
{
StringComparison stringComparison = (options == CompareOptions.IgnoreCase) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture;
Expand All@@ -165,20 +191,7 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va
Assert.Equal(expected, source.LastIndexOf(value, startIndex, count, stringComparison));

// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
// Filter differences betweeen string-based and Span-based LastIndexOf
// - Empty value handling - https://github.com/dotnet/runtime/issues/13382
// - Negative count
if (value.Length == 0 || count < 0)
return;

if (startIndex == source.Length)
{
startIndex--;
if (count > 0)
count--;
}
int leftStartIndex = (startIndex - count + 1);
Assert.Equal((expected == -1) ? -1 : (expected - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), stringComparison));
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
}

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -310,7 +310,7 @@ public static IEnumerable<object[]> SortKey_TestData()

public static IEnumerable<object[]> IndexOf_TestData()
{
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "foo", "", 0, 0, 1 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, 0 };
yield return new object[] { s_invariantCompare, "Hello", "l", 0, 2, -1 };
yield return new object[] { s_invariantCompare, "Hello", "l", 3, 3, 3 };
Expand All@@ -321,10 +321,14 @@ public static IEnumerable<object[]> IndexOf_TestData()

public static IEnumerable<object[]> IsSortable_TestData()
{
yield return new object[] { "", false, false };
yield return new object[] { "abcdefg", false, true };
yield return new object[] { "\uD800\uDC00", true, true };
yield return new object[] { "\uD800\uD800", true, false };
yield return new object[] { "", false };
yield return new object[] { "abcdefg", true };
yield return new object[] { "\uD800\uDC00", true };

// VS test runner for xunit doesn't handle ill-formed UTF-16 strings properly.
// We'll send this one through as an array to avoid U+FFFD substitution.

yield return new object[] { new char[] { '\uD800', '\uD800' }, false };
}

[Theory]
Expand DownExpand Up@@ -413,27 +417,30 @@ public void SortKeyMiscTest()
public void IndexOfTest(CompareInfo compareInfo, string source, string value, int startIndex, int indexOfExpected, int lastIndexOfExpected)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(indexOfExpected, compareInfo.IndexOf(source, value[0], startIndex));
}

Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value, startIndex));
if (value.Length > 0)
if (value.Length == 1)
{
Assert.Equal(lastIndexOfExpected, compareInfo.LastIndexOf(source, value[0], startIndex));
}
}

[Theory]
[MemberData(nameof(IsSortable_TestData))]
public void IsSortableTest(string source, bool hasSurrogate, bool expected)
public void IsSortableTest(object sourceObj, bool expected)
{
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));

bool charExpectedResults = hasSurrogate ? false : expected;
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.

foreach (char c in source)
Assert.Equal(charExpectedResults, CompareInfo.IsSortable(c));
Assert.Equal(expected && !char.IsSurrogate(c), CompareInfo.IsSortable(c));
}

[Fact]
Expand Down
Loading