| // RUN: %target-run-simple-swift |
| // REQUIRES: executable_test |
| |
| // |
| // Tests for the non-Foundation API of String |
| // |
| |
| import StdlibUnittest |
| |
| #if _runtime(_ObjC) |
| import Foundation |
| #endif |
| |
| |
| var StringTests = TestSuite("StringTests") |
| |
| struct ComparisonTest { |
| let expectedUnicodeCollation: ExpectedComparisonResult |
| let lhs: String |
| let rhs: String |
| let loc: SourceLoc |
| let xfail: TestRunPredicate |
| |
| init( |
| _ expectedUnicodeCollation: ExpectedComparisonResult, |
| _ lhs: String, _ rhs: String, |
| xfail: TestRunPredicate = .never, |
| file: String = #file, line: UInt = #line |
| ) { |
| self.expectedUnicodeCollation = expectedUnicodeCollation |
| self.lhs = lhs |
| self.rhs = rhs |
| self.loc = SourceLoc(file, line, comment: "test data") |
| self.xfail = xfail |
| } |
| |
| func replacingPredicate(_ xfail: TestRunPredicate) -> ComparisonTest { |
| return ComparisonTest(expectedUnicodeCollation, lhs, rhs, |
| xfail: xfail, file: loc.file, line: loc.line) |
| } |
| } |
| |
| // List test cases for comparisons and prefix/suffix. Ideally none fail. |
| |
| let tests = [ |
| ComparisonTest(.eq, "", ""), |
| ComparisonTest(.lt, "", "a"), |
| |
| // ASCII cases |
| ComparisonTest(.lt, "t", "tt"), |
| ComparisonTest(.gt, "t", "Tt"), |
| ComparisonTest(.gt, "\u{0}", ""), |
| ComparisonTest(.eq, "\u{0}", "\u{0}"), |
| |
| ComparisonTest(.lt, "\r\n", "t"), |
| ComparisonTest(.gt, "\r\n", "\n"), |
| ComparisonTest(.lt, "\u{0}", "\u{0}\u{0}"), |
| |
| // Whitespace |
| // U+000A LINE FEED (LF) |
| // U+000B LINE TABULATION |
| // U+000C FORM FEED (FF) |
| // U+0085 NEXT LINE (NEL) |
| // U+2028 LINE SEPARATOR |
| // U+2029 PARAGRAPH SEPARATOR |
| ComparisonTest(.gt, "\u{0085}", "\n"), |
| ComparisonTest(.gt, "\u{000b}", "\n"), |
| ComparisonTest(.gt, "\u{000c}", "\n"), |
| ComparisonTest(.gt, "\u{2028}", "\n"), |
| ComparisonTest(.gt, "\u{2029}", "\n"), |
| ComparisonTest(.gt, "\r\n\r\n", "\r\n"), |
| |
| // U+0301 COMBINING ACUTE ACCENT |
| // U+00E1 LATIN SMALL LETTER A WITH ACUTE |
| ComparisonTest(.eq, "a\u{301}", "\u{e1}"), |
| ComparisonTest(.lt, "a", "a\u{301}"), |
| ComparisonTest(.lt, "a", "\u{e1}"), |
| |
| // U+304B HIRAGANA LETTER KA |
| // U+304C HIRAGANA LETTER GA |
| // U+3099 COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK |
| ComparisonTest(.eq, "\u{304b}", "\u{304b}"), |
| ComparisonTest(.eq, "\u{304c}", "\u{304c}"), |
| ComparisonTest(.lt, "\u{304b}", "\u{304c}"), |
| ComparisonTest(.lt, "\u{304b}", "\u{304c}\u{3099}"), |
| ComparisonTest(.eq, "\u{304c}", "\u{304b}\u{3099}"), |
| ComparisonTest(.lt, "\u{304c}", "\u{304c}\u{3099}"), |
| |
| // U+212B ANGSTROM SIGN |
| // U+030A COMBINING RING ABOVE |
| // U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE |
| ComparisonTest(.eq, "\u{212b}", "A\u{30a}"), |
| ComparisonTest(.eq, "\u{212b}", "\u{c5}"), |
| ComparisonTest(.eq, "A\u{30a}", "\u{c5}"), |
| ComparisonTest(.gt, "A\u{30a}", "a"), |
| ComparisonTest(.lt, "A", "A\u{30a}"), |
| |
| // U+2126 OHM SIGN |
| // U+03A9 GREEK CAPITAL LETTER OMEGA |
| ComparisonTest(.eq, "\u{2126}", "\u{03a9}"), |
| |
| // U+0323 COMBINING DOT BELOW |
| // U+0307 COMBINING DOT ABOVE |
| // U+1E63 LATIN SMALL LETTER S WITH DOT BELOW |
| // U+1E69 LATIN SMALL LETTER S WITH DOT BELOW AND DOT ABOVE |
| ComparisonTest(.eq, "\u{1e69}", "s\u{323}\u{307}"), |
| ComparisonTest(.eq, "\u{1e69}", "s\u{307}\u{323}"), |
| ComparisonTest(.eq, "\u{1e69}", "\u{1e63}\u{307}"), |
| ComparisonTest(.eq, "\u{1e63}", "s\u{323}"), |
| ComparisonTest(.eq, "\u{1e63}\u{307}", "s\u{323}\u{307}"), |
| ComparisonTest(.eq, "\u{1e63}\u{307}", "s\u{307}\u{323}"), |
| ComparisonTest(.lt, "s\u{323}", "\u{1e69}"), |
| |
| // U+FB01 LATIN SMALL LIGATURE FI |
| ComparisonTest(.eq, "\u{fb01}", "\u{fb01}"), |
| ComparisonTest(.lt, "fi", "\u{fb01}"), |
| |
| // U+1F1E7 REGIONAL INDICATOR SYMBOL LETTER B |
| // \u{1F1E7}\u{1F1E7} Flag of Barbados |
| ComparisonTest(.lt, "\u{1F1E7}", "\u{1F1E7}\u{1F1E7}"), |
| |
| // Test that Unicode collation is performed in deterministic mode. |
| // |
| // U+0301 COMBINING ACUTE ACCENT |
| // U+0341 COMBINING ACUTE TONE MARK |
| // U+0954 DEVANAGARI ACUTE ACCENT |
| // |
| // Collation elements from DUCET: |
| // 0301 ; [.0000.0024.0002] # COMBINING ACUTE ACCENT |
| // 0341 ; [.0000.0024.0002] # COMBINING ACUTE TONE MARK |
| // 0954 ; [.0000.0024.0002] # DEVANAGARI ACUTE ACCENT |
| // |
| // U+0301 and U+0954 don't decompose in the canonical decomposition mapping. |
| // U+0341 has a canonical decomposition mapping of U+0301. |
| ComparisonTest(.eq, "\u{0301}", "\u{0341}"), |
| ComparisonTest(.lt, "\u{0301}", "\u{0954}"), |
| ComparisonTest(.lt, "\u{0341}", "\u{0954}"), |
| |
| // (U+212A KELVIN SIGN) normalizes to ASCII "K" |
| ComparisonTest(.eq, "K", "\u{212A}"), |
| ] |
| |
| func checkStringComparison( |
| _ expected: ExpectedComparisonResult, |
| _ lhs: String, _ rhs: String, _ stackTrace: SourceLocStack |
| ) { |
| // String / String |
| expectEqual(expected.isEQ(), lhs == rhs, stackTrace: stackTrace) |
| expectEqual(expected.isNE(), lhs != rhs, stackTrace: stackTrace) |
| checkHashable( |
| expectedEqual: expected.isEQ(), |
| lhs, rhs, stackTrace: stackTrace.withCurrentLoc()) |
| |
| expectEqual(expected.isLT(), lhs < rhs, stackTrace: stackTrace) |
| expectEqual(expected.isLE(), lhs <= rhs, stackTrace: stackTrace) |
| expectEqual(expected.isGE(), lhs >= rhs, stackTrace: stackTrace) |
| expectEqual(expected.isGT(), lhs > rhs, stackTrace: stackTrace) |
| checkComparable(expected, lhs, rhs, stackTrace: stackTrace.withCurrentLoc()) |
| |
| #if _runtime(_ObjC) |
| // NSString / NSString |
| let lhsNSString = lhs as NSString |
| let rhsNSString = rhs as NSString |
| let expectedEqualUnicodeScalars = |
| Array(lhs.unicodeScalars) == Array(rhs.unicodeScalars) |
| // FIXME: Swift String and NSString comparison may not be equal. |
| expectEqual( |
| expectedEqualUnicodeScalars, lhsNSString == rhsNSString, |
| stackTrace: stackTrace) |
| expectEqual( |
| !expectedEqualUnicodeScalars, lhsNSString != rhsNSString, |
| stackTrace: stackTrace) |
| checkHashable( |
| expectedEqual: expectedEqualUnicodeScalars, |
| lhsNSString, rhsNSString, |
| stackTrace: stackTrace.withCurrentLoc()) |
| #endif |
| } |
| |
| // Mark the test cases that are expected to fail in checkStringComparison |
| |
| let comparisonTests = tests |
| |
| for test in comparisonTests { |
| StringTests.test("String.{Equatable,Hashable,Comparable}: line \(test.loc.line)") |
| .xfail(test.xfail) |
| .code { |
| checkStringComparison( |
| test.expectedUnicodeCollation, test.lhs, test.rhs, |
| test.loc.withCurrentLoc()) |
| checkStringComparison( |
| test.expectedUnicodeCollation.flip(), test.rhs, test.lhs, |
| test.loc.withCurrentLoc()) |
| } |
| } |
| |
| func checkCharacterComparison( |
| _ expected: ExpectedComparisonResult, |
| _ lhs: Character, _ rhs: Character, _ stackTrace: SourceLocStack |
| ) { |
| // Character / Character |
| expectEqual(expected.isEQ(), lhs == rhs, stackTrace: stackTrace) |
| expectEqual(expected.isNE(), lhs != rhs, stackTrace: stackTrace) |
| checkHashable( |
| expectedEqual: expected.isEQ(), |
| lhs, rhs, stackTrace: stackTrace.withCurrentLoc()) |
| |
| expectEqual(expected.isLT(), lhs < rhs, stackTrace: stackTrace) |
| expectEqual(expected.isLE(), lhs <= rhs, stackTrace: stackTrace) |
| expectEqual(expected.isGE(), lhs >= rhs, stackTrace: stackTrace) |
| expectEqual(expected.isGT(), lhs > rhs, stackTrace: stackTrace) |
| checkComparable(expected, lhs, rhs, stackTrace: stackTrace.withCurrentLoc()) |
| } |
| |
| for test in comparisonTests { |
| if test.lhs.count == 1 && test.rhs.count == 1 { |
| StringTests.test("Character.{Equatable,Hashable,Comparable}: line \(test.loc.line)") |
| .xfail(test.xfail) |
| .code { |
| let lhsCharacter = Character(test.lhs) |
| let rhsCharacter = Character(test.rhs) |
| checkCharacterComparison( |
| test.expectedUnicodeCollation, lhsCharacter, rhsCharacter, |
| test.loc.withCurrentLoc()) |
| checkCharacterComparison( |
| test.expectedUnicodeCollation.flip(), rhsCharacter, lhsCharacter, |
| test.loc.withCurrentLoc()) |
| } |
| } |
| } |
| |
| func checkHasPrefixHasSuffix( |
| _ lhs: String, _ rhs: String, _ stackTrace: SourceLocStack |
| ) { |
| #if _runtime(_ObjC) |
| if rhs == "" { |
| expectTrue(lhs.hasPrefix(rhs), stackTrace: stackTrace) |
| expectTrue(lhs.hasSuffix(rhs), stackTrace: stackTrace) |
| return |
| } |
| if lhs == "" { |
| expectFalse(lhs.hasPrefix(rhs), stackTrace: stackTrace) |
| expectFalse(lhs.hasSuffix(rhs), stackTrace: stackTrace) |
| return |
| } |
| |
| // To determine the expected results, compare grapheme clusters, |
| // scalar-to-scalar, of the NFD form of the strings. |
| let lhsNFDGraphemeClusters = |
| lhs.decomposedStringWithCanonicalMapping.map { |
| Array(String($0).unicodeScalars) |
| } |
| let rhsNFDGraphemeClusters = |
| rhs.decomposedStringWithCanonicalMapping.map { |
| Array(String($0).unicodeScalars) |
| } |
| let expectHasPrefix = lhsNFDGraphemeClusters.starts( |
| with: rhsNFDGraphemeClusters, by: (==)) |
| |
| let expectHasSuffix = lhsNFDGraphemeClusters.lazy.reversed() |
| .starts(with: rhsNFDGraphemeClusters.lazy.reversed(), by: (==)) |
| |
| expectEqual(expectHasPrefix, lhs.hasPrefix(rhs), stackTrace: stackTrace) |
| expectEqual(expectHasSuffix, lhs.hasSuffix(rhs), stackTrace: stackTrace) |
| #endif |
| } |
| |
| StringTests.test("LosslessStringConvertible") { |
| checkLosslessStringConvertible(comparisonTests.map { $0.lhs }) |
| checkLosslessStringConvertible(comparisonTests.map { $0.rhs }) |
| } |
| |
| // Mark the test cases that are expected to fail in checkHasPrefixHasSuffix |
| |
| let substringTests = tests.map { |
| (test: ComparisonTest) -> ComparisonTest in |
| switch (test.expectedUnicodeCollation, test.lhs, test.rhs) { |
| |
| case (.gt, "\r\n", "\n"): |
| return test.replacingPredicate(.objCRuntime( |
| "blocked on rdar://problem/19036555")) |
| |
| default: |
| return test |
| } |
| } |
| |
| for test in substringTests { |
| StringTests.test("hasPrefix,hasSuffix: line \(test.loc.line)") |
| .skip(.nativeRuntime( |
| "String.has{Prefix,Suffix} defined when _runtime(_ObjC)")) |
| .xfail(test.xfail) |
| .code { |
| checkHasPrefixHasSuffix(test.lhs, test.rhs, test.loc.withCurrentLoc()) |
| checkHasPrefixHasSuffix(test.rhs, test.lhs, test.loc.withCurrentLoc()) |
| |
| let fragment = "abc" |
| let combiner = "\u{0301}" // combining acute accent |
| |
| checkHasPrefixHasSuffix(test.lhs + fragment, test.rhs, test.loc.withCurrentLoc()) |
| checkHasPrefixHasSuffix(fragment + test.lhs, test.rhs, test.loc.withCurrentLoc()) |
| checkHasPrefixHasSuffix(test.lhs + combiner, test.rhs, test.loc.withCurrentLoc()) |
| checkHasPrefixHasSuffix(combiner + test.lhs, test.rhs, test.loc.withCurrentLoc()) |
| } |
| } |
| |
| StringTests.test("SameTypeComparisons") { |
| // U+0323 COMBINING DOT BELOW |
| // U+0307 COMBINING DOT ABOVE |
| // U+1E63 LATIN SMALL LETTER S WITH DOT BELOW |
| let xs = "\u{1e69}" |
| expectTrue(xs == "s\u{323}\u{307}") |
| expectFalse(xs != "s\u{323}\u{307}") |
| expectTrue("s\u{323}\u{307}" == xs) |
| expectFalse("s\u{323}\u{307}" != xs) |
| expectTrue("\u{1e69}" == "s\u{323}\u{307}") |
| expectFalse("\u{1e69}" != "s\u{323}\u{307}") |
| expectTrue(xs == xs) |
| expectFalse(xs != xs) |
| } |
| |
| StringTests.test("CompareStringsWithUnpairedSurrogates") |
| .xfail( |
| .always("<rdar://problem/18029104> Strings referring to underlying " + |
| "storage with unpaired surrogates compare unequal")) |
| .code { |
| let donor = "abcdef" |
| let acceptor = "\u{1f601}\u{1f602}\u{1f603}" |
| |
| expectEqual("\u{fffd}\u{1f602}\u{fffd}", |
| acceptor[ |
| donor.index(donor.startIndex, offsetBy: 1) ..< |
| donor.index(donor.startIndex, offsetBy: 5) |
| ] |
| ) |
| } |
| |
| StringTests.test("[String].joined() -> String") { |
| let s = ["hello", "world"].joined() |
| _ = s == "" // should compile without error |
| } |
| |
| StringTests.test("UnicodeScalarView.Iterator.Lifetime") { |
| // Tests that String.UnicodeScalarView.Iterator is maintaining the lifetime of |
| // an underlying String buffer. https://bugs.swift.org/browse/SR-5401 |
| // |
| // WARNING: it is very easy to write this test so it produces false negatives |
| // (i.e. passes even when the code is broken). The array, for example, seems |
| // to be a requirement. So perturb this test with care! |
| let sources = ["𝓣his 𝓘s 𝓜uch 𝓛onger 𝓣han 𝓐ny 𝓢mall 𝓢tring 𝓑uffer"] |
| for s in sources { |
| // Append something to s so that it creates a dynamically-allocated buffer. |
| let i = (s + "X").unicodeScalars.makeIterator() |
| expectEqualSequence(s.unicodeScalars, IteratorSequence(i).dropLast(), |
| "Actual Contents: \(Array(IteratorSequence(i)))") |
| } |
| } |
| |
| StringTests.test("Regression/rdar-33276845") { |
| // These two cases fail slightly differently when the code is broken |
| // See rdar://33276845 |
| do { |
| let s = String(repeating: "x", count: 0xffff) |
| let a = Array(s.utf8) |
| expectNotEqual(0, a.count) |
| } |
| do { |
| let s = String(repeating: "x", count: 0x1_0010) |
| let a = Array(s.utf8) |
| expectNotEqual(0, a.count) |
| } |
| } |
| |
| StringTests.test("Regression/corelibs-foundation") { |
| struct NSRange { var location, length: Int } |
| |
| func NSFakeRange(_ location: Int, _ length: Int) -> NSRange { |
| return NSRange(location: location, length: length) |
| } |
| |
| func substring(of _storage: String, with range: NSRange) -> String { |
| let start = _storage.utf16.startIndex |
| let min = _storage.utf16.index(start, offsetBy: range.location) |
| let max = _storage.utf16.index( |
| start, offsetBy: range.location + range.length) |
| |
| if let substr = String(_storage.utf16[min..<max]) { |
| return substr |
| } |
| //If we come here, then the range has created unpaired surrogates on either end. |
| //An unpaired surrogate is replaced by OXFFFD - the Unicode Replacement Character. |
| //The CRLF ("\r\n") sequence is also treated like a surrogate pair, but its constinuent |
| //characters "\r" and "\n" can exist outside the pair! |
| |
| let replacementCharacter = String(describing: UnicodeScalar(0xFFFD)!) |
| let CR: UInt16 = 13 //carriage return |
| let LF: UInt16 = 10 //new line |
| |
| //make sure the range is of non-zero length |
| guard range.length > 0 else { return "" } |
| |
| //if the range is pointing to a single unpaired surrogate |
| if range.length == 1 { |
| switch _storage.utf16[min] { |
| case CR: return "\r" |
| case LF: return "\n" |
| default: return replacementCharacter |
| } |
| } |
| |
| //set the prefix and suffix characters |
| let prefix = _storage.utf16[min] == LF ? "\n" : replacementCharacter |
| let suffix = _storage.utf16[_storage.utf16.index(before: max)] == CR |
| ? "\r" : replacementCharacter |
| |
| let postMin = _storage.utf16.index(after: min) |
| |
| //if the range breaks a surrogate pair at the beginning of the string |
| if let substrSuffix = String( |
| _storage.utf16[postMin..<max]) { |
| return prefix + substrSuffix |
| } |
| |
| let preMax = _storage.utf16.index(before: max) |
| //if the range breaks a surrogate pair at the end of the string |
| if let substrPrefix = String(_storage.utf16[min..<preMax]) { |
| return substrPrefix + suffix |
| } |
| |
| //the range probably breaks surrogate pairs at both the ends |
| guard postMin <= preMax else { return prefix + suffix } |
| |
| let substr = String(_storage.utf16[postMin..<preMax])! |
| return prefix + substr + suffix |
| } |
| |
| let trivial = "swift.org" |
| expectEqual(substring(of: trivial, with: NSFakeRange(0, 5)), "swift") |
| |
| let surrogatePairSuffix = "Hurray🎉" |
| expectEqual(substring(of: surrogatePairSuffix, with: NSFakeRange(0, 7)), "Hurray�") |
| |
| let surrogatePairPrefix = "🐱Cat" |
| expectEqual(substring(of: surrogatePairPrefix, with: NSFakeRange(1, 4)), "�Cat") |
| |
| let singleChar = "😹" |
| expectEqual(substring(of: singleChar, with: NSFakeRange(0,1)), "�") |
| |
| let crlf = "\r\n" |
| expectEqual(substring(of: crlf, with: NSFakeRange(0,1)), "\r") |
| expectEqual(substring(of: crlf, with: NSFakeRange(1,1)), "\n") |
| expectEqual(substring(of: crlf, with: NSFakeRange(1,0)), "") |
| |
| let bothEnds1 = "😺😺" |
| expectEqual(substring(of: bothEnds1, with: NSFakeRange(1,2)), "��") |
| |
| let s1 = "😺\r\n" |
| expectEqual(substring(of: s1, with: NSFakeRange(1,2)), "�\r") |
| |
| let s2 = "\r\n😺" |
| expectEqual(substring(of: s2, with: NSFakeRange(1,2)), "\n�") |
| |
| let s3 = "😺cats😺" |
| expectEqual(substring(of: s3, with: NSFakeRange(1,6)), "�cats�") |
| |
| let s4 = "😺cats\r\n" |
| expectEqual(substring(of: s4, with: NSFakeRange(1,6)), "�cats\r") |
| |
| let s5 = "\r\ncats😺" |
| expectEqual(substring(of: s5, with: NSFakeRange(1,6)), "\ncats�") |
| } |
| |
| |
| runAllTests() |