From 477a465ba08c2517cf8ca495a452d20e2c9d52d8 Mon Sep 17 00:00:00 2001 From: scrieve <86650480+scrieve@users.noreply.github.com> Date: Fri, 23 Oct 2026 20:47:33 +0000 Subject: [PATCH 1/3] gh-160501: Cover the UCS4 adaptive search path in test_str --- Lib/test/test_str.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index bef0e62fb42bcc..faee0e826ad0aa 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -267,6 +267,22 @@ class MyStr(str): pass self.checkequal(3, MyStr('aaa'), 'count', 'a') + def test_find_adaptive_nonbmp(self): + # The adaptive path in Objects/stringlib/fastsearch.h needs a haystack + # of at least 2500 characters and a needle of at least 100, with the + # needle at least a third of the haystack. test_adaptive_find only + # uses ASCII, so the UCS4 variant of adaptive_find never ran. + a, b = '\U0001d11e', '\U0001d11f' + for N in 1000, 10_000: + A, B = a * N, b * N + haystack = A + A + B + A + A + needle = A + B + B + A + self.assertEqual(haystack.find(needle), -1) + self.assertEqual(haystack.count(needle), 0) + combined = haystack + needle + self.assertEqual(combined.find(needle), len(haystack)) + self.assertEqual(combined.count(needle), 1) + def test_find(self): string_tests.StringLikeTest.test_find(self) # test implementation details of the memchr fast path From fc31458dafababfd2b32eea4e9f21a689fecdfef Mon Sep 17 00:00:00 2001 From: scrieve <86650480+scrieve@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:24:50 +0000 Subject: [PATCH 2/3] gh-160501: Wrap the UCS4 adaptive search cases in subTest --- Lib/test/test_str.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index faee0e826ad0aa..545a28497bb1a5 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -274,14 +274,15 @@ def test_find_adaptive_nonbmp(self): # uses ASCII, so the UCS4 variant of adaptive_find never ran. a, b = '\U0001d11e', '\U0001d11f' for N in 1000, 10_000: - A, B = a * N, b * N - haystack = A + A + B + A + A - needle = A + B + B + A - self.assertEqual(haystack.find(needle), -1) - self.assertEqual(haystack.count(needle), 0) - combined = haystack + needle - self.assertEqual(combined.find(needle), len(haystack)) - self.assertEqual(combined.count(needle), 1) + with self.subTest(N=N): + A, B = a * N, b * N + haystack = A + A + B + A + A + needle = A + B + B + A + self.assertEqual(haystack.find(needle), -1) + self.assertEqual(haystack.count(needle), 0) + combined = haystack + needle + self.assertEqual(combined.find(needle), len(haystack)) + self.assertEqual(combined.count(needle), 1) def test_find(self): string_tests.StringLikeTest.test_find(self) From 4d1e64d8e96a7101bfd5a0265b7cf230bda93212 Mon Sep 17 00:00:00 2001 From: scrieve <86650480+scrieve@users.noreply.github.com> Date: Thu, 29 Oct 2026 20:10:53 +0000 Subject: [PATCH 3/3] gh-160501: Cover the adaptive search path at every character width adaptive_find is compiled once per character width, and the only test that reached the adaptive branch used ASCII, so the other three copies never ran. Run the same shape at all four widths and check a miss and a match. --- Lib/test/test_str.py | 33 ++++++++++++++++++++------------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/Lib/test/test_str.py b/Lib/test/test_str.py index 545a28497bb1a5..cc7618bf414cfe 100644 --- a/Lib/test/test_str.py +++ b/Lib/test/test_str.py @@ -267,22 +267,29 @@ class MyStr(str): pass self.checkequal(3, MyStr('aaa'), 'count', 'a') - def test_find_adaptive_nonbmp(self): + def test_find_adaptive_widths(self): # The adaptive path in Objects/stringlib/fastsearch.h needs a haystack - # of at least 2500 characters and a needle of at least 100, with the - # needle at least a third of the haystack. test_adaptive_find only - # uses ASCII, so the UCS4 variant of adaptive_find never ran. - a, b = '\U0001d11e', '\U0001d11f' - for N in 1000, 10_000: - with self.subTest(N=N): - A, B = a * N, b * N - haystack = A + A + B + A + A - needle = A + B + B + A + # of at least 2500 characters and a needle of at least a third of it, + # and it is compiled once per character width. Exercise all four. + cases = [ + ('ascii', 'a', 'b', 'c'), + ('latin1', '\xe9', '\xea', '\xeb'), + ('ucs2', '\u0100', '\u0101', '\u0102'), + ('ucs4', '\U0001d11e', '\U0001d11f', '\U0001d120'), + ] + for kind, c, d, x in cases: + # A needle of 2000 characters, none of which is x. + needle = c * 1000 + d * 1000 + with self.subTest(kind=kind): + # 4000 characters, mostly x: the skip branches jump by m. + haystack = c * 1000 + x * 3000 self.assertEqual(haystack.find(needle), -1) self.assertEqual(haystack.count(needle), 0) - combined = haystack + needle - self.assertEqual(combined.find(needle), len(haystack)) - self.assertEqual(combined.count(needle), 1) + # A match at 3000 in a haystack of 5000, so count() reaches + # the match branch inside adaptive_find rather than two-way. + haystack = c * 1000 + x * 2000 + needle + self.assertEqual(haystack.find(needle), 3000) + self.assertEqual(haystack.count(needle), 1) def test_find(self): string_tests.StringLikeTest.test_find(self)