diff --git a/src/libraries/Common/Common.Tests.slnx b/src/libraries/Common/Common.Tests.slnx index c6b9f2a7939d0f..218fb5ae746858 100644 --- a/src/libraries/Common/Common.Tests.slnx +++ b/src/libraries/Common/Common.Tests.slnx @@ -2,9 +2,11 @@ + + diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj new file mode 100644 index 00000000000000..386a669efe325c --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -0,0 +1,52 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkCurrent) + enable + true + true + 16.0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CharUnicodeInfo\UnicodeData.$(UnicodeUcdVersion).txt + UnicodeData.txt + + + + diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt b/src/libraries/Common/tests/ComplianceTests/GB18030/Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt new file mode 100644 index 00000000000000..7534f5969d0079 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt @@ -0,0 +1,45 @@ +Short sample strings for inputs with length limitation: +�5�0���8�7G�9�0A�9�4D�4�6���8�5C�@�2�3�5�7�9�2�ښ7�6�4�2�5�9�J�9�69�9�7�W�5�7�4�8 +�י8�0Q�5�2�9�6d�9�6�1�4�6�4C�F�9�0���5�8���9�9g�7�9�9�5�9�8C�4�9�O�5�0�2�2�x�5�8 +���3�8c�9�0d�ҙ2�3�9�0��a�ܘ9�1�5�5�3�9�9�9�9�4�٘5�9�4�9G�7�9�S�5�1A�5�9�9�9�7�8 +�3�9d�9�6�5�9�8�2�o�9�0C�9�8�9�7���6�5�ј5�0�9�0���P�5�2�9�5���5�0s�8�0�4�0�0�9 +�9�1�M�5�45�9�0���2�9r�9�6��5�2�5�2�Ș5�1�I�9�1�9�4�ښ8�9�8�4�0�9�5�3�4�0a +A�5�9�9�0��r�8�1G�8�1�6�1�O�9�5���3�3�ς5�4�9�7�9�1�5�2�0�2�4�9�����8�0�0�0�5�2 +�D�6�4���8�2�5�1�ׂ0�3N�9�4D�8�3�3�8�K�9�8�0�9��9�3���5�5�3�2�5�3�5�32�4�0�8�7 +��Ѡ�1�4A�3�6�5�0�9�6D�@�1�76�5�4�1�4���R�9�2���9�9�4�9�8�9�0�4�5�4�7�0G�5�6�7�2 +�S�5�8εP���1�5O�0�8���8�4v�9�0�3�0�4�6q�9�0�9�7�@�5�5�1�8�8�5�0�0�4�3���5�5�5�7 +�5�6ط���1�3�9�50���0�2�3�9�9�9D�0�8�M�9�1�����1�7�5�8�7�0�9�1�0�7�5�2�5�8�4�0 + +Long sample strings for inputs with extended length limitation�� +Group0: +�5�9�5�2�����1�4�9�1�@�O�9�9�5�88e�2�6�5�6�@���9�2�3�3�����4�8�9�0�9�8�9�9rP�5�7�5�5�9�6�0�7�ƪǪ�U1������U2�x�y�zU3�7�6�1�7�٨�9�7�9�1�9�2�9�5�5�7�5�2�5�9�5�8�4�2�7�0�5�8�9�66n + +Group1: +�����1�4�4�60u�9�9�9�6A�9�3�9�9�H���D�E�G�2�6�2�1�����7�8�9�7i�5�7�5�8�5�3�5�7�4�8�9�3�9�28�5�7�5�8�9�2�9�5�9�7�J�O�9�7�9�8�9�6�9�7�0�7�0�5�5�9�5�0�4�2�4�4�4�5�4�6�7�6�7�7�7�8�7�9�9�2�9�8����U1����U2�@�A�BU3 + +Group2: +�������9�1�8�5�J���W�9�7�1�4�2�1�@���8�7�8�8�8�58A�3�0�3�7�3�0�8�9�9�6�8�1FOC�8�2�8�3�8�7�8�7�2�4�2�5�7�0�ã�5�9�5�0�9�9�9�0�0�5�0�6�0�1�0�2�0�5�Q�5�9�5�0�9�0�9�9�4�6�4�9�4�7�4�2�4�3�4�4�ƪǪ�U1������U2�x�y�zU3�5�1�5�2�9�6�9�3�9�3�9�6�9�6�8�0 + +Group3: +�8�7�3�0����D�Ňև刊�۬@�A���B�cz�1�5�1�9�2�0�9�6�9�1�9�8ao�5�9�5�9���і7�4�7�8�0�9�0�2�0�3�0�5�3�0�3�8�P�R�����9�1�9�7�5�1�5�2�8�0�7�0�4�6�4�7�4�8�4�9����U1������U2�`�a�bU3�8�6�8�7�8�3�8�3�8�4E9z�8�1�8�4�8�2�5�1�5�2�9�0�9�8�5�3�5�4C + +Group4: +�5�8�5�0���������3�8�9�6�H�J�L�O50R�8�6�8�2�8�5�0�0�5�9�5�2�9�0�9�6�9�38U5�1�3�3�0�4�0�2�8�3�0�3�9�5�3�5�4�S���9�0�9�18U5�0�0�0�1�0�4�0�5�7�9�7�3�7�3����U1������U2�@�A�BU3�5�3�5�4�9�0�9�9�5�5�5�6�����Κ4�3�4�9�4�3�4�6�8�5�8�0�8�8�8�1�򆿇� + +Group5: +���Q�F���9�6�8�6�5�5�5�6�4�5�4�6�4�78bبأؤإ�6�6�6�5b�9�6�9�3�9�8�B�D�F�G�6�0�6�0�8�9�8�0�9�3�8�4�0�5�0�6�0�7�0�8�5�5�5�6�5�3�2�5�šȘ5�5�5�7�5�7�5�8�9�0�9�0�9�7�9�9��������U1������U2������U3�3�5�3�6�M�����4�7�4�858b + +Group6: +�5�1�9�9�0�0�5�5�5�1ثج�壘5�6�5�9�Ǧ��j�����X�8�0�8�1�9�0�9�70wL�5�2�6�8�9�1�9�3�9�4�5�4�5�5�5�6�5�8�2�3�2�9�9�0�9�8�8�5�8�6�8�7�8�8�8�9�0�1�0�2�0�0�0�3A�5�7�5�8�8�7�6�9�Ь��U1������U2�P�a�rU3�5�9�5�2�T���5�8�5�0g9�9�1�9�9�H�J�L�M�} + +Group7: +�5�8�5�2�����嫘8�6�8�0pW5�2�2�1�0�0�3�4�1�����������6�4�8�6�8�5�8�4@pU�2�2�7�9�7�1�7�3���ã��J�N�9�3�9�4�9�8�4�0�4�9�4�1�R���R�M�]�5�1�5�2�0�3�0�3�0�4�0�8e�9�0�9�9�8�3�8�4�8�4�8�1�5�9�5�7�5�0�5�5�9�0�9�6�6�2�6�3�6�4�6�0�6�7�6�8�2�0�2�0�2�5����U1������U2�q�r�sU3 + +Group8: +�@�f���Y������˜4�1�3�5�5�7�5�6�8�9�8�3yG�0�3�0�2�0�8�0�7A�2�7�2�8�򣹣��ژ9�3�9�6�9�4�9�8y0R�0�2�0�3�0�1�0�2����������2�0�2�1�2�3�2�1�J�S�5�8�5�5�9�0�9�9�5�9�5�0���¯ï�U1����������U2����������U3�5�7�5�8�4�7�4�8�4�9y0R�6�2�6�2�6�1�6�2�6�6�8�8�8�9�9�3�9�2�9�3�9�5�9�3 + +Group9: +�������0�0�0�4�0�5Qa�O�L�MA3c�5�2�5�8�����������3�5�3�6�3�8�1�7�8�4�9�0�9�3�9�4�9�8V9�4�3�4�6�4�7�R���0�8�0�6�0�7�0�8�9�0�9�1�6�3�6�7�6�5�7�4�7�6�ئ¨��5�5�5�1�9�0�9�9�9�8�9�9�9�4�7�8�6�9�8�0�9�6m0�9�5�9�1�5�1�5�2�2�9�1�7����U1������U2������U3�5�9�5�0 + +Group10: +�5�0�5�1���������2�4�5�4�Z�w���N���2�2�1�2�1�7a4�1�9�0�9�0�9�9�6�9�1�9�4�9�8�7�9�7�0�7�3�7�1��Ǩ��1�0�9�8�8�5�9�6�8�2�5�5�5�8�5�8�5�0�4�1�4�2�4�4�J���5�3�5�5�0�3�0�4�0�5�0�4�0�7���������F�1�2�1�3�1�4�1�5�1�6�1�7�7�6�7�7�7�8�7�9�7�0�9�1�9�2�ɮʮ�U1��������U2�u�v�wU3iv \ No newline at end of file diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs new file mode 100644 index 00000000000000..c1d04bfff98641 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -0,0 +1,156 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Globalization.Tests; +using System.IO; +using System.Linq; +using System.Text; +using Xunit; + +namespace GB18030.Tests; + +public static class TestHelper +{ + // New Code Points in existing ranges + internal static IEnumerable CjkNewCodePoints { get; } = CreateRange(0x9FF0, 0x9FFF); + internal static IEnumerable CjkExtensionANewCodePoints { get; } = CreateRange(0x4DB6, 0x4DBF); + internal static IEnumerable CjkExtensionBNewCodePoints { get; } = CreateRange(0x2A6D7, 0x2A6DF); + internal static IEnumerable CjkExtensionCNewCodePoints { get; } = CreateRange(0x2B735, 0x2B739); + + // New ranges + internal static IEnumerable CjkExtensionG { get; } = CreateRange(0x30000, 0x3134A); + internal static IEnumerable CjkExtensionH { get; } = CreateRange(0x31350, 0x323AF); + internal static IEnumerable CjkExtensionI { get; } = CreateRange(0x2EBF0, 0x2EE5D); + + private static IEnumerable CreateRange(int first, int last) => Enumerable.Range(first, last - first + 1); + + private static IEnumerable s_gb18030CharUnicodeInfo { get; } = GetGB18030CharUnicodeInfo(); + private static IEnumerable GetGB18030CharUnicodeInfo() + { + const int CodePointsTotal = 9793; // Make sure a Unicode version downgrade doesn't make us lose coverage. + + var ret = CharUnicodeInfoTestData.TestCases.Where(tc => IsInGB18030Range(tc.CodePoint)); + Assert.Equal(CodePointsTotal, ret.Count()); + return ret; + + static bool IsInGB18030Range(int codePoint) + => (codePoint >= 0x9FF0 && codePoint <= 0x9FFF) || + (codePoint >= 0x4DB6 && codePoint <= 0x4DBF) || + (codePoint >= 0x2A6D7 && codePoint <= 0x2A6DF) || + (codePoint >= 0x2B735 && codePoint <= 0x2B739) || + (codePoint >= 0x30000 && codePoint <= 0x3134A) || + (codePoint >= 0x31350 && codePoint <= 0x323AF) || + (codePoint >= 0x2EBF0 && codePoint <= 0x2EE5D); + } + + internal static CultureInfo[] Cultures { get; } = [ + CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, + new CultureInfo("zh-CN")]; + + internal static CompareOptions[] CompareOptions { get; } = [ + System.Globalization.CompareOptions.None, + System.Globalization.CompareOptions.IgnoreCase]; + + internal static StringComparison[] NonOrdinalStringComparisons { get; } = [ + StringComparison.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase]; + + internal static string TestDataFilePath { get; } = Path.Combine(AppContext.BaseDirectory, "GB18030", "Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt"); + + private static Encoding? s_gb18030Encoding; + internal static Encoding GB18030Encoding + { + get + { + if (s_gb18030Encoding is null) + { +#if !NETFRAMEWORK + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + s_gb18030Encoding = Encoding.GetEncoding("gb18030"); + } + + return s_gb18030Encoding; + } + } + + private static readonly IEnumerable s_encodedTestData = GetTestData(); + + internal static IEnumerable DecodedTestData { get; } = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)); + + private static readonly IEnumerable s_splitNewLineDecodedTestData = DecodedTestData.SelectMany( + data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)); + + internal static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = + s_splitNewLineDecodedTestData.SelectMany( + (data) => + { + const int MaxPathSegmentName = 128; + Encoding fileSystemEncoding = PlatformDetection.IsWindows ? Encoding.Unicode : Encoding.UTF8; + + if (fileSystemEncoding.GetByteCount(data) <= MaxPathSegmentName) + return [data]; + + List result = new(); + string current = string.Empty; + foreach (string element in GetTextElements(data)) + { + if (fileSystemEncoding.GetByteCount(current) > MaxPathSegmentName) + { + result.Add(current); + current = string.Empty; + } + current += element; + } + result.Add(current); + return result; + }); + + public static IEnumerable EncodedMemberData { get; } = s_encodedTestData.Select(data => new object[] { data }); + public static IEnumerable DecodedMemberData { get; } = DecodedTestData.Select(data => new object[] { data }); + public static IEnumerable NonExceedingPathNameMaxDecodedMemberData { get; } = NonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }); + public static IEnumerable GB18030CharUnicodeInfoMemberData { get; } = s_gb18030CharUnicodeInfo.Select(data => new object[] { data }); + + private static IEnumerable GetTestData() + { + byte[] startDelimiter = GB18030Encoding.GetBytes($":{Environment.NewLine}"); + byte[] endDelimiter = GB18030Encoding.GetBytes($"{Environment.NewLine}{Environment.NewLine}"); + + // Instead of inlining the data in source, parse the test data from the file to prevent encoding issues. + ReadOnlyMemory testFileBytes = File.ReadAllBytes(TestDataFilePath); + + while (testFileBytes.Length > 0) + { + int start = testFileBytes.Span.IndexOf(startDelimiter); + testFileBytes = testFileBytes.Slice(start + startDelimiter.Length); + + int end = testFileBytes.Span.IndexOf(endDelimiter); + if (end == -1) + end = testFileBytes.Length; + + yield return testFileBytes.Slice(0, end).ToArray(); + + testFileBytes = testFileBytes.Slice(end); + } + + // Add a few additional test cases to exercise test correctness. + yield return GB18030Encoding.GetBytes("aaa"); + yield return GB18030Encoding.GetBytes("abc"); + yield return GB18030Encoding.GetBytes("𫓧𫓧"); + } + + internal static IEnumerable GetTextElements(string input) + { + TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(input); + while (enumerator.MoveNext()) + { + yield return enumerator.GetTextElement(); + } + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs new file mode 100644 index 00000000000000..502c94612b0def --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -0,0 +1,127 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Globalization.Tests; +using Xunit; + +namespace GB18030.Tests; + +[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] +public class CharTests +{ + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void Convert(CharUnicodeInfoTestCase testCase) + { + Assert.Equal(testCase.CodePoint, char.ConvertToUtf32(char.ConvertFromUtf32(testCase.CodePoint), 0)); + + string utf32String = testCase.Utf32CodeValue; + if (char.IsSurrogate(utf32String[0])) + { + Assert.Equal(2, utf32String.Length); + Assert.Equal(testCase.CodePoint, char.ConvertToUtf32(utf32String[0], utf32String[1])); + } + } + + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void Parse(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + if (utf32String.Length > 1) + { + Assert.False(char.TryParse(utf32String, out _)); + return; + } + + char c = char.Parse(utf32String); + Assert.Equal(testCase.CodePoint, c); + + bool succeed = char.TryParse(utf32String, out c); + Assert.True(succeed); + Assert.Equal(testCase.CodePoint, c); + } + + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void IsSurrogate(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + if (utf32String.Length > 1) + { + Assert.Equal(2, utf32String.Length); + char high = utf32String[0]; + char low = utf32String[1]; + Assert.True(char.IsSurrogate(low)); + Assert.True(char.IsSurrogate(high)); + Assert.True(char.IsLowSurrogate(low)); + Assert.True(char.IsHighSurrogate(high)); + Assert.True(char.IsSurrogatePair(high, low)); + } + else + { + char c = utf32String[0]; + Assert.False(char.IsSurrogate(c)); + Assert.False(char.IsLowSurrogate(c)); + Assert.False(char.IsHighSurrogate(c)); + Assert.False(char.IsSurrogatePair(c, c)); + } + } + + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void IsLetter(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + Assert.True(char.IsLetter(utf32String, 0)); + Assert.True(char.IsLetterOrDigit(utf32String, 0)); + + if (utf32String.Length < 2) + { + Assert.True(char.IsLetter(utf32String[0])); + Assert.True(char.IsLetterOrDigit(utf32String[0])); + } + } + + + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void IsNonLetter_False(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + Assert.False(char.IsControl(utf32String, 0)); + Assert.False(char.IsDigit(utf32String, 0)); + Assert.False(char.IsLower(utf32String, 0)); + Assert.False(char.IsNumber(utf32String, 0)); + Assert.False(char.IsPunctuation(utf32String, 0)); + Assert.False(char.IsSeparator(utf32String, 0)); + Assert.False(char.IsSymbol(utf32String, 0)); + Assert.False(char.IsUpper(utf32String, 0)); + Assert.False(char.IsWhiteSpace(utf32String, 0)); + + if (utf32String.Length < 2) + { + char c = utf32String[0]; +#if !NETFRAMEWORK + Assert.False(char.IsAscii(c)); + Assert.False(char.IsAsciiDigit(c)); + Assert.False(char.IsAsciiHexDigit(c)); + Assert.False(char.IsAsciiHexDigitLower(c)); + Assert.False(char.IsAsciiHexDigitUpper(c)); + Assert.False(char.IsAsciiLetter(c)); + Assert.False(char.IsAsciiLetterOrDigit(c)); + Assert.False(char.IsAsciiLetterLower(c)); + Assert.False(char.IsAsciiLetterUpper(c)); +#endif + Assert.False(char.IsControl(c)); + Assert.False(char.IsDigit(c)); + Assert.False(char.IsLower(c)); + Assert.False(char.IsNumber(c)); + Assert.False(char.IsPunctuation(c)); + Assert.False(char.IsSeparator(c)); + Assert.False(char.IsSymbol(c)); + Assert.False(char.IsUpper(c)); + Assert.False(char.IsWhiteSpace(c)); + } + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs new file mode 100644 index 00000000000000..530879e48023ee --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Globalization; +using System.Globalization.Tests; +using Xunit; + +namespace GB18030.Tests; + +[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] +public class CharUnicodeInfoTests +{ + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void GetUnicodeCategory(CharUnicodeInfoTestCase testCase) + { + UnicodeCategory expected = PlatformDetection.IsNetFramework ? UnicodeCategory.OtherNotAssigned : UnicodeCategory.OtherLetter; + + if (testCase.Utf32CodeValue.Length == 1) + { + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue[0])); + } + + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue, 0)); +#if !NETFRAMEWORK + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.CodePoint)); +#endif + Assert.True(PlatformDetection.IsNetFramework || testCase.GeneralCategory == expected); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs new file mode 100644 index 00000000000000..cd9a681ad47e9d --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace GB18030.Tests; + +public class ConsoleTests +{ + protected static readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] + public void StandardOutput(string decodedText) + { + var remoteOptions = new RemoteInvokeOptions(); + remoteOptions.StartInfo.RedirectStandardOutput = true; + remoteOptions.StartInfo.StandardOutputEncoding = TestHelper.GB18030Encoding; + + using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(line => + { + Console.OutputEncoding = TestHelper.GB18030Encoding; + Console.Write(line); + + return 42; + }, decodedText, remoteOptions); + + + Assert.Equal(decodedText, remoteHandle.Process.StandardOutput.ReadToEnd()); + Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] + public void StandardInput(string decodedText) + { + var remoteOptions = new RemoteInvokeOptions(); + remoteOptions.StartInfo.RedirectStandardInput = true; +#if !NETFRAMEWORK + remoteOptions.StartInfo.StandardInputEncoding = TestHelper.GB18030Encoding; +#endif + + using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(line => + { + Console.InputEncoding = TestHelper.GB18030Encoding; + Assert.Equal(line, Console.In.ReadToEnd()); + + return 42; + }, decodedText, remoteOptions); + + if (PlatformDetection.IsNetFramework) + { + // there's no StandardInputEncoding in .NET Framework, re-encode and write. + byte[] encoded = TestHelper.GB18030Encoding.GetBytes(decodedText); + remoteHandle.Process.StandardInput.BaseStream.Write(encoded, 0, encoded.Length); + remoteHandle.Process.StandardInput.Close(); + } + else + { + remoteHandle.Process.StandardInput.Write(decodedText); + remoteHandle.Process.StandardInput.Close(); + } + + Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); + } + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] + public void StandardError(string decodedText) + { + var remoteOptions = new RemoteInvokeOptions(); + remoteOptions.StartInfo.RedirectStandardError = true; + remoteOptions.StartInfo.StandardErrorEncoding = TestHelper.GB18030Encoding; + + using RemoteInvokeHandle remoteHandle = RemoteExecutor.Invoke(line => + { + Console.OutputEncoding = TestHelper.GB18030Encoding; + Console.Error.Write(line); + + return 42; + }, decodedText, remoteOptions); + + + Assert.Equal(decodedText, remoteHandle.Process.StandardError.ReadToEnd()); + Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs new file mode 100644 index 00000000000000..27fcfd69c677ee --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs @@ -0,0 +1,47 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace GB18030.Tests; + +public class DirectoryInfoTests : DirectoryTestBase +{ + protected override void CreateDirectory(string path) => new DirectoryInfo(path).Create(); + protected override void DeleteDirectory(string path, bool recursive) => new DirectoryInfo(path).Delete(recursive); + protected override void MoveDirectory(string source, string destination) => new DirectoryInfo(source).MoveTo(destination); + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void CreateSubdirectory(string gb18030Line) + { + var subDirInfo = TempDirectory.CreateSubdirectory(gb18030Line); + + Assert.True(subDirInfo.Exists); + Assert.Equal(gb18030Line, subDirInfo.Name); + Assert.Equal(Path.Combine(TempDirectory.FullName, gb18030Line), subDirInfo.FullName); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemInfos(string gb18030Line) + { + string rootDir = TempDirectory.FullName; + List expected = []; + + string gb18030Dir = Path.Combine(rootDir, gb18030Line); + var dirInfo = new DirectoryInfo(gb18030Dir); + dirInfo.Create(); + expected.Add(dirInfo); + + string gb18030File = Path.Combine(rootDir, gb18030Line + ".txt"); + var fileInfo = new FileInfo(gb18030File); + fileInfo.Create().Dispose(); + expected.Add(fileInfo); + + Assert.Equivalent(expected, new DirectoryInfo(rootDir).EnumerateFileSystemInfos()); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs new file mode 100644 index 00000000000000..d922db43d988ad --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -0,0 +1,79 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Xunit; + +namespace GB18030.Tests; + +public abstract class DirectoryTestBase : IDisposable +{ + protected abstract void CreateDirectory(string path); + protected abstract void DeleteDirectory(string path, bool recursive); + protected abstract void MoveDirectory(string source, string destination); + + protected DirectoryInfo TempDirectory { get; } + + public DirectoryTestBase() + { + TempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Create(string gb18030Line) + { + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + CreateDirectory(gb18030Path); + + var dirInfo = new DirectoryInfo(gb18030Path); + Assert.True(dirInfo.Exists); + Assert.Equal(gb18030Line, dirInfo.Name); + } + + public static IEnumerable Delete_MemberData() => + TestHelper.NonExceedingPathNameMaxDecodedTestData.SelectMany(testData => + new int[] { 0, 2, 8 }.Select(recurseLevel => new object[] { testData, recurseLevel })); + + [Theory] + [MemberData(nameof(Delete_MemberData))] + public void Delete(string gb18030Line, int recurseLevel) + { + string firstPath = Path.Combine(TempDirectory.FullName, gb18030Line); + string nestedDirPath = Path.Combine(firstPath, Path.Combine(Enumerable.Repeat(gb18030Line, recurseLevel).ToArray())); + Assert.True(recurseLevel > 0 || firstPath.Equals(nestedDirPath)); + + Directory.CreateDirectory(nestedDirPath); + Assert.True(Directory.Exists(nestedDirPath)); + + DeleteDirectory(firstPath, recursive: recurseLevel > 0); + + Assert.False(Directory.Exists(firstPath)); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Move(string gb18030Line) + { + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + Directory.CreateDirectory(gb18030Path); + Assert.True(Directory.Exists(gb18030Path)); + + string newPath = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + MoveDirectory(gb18030Path, newPath); + Assert.True(Directory.Exists(newPath)); + Assert.False(Directory.Exists(gb18030Path)); + + MoveDirectory(newPath, gb18030Path); + Assert.True(Directory.Exists(gb18030Path)); + Assert.False(Directory.Exists(newPath)); + } + + public void Dispose() + { + TempDirectory.Delete(true); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs new file mode 100644 index 00000000000000..5f7a85186285b5 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.IO; +using Xunit; + +namespace GB18030.Tests; + +public class DirectoryTests : DirectoryTestBase +{ + protected override void CreateDirectory(string path) => Directory.CreateDirectory(path); + protected override void DeleteDirectory(string path, bool recursive) => Directory.Delete(path, recursive); + protected override void MoveDirectory(string source, string destination) => Directory.Move(source, destination); + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemEntries(string gb18030Line) + { + string rootDir = TempDirectory.FullName; + List expected = []; + + string gb18030Dir = Path.Combine(rootDir, gb18030Line); + Directory.CreateDirectory(gb18030Dir); + expected.Add(gb18030Dir); + + string gb18030File = Path.Combine(rootDir, gb18030Line + ".txt"); + File.Create(gb18030File).Dispose(); + expected.Add(gb18030File); + + Assert.Equivalent(expected, Directory.EnumerateFileSystemEntries(rootDir)); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs new file mode 100644 index 00000000000000..d285eadb9daa48 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using Xunit; + +namespace GB18030.Tests; + +public class EncodingTests +{ + [Theory] + [MemberData(nameof(TestHelper.EncodedMemberData), MemberType = typeof(TestHelper))] + public void Roundtrips(byte[] testData) + { + Assert.True(testData.AsSpan().SequenceEqual( + TestHelper.GB18030Encoding.GetBytes(TestHelper.GB18030Encoding.GetString(testData)))); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs new file mode 100644 index 00000000000000..56684a200c8425 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO; + +namespace GB18030.Tests; + +public class FileInfoTests : FileTestBase +{ + protected override void CreateFile(string path) => new FileInfo(path).Create().Dispose(); + protected override void DeleteFile(string path) => new FileInfo(path).Delete(); + protected override void MoveFile(string source, string destination) => new FileInfo(source).MoveTo(destination); + protected override void CopyFile(string source, string destination) => new FileInfo(source).CopyTo(destination); +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs new file mode 100644 index 00000000000000..981246bf7433d9 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs @@ -0,0 +1,90 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using Xunit; + +namespace GB18030.Tests; + +public abstract class FileTestBase : IDisposable +{ + protected abstract void CreateFile(string path); + protected abstract void DeleteFile(string path); + protected abstract void MoveFile(string source, string destination); + protected abstract void CopyFile(string source, string destination); + + protected DirectoryInfo TempDirectory { get; } + + public FileTestBase() + { + TempDirectory = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName())); + } + + public void Dispose() + { + TempDirectory.Delete(true); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Create(string gb18030Line) + { + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + CreateFile(gb18030Path); + + var fileInfo = new FileInfo(gb18030Path); + Assert.True(fileInfo.Exists); + Assert.Equal(fileInfo.Name, gb18030Line); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Delete(string gb18030Line) + { + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + File.Create(gb18030Path).Dispose(); + Assert.True(File.Exists(gb18030Path)); + + DeleteFile(gb18030Path); + + Assert.False(File.Exists(gb18030Path)); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Move(string gb18030Line) + { + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + File.Create(gb18030Path).Dispose(); + + string newPath = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + MoveFile(gb18030Path, newPath); + Assert.True(File.Exists(newPath)); + Assert.False(File.Exists(gb18030Path)); + + File.Move(newPath, gb18030Path); + Assert.True(File.Exists(gb18030Path)); + Assert.False(File.Exists(newPath)); + } + + [Theory] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] + public void Copy(string gb18030Line) + { + ReadOnlySpan sampleContent = "File_Copy"u8; + string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); + File.WriteAllBytes(gb18030Path, sampleContent.ToArray()); + + string newPath = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + CopyFile(gb18030Path, newPath); + Assert.True(File.Exists(newPath)); + + File.Delete(gb18030Path); + Assert.False(File.Exists(gb18030Path)); + + CopyFile(newPath, gb18030Path); + Assert.True(File.Exists(gb18030Path)); + Assert.True(sampleContent.SequenceEqual(File.ReadAllBytes(gb18030Path))); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs new file mode 100644 index 00000000000000..80fd1bebc3a25b --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -0,0 +1,68 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.IO; +using System.Linq; +using Xunit; + +namespace GB18030.Tests; + +public class FileTests : FileTestBase +{ + private static readonly byte[] s_expectedBytes = File.ReadAllBytes(TestHelper.TestDataFilePath); + private static readonly string s_expectedText = TestHelper.GB18030Encoding.GetString(s_expectedBytes); + + protected override void CreateFile(string path) => File.Create(path).Dispose(); + protected override void DeleteFile(string path) => File.Delete(path); + protected override void MoveFile(string source, string destination) => File.Move(source, destination); + protected override void CopyFile(string source, string destination) => File.Copy(source, destination); + + [Fact] + public void ReadAllText() + { + Assert.Equal(s_expectedText, File.ReadAllText(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); + } + + [Fact] + public void ReadAllLines() + { + Assert.Equal( + s_expectedText.Split([Environment.NewLine], StringSplitOptions.None), + File.ReadAllLines(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); + } + + [Fact] + public void WriteAllText() + { + string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + File.WriteAllText(tempFile, s_expectedText, TestHelper.GB18030Encoding); + + Assert.True(s_expectedBytes.AsSpan().SequenceEqual(File.ReadAllBytes(tempFile))); + } + + [Fact] + public void WriteAllLines() + { + string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + string[] lines = s_expectedText.Split([Environment.NewLine], StringSplitOptions.None); + File.WriteAllLines(tempFile, lines, TestHelper.GB18030Encoding); + + // WriteAllLines uses TextWriter.WriteLine which concats a newline to each provided line, + // the result is the expected text with an additional newline at the end. + byte[] expected = TestHelper.GB18030Encoding.GetBytes(s_expectedText + Environment.NewLine); + Assert.True(expected.AsSpan().SequenceEqual(File.ReadAllBytes(tempFile))); + } + + [Fact] + public void AppendAllText() + { + string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + const string initialContent = "Initial content: "; + File.WriteAllText(tempFile, initialContent, TestHelper.GB18030Encoding); + File.AppendAllText(tempFile, s_expectedText, TestHelper.GB18030Encoding); + + byte[] expected = TestHelper.GB18030Encoding.GetBytes(initialContent + s_expectedText); + Assert.True(expected.AsSpan().SequenceEqual(File.ReadAllBytes(tempFile))); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs new file mode 100644 index 00000000000000..620dfba0103bff --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using System.Text.RegularExpressions.Tests; +using System.Threading.Tasks; +using Xunit; + +namespace GB18030.Tests; + +/// +/// Regex does not support surrogate pairs, which drastically reduces the number of characters in GB18030 that can be tested. +/// +[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] +public class RegexTests +{ + public enum RegexNamedBlock + { + IsCJKUnifiedIdeographs, + IsCJKUnifiedIdeographsExtensionA, + } + + private static readonly IEnumerable s_cjkAndCjkExtensionANewChars = TestHelper.CjkNewCodePoints.Union(TestHelper.CjkExtensionANewCodePoints).Select(c => ((char)c).ToString()); + + private static readonly List<(RegexNamedBlock, string[])> s_namedBlocks = new() + { + (RegexNamedBlock.IsCJKUnifiedIdeographs, TestHelper.CjkNewCodePoints.Select(c => ((char)c).ToString()).ToArray()), + (RegexNamedBlock.IsCJKUnifiedIdeographsExtensionA, TestHelper.CjkExtensionANewCodePoints.Select(c => ((char)c).ToString()).ToArray()) + }; + + public static IEnumerable UnicodeCategories_MemberData() => + RegexHelpers.AvailableEngines.SelectMany(engine => + TestHelper.Cultures.Select(culture => new object[] { engine, culture })); + + [Theory] + [MemberData(nameof(UnicodeCategories_MemberData))] + public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo culture) + { + Regex r = await RegexHelpers.GetRegexAsync(engine, @"\p{Lo}", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[\p{Lo}]", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"\p{L}", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[\p{L}]", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.Matches(r, element); + } + + [Theory] + [MemberData(nameof(UnicodeCategories_MemberData))] + public async Task UnicodeCategory_ExclusionAsync(RegexEngine engine, CultureInfo culture) + { + Regex r = await RegexHelpers.GetRegexAsync(engine, @"\P{Lo}", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[^\p{Lo}]", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"\P{L}", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[^\p{L}]", RegexOptions.None, culture); + foreach (string element in s_cjkAndCjkExtensionANewChars) + Assert.DoesNotMatch(r, element); + } + + public static IEnumerable NamedBlock_MemberData() => + s_namedBlocks.SelectMany(namedBlock => + RegexHelpers.AvailableEngines.SelectMany(engine => + TestHelper.Cultures.Select(culture => new object[] { namedBlock.Item2, namedBlock.Item1, engine, culture }))); + + [Theory] + [MemberData(nameof(NamedBlock_MemberData))] + public async Task NamedBlock_InclusionAsync(string[]characters, RegexNamedBlock namedBlock, RegexEngine engine, CultureInfo culture) + { + Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\p{{{namedBlock}}}", RegexOptions.None, culture); + foreach (string c in characters) + Assert.Matches(r, c); + + r = await RegexHelpers.GetRegexAsync(engine, $@"[\p{{{namedBlock}}}]", RegexOptions.None, culture); + foreach (string c in characters) + Assert.Matches(r, c); + } + + [Theory] + [MemberData(nameof(NamedBlock_MemberData))] + public async Task NamedBlock_ExclusionAsync(string[] characters, RegexNamedBlock namedBlock, RegexEngine engine, CultureInfo culture) + { + Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\P{{{namedBlock}}}", RegexOptions.None, culture); + foreach (string c in characters) + Assert.DoesNotMatch(r, c); + + r = await RegexHelpers.GetRegexAsync(engine, $@"[^\p{{{namedBlock}}}]", RegexOptions.None, culture); + foreach (string c in characters) + Assert.DoesNotMatch(r, c); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs new file mode 100644 index 00000000000000..0e8280016637a7 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -0,0 +1,300 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Xunit; +#pragma warning disable xUnit2009 // Do not use boolean check to check for substrings +#pragma warning disable xUnit2010 // Do not use boolean check to check for string equality + +namespace GB18030.Tests; + +public class StringTests +{ + private const string Dummy = "\uFFFF"; + + [Theory] + [MemberData(nameof(TestHelper.EncodedMemberData), MemberType = typeof(TestHelper))] + public unsafe void Ctor(byte[] encoded) + { + fixed (sbyte* p = (sbyte[])(object)encoded) + { + string s = new string(p, 0, encoded.Length, TestHelper.GB18030Encoding); + Assert.True(encoded.AsSpan().SequenceEqual(TestHelper.GB18030Encoding.GetBytes(s))); + } + } + + public static IEnumerable Compare_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => + TestHelper.Cultures.SelectMany(culture => + TestHelper.CompareOptions.Select(option => new object[] { testData, culture, option }))); + + [Theory] + [MemberData(nameof(Compare_MemberData))] + public void Compare(string decoded, CultureInfo culture, CompareOptions option) + { +#pragma warning disable 0618 // suppress obsolete warning for String.Copy + string copy = string.Copy(decoded); +#pragma warning restore 0618 + Assert.True(string.Compare(decoded, copy, culture, option) == 0); + } + + public static IEnumerable Contains_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => + TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { testData, comparison })); + + [Theory] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [MemberData(nameof(Contains_MemberData))] + public void Contains(string decoded, StringComparison comparison) + { + string current = string.Empty; + foreach (string element in TestHelper.GetTextElements(decoded)) + { + current += element; + Assert.True(decoded.Contains(current, comparison)); + } + + current = string.Empty; + foreach (string element in TestHelper.GetTextElements(decoded).Reverse()) + { + current = element + current; + Assert.True(decoded.Contains(current, comparison)); + } + } + + public static IEnumerable StringComparison_MemberData() => + TestHelper.DecodedTestData.SelectMany(decoded => + TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { decoded, comparison })); + + [Theory] + [MemberData(nameof(StringComparison_MemberData))] + public void String_Equals(string decoded, StringComparison comparison) + { +#pragma warning disable 0618 // suppress obsolete warning for String.Copy + string copy = string.Copy(decoded); +#pragma warning restore 0618 + + Assert.True(decoded.Equals(copy, comparison)); + Assert.True(string.Equals(decoded, copy, comparison)); + + string[] elements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 0; i < elements.Length; i++) + { + string left = string.Concat(elements.Take(i)); + string right = string.Concat(elements.Skip(i)); + Assert.True(decoded.Equals(left + '\0' + right, comparison)); + } + } + + public static IEnumerable EndsStartsWith_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => + TestHelper.Cultures.Select(culture => new object[] { testData, culture })); + + [Theory] + [MemberData(nameof(EndsStartsWith_MemberData))] + public void EndsWith(string decoded, CultureInfo culture) + { + string suffix = string.Empty; + foreach (string textElement in TestHelper.GetTextElements(decoded).Reverse()) + { + suffix = textElement + suffix; + Assert.True(decoded.EndsWith(suffix, ignoreCase: false, culture)); + } + } + + [Theory] + [MemberData(nameof(EndsStartsWith_MemberData))] + public void StartsWith(string decoded, CultureInfo culture) + { + string prefix = string.Empty; + foreach (string textElement in TestHelper.GetTextElements(decoded)) + { + prefix += textElement; + Assert.True(decoded.StartsWith(prefix, ignoreCase: false, culture)); + } + } + + [Theory] + [MemberData(nameof(StringComparison_MemberData))] + public void IndexOf_MultipleElements(string decoded, StringComparison comparison) + { + Assert.NotEqual(StringComparison.Ordinal, comparison); + Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); + + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 0; i < textElements.Length; i++) + { + string left = string.Concat(textElements.Take(i)); + string right = string.Concat(textElements.Skip(i)); + Assert.Equal(decoded.Length, left.Length + right.Length); + + Assert.Equal(0, decoded.IndexOf(left, comparison)); + Assert.Equal(left.Length, decoded.IndexOf(right, startIndex: left.Length, comparison)); + } + } + + [Theory] + [MemberData(nameof(StringComparison_MemberData))] + public void IndexOf_SingleElement(string decoded, StringComparison comparison) + { + Assert.NotEqual(StringComparison.Ordinal, comparison); + Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); + + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 0; i < textElements.Length; i++) + { + string current = textElements[i]; + int expectedIndex = textElements.Take(i).Sum(e => e.Length); + // Fast-check the expected index. + Assert.Equal(expectedIndex, decoded.IndexOf(current, startIndex: expectedIndex, comparison)); + IndexOf_SingleElement_Slow(current, expectedIndex); + } + + void IndexOf_SingleElement_Slow(string current, int expectedIndex) + { + int startIndex = 0; + while (true) + { + int result = decoded.IndexOf(current, startIndex, comparison); + + if (result == -1 || result > expectedIndex) + Assert.Fail($"'{current}' not found or found too late in '{decoded}'"); + else if (result < expectedIndex) + startIndex = result + current.Length; + else + { + Assert.Equal(expectedIndex, result); + break; + } + } + } + } + + [Theory] + [MemberData(nameof(StringComparison_MemberData))] + public void LastIndexOf_MultipleElements(string decoded, StringComparison comparison) + { + Assert.NotEqual(StringComparison.Ordinal, comparison); + Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); + + // Don't deal with LastIndexOf(string.Empty) nuances, test against full length outside of the loop. + // see https://learn.microsoft.com/dotnet/core/compatibility/core-libraries/5.0/lastindexof-improved-handling-of-empty-values + Assert.Equal(0, decoded.LastIndexOf(decoded, comparison)); + + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 1; i < textElements.Length; i++) + { + string left = string.Concat(textElements.Take(i)); + string right = string.Concat(textElements.Skip(i)); + Assert.Equal(decoded.Length, left.Length + right.Length); + + Assert.Equal(0, decoded.LastIndexOf(left, startIndex: left.Length - 1, comparison)); + Assert.Equal(left.Length, decoded.LastIndexOf(right, comparison)); + } + } + + [Theory] + [MemberData(nameof(StringComparison_MemberData))] + public void LastIndexOf_SingleElement(string decoded, StringComparison comparison) + { + Assert.NotEqual(StringComparison.Ordinal, comparison); + Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); + + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 0; i < textElements.Length; i++) + { + string current = textElements[i]; + int expectedIndex = textElements.Take(i).Sum(e => e.Length); + // Fast-check the expected index. + Assert.Equal(expectedIndex, decoded.LastIndexOf(current, startIndex: expectedIndex + current.Length - 1, comparison)); + LastIndexOf_SingleElement_Slow(current, expectedIndex); + } + + void LastIndexOf_SingleElement_Slow(string current, int expectedIndex) + { + int startIndex = decoded.Length - 1; + while (true) + { + int result = decoded.LastIndexOf(current, startIndex, comparison); + + if (result == -1 || result < expectedIndex) + Assert.Fail($"'{current}' not found or found too late in '{decoded}'"); + else if (result > expectedIndex) + startIndex = result - 1; + else + { + Assert.Equal(expectedIndex, result); + break; + } + } + } + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] + public void Replace(string decoded) + { + Assert.False(decoded.Contains(Dummy)); + + foreach (string textElement in TestHelper.GetTextElements(decoded)) + { + int occurrences = SplitHelper(decoded, textElement).Length; + string replaced = decoded.Replace(textElement, Dummy); + Assert.Equal(occurrences, SplitHelper(replaced, Dummy).Length); + + if (textElement.Length == 1) + { + replaced = decoded.Replace(textElement[0], Dummy[0]); + Assert.Equal(occurrences, SplitHelper(replaced, Dummy).Length); + } + } + } + + public static IEnumerable Replace_NetCore_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => + TestHelper.Cultures.Select(culture => new object[] { testData, culture })); + +#if NETCOREAPP + [Theory] + [MemberData(nameof(Replace_NetCore_MemberData))] + public void Replace_CultureInfo(string decoded, CultureInfo culture) + { + Assert.False(decoded.Contains(Dummy)); + + foreach (string textElement in TestHelper.GetTextElements(decoded)) + { + int expected = SplitHelper(decoded, textElement).Length; + string replaced = decoded.Replace(textElement, Dummy, ignoreCase: false, culture); + Assert.True(expected == SplitHelper(replaced, Dummy).Length || + // Exception for non zh-CN culture, where '0' and '〇' are considered equal. + (culture.Name != "zh-CN" && textElement == "\u3007" && decoded.Contains('0')) || + (culture.Name != "zh-CN" && textElement == "0" && decoded.Contains('\u3007')), + $"Values differ for text element {textElement}"); + } + } +#endif + + [Theory] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] + public void Split(string decoded) + { + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + + for (int i = 0; i < textElements.Length; i++) + { + var result = SplitHelper(decoded, textElements[i]); + Assert.True(result.Length > 1); + } + } + + private static string[] SplitHelper(string str, params T[] separators) => + separators switch + { + char[] chars => str.Split(chars, StringSplitOptions.None), + string[] strings => str.Split(strings, StringSplitOptions.None), + _ => throw new ArgumentException() + }; +} diff --git a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/CharUnicodeInfoTestData.cs b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/CharUnicodeInfoTestData.cs index 224d3c8df98ff2..2266e1145330cc 100644 --- a/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/CharUnicodeInfoTestData.cs +++ b/src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/CharUnicodeInfoTestData.cs @@ -13,12 +13,12 @@ public static class CharUnicodeInfoTestData { List testCases = new List(); string fileName = "UnicodeData.txt"; - Stream stream = typeof(CharUnicodeInfoTestData).GetTypeInfo().Assembly.GetManifestResourceStream(fileName); + Stream stream = typeof(CharUnicodeInfoTestData).GetTypeInfo().Assembly.GetManifestResourceStream(fileName)!; using (StreamReader reader = new StreamReader(stream)) { while (!reader.EndOfStream) { - Parse(testCases, reader.ReadLine()); + Parse(testCases, reader.ReadLine()!); } } return testCases; @@ -153,7 +153,7 @@ public enum StrongBidiCategory public class CharUnicodeInfoTestCase { - public string Utf32CodeValue { get; set; } + public string Utf32CodeValue { get; set; } = null!; // Can't use required in net481. public int CodePoint { get; set; } public UnicodeCategory GeneralCategory { get; set; } public double NumericValue { get; set; } diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Tests.Common.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Tests.Common.cs index 601bdc85a39459..76d8b5dc31c450 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Tests.Common.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/Regex.Tests.Common.cs @@ -234,6 +234,6 @@ public CaptureData(string value, int index, int length, CaptureData[] captures) public string Value { get; } public int Index { get; } public int Length { get; } - public CaptureData[] Captures { get; } + public CaptureData[]? Captures { get; } } } diff --git a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs index 677bf16d32ff0a..cbf143eb3b3cb3 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -46,7 +46,7 @@ private static MetadataReference[] CreateReferences() return new[] { MetadataReference.CreateFromFile(typeof(object).Assembly.Location), - MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(corelibPath), "System.Runtime.dll")), + MetadataReference.CreateFromFile(Path.Combine(Path.GetDirectoryName(corelibPath)!, "System.Runtime.dll")), MetadataReference.CreateFromFile(typeof(Unsafe).Assembly.Location), MetadataReference.CreateFromFile(typeof(Regex).Assembly.Location), }; @@ -175,7 +175,7 @@ internal static async Task SourceGenRegexAsync( code.Append($" [GeneratedRegex({SymbolDisplay.FormatLiteral(regex.pattern, quote: true)}"); if (regex.options is not null) { - code.Append($", {string.Join(" | ", regex.options.ToString().Split(',').Select(o => $"RegexOptions.{o.Trim()}"))}"); + code.Append($", {string.Join(" | ", regex.options.ToString()!.Split(',').Select(o => $"RegexOptions.{o.Trim()}"))}"); if (regex.matchTimeout is not null) { code.Append(string.Create(CultureInfo.InvariantCulture, $", {(int)regex.matchTimeout.Value.TotalMilliseconds}")); @@ -215,7 +215,7 @@ internal static async Task SourceGenRegexAsync( .AddDocument("RegexGenerator.g.cs", SourceText.From("// Empty", Encoding.UTF8)).Project; Assert.True(proj.Solution.Workspace.TryApplyChanges(proj.Solution)); - s_compilation = comp = await proj!.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false); + s_compilation = comp = (await proj!.GetCompilationAsync(CancellationToken.None).ConfigureAwait(false))!; Debug.Assert(comp is not null); } @@ -265,7 +265,7 @@ internal static async Task SourceGenRegexAsync( for (int i = 0; i < instances.Length; i++) { string memberName = $"Get{i}"; - instances[i] = (Regex)(c.GetMethod(memberName) ?? c.GetProperty(memberName).GetGetMethod())!.Invoke(null, null)!; + instances[i] = (Regex)(c.GetMethod(memberName) ?? c.GetProperty(memberName)!.GetGetMethod())!.Invoke(null, null)!; } // Issue an unload on the ALC, so it'll be collected once the Regex instance is collected