Skip to content

Commit 1c9c5cc

Browse files
committed
[test] test/StringIndex: Add some tests exercising replaceSubrange
The exhaustive substring.replaceSubrange test probably takes too long to include in regular testing, but let’s enable it for now: it has caught a bunch of problems already and it will probably catch more before this lands.
1 parent b29d8f4 commit 1c9c5cc

File tree

1 file changed

+187
-13
lines changed

1 file changed

+187
-13
lines changed

test/stdlib/StringIndex.swift

Lines changed: 187 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ enum SimpleString: String {
1818
case emoji = "😀😃🤢🤮👩🏿‍🎤🧛🏻‍♂️🧛🏻‍♂️👩‍👩‍👦‍👦"
1919
}
2020

21+
/// Print out a full list of indices in every view of `string`.
22+
/// This is useful while debugging test failures in this test.
2123
func dumpIndices(_ string: String) {
2224
print("-------------------------------------------------------------------")
2325
print("String: \(String(reflecting: string))")
@@ -350,7 +352,7 @@ suite.test("Exhaustive Index Interchange")
350352
return
351353
}
352354

353-
dumpIndices(str)
355+
//dumpIndices(str)
354356

355357
var curCharIdx = str.startIndex
356358
var curScalarIdx = str.startIndex
@@ -481,10 +483,9 @@ suite.test("Exhaustive Index Interchange")
481483
#endif
482484

483485
extension Collection {
484-
// Assuming both `self` and `other` are sorted, call `body` for each element
485-
// `a` in `other` together with the slice in `self` that starts with the first
486-
// element in `self` that is greater than or equal to `a`, up to the first
487-
// element that is greater than or equal to the next value in `other`.
486+
// Assuming both `self` and `other` use the same index space, call `body` for
487+
// each index `i` in `other`, along with the slice in `self` that begins at
488+
// `i` and ends at the index following it in `other`.
488489
//
489490
// `other` must start with an item that is less than or equal to the first
490491
// item in `self`.
@@ -516,9 +517,9 @@ extension Collection {
516517
}
517518

518519
extension String {
519-
// Returns a list of every valid index in every string view, including end
520-
// indices. We keep equal indices originating from different views because
521-
// they may have different grapheme size caches or flags etc.
520+
// Returns a list of every valid index in every string view, optionally
521+
// including end indices. We keep equal indices originating from different
522+
// views because they may have different grapheme size caches or flags etc.
522523
func allIndices(includingEnd: Bool = true) -> [String.Index] {
523524
var r = Array(self.indices)
524525
if includingEnd { r.append(self.endIndex) }
@@ -567,6 +568,23 @@ extension String {
567568
}
568569
}
569570

571+
extension Substring {
572+
// Returns a list of every valid index in every string view, optionally
573+
// including end indices. We keep equal indices originating from different
574+
// views because they may have different grapheme size caches or flags etc.
575+
func allIndices(includingEnd: Bool = true) -> [String.Index] {
576+
var r = Array(self.indices)
577+
if includingEnd { r.append(self.endIndex) }
578+
r += Array(self.unicodeScalars.indices)
579+
if includingEnd { r.append(self.unicodeScalars.endIndex) }
580+
r += Array(self.utf8.indices)
581+
if includingEnd { r.append(self.utf8.endIndex) }
582+
r += Array(self.utf16.indices)
583+
if includingEnd { r.append(self.utf16.endIndex) }
584+
return r
585+
}
586+
}
587+
570588
suite.test("Fully exhaustive index interchange")
571589
.forEach(in: examples) { string in
572590
guard #available(SwiftStdlib 5.7, *) else {
@@ -578,7 +596,7 @@ suite.test("Fully exhaustive index interchange")
578596
return
579597
}
580598

581-
dumpIndices(string)
599+
//dumpIndices(string)
582600

583601
let scalarMap = string.scalarMap()
584602
let characterMap = string.characterMap()
@@ -627,7 +645,9 @@ suite.test("Fully exhaustive index interchange")
627645
// The substring `string[i..<j]` does not round its bounds to
628646
// `Character` boundaries, so it may have a different count than what
629647
// the base string reports.
630-
let s = String.UnicodeScalarView(string.unicodeScalars[i ..< j])
648+
let si = scalarMap[i]!.index
649+
let sj = scalarMap[j]!.index
650+
let s = String.UnicodeScalarView(string.unicodeScalars[si ..< sj])
631651
let substringDistance = String(s).count
632652

633653
let substring = string[i ..< j]
@@ -650,6 +670,12 @@ suite.test("Fully exhaustive index interchange")
650670
""")
651671

652672
// Check reachability of substring bounds.
673+
var i = substring.startIndex
674+
for _ in 0 ..< substringDistance {
675+
substring.formIndex(after: &i)
676+
}
677+
expectEqual(i, substring.endIndex)
678+
653679
expectEqual(
654680
substring.index(substring.startIndex, offsetBy: substringDistance),
655681
substring.endIndex,
@@ -763,7 +789,6 @@ suite.test("Global vs local grapheme cluster boundaries") {
763789
expectEqual(str.index(before: s[2]), s[0]) // s[2] ≅ s[1]
764790
expectEqual(str.index(before: s[1]), s[0])
765791

766-
dumpIndices(str)
767792
// UTF-8
768793
expectEqual(str.utf8.count, 18)
769794
expectEqual(str.index(after: u8[0]), u8[1])
@@ -839,14 +864,14 @@ suite.test("Index encoding correction") {
839864
// If the mutation's effect included the data addressed by the original index,
840865
// then we may still get nonsensical results.
841866
var s = ("🫱🏼‍🫲🏽 a 🧑🏽‍🌾 b" as NSString) as String
842-
dumpIndices(s)
867+
//dumpIndices(s)
843868

844869
let originals = s.allIndices(includingEnd: false).map {
845870
($0, s[$0], s.unicodeScalars[$0], s.utf8[$0], s.utf16[$0])
846871
}
847872

848873
s.append(".")
849-
dumpIndices(s)
874+
//dumpIndices(s)
850875

851876
for (i, char, scalar, u8, u16) in originals {
852877
expectEqual(s[i], char, "i: \(i)")
@@ -856,3 +881,152 @@ suite.test("Index encoding correction") {
856881
}
857882
}
858883
#endif
884+
885+
suite.test("String.replaceSubrange index validation")
886+
.forEach(in: examples) { string in
887+
guard #available(SwiftStdlib 5.7, *) else {
888+
// Index navigation in 5.7 always rounds input indices down to the nearest
889+
// Character, so that we always have a well-defined distance between
890+
// indices, even if they aren't valid.
891+
//
892+
// 5.6 and below did not behave consistently in this case.
893+
return
894+
}
895+
896+
//dumpIndices(string)
897+
898+
let scalarMap = string.scalarMap()
899+
let allIndices = string.allIndices()
900+
901+
for i in allIndices {
902+
for j in allIndices {
903+
guard i <= j else { continue }
904+
let si = scalarMap[i]!.index
905+
let sj = scalarMap[j]!.index
906+
907+
// Check String.replaceSubrange(_:with:)
908+
do {
909+
let replacement = "x"
910+
911+
var expected = "".unicodeScalars
912+
expected += string.unicodeScalars[..<si]
913+
expected += replacement.unicodeScalars
914+
expected += string.unicodeScalars[sj...]
915+
916+
var actual = string
917+
actual.replaceSubrange(i ..< j, with: replacement)
918+
919+
expectEqual(actual, String(expected),
920+
"""
921+
string: \(string.debugDescription)
922+
i: \(i)
923+
j: \(j)
924+
""")
925+
}
926+
927+
// Check String.unicodeScalars.replaceSubrange(_:with:)
928+
do {
929+
let replacement = "x".unicodeScalars
930+
931+
var expected = "".unicodeScalars
932+
expected += string.unicodeScalars[..<si]
933+
expected += replacement
934+
expected += string.unicodeScalars[sj...]
935+
936+
var actual = string
937+
actual.unicodeScalars.replaceSubrange(i ..< j, with: replacement)
938+
939+
expectEqual(actual, String(expected),
940+
"""
941+
string: \(string.debugDescription)
942+
i: \(i)
943+
j: \(j)
944+
""")
945+
}
946+
}
947+
}
948+
}
949+
950+
suite.test("Substring.replaceSubrange index validation")
951+
.forEach(in: examples) { string in
952+
guard #available(SwiftStdlib 5.7, *) else {
953+
// Index navigation in 5.7 always rounds input indices down to the nearest
954+
// Character, so that we always have a well-defined distance between
955+
// indices, even if they aren't valid.
956+
//
957+
// 5.6 and below did not behave consistently in this case.
958+
return
959+
}
960+
961+
dumpIndices(string)
962+
963+
let scalarMap = string.scalarMap()
964+
let allIndices = string.allIndices()
965+
966+
for i in allIndices {
967+
for j in allIndices {
968+
guard i <= j else { continue }
969+
let si = scalarMap[i]!.index
970+
let sj = scalarMap[j]!.index
971+
972+
let substring = string[i ..< j]
973+
974+
let subindices = substring.allIndices()
975+
for m in subindices {
976+
for n in subindices {
977+
guard m <= n else { continue }
978+
let sm = scalarMap[m]!.index
979+
let sn = scalarMap[n]!.index
980+
981+
// Check Substring.replaceSubrange(_:with:)
982+
do {
983+
let replacement = "x"
984+
985+
var expected = "".unicodeScalars
986+
expected += string.unicodeScalars[si ..< sm]
987+
expected += replacement.unicodeScalars
988+
expected += string.unicodeScalars[sn ..< sj]
989+
990+
var actual = substring
991+
actual.replaceSubrange(m ..< n, with: replacement)
992+
993+
expectEqual(actual, Substring(expected[...]),
994+
"""
995+
string: \(string.debugDescription)
996+
i: \(i)
997+
j: \(j)
998+
m: \(m)
999+
n: \(n)
1000+
""")
1001+
}
1002+
1003+
// Check String.unicodeScalars.replaceSubrange(_:with:)
1004+
do {
1005+
let replacement = "x".unicodeScalars
1006+
1007+
var expected = "".unicodeScalars
1008+
expected += string.unicodeScalars[si ..< sm]
1009+
expected += replacement
1010+
expected += string.unicodeScalars[sn ..< sj]
1011+
1012+
var actual = substring
1013+
actual.unicodeScalars.replaceSubrange(m ..< n, with: replacement)
1014+
1015+
expectEqual(actual, Substring(expected[...]),
1016+
"""
1017+
string: \(string.debugDescription)
1018+
i: \(i)
1019+
j: \(j)
1020+
m: \(m)
1021+
n: \(n)
1022+
substring.startIndex: \(substring.startIndex)
1023+
substring.endIndex: \(substring.endIndex)
1024+
actual.startIndex: \(actual.startIndex)
1025+
actual.endIndex: \(actual.endIndex)
1026+
""")
1027+
}
1028+
}
1029+
}
1030+
}
1031+
}
1032+
}

0 commit comments

Comments
 (0)