Skip to content

Commit

Permalink
[iOS] Enable and disable tests for hybrid globalization on Apple (dot…
Browse files Browse the repository at this point in the history
…net#108187)

Enable and disable tests for hybrid globalization on Apple
  • Loading branch information
mkhamoyan authored Sep 27, 2024
1 parent 01aa3d9 commit c315bac
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 48 deletions.
6 changes: 6 additions & 0 deletions docs/design/features/globalization-hybrid-mode.md
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,12 @@ Affected public APIs:
- String.Compare,
- String.Equals.

Mapped to Apple Native API `compare:options:range:locale:`(https://developer.apple.com/documentation/foundation/nsstring/1414561-compare?language=objc)
This implementation uses normalization techniques such as `precomposedStringWithCanonicalMapping`,
which can result in behavior differences compared to other platforms.
Specifically, the use of precomposed strings and additional locale-based string folding can affect the results of comparisons.
Due to these differences, the exact result of string compariso on Apple platforms may differ.

The number of `CompareOptions` and `NSStringCompareOptions` combinations are limited. Originally supported combinations can be found [here for CompareOptions](https://learn.microsoft.com/dotnet/api/system.globalization.compareoptions) and [here for NSStringCompareOptions](https://developer.apple.com/documentation/foundation/nsstringcompareoptions).

- `IgnoreSymbols` is not supported because there is no equivalent in native api. Throws `PlatformNotSupportedException`.
Expand Down
76 changes: 41 additions & 35 deletions src/libraries/Common/tests/Tests/System/StringTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1011,7 +1011,6 @@ public static void MakeSureNoCompareToChecksGoOutOfRange_StringComparison()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
public static void CompareToNoMatch_StringComparison()
{
for (int length = 1; length < 150; length++)
Expand All @@ -1035,24 +1034,29 @@ public static void CompareToNoMatch_StringComparison()
var secondSpan = new ReadOnlySpan<char>(second);
Assert.True(0 > firstSpan.CompareTo(secondSpan, StringComparison.Ordinal));

// Due to differences in the implementation, the exact result of CompareTo will not necessarily match with string.Compare.
// However, the sign will match, which is what defines correctness.
Assert.Equal(
Math.Sign(string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.OrdinalIgnoreCase)),
Math.Sign(firstSpan.CompareTo(secondSpan, StringComparison.OrdinalIgnoreCase)));

Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCulture),
firstSpan.CompareTo(secondSpan, StringComparison.CurrentCulture));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase),
firstSpan.CompareTo(secondSpan, StringComparison.CurrentCultureIgnoreCase));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCulture),
firstSpan.CompareTo(secondSpan, StringComparison.InvariantCulture));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase),
firstSpan.CompareTo(secondSpan, StringComparison.InvariantCultureIgnoreCase));
// On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques
// like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms.
if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform)
{
// Due to differences in the implementation, the exact result of CompareTo will not necessarily match with string.Compare.
// However, the sign will match, which is what defines correctness.
Assert.Equal(
Math.Sign(string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.OrdinalIgnoreCase)),
Math.Sign(firstSpan.CompareTo(secondSpan, StringComparison.OrdinalIgnoreCase)));

Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCulture),
firstSpan.CompareTo(secondSpan, StringComparison.CurrentCulture));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase),
firstSpan.CompareTo(secondSpan, StringComparison.CurrentCultureIgnoreCase));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCulture),
firstSpan.CompareTo(secondSpan, StringComparison.InvariantCulture));
Assert.Equal(
string.Compare(firstSpan.ToString(), secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase),
firstSpan.CompareTo(secondSpan, StringComparison.InvariantCultureIgnoreCase));
}
}
}
}
Expand Down Expand Up @@ -1286,7 +1290,6 @@ public static void ContainsMatchDifferentSpans_StringComparison()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
public static void ContainsNoMatch_StringComparison()
{
for (int length = 1; length < 150; length++)
Expand All @@ -1312,19 +1315,24 @@ public static void ContainsNoMatch_StringComparison()

Assert.False(firstSpan.Contains(secondSpan, StringComparison.OrdinalIgnoreCase));

// Different behavior depending on OS
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCulture),
firstSpan.Contains(secondSpan, StringComparison.CurrentCulture));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase),
firstSpan.Contains(secondSpan, StringComparison.CurrentCultureIgnoreCase));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCulture),
firstSpan.Contains(secondSpan, StringComparison.InvariantCulture));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase),
firstSpan.Contains(secondSpan, StringComparison.InvariantCultureIgnoreCase));
// On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques
// like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms.
if (PlatformDetection.IsNotHybridGlobalizationOnApplePlatform)
{
// Different behavior depending on OS
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCulture),
firstSpan.Contains(secondSpan, StringComparison.CurrentCulture));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.CurrentCultureIgnoreCase),
firstSpan.Contains(secondSpan, StringComparison.CurrentCultureIgnoreCase));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCulture),
firstSpan.Contains(secondSpan, StringComparison.InvariantCulture));
Assert.Equal(
firstSpan.ToString().StartsWith(secondSpan.ToString(), StringComparison.InvariantCultureIgnoreCase),
firstSpan.Contains(secondSpan, StringComparison.InvariantCultureIgnoreCase));
}
}
}
}
Expand Down Expand Up @@ -2113,7 +2121,6 @@ public static void EndsWithMatchDifferentSpans_StringComparison()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
public static void EndsWithNoMatch_StringComparison()
{
for (int length = 1; length < 150; length++)
Expand Down Expand Up @@ -7379,7 +7386,6 @@ public static void StartsWithMatchDifferentSpans_StringComparison()
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
public static void StartsWithNoMatch_StringComparison()
{
for (int length = 1; length < 150; length++)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ public static class SqlStringSortingTest

private static readonly UnicodeEncoding s_unicodeEncoding = new UnicodeEncoding(bigEndian: false, byteOrderMark: false, throwOnInvalidBytes: true);

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
// On Apple platforms, the string comparison implementation relies on native Apple functions which uses normalization techniques, which can result in behavior differences compared to other platforms.
// Specifically, the use of precomposed strings and additional locale-based string folding can affect the results of comparisons with certain options like `IgnoreKanaType`.
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))]
[InlineData("ja-JP", 0x0411)] // Japanese - Japan
[InlineData("ar-SA", 0x0401)] // Arabic - Saudi Arabia
[InlineData("de-DE", 0x0407)] // German - Germany
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ public void GetStringComparer_Invalid()
AssertExtensions.Throws<ArgumentException>("options", () => new CultureInfo("tr-TR").CompareInfo.GetStringComparer(CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreCase));
}

[Theory]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))]
[InlineData("hello", "hello", "fr-FR", CompareOptions.IgnoreCase, 0, 0)]
[InlineData("hello", "HELLo", "fr-FR", CompareOptions.IgnoreCase, 0, 0)]
[InlineData("hello", null, "fr-FR", CompareOptions.IgnoreCase, 1, 1)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ public class CompareInfoHashCodeTests : CompareInfoTestsBase
{

[OuterLoop]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
// On Apple platforms, string comparison is handled by native Apple functions, which apply normalization techniques
// like `precomposedStringWithCanonicalMapping`. This can lead to differences in behavior compared to other platforms.
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))]
public void CheckHashingInLineWithEqual()
{
int additionalCollisions = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,8 +221,7 @@ public void CultureName_Set(AssemblyName assemblyName, string originalCultureNam
Assert.Equal(new AssemblyName(expectedEqualString).FullName, assemblyName.FullName);
}

[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))]
public void CultureName_Set_Invalid_ThrowsCultureNotFoundException()
{
var assemblyName = new AssemblyName("Test");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1982,7 +1982,10 @@ public static IEnumerable<object[]> Parse_ValidInput_Succeeds_MemberData()
yield return new object[] { "#2020-5-7T09:37:00.0000000+00:00#\0", CultureInfo.InvariantCulture, TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2020, 5, 7, 9, 37, 0, DateTimeKind.Utc), TimeZoneInfo.Local) };
yield return new object[] { "2020-5-7T09:37:00.0000000+00:00", CultureInfo.InvariantCulture, TimeZoneInfo.ConvertTimeFromUtc(new DateTime(2020, 5, 7, 9, 37, 0, DateTimeKind.Utc), TimeZoneInfo.Local) };

if (PlatformDetection.IsNotInvariantGlobalization)
// On Apple platforms, the handling calendars relies on native Apple APIs (NSCalendar).
// These APIs can cause differences in behavior when parsing or formatting dates compared to other platforms.
// Specifically, the way Apple handles calendar identifiers and date formats for cultures like "he-IL" may lead to variations in the output.
if (PlatformDetection.IsNotInvariantGlobalization && PlatformDetection.IsNotHybridGlobalizationOnApplePlatform)
{
DateTime today = DateTime.Today;
var hebrewCulture = new CultureInfo("he-IL");
Expand All @@ -2003,7 +2006,6 @@ public static IEnumerable<object[]> Parse_ValidInput_Succeeds_MemberData()
}

[Theory]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
[MemberData(nameof(Parse_ValidInput_Succeeds_MemberData))]
public static void Parse_ValidInput_Succeeds(string input, CultureInfo culture, DateTime? expected)
{
Expand Down Expand Up @@ -2464,7 +2466,6 @@ public static IEnumerable<object[]> ToString_MatchesExpected_MemberData()
}

[Theory]
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
[MemberData(nameof(Parse_ValidInput_Succeeds_MemberData))]
public static void Parse_Span_ValidInput_Succeeds(string input, CultureInfo culture, DateTime? expected)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ public static void Casing_Invariant(int original, int upper, int lower)
Assert.Equal(new Rune(lower), Rune.ToLowerInvariant(rune));
}

[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalizationAndNotHybridOnBrowser))]
[ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalizationAndNotHybridOnBrowser), nameof(PlatformDetection.IsNotHybridGlobalizationOnApplePlatform))]
// HybridGlobalization on Apple mobile platforms has issues with casing dotless I
// HybridGlobalization on Browser uses Invariant HashCode and SortKey, so its effect does not match this of ICU
[InlineData('0', '0', '0')]
[InlineData('a', 'A', 'a')]
Expand All @@ -71,7 +72,6 @@ public static void Casing_Invariant(int original, int upper, int lower)
[InlineData('\u0131', '\u0131', '\u0131')] // U+0131 LATIN SMALL LETTER DOTLESS I
[InlineData(0x10400, 0x10400, 0x10428)] // U+10400 DESERET CAPITAL LETTER LONG I
[InlineData(0x10428, 0x10400, 0x10428)] // U+10428 DESERET SMALL LETTER LONG I
[ActiveIssue("https://github.com/dotnet/runtime/issues/95338", typeof(PlatformDetection), nameof(PlatformDetection.IsHybridGlobalizationOnApplePlatform))]
public static void ICU_Casing_Invariant(int original, int upper, int lower)
{
var rune = new Rune(original);
Expand Down

0 comments on commit c315bac

Please sign in to comment.