Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] Enable and disable tests for hybrid globalization on Apple #108187

Merged
merged 4 commits into from
Sep 27, 2024
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
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
Loading