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: +‚50°¦˜8á7G9ï0A˜9°4D•4Î6¢¨˜8•5C@™2è3˜5ó79‹2ªÚš7ø6š4„2‚5‡9©J™9ø69˜9ž7ÇW‚5–7™48 +××™8ð0Q‚5’2˜9Ü6d9ï6™1Ã4˜6Ë4CæF˜9´0——˜5ó8£¸™9ø9gš7ø99 5˜9ž8Cš4„9©O‚5ˆ0•2Œ2¡x‚5–8 +ûœ™3Ô8c™9ï0dˆÒ™2è3—9«0¦ÁaÜܘ9´1‚55‚3þ9˜9ž99•4ªÙ˜5ó9š4…9Gš7ù9©S‚5ˆ1A‚5–9™9ù9˜7Ø8 +‚39d˜9³6‚5‘9—8þ2ßo™9’0C˜9µ8˜9Ø7¨¯˜6¬5Ñј5ô0˜9Ÿ0ª¯©P‚5ˆ295––‚5—0sš8’0š4—0š09 +˜9‘1ýM‚5‡45™9÷0žž™2£9r˜9²6òò‚52˜5ó2øÈ˜5ô1©I˜9Ÿ19›4£Úš8¨9˜8¼4š0–9‚5ˆ3š4¯0a +A‚59™9Á0©–r˜8ö1G•8À1˜6¤1ëO˜9ª5§™3þ3£Ï‚5ˆ4˜9ž79–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¨¯©R9’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Ÿ09ž7¡@˜5ô5™1”8š8Ì5š0¯0™4Ñ3©–‚5—5‚5ˆ7 +‚56Ø·ª‚1Á3˜9’50 þ™0™2™3¬9˜9´9D˜0À8©M˜9Ÿ1ø¡£¶›1£7˜5ó8š7ø09˜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ª@þO9î9‚5‡88e•2‚6˜5ó6@ þ˜9¶2™3þ3©•©–™48™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Ÿ19‹29 5‚5–7‚5˜2‚5‡9‚5ˆ8š4„2š7ø0˜5÷8˜9ž66n + +Group1: +°¡°¦™1Ã4•4Î60u9î99ï6A˜9¡3˜9¡9ªHýˆDEG•2‚6•2ƒ1¢§¨¯˜7Ø8˜9‚7i˜5ó7˜5ó8‚53‚57™48™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ý99‹29–8ª¡ª¢U1ø¡ø¢U2¡@¡A¡BU3 + +Group2: +°©°¯÷ý˜9‘1˜8â5JŠ‹ˆW9ï7‚1 4‚2§1ª@«ž˜8á7˜88˜8â58A™3þ0™3í7™3â0˜8º9˜9˜6˜8ö1FOCš82š83š8Š7š8Ÿ7•2ƒ4•2ƒ5–7×0¢Ã£ë˜5ó9˜5ô0˜9ž9˜9Ÿ0š05š06š0™1š0™2š0™5©Q‚5–9‚5—09Œ09’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‚59¨»£Ñ–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š86š87š8•3š8—3š8¹4E9z˜8¼1˜8¼4˜8Ì2‚5—1‚5—29–09™8‚5ˆ3‚5ˆ4C + +Group4: +‚58‚50½ª½­½®½¯™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—49š09›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‚55‚56‚4¡5‚4¡6‚4¡78bبأؤإ˜6Ç6˜6‰5b˜9³6˜9ª3˜9µ8BDFG™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ˆ89œ090˜9ž7˜9ž9¬¥¬¦¬§¬¸U1ú×úØúÙU2£”£•£–U3™3’5™3¥6©M©•©–š4ñ7š4ñ858b + +Group6: +‚5‡19õ9‚0é0‚5‘5‚51ثجå¢å£˜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˜87˜69¬Ð¬á¬ò­£U1û°ûÁûÒU2¥P¥a¥rU3‚5‡9‚5ˆ2©T¨“˜5ó8˜5ô0g99ž19ž9HJLMŠ} + +Group7: +‚58‚52å¤å¥å¦å¨å©å«˜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ˆ59Ÿ09Ÿ6š6†2š6†3š6†4š60š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£39Ÿ59ž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š74š7‡6£Ø¦Â¨§˜5ô5˜5ô1š9Õ0š9·9š9¾8š9¾9š9Ó4˜7Ø8˜69˜8Ë0˜9‚6m09›59™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‚17a4™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ø09“19“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 b0a9b6549492bc..ff80171c30fa1d 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 @@ -236,6 +236,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