From 5d3733bc056194c656f3ad3f06ab4d0eaee59b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Fri, 25 Jul 2025 17:36:29 -0500 Subject: [PATCH 01/21] Add Compliance tests for GB18030-2022 --- src/libraries/Common/Common.Tests.slnx | 1 + .../ComplianceTests/Compliance.Tests.csproj | 28 ++ ...Test_Data_for_Mid_to_High_Volume_cases.txt | 45 ++ .../ComplianceTests/GB18030/TestHelper.cs | 96 +++++ .../GB18030/Tests/ConsoleTests.cs | 84 ++++ .../GB18030/Tests/DirectoryInfoTests.cs | 50 +++ .../GB18030/Tests/DirectoryTestBase.cs | 76 ++++ .../GB18030/Tests/DirectoryTests.cs | 34 ++ .../GB18030/Tests/EncodingTests.cs | 15 + .../GB18030/Tests/FileInfoTests.cs | 11 + .../GB18030/Tests/FileTestBase.cs | 87 ++++ .../GB18030/Tests/FileTests.cs | 65 +++ .../GB18030/Tests/StringTests.cs | 405 ++++++++++++++++++ 13 files changed, 997 insertions(+) create mode 100644 src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs diff --git a/src/libraries/Common/Common.Tests.slnx b/src/libraries/Common/Common.Tests.slnx index c6b9f2a7939d0f..154e0a5b1a48f0 100644 --- a/src/libraries/Common/Common.Tests.slnx +++ b/src/libraries/Common/Common.Tests.slnx @@ -5,6 +5,7 @@ + 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..840c426ec005d8 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -0,0 +1,28 @@ + + + $(NetCoreAppCurrent);$(NetFrameworkCurrent) + enable + true + true + + + + + + + + + + + + + + + + + + Always + + + + 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..998682b643bcbe --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text; + +namespace GB18030.Tests; + +public static class TestHelper +{ + internal static CultureInfo[] s_cultureInfos = [ + CultureInfo.CurrentCulture, + CultureInfo.InvariantCulture, + new CultureInfo("zh-CN")]; + + internal static CompareOptions[] s_compareOptions = [ + CompareOptions.None, + CompareOptions.IgnoreCase, + CompareOptions.Ordinal, + CompareOptions.OrdinalIgnoreCase]; + + internal static readonly StringComparison[] s_ordinalStringComparisons = [ + StringComparison.Ordinal, + StringComparison.OrdinalIgnoreCase]; + + internal static readonly StringComparison[] s_nonOrdinalStringComparisons = [ + StringComparison.CurrentCulture, + StringComparison.CurrentCultureIgnoreCase, + StringComparison.InvariantCulture, + StringComparison.InvariantCultureIgnoreCase]; + + internal static readonly StringComparison[] s_allStringComparisons = [ + .. s_ordinalStringComparisons, + .. s_nonOrdinalStringComparisons]; + + internal static string s_testDataFilePath = 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 !NETFRAMEWORK + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); +#endif + return s_gb18030Encoding ??= Encoding.GetEncoding("gb18030"); + } + } + + private static readonly IEnumerable s_encodedTestData = GetTestData(); + internal static readonly IEnumerable s_decodedTestData = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)); + internal static readonly IEnumerable s_splitNewLineDecodedTestData = s_decodedTestData.SelectMany( + data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)); + + public static IEnumerable EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data }); + public static IEnumerable DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data }); + public static IEnumerable SplitNewLineDecodedTestData { get; } = s_splitNewLineDecodedTestData.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(s_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/ConsoleTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs new file mode 100644 index 00000000000000..ea99e3dc7a309f --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs @@ -0,0 +1,84 @@ +using System; +using Microsoft.DotNet.RemoteExecutor; +using Xunit; + +namespace GB18030.Tests; + +public class ConsoleTests +{ + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), 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(5_000)); + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), 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(5_000)); + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), 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(5_000)); + } +} 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..59c8073976a889 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs @@ -0,0 +1,50 @@ +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.DecodedTestData), MemberType = typeof(TestHelper))] + public void CreateSubdirectory(string decoded) + { + foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) + { + 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.DecodedTestData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemInfos(string decoded) + { + string rootDir = TempDirectory.FullName; + List expected = []; + + foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) + { + 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..f227312aa87406 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -0,0 +1,76 @@ +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.SplitNewLineDecodedTestData), 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_TestData() => + new int[] { 0, 2, 8 }.SelectMany(recurseLevel => + TestHelper.s_splitNewLineDecodedTestData.Select(testData => new object[] { recurseLevel, testData })); + + [Theory] + [MemberData(nameof(Delete_TestData))] + public void Delete(int recurseLevel, string gb18030Line) + { + 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.SplitNewLineDecodedTestData), 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..e90d1ed889ca53 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs @@ -0,0 +1,34 @@ +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.DecodedTestData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemEntries(string decoded) + { + string rootDir = TempDirectory.FullName; + List expected = []; + + foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) + { + 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..dfebb4485b0832 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs @@ -0,0 +1,15 @@ +using System; +using Xunit; + +namespace GB18030.Tests; + +public class EncodingTests +{ + [Theory] + [MemberData(nameof(TestHelper.EncodedTestData), 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..77f04c813c3a4f --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs @@ -0,0 +1,11 @@ +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..e201186189d9be --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs @@ -0,0 +1,87 @@ +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.SplitNewLineDecodedTestData), 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.SplitNewLineDecodedTestData), 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.SplitNewLineDecodedTestData), 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.SplitNewLineDecodedTestData), 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..91cc7573e89324 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -0,0 +1,65 @@ +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.s_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.s_testDataFilePath, TestHelper.GB18030Encoding)); + } + + [Fact] + public void ReadAllLines() + { + Assert.Equal( + s_expectedText.Split([Environment.NewLine], StringSplitOptions.None), + File.ReadAllLines(TestHelper.s_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/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs new file mode 100644 index 00000000000000..7d9a6029e38433 --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -0,0 +1,405 @@ +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.EncodedTestData), 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_TestData() => + TestHelper.s_cultureInfos.SelectMany(culture => + TestHelper.s_compareOptions.SelectMany(option => + TestHelper.s_decodedTestData.Select(testData => new object[] { culture, option, testData }))); + + [Theory] + [MemberData(nameof(Compare_TestData))] + public void Compare(CultureInfo culture, CompareOptions option, string decoded) + { +#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); + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + public void Contains(string decoded) + { + for (int i = 0; i < decoded.Length; i++) + Assert.True(decoded.Contains(decoded.Substring(0, i))); + + for (int i = decoded.Length - 1; i >= 0; i--) + Assert.True(decoded.Contains(decoded.Substring(i))); + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + public void String_Equals(string decoded) + { +#pragma warning disable 0618 // suppress obsolete warning for String.Copy + string copy = string.Copy(decoded); +#pragma warning restore 0618 + Assert.True(decoded.Equals(decoded)); + Assert.True(decoded.Equals(copy)); + Assert.True(decoded.Equals((object)copy)); + Assert.True(string.Equals(decoded, copy)); + + Assert.False(decoded.Equals(copy + Dummy)); + Assert.False(decoded.Equals(Dummy + copy)); + Assert.False(decoded.Equals(copy.Substring(0, copy.Length / 2) + Dummy + copy.Substring(copy.Length / 2))); + Assert.False(decoded.Equals(null)); + } + + public static IEnumerable StringComparison_TestData() => + TestHelper.s_allStringComparisons.SelectMany(comparison => + TestHelper.s_decodedTestData.Select(decoded => new object[] { comparison, decoded })); + + [Theory] + [MemberData(nameof(StringComparison_TestData))] + public void String_Equals_StringComparison(StringComparison comparison, string decoded) + { +#pragma warning disable 0618 // suppress obsolete warning for String.Copy + string copy = string.Copy(decoded); +#pragma warning restore 0618 + if ((int)comparison % 2 != 0) // Odd values are *IgnoreCase + { + Assert.True(decoded.ToLower().Equals(copy.ToUpper(), comparison)); + Assert.True(string.Equals(decoded.ToUpper(), copy.ToLower(), comparison)); + } + else + { + Assert.True(decoded.Equals(copy, comparison)); + Assert.True(string.Equals(decoded, copy, comparison)); + } + } + + public static IEnumerable EndsStartsWith_TestData() => + TestHelper.s_cultureInfos.SelectMany(culture => + new bool[] { true, false }.SelectMany(ignoreCase => + TestHelper.s_decodedTestData.Select(testData => new object[] { culture, ignoreCase, testData }))); + + [Theory] + [MemberData(nameof(EndsStartsWith_TestData))] + public void EndsWith(CultureInfo culture, bool ignoreCase, string decoded) + { + string suffix = string.Empty; + foreach (string textElement in TestHelper.GetTextElements(decoded).Reverse()) + { + suffix = textElement + suffix; + if (ignoreCase) + Assert.True(decoded.ToUpper().EndsWith(suffix.ToLower(), ignoreCase, culture)); + else + Assert.True(decoded.EndsWith(suffix, ignoreCase, culture)); + } + } + + public static IEnumerable EndsStartsWith_Ordinal_TestData() => + new StringComparison[] + { + StringComparison.Ordinal, + StringComparison.OrdinalIgnoreCase + } + .SelectMany(culture => + TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); + + [Theory] + [MemberData(nameof(EndsStartsWith_Ordinal_TestData))] + public void EndsWith_Ordinal(StringComparison comparison, string decoded) + { + for (int i = decoded.Length - 1; i >= 0; i--) + { + if (comparison == StringComparison.OrdinalIgnoreCase) + Assert.True(decoded.ToLower().EndsWith(decoded.Substring(i).ToUpper(), comparison)); + else + Assert.True(decoded.EndsWith(decoded.Substring(i), comparison)); + } + } + + [Theory] + [MemberData(nameof(EndsStartsWith_TestData))] + public void StartsWith(CultureInfo culture, bool ignoreCase, string decoded) + { + string prefix = string.Empty; + foreach (string textElement in TestHelper.GetTextElements(decoded)) + { + prefix += textElement; + if (ignoreCase) + Assert.True(decoded.ToUpper().StartsWith(prefix.ToLower(), ignoreCase, culture)); + else + Assert.True(decoded.StartsWith(prefix, ignoreCase, culture)); + } + } + + [Theory] + [MemberData(nameof(EndsStartsWith_Ordinal_TestData))] + public void StartsWith_Ordinal(StringComparison comparison, string decoded) + { + for (int i = 0; i < decoded.Length; i++) + { + if (comparison == StringComparison.OrdinalIgnoreCase) + Assert.True(decoded.ToLower().StartsWith(decoded.Substring(0, i).ToUpper(), comparison)); + else + Assert.True(decoded.StartsWith(decoded.Substring(0, i), comparison)); + } + } + + [Theory] + [MemberData(nameof(StringComparison_TestData))] + public void IndexOf_MultipleElements(StringComparison comparison, string decoded) + { + bool ignoreCase = (int)comparison % 2 != 0; + if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) + { + for (int i = 0; i < decoded.Length; i++) + { + string left = decoded.Substring(0, i); + string right = decoded.Substring(i); + Assert.Equal(decoded.Length, left.Length + right.Length); + + Assert.Equal(0, decoded.IndexOf(left, comparison)); + // right substring starts at the end of left one. + Assert.Equal(left.Length, decoded.IndexOf(right, startIndex: left.Length, comparison)); + + if (ignoreCase) + { + Assert.Equal(0, decoded.ToLower().IndexOf(left.ToUpper(), comparison)); + Assert.Equal(left.Length, decoded.ToLower().IndexOf(right.ToUpper(), startIndex: left.Length, comparison)); + } + } + } + else + { + 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)); + + if (ignoreCase) + { + Assert.Equal(0, decoded.ToLower().IndexOf(left.ToUpper(), comparison)); + Assert.Equal(left.Length, decoded.ToLower().IndexOf(right.ToUpper(), startIndex: left.Length, comparison)); + } + } + } + } + + [Theory] + [MemberData(nameof(StringComparison_TestData))] + public void IndexOf_SingleElement(StringComparison comparison, string decoded) + { + bool ignoreCase = (int)comparison % 2 != 0; + if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) + { + for (int i = 0; i < decoded.Length; i++) + { + string current = decoded.Substring(i, 1); + // Fast-check the expected index. + Assert.Equal(i, decoded.IndexOf(current, startIndex: i, comparison)); + IndexOf_SingleElement_Core(current, expectedIndex: i, ignoreCase); + } + } + else + { + 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_Core(current, expectedIndex, ignoreCase); + } + } + + void IndexOf_SingleElement_Core(string current, int expectedIndex, bool ignoreCase) + { + int startIndex = 0; + while (true) + { + int result = ignoreCase ? + decoded.ToLower().IndexOf(current.ToUpper(), startIndex, comparison) : + 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_TestData))] + public void LastIndexOf_MultipleElements(StringComparison comparison, string decoded) + { + bool ignoreCase = (int)comparison % 2 != 0; + + // 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)); + if (ignoreCase) + Assert.Equal(0, decoded.ToLower().LastIndexOf(decoded.ToUpper(), comparison)); + + if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) + { + + for (int i = 1; i < decoded.Length; i++) + { + string left = decoded.Substring(0, i); + string right = decoded.Substring(i); + Assert.Equal(decoded.Length, left.Length + right.Length); + + Assert.Equal(0, decoded.LastIndexOf(left, startIndex: left.Length - 1, comparison)); + // right substring starts at the end of left one. + Assert.Equal(left.Length, decoded.LastIndexOf(right, comparison)); + + if (ignoreCase) + { + Assert.Equal(0, decoded.ToLower().LastIndexOf(left.ToUpper(), startIndex: left.Length - 1, comparison)); + Assert.Equal(left.Length, decoded.ToLower().LastIndexOf(right.ToUpper(), comparison)); + } + } + } + else + { + 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)); + + if (ignoreCase) + { + Assert.Equal(0, decoded.ToLower().LastIndexOf(left.ToUpper(), startIndex: left.Length - 1, comparison)); + Assert.Equal(left.Length, decoded.ToLower().LastIndexOf(right.ToUpper(), comparison)); + } + } + } + } + + [Theory] + [MemberData(nameof(StringComparison_TestData))] + public void LastIndexOf_SingleElement(StringComparison comparison, string decoded) + { + bool ignoreCase = (int)comparison % 2 != 0; + if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) + { + for (int i = 0; i < decoded.Length; i++) + { + string current = decoded.Substring(i, 1); + // Fast-check the expected index. + Assert.Equal(i, decoded.LastIndexOf(current, startIndex: i, comparison)); + LastIndexOf_SingleElement_Core(current, expectedIndex: i, ignoreCase); + } + } + else + { + 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_Core(current, expectedIndex, ignoreCase); + } + } + + void LastIndexOf_SingleElement_Core(string current, int expectedIndex, bool ignoreCase) + { + int startIndex = decoded.Length - 1; + while (true) + { + int result = ignoreCase ? + decoded.ToLower().LastIndexOf(current.ToUpper(), startIndex, comparison) : + 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.DecodedTestData), MemberType = typeof(TestHelper))] + public void Replace(string decoded) + { + Assert.False(decoded.Contains(Dummy)); + + foreach (string textElement in TestHelper.GetTextElements(decoded)) + { + int occurrences = decoded.Split([textElement], StringSplitOptions.None).Length; + + string replaced = decoded.Replace(textElement, Dummy); + Assert.Equal(occurrences, replaced.Split([Dummy], StringSplitOptions.None).Length); + + if (textElement.Length == 1) + { + replaced = decoded.Replace(textElement[0], Dummy[0]); + Assert.Equal(occurrences, replaced.Split([Dummy], StringSplitOptions.None).Length); + } + } + } + + [Theory] + [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + public void Split(string decoded) + { + Assert.Single(Split(decoded, Dummy)); + Assert.Single(Split(decoded, Dummy[0])); + + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + + for (int i = 0; i < textElements.Length; i++) + { + var result = Split(decoded, textElements[i]); + Assert.True(result.Length > 1); + } + + static string[] Split(string str, params T[] separators) => + separators switch + { + char[] chars => str.Split(chars, StringSplitOptions.None), + string[] strings => str.Split(strings, StringSplitOptions.None), + _ => throw new ArgumentException() + }; + } +} From 2d68cfc77d2d37ac916f67194d64c380268b5322 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 28 Jul 2025 12:14:13 -0500 Subject: [PATCH 02/21] Address feedback Fix PathTooLong CI issue --- .../ComplianceTests/GB18030/TestHelper.cs | 41 +++++++++++++++-- .../GB18030/Tests/ConsoleTests.cs | 17 ++++--- .../GB18030/Tests/DirectoryInfoTests.cs | 45 +++++++++---------- .../GB18030/Tests/DirectoryTestBase.cs | 9 ++-- .../GB18030/Tests/DirectoryTests.cs | 22 ++++----- .../GB18030/Tests/EncodingTests.cs | 3 ++ .../GB18030/Tests/FileInfoTests.cs | 3 ++ .../GB18030/Tests/FileTestBase.cs | 11 +++-- .../GB18030/Tests/FileTests.cs | 3 ++ .../GB18030/Tests/StringTests.cs | 5 ++- 10 files changed, 106 insertions(+), 53 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 998682b643bcbe..3f224a932438cd 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -1,3 +1,6 @@ +// 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; @@ -41,21 +44,51 @@ internal static Encoding GB18030Encoding { get { + if (s_gb18030Encoding is null) + { #if !NETFRAMEWORK - Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); + Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); #endif - return s_gb18030Encoding ??= Encoding.GetEncoding("gb18030"); + s_gb18030Encoding = Encoding.GetEncoding("gb18030"); + } + + return s_gb18030Encoding; } } private static readonly IEnumerable s_encodedTestData = GetTestData(); internal static readonly IEnumerable s_decodedTestData = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)); - internal static readonly IEnumerable s_splitNewLineDecodedTestData = s_decodedTestData.SelectMany( + private static readonly IEnumerable s_splitNewLineDecodedTestData = s_decodedTestData.SelectMany( data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)); + internal static readonly IEnumerable s_nonExceedingPathNameMaxDecodedTestData = + s_splitNewLineDecodedTestData.SelectMany( + (data) => + { + const int MaxPathSegmentName = 255; + 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 EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data }); public static IEnumerable DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data }); - public static IEnumerable SplitNewLineDecodedTestData { get; } = s_splitNewLineDecodedTestData.Select(data => new object[] { data }); + public static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_nonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }); private static IEnumerable GetTestData() { diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs index ea99e3dc7a309f..d96e586a14ce92 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs @@ -1,3 +1,6 @@ +// 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; @@ -6,7 +9,9 @@ namespace GB18030.Tests; public class ConsoleTests { - [Theory] + protected static readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; + + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] public void StandardOutput(string decodedText) { @@ -24,10 +29,10 @@ public void StandardOutput(string decodedText) Assert.Equal(decodedText, remoteHandle.Process.StandardOutput.ReadToEnd()); - Assert.True(remoteHandle.Process.WaitForExit(5_000)); + Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); } - [Theory] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] public void StandardInput(string decodedText) { @@ -58,10 +63,10 @@ public void StandardInput(string decodedText) remoteHandle.Process.StandardInput.Close(); } - Assert.True(remoteHandle.Process.WaitForExit(5_000)); + Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); } - [Theory] + [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] public void StandardError(string decodedText) { @@ -79,6 +84,6 @@ public void StandardError(string decodedText) Assert.Equal(decodedText, remoteHandle.Process.StandardError.ReadToEnd()); - Assert.True(remoteHandle.Process.WaitForExit(5_000)); + 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 index 59c8073976a889..bf35b8e96771bc 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs @@ -1,3 +1,6 @@ +// 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; @@ -12,38 +15,32 @@ public class DirectoryInfoTests : DirectoryTestBase protected override void MoveDirectory(string source, string destination) => new DirectoryInfo(source).MoveTo(destination); [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] - public void CreateSubdirectory(string decoded) + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + public void CreateSubdirectory(string gb18030Line) { - foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) - { - var subDirInfo = TempDirectory.CreateSubdirectory(gb18030Line); - - Assert.True(subDirInfo.Exists); - Assert.Equal(gb18030Line, subDirInfo.Name); - Assert.Equal(Path.Combine(TempDirectory.FullName, gb18030Line), subDirInfo.FullName); - } + 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.DecodedTestData), MemberType = typeof(TestHelper))] - public void EnumerateFileSystemInfos(string decoded) + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemInfos(string gb18030Line) { string rootDir = TempDirectory.FullName; List expected = []; - foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) - { - 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); - } + 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 index f227312aa87406..b03e58cf6c46a5 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -1,3 +1,6 @@ +// 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; @@ -20,7 +23,7 @@ public DirectoryTestBase() } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Create(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -33,7 +36,7 @@ public void Create(string gb18030Line) public static IEnumerable Delete_TestData() => new int[] { 0, 2, 8 }.SelectMany(recurseLevel => - TestHelper.s_splitNewLineDecodedTestData.Select(testData => new object[] { recurseLevel, testData })); + TestHelper.s_nonExceedingPathNameMaxDecodedTestData.Select(testData => new object[] { recurseLevel, testData })); [Theory] [MemberData(nameof(Delete_TestData))] @@ -52,7 +55,7 @@ public void Delete(int recurseLevel, string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Move(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs index e90d1ed889ca53..0132ba27bb1d62 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs @@ -1,3 +1,6 @@ +// 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; @@ -12,22 +15,19 @@ public class DirectoryTests : DirectoryTestBase protected override void MoveDirectory(string source, string destination) => Directory.Move(source, destination); [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] - public void EnumerateFileSystemEntries(string decoded) + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + public void EnumerateFileSystemEntries(string gb18030Line) { string rootDir = TempDirectory.FullName; List expected = []; - foreach (string gb18030Line in decoded.Split([Environment.NewLine], StringSplitOptions.None)) - { - string gb18030Dir = Path.Combine(rootDir, gb18030Line); - Directory.CreateDirectory(gb18030Dir); - expected.Add(gb18030Dir); + 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); - } + 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 index dfebb4485b0832..f12ac3292e48a9 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs @@ -1,3 +1,6 @@ +// 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; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs index 77f04c813c3a4f..56684a200c8425 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileInfoTests.cs @@ -1,3 +1,6 @@ +// 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; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs index e201186189d9be..e1f45beef92e2c 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs @@ -1,3 +1,6 @@ +// 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; @@ -24,7 +27,7 @@ public void Dispose() } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Create(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -36,7 +39,7 @@ public void Create(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Delete(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -49,7 +52,7 @@ public void Delete(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Move(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -66,7 +69,7 @@ public void Move(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] public void Copy(string gb18030Line) { ReadOnlySpan sampleContent = "File_Copy"u8; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs index 91cc7573e89324..38ae3b3b55649a 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -1,3 +1,6 @@ +// 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; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index 7d9a6029e38433..b7b336cc5658da 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -1,4 +1,7 @@ -using System; +// 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; From 570c9b4292bc188705477a72a0dc00f18db9a89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 6 Aug 2025 11:12:29 -0500 Subject: [PATCH 03/21] Fix StringTests - remove ordinal comparison tests --- .../ComplianceTests/GB18030/TestHelper.cs | 14 +- .../GB18030/Tests/StringTests.cs | 338 ++++++------------ 2 files changed, 117 insertions(+), 235 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 3f224a932438cd..7b261cf94f86ab 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -12,20 +12,14 @@ namespace GB18030.Tests; public static class TestHelper { - internal static CultureInfo[] s_cultureInfos = [ + internal static CultureInfo[] s_cultures = [ CultureInfo.CurrentCulture, CultureInfo.InvariantCulture, new CultureInfo("zh-CN")]; internal static CompareOptions[] s_compareOptions = [ CompareOptions.None, - CompareOptions.IgnoreCase, - CompareOptions.Ordinal, - CompareOptions.OrdinalIgnoreCase]; - - internal static readonly StringComparison[] s_ordinalStringComparisons = [ - StringComparison.Ordinal, - StringComparison.OrdinalIgnoreCase]; + CompareOptions.IgnoreCase]; internal static readonly StringComparison[] s_nonOrdinalStringComparisons = [ StringComparison.CurrentCulture, @@ -33,10 +27,6 @@ public static class TestHelper StringComparison.InvariantCulture, StringComparison.InvariantCultureIgnoreCase]; - internal static readonly StringComparison[] s_allStringComparisons = [ - .. s_ordinalStringComparisons, - .. s_nonOrdinalStringComparisons]; - internal static string s_testDataFilePath = Path.Combine(AppContext.BaseDirectory, "GB18030", "Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt"); private static Encoding? s_gb18030Encoding; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index b7b336cc5658da..4e9633314abd06 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -27,7 +27,7 @@ public unsafe void Ctor(byte[] encoded) } public static IEnumerable Compare_TestData() => - TestHelper.s_cultureInfos.SelectMany(culture => + TestHelper.s_cultures.SelectMany(culture => TestHelper.s_compareOptions.SelectMany(option => TestHelper.s_decodedTestData.Select(testData => new object[] { culture, option, testData }))); @@ -41,125 +41,79 @@ public void Compare(CultureInfo culture, CompareOptions option, string decoded) Assert.True(string.Compare(decoded, copy, culture, option) == 0); } - [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] - public void Contains(string decoded) - { - for (int i = 0; i < decoded.Length; i++) - Assert.True(decoded.Contains(decoded.Substring(0, i))); - - for (int i = decoded.Length - 1; i >= 0; i--) - Assert.True(decoded.Contains(decoded.Substring(i))); - } + public static IEnumerable Contains_TestData() => + TestHelper.s_nonOrdinalStringComparisons.SelectMany(comparison => + TestHelper.s_decodedTestData.Select(testData => new object[] { comparison, testData })); [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] - public void String_Equals(string decoded) + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [MemberData(nameof(Contains_TestData))] + public void Contains(StringComparison comparison, string decoded) { -#pragma warning disable 0618 // suppress obsolete warning for String.Copy - string copy = string.Copy(decoded); -#pragma warning restore 0618 - Assert.True(decoded.Equals(decoded)); - Assert.True(decoded.Equals(copy)); - Assert.True(decoded.Equals((object)copy)); - Assert.True(string.Equals(decoded, copy)); - - Assert.False(decoded.Equals(copy + Dummy)); - Assert.False(decoded.Equals(Dummy + copy)); - Assert.False(decoded.Equals(copy.Substring(0, copy.Length / 2) + Dummy + copy.Substring(copy.Length / 2))); - Assert.False(decoded.Equals(null)); + 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_TestData() => - TestHelper.s_allStringComparisons.SelectMany(comparison => + TestHelper.s_nonOrdinalStringComparisons.SelectMany(comparison => TestHelper.s_decodedTestData.Select(decoded => new object[] { comparison, decoded })); [Theory] [MemberData(nameof(StringComparison_TestData))] - public void String_Equals_StringComparison(StringComparison comparison, string decoded) + public void String_Equals(StringComparison comparison, string decoded) { #pragma warning disable 0618 // suppress obsolete warning for String.Copy string copy = string.Copy(decoded); #pragma warning restore 0618 - if ((int)comparison % 2 != 0) // Odd values are *IgnoreCase - { - Assert.True(decoded.ToLower().Equals(copy.ToUpper(), comparison)); - Assert.True(string.Equals(decoded.ToUpper(), copy.ToLower(), comparison)); - } - else + + 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++) { - Assert.True(decoded.Equals(copy, comparison)); - Assert.True(string.Equals(decoded, copy, comparison)); + 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_TestData() => - TestHelper.s_cultureInfos.SelectMany(culture => - new bool[] { true, false }.SelectMany(ignoreCase => - TestHelper.s_decodedTestData.Select(testData => new object[] { culture, ignoreCase, testData }))); + TestHelper.s_cultures.SelectMany(culture => + TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); [Theory] [MemberData(nameof(EndsStartsWith_TestData))] - public void EndsWith(CultureInfo culture, bool ignoreCase, string decoded) + public void EndsWith(CultureInfo culture, string decoded) { string suffix = string.Empty; foreach (string textElement in TestHelper.GetTextElements(decoded).Reverse()) { suffix = textElement + suffix; - if (ignoreCase) - Assert.True(decoded.ToUpper().EndsWith(suffix.ToLower(), ignoreCase, culture)); - else - Assert.True(decoded.EndsWith(suffix, ignoreCase, culture)); - } - } - - public static IEnumerable EndsStartsWith_Ordinal_TestData() => - new StringComparison[] - { - StringComparison.Ordinal, - StringComparison.OrdinalIgnoreCase - } - .SelectMany(culture => - TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); - - [Theory] - [MemberData(nameof(EndsStartsWith_Ordinal_TestData))] - public void EndsWith_Ordinal(StringComparison comparison, string decoded) - { - for (int i = decoded.Length - 1; i >= 0; i--) - { - if (comparison == StringComparison.OrdinalIgnoreCase) - Assert.True(decoded.ToLower().EndsWith(decoded.Substring(i).ToUpper(), comparison)); - else - Assert.True(decoded.EndsWith(decoded.Substring(i), comparison)); + Assert.True(decoded.EndsWith(suffix, ignoreCase: false, culture)); } } [Theory] [MemberData(nameof(EndsStartsWith_TestData))] - public void StartsWith(CultureInfo culture, bool ignoreCase, string decoded) + public void StartsWith(CultureInfo culture, string decoded) { string prefix = string.Empty; foreach (string textElement in TestHelper.GetTextElements(decoded)) { prefix += textElement; - if (ignoreCase) - Assert.True(decoded.ToUpper().StartsWith(prefix.ToLower(), ignoreCase, culture)); - else - Assert.True(decoded.StartsWith(prefix, ignoreCase, culture)); - } - } - - [Theory] - [MemberData(nameof(EndsStartsWith_Ordinal_TestData))] - public void StartsWith_Ordinal(StringComparison comparison, string decoded) - { - for (int i = 0; i < decoded.Length; i++) - { - if (comparison == StringComparison.OrdinalIgnoreCase) - Assert.True(decoded.ToLower().StartsWith(decoded.Substring(0, i).ToUpper(), comparison)); - else - Assert.True(decoded.StartsWith(decoded.Substring(0, i), comparison)); + Assert.True(decoded.StartsWith(prefix, ignoreCase: false, culture)); } } @@ -167,44 +121,18 @@ public void StartsWith_Ordinal(StringComparison comparison, string decoded) [MemberData(nameof(StringComparison_TestData))] public void IndexOf_MultipleElements(StringComparison comparison, string decoded) { - bool ignoreCase = (int)comparison % 2 != 0; - if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) - { - for (int i = 0; i < decoded.Length; i++) - { - string left = decoded.Substring(0, i); - string right = decoded.Substring(i); - Assert.Equal(decoded.Length, left.Length + right.Length); + Assert.NotEqual(StringComparison.Ordinal, comparison); + Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); - Assert.Equal(0, decoded.IndexOf(left, comparison)); - // right substring starts at the end of left one. - Assert.Equal(left.Length, decoded.IndexOf(right, startIndex: left.Length, comparison)); - - if (ignoreCase) - { - Assert.Equal(0, decoded.ToLower().IndexOf(left.ToUpper(), comparison)); - Assert.Equal(left.Length, decoded.ToLower().IndexOf(right.ToUpper(), startIndex: left.Length, comparison)); - } - } - } - else + string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); + for (int i = 0; i < textElements.Length; i++) { - 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)); + string left = string.Concat(textElements.Take(i)); + string right = string.Concat(textElements.Skip(i)); + Assert.Equal(decoded.Length, left.Length + right.Length); - if (ignoreCase) - { - Assert.Equal(0, decoded.ToLower().IndexOf(left.ToUpper(), comparison)); - Assert.Equal(left.Length, decoded.ToLower().IndexOf(right.ToUpper(), startIndex: left.Length, comparison)); - } - } + Assert.Equal(0, decoded.IndexOf(left, comparison)); + Assert.Equal(left.Length, decoded.IndexOf(right, startIndex: left.Length, comparison)); } } @@ -212,38 +140,25 @@ public void IndexOf_MultipleElements(StringComparison comparison, string decoded [MemberData(nameof(StringComparison_TestData))] public void IndexOf_SingleElement(StringComparison comparison, string decoded) { - bool ignoreCase = (int)comparison % 2 != 0; - if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) - { - for (int i = 0; i < decoded.Length; i++) - { - string current = decoded.Substring(i, 1); - // Fast-check the expected index. - Assert.Equal(i, decoded.IndexOf(current, startIndex: i, comparison)); - IndexOf_SingleElement_Core(current, expectedIndex: i, ignoreCase); - } - } - else + 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[] 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_Core(current, expectedIndex, ignoreCase); - } + 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_Core(string current, int expectedIndex, bool ignoreCase) + void IndexOf_SingleElement_Slow(string current, int expectedIndex) { int startIndex = 0; while (true) { - int result = ignoreCase ? - decoded.ToLower().IndexOf(current.ToUpper(), startIndex, comparison) : - decoded.IndexOf(current, startIndex, comparison); + int result = decoded.IndexOf(current, startIndex, comparison); if (result == -1 || result > expectedIndex) Assert.Fail($"'{current}' not found or found too late in '{decoded}'"); @@ -262,52 +177,22 @@ void IndexOf_SingleElement_Core(string current, int expectedIndex, bool ignoreCa [MemberData(nameof(StringComparison_TestData))] public void LastIndexOf_MultipleElements(StringComparison comparison, string decoded) { - bool ignoreCase = (int)comparison % 2 != 0; + 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)); - if (ignoreCase) - Assert.Equal(0, decoded.ToLower().LastIndexOf(decoded.ToUpper(), comparison)); - if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) + 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); - for (int i = 1; i < decoded.Length; i++) - { - string left = decoded.Substring(0, i); - string right = decoded.Substring(i); - Assert.Equal(decoded.Length, left.Length + right.Length); - - Assert.Equal(0, decoded.LastIndexOf(left, startIndex: left.Length - 1, comparison)); - // right substring starts at the end of left one. - Assert.Equal(left.Length, decoded.LastIndexOf(right, comparison)); - - if (ignoreCase) - { - Assert.Equal(0, decoded.ToLower().LastIndexOf(left.ToUpper(), startIndex: left.Length - 1, comparison)); - Assert.Equal(left.Length, decoded.ToLower().LastIndexOf(right.ToUpper(), comparison)); - } - } - } - else - { - 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)); - - if (ignoreCase) - { - Assert.Equal(0, decoded.ToLower().LastIndexOf(left.ToUpper(), startIndex: left.Length - 1, comparison)); - Assert.Equal(left.Length, decoded.ToLower().LastIndexOf(right.ToUpper(), comparison)); - } - } + Assert.Equal(0, decoded.LastIndexOf(left, startIndex: left.Length - 1, comparison)); + Assert.Equal(left.Length, decoded.LastIndexOf(right, comparison)); } } @@ -315,38 +200,25 @@ public void LastIndexOf_MultipleElements(StringComparison comparison, string dec [MemberData(nameof(StringComparison_TestData))] public void LastIndexOf_SingleElement(StringComparison comparison, string decoded) { - bool ignoreCase = (int)comparison % 2 != 0; - if (comparison is StringComparison.Ordinal or StringComparison.OrdinalIgnoreCase) - { - for (int i = 0; i < decoded.Length; i++) - { - string current = decoded.Substring(i, 1); - // Fast-check the expected index. - Assert.Equal(i, decoded.LastIndexOf(current, startIndex: i, comparison)); - LastIndexOf_SingleElement_Core(current, expectedIndex: i, ignoreCase); - } - } - else + 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[] 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_Core(current, expectedIndex, ignoreCase); - } + 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_Core(string current, int expectedIndex, bool ignoreCase) + void LastIndexOf_SingleElement_Slow(string current, int expectedIndex) { int startIndex = decoded.Length - 1; while (true) { - int result = ignoreCase ? - decoded.ToLower().LastIndexOf(current.ToUpper(), startIndex, comparison) : - decoded.LastIndexOf(current, startIndex, comparison); + int result = decoded.LastIndexOf(current, startIndex, comparison); if (result == -1 || result < expectedIndex) Assert.Fail($"'{current}' not found or found too late in '{decoded}'"); @@ -369,40 +241,60 @@ public void Replace(string decoded) foreach (string textElement in TestHelper.GetTextElements(decoded)) { - int occurrences = decoded.Split([textElement], StringSplitOptions.None).Length; - + int occurrences = SplitHelper(decoded, textElement).Length; string replaced = decoded.Replace(textElement, Dummy); - Assert.Equal(occurrences, replaced.Split([Dummy], StringSplitOptions.None).Length); + Assert.Equal(occurrences, SplitHelper(replaced, Dummy).Length); if (textElement.Length == 1) { replaced = decoded.Replace(textElement[0], Dummy[0]); - Assert.Equal(occurrences, replaced.Split([Dummy], StringSplitOptions.None).Length); + Assert.Equal(occurrences, SplitHelper(replaced, Dummy).Length); } } } + public static IEnumerable Replace_NetCore_TestData() => + TestHelper.s_cultures.SelectMany(culture => + TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); + +#if NETCOREAPP + [Theory] + [MemberData(nameof(Replace_NetCore_TestData))] + public void Replace_CultureInfo(CultureInfo culture, string decoded) + { + 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.DecodedTestData), MemberType = typeof(TestHelper))] public void Split(string decoded) { - Assert.Single(Split(decoded, Dummy)); - Assert.Single(Split(decoded, Dummy[0])); - string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); for (int i = 0; i < textElements.Length; i++) { - var result = Split(decoded, textElements[i]); + var result = SplitHelper(decoded, textElements[i]); Assert.True(result.Length > 1); } - - static string[] Split(string str, params T[] separators) => - separators switch - { - char[] chars => str.Split(chars, StringSplitOptions.None), - string[] strings => str.Split(strings, StringSplitOptions.None), - _ => throw new ArgumentException() - }; } + + 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() + }; } From e281e13fcc7287f5c15c707f435ae323ed3b1111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 6 Aug 2025 12:38:10 -0500 Subject: [PATCH 04/21] Change parameter ordering --- .../GB18030/Tests/DirectoryTestBase.cs | 6 +-- .../GB18030/Tests/StringTests.cs | 40 +++++++++---------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs index b03e58cf6c46a5..c62f85f53245a6 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -35,12 +35,12 @@ public void Create(string gb18030Line) } public static IEnumerable Delete_TestData() => - new int[] { 0, 2, 8 }.SelectMany(recurseLevel => - TestHelper.s_nonExceedingPathNameMaxDecodedTestData.Select(testData => new object[] { recurseLevel, testData })); + TestHelper.s_nonExceedingPathNameMaxDecodedTestData.SelectMany(testData => + new int[] { 0, 2, 8 }.Select(recurseLevel => new object[] { testData, recurseLevel })); [Theory] [MemberData(nameof(Delete_TestData))] - public void Delete(int recurseLevel, string gb18030Line) + 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())); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index 4e9633314abd06..7bf1544218c371 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -27,13 +27,13 @@ public unsafe void Ctor(byte[] encoded) } public static IEnumerable Compare_TestData() => + TestHelper.s_decodedTestData.SelectMany(testData => TestHelper.s_cultures.SelectMany(culture => - TestHelper.s_compareOptions.SelectMany(option => - TestHelper.s_decodedTestData.Select(testData => new object[] { culture, option, testData }))); + TestHelper.s_compareOptions.Select(option => new object[] { testData, culture, option }))); [Theory] [MemberData(nameof(Compare_TestData))] - public void Compare(CultureInfo culture, CompareOptions option, string decoded) + public void Compare(string decoded, CultureInfo culture, CompareOptions option) { #pragma warning disable 0618 // suppress obsolete warning for String.Copy string copy = string.Copy(decoded); @@ -42,13 +42,13 @@ public void Compare(CultureInfo culture, CompareOptions option, string decoded) } public static IEnumerable Contains_TestData() => - TestHelper.s_nonOrdinalStringComparisons.SelectMany(comparison => - TestHelper.s_decodedTestData.Select(testData => new object[] { comparison, testData })); + TestHelper.s_decodedTestData.SelectMany(testData => + TestHelper.s_nonOrdinalStringComparisons.Select(comparison => new object[] { testData, comparison })); [Theory] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] [MemberData(nameof(Contains_TestData))] - public void Contains(StringComparison comparison, string decoded) + public void Contains(string decoded, StringComparison comparison) { string current = string.Empty; foreach (string element in TestHelper.GetTextElements(decoded)) @@ -66,12 +66,12 @@ public void Contains(StringComparison comparison, string decoded) } public static IEnumerable StringComparison_TestData() => - TestHelper.s_nonOrdinalStringComparisons.SelectMany(comparison => - TestHelper.s_decodedTestData.Select(decoded => new object[] { comparison, decoded })); + TestHelper.s_decodedTestData.SelectMany(decoded => + TestHelper.s_nonOrdinalStringComparisons.Select(comparison => new object[] { decoded, comparison })); [Theory] [MemberData(nameof(StringComparison_TestData))] - public void String_Equals(StringComparison comparison, string decoded) + public void String_Equals(string decoded, StringComparison comparison) { #pragma warning disable 0618 // suppress obsolete warning for String.Copy string copy = string.Copy(decoded); @@ -90,12 +90,12 @@ public void String_Equals(StringComparison comparison, string decoded) } public static IEnumerable EndsStartsWith_TestData() => - TestHelper.s_cultures.SelectMany(culture => - TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); + TestHelper.s_decodedTestData.SelectMany(testData => + TestHelper.s_cultures.Select(culture => new object[] { testData, culture })); [Theory] [MemberData(nameof(EndsStartsWith_TestData))] - public void EndsWith(CultureInfo culture, string decoded) + public void EndsWith(string decoded, CultureInfo culture) { string suffix = string.Empty; foreach (string textElement in TestHelper.GetTextElements(decoded).Reverse()) @@ -107,7 +107,7 @@ public void EndsWith(CultureInfo culture, string decoded) [Theory] [MemberData(nameof(EndsStartsWith_TestData))] - public void StartsWith(CultureInfo culture, string decoded) + public void StartsWith(string decoded, CultureInfo culture) { string prefix = string.Empty; foreach (string textElement in TestHelper.GetTextElements(decoded)) @@ -119,7 +119,7 @@ public void StartsWith(CultureInfo culture, string decoded) [Theory] [MemberData(nameof(StringComparison_TestData))] - public void IndexOf_MultipleElements(StringComparison comparison, string decoded) + public void IndexOf_MultipleElements(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); @@ -138,7 +138,7 @@ public void IndexOf_MultipleElements(StringComparison comparison, string decoded [Theory] [MemberData(nameof(StringComparison_TestData))] - public void IndexOf_SingleElement(StringComparison comparison, string decoded) + public void IndexOf_SingleElement(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); @@ -175,7 +175,7 @@ void IndexOf_SingleElement_Slow(string current, int expectedIndex) [Theory] [MemberData(nameof(StringComparison_TestData))] - public void LastIndexOf_MultipleElements(StringComparison comparison, string decoded) + public void LastIndexOf_MultipleElements(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); @@ -198,7 +198,7 @@ public void LastIndexOf_MultipleElements(StringComparison comparison, string dec [Theory] [MemberData(nameof(StringComparison_TestData))] - public void LastIndexOf_SingleElement(StringComparison comparison, string decoded) + public void LastIndexOf_SingleElement(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); Assert.NotEqual(StringComparison.OrdinalIgnoreCase, comparison); @@ -254,13 +254,13 @@ public void Replace(string decoded) } public static IEnumerable Replace_NetCore_TestData() => - TestHelper.s_cultures.SelectMany(culture => - TestHelper.s_decodedTestData.Select(testData => new object[] { culture, testData })); + TestHelper.s_decodedTestData.SelectMany(testData => + TestHelper.s_cultures.Select(culture => new object[] { testData, culture })); #if NETCOREAPP [Theory] [MemberData(nameof(Replace_NetCore_TestData))] - public void Replace_CultureInfo(CultureInfo culture, string decoded) + public void Replace_CultureInfo(string decoded, CultureInfo culture) { Assert.False(decoded.Contains(Dummy)); From b7af2dc27881e6fefba09d50bf22f85220929eb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Wed, 6 Aug 2025 17:29:39 -0500 Subject: [PATCH 05/21] Add RegexTests --- src/libraries/Common/Common.Tests.slnx | 1 + .../ComplianceTests/Compliance.Tests.csproj | 23 +++- .../GB18030/Tests/RegexTests.cs | 122 ++++++++++++++++++ .../FunctionalTests/Regex.Tests.Common.cs | 2 +- .../RegexGeneratorHelper.netcoreapp.cs | 10 +- 5 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs diff --git a/src/libraries/Common/Common.Tests.slnx b/src/libraries/Common/Common.Tests.slnx index 154e0a5b1a48f0..218fb5ae746858 100644 --- a/src/libraries/Common/Common.Tests.slnx +++ b/src/libraries/Common/Common.Tests.slnx @@ -2,6 +2,7 @@ + diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj index 840c426ec005d8..2403110d454bfe 100644 --- a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -16,13 +16,30 @@ + + + + + + + + + + + + + + - - Always - + 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..37eca98d56d96e --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -0,0 +1,122 @@ +// 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.Text.Unicode; +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. +/// +public class RegexTests +{ + // Ranges added in GB18030-2020 + private static readonly UnicodeRange s_cjkNewRange = UnicodeRange.Create((char)0x9FF0, (char)0x9FFF); + private static readonly UnicodeRange s_cjkExtensionANewRange = UnicodeRange.Create((char)0x4DB6, (char)0x4DBF); + + private static readonly IEnumerable s_cjkNewCharacters = Enumerable.Range(s_cjkNewRange.FirstCodePoint, s_cjkNewRange.Length).Select(c => ((char)c).ToString()); + private static readonly IEnumerable s_cjkExtensionANewCharacters = Enumerable.Range(s_cjkExtensionANewRange.FirstCodePoint, s_cjkExtensionANewRange.Length).Select(c => ((char)c).ToString()); + private static readonly IEnumerable s_allNewCharacters = s_cjkNewCharacters.Union(s_cjkExtensionANewCharacters); + + // https://learn.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#supported-named-blocks + private static readonly Dictionary s_rangeToRegexMap = new() + { + { s_cjkNewRange, ("IsCJKUnifiedIdeographs", s_cjkNewCharacters.ToArray()) }, + { s_cjkExtensionANewRange, ("IsCJKUnifiedIdeographsExtensionA", s_cjkExtensionANewCharacters.ToArray()) } + }; + + public static IEnumerable UnicodeCategories_TestData() => + RegexHelpers.AvailableEngines.SelectMany(engine => + TestHelper.s_cultures.Select(culture => new object[] { engine, culture })); + + [Theory] + [MemberData(nameof(UnicodeCategories_TestData))] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] + 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_allNewCharacters) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[\p{Lo}]", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"\p{L}", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[\p{L}]", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.Matches(r, element); + } + + [Theory] + [MemberData(nameof(UnicodeCategories_TestData))] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] + 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_allNewCharacters) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[^\p{Lo}]", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"\P{L}", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.DoesNotMatch(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, @"[^\p{L}]", RegexOptions.None, culture); + foreach (string element in s_allNewCharacters) + Assert.DoesNotMatch(r, element); + } + + public static IEnumerable NamedBlock_TestData() => + s_rangeToRegexMap.SelectMany(rangeKvp => + RegexHelpers.AvailableEngines.SelectMany(engine => + TestHelper.s_cultures.Select(culture => new object[] { rangeKvp.Key, engine, culture }))); + + [Theory] + [MemberData(nameof(NamedBlock_TestData))] + public async Task NamedBlock_InclusionAsync(UnicodeRange range, RegexEngine engine, CultureInfo culture) + { + (string namedBlock, string[] charactersInRange) = s_rangeToRegexMap[range]; + + Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\p{{{namedBlock}}}", RegexOptions.None, culture); + foreach (string element in charactersInRange) + Assert.Matches(r, element); + + r = await RegexHelpers.GetRegexAsync(engine, $@"[\p{{{namedBlock}}}]", RegexOptions.None, culture); + foreach (string element in charactersInRange) + Assert.Matches(r, element); + } + + [Theory] + [MemberData(nameof(NamedBlock_TestData))] + public async Task NamedBlock_ExclusionAsync(UnicodeRange range, RegexEngine engine, CultureInfo culture) + { + (string namedBlock, string[] charactersInRange) = s_rangeToRegexMap[range]; + + Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\P{{{namedBlock}}}", RegexOptions.None, culture); + foreach (string element in charactersInRange) + { + Assert.DoesNotMatch(r, element); + } + + r = await RegexHelpers.GetRegexAsync(engine, $@"[^\p{{{namedBlock}}}]", RegexOptions.None, culture); + foreach (string element in charactersInRange) + { + Assert.DoesNotMatch(r, element); + } + } +} 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..21db5608cd2168 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -18,7 +18,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; -using Xunit; +using Xunit; namespace System.Text.RegularExpressions.Tests { @@ -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 From 3fd1b0f007db42cfa52d808ceb4b704e58d18c43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Thu, 7 Aug 2025 22:16:40 -0500 Subject: [PATCH 06/21] Add CharTests --- .../ComplianceTests/Compliance.Tests.csproj | 1 + .../ComplianceTests/GB18030/TestHelper.cs | 1 + .../GB18030/Tests/CharTests.cs | 52 +++++++++++++++++++ .../GB18030/Tests/RegexTests.cs | 1 - 4 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj index 2403110d454bfe..99898c8292d235 100644 --- a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 7b261cf94f86ab..94f2e95ba372c2 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -78,6 +78,7 @@ internal static Encoding GB18030Encoding public static IEnumerable EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data }); public static IEnumerable DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data }); + public static IEnumerable SplitNewLineDecodedTestData { get; } = s_splitNewLineDecodedTestData.Select(data => new object[] { data }); public static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_nonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }); private static IEnumerable GetTestData() 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..a41ffb2b29981d --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Xunit; + +namespace GB18030.Tests; + +public class CharTests +{ + [Theory] + [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + public void ConvertRoundtrips(string decoded) + { + foreach (string element in TestHelper.GetTextElements(decoded)) + { + Assert.Equal(element, char.ConvertFromUtf32(char.ConvertToUtf32(element, 0))); + } + } + + [Theory] + [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + public void Surrogate(string decoded) + { + foreach (string element in TestHelper.GetTextElements(decoded).Where(e => e.Length > 1)) + { + Assert.Equal(2, element.Length); + char high = element[0]; + char low = element[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)); + } + } + + [Theory] + [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] + public void NonSurrogate(string decoded) + { + foreach (string element in TestHelper.GetTextElements(decoded).Where(e => e.Length <= 1)) + { + Assert.Equal(1, element.Length); + char c = element[0]; + Assert.False(char.IsSurrogate(c)); + Assert.False(char.IsLowSurrogate(c)); + Assert.False(char.IsHighSurrogate(c)); + Assert.False(char.IsSurrogatePair(c, c)); + } + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs index 37eca98d56d96e..725d8b38ff0392 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -26,7 +26,6 @@ public class RegexTests private static readonly IEnumerable s_cjkExtensionANewCharacters = Enumerable.Range(s_cjkExtensionANewRange.FirstCodePoint, s_cjkExtensionANewRange.Length).Select(c => ((char)c).ToString()); private static readonly IEnumerable s_allNewCharacters = s_cjkNewCharacters.Union(s_cjkExtensionANewCharacters); - // https://learn.microsoft.com/en-us/dotnet/standard/base-types/character-classes-in-regular-expressions#supported-named-blocks private static readonly Dictionary s_rangeToRegexMap = new() { { s_cjkNewRange, ("IsCJKUnifiedIdeographs", s_cjkNewCharacters.ToArray()) }, From c5bd64cf726a92cf690e90729181d68e4e824a36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 11 Aug 2025 11:43:54 -0500 Subject: [PATCH 07/21] Add CharUnicodeInfoTests, more CharTests, and cleanup RegexTests --- .../ComplianceTests/Compliance.Tests.csproj | 22 ++-- .../ComplianceTests/GB18030/TestHelper.cs | 50 ++++++-- .../GB18030/Tests/CharTests.cs | 117 +++++++++++++++--- .../GB18030/Tests/CharUnicodeInfoTests.cs | 27 ++++ .../GB18030/Tests/FileTests.cs | 6 +- .../GB18030/Tests/RegexTests.cs | 69 +++++------ .../GB18030/Tests/StringTests.cs | 12 +- .../Globalization/CharUnicodeInfoTestData.cs | 6 +- 8 files changed, 223 insertions(+), 86 deletions(-) create mode 100644 src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj index 99898c8292d235..386a669efe325c 100644 --- a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -4,11 +4,13 @@ enable true true + 16.0 + @@ -19,18 +21,14 @@ - + + - + - + @@ -43,4 +41,12 @@ + + + + CharUnicodeInfo\UnicodeData.$(UnicodeUcdVersion).txt + UnicodeData.txt + + + diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 94f2e95ba372c2..200ca4b58bbb21 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -4,30 +4,64 @@ 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 { - internal static CultureInfo[] s_cultures = [ + // 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[] s_compareOptions = [ - CompareOptions.None, - CompareOptions.IgnoreCase]; + internal static CompareOptions[] CompareOptions { get; } = [ + System.Globalization.CompareOptions.None, + System.Globalization.CompareOptions.IgnoreCase]; - internal static readonly StringComparison[] s_nonOrdinalStringComparisons = [ + internal static StringComparison[] NonOrdinalStringComparisons { get; } = [ StringComparison.CurrentCulture, StringComparison.CurrentCultureIgnoreCase, StringComparison.InvariantCulture, StringComparison.InvariantCultureIgnoreCase]; - internal static string s_testDataFilePath = Path.Combine(AppContext.BaseDirectory, "GB18030", "Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt"); + 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 @@ -78,8 +112,8 @@ internal static Encoding GB18030Encoding public static IEnumerable EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data }); public static IEnumerable DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data }); - public static IEnumerable SplitNewLineDecodedTestData { get; } = s_splitNewLineDecodedTestData.Select(data => new object[] { data }); public static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_nonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }); + public static IEnumerable GB18030CharUnicodeInfoTestData { get; } = s_gb18030CharUnicodeInfo.Select(data => new object[] { data }); private static IEnumerable GetTestData() { @@ -87,7 +121,7 @@ private static IEnumerable GetTestData() 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(s_testDataFilePath); + ReadOnlyMemory testFileBytes = File.ReadAllBytes(TestDataFilePath); while (testFileBytes.Length > 0) { diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs index a41ffb2b29981d..a8a1c1e4e77e0a 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -1,52 +1,131 @@ // 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; +using System.Globalization.Tests; using System.Linq; +using System.Xml.Linq; +using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Xunit; +using Xunit.Sdk; namespace GB18030.Tests; public class CharTests { [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] - public void ConvertRoundtrips(string decoded) + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + public void Convert(CharUnicodeInfoTestCase testCase) { - foreach (string element in TestHelper.GetTextElements(decoded)) + 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.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + public void Parse(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + if (utf32String.Length > 1) { - Assert.Equal(element, char.ConvertFromUtf32(char.ConvertToUtf32(element, 0))); + 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.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] - public void Surrogate(string decoded) + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + public void IsSurrogate(CharUnicodeInfoTestCase testCase) { - foreach (string element in TestHelper.GetTextElements(decoded).Where(e => e.Length > 1)) + string utf32String = testCase.Utf32CodeValue; + if (utf32String.Length > 1) { - Assert.Equal(2, element.Length); - char high = element[0]; - char low = element[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)); } - } - - [Theory] - [MemberData(nameof(TestHelper.SplitNewLineDecodedTestData), MemberType = typeof(TestHelper))] - public void NonSurrogate(string decoded) - { - foreach (string element in TestHelper.GetTextElements(decoded).Where(e => e.Length <= 1)) + else { - Assert.Equal(1, element.Length); - char c = element[0]; + 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.GB18030CharUnicodeInfoTestData), 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.GB18030CharUnicodeInfoTestData), 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..90e9100731436e --- /dev/null +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs @@ -0,0 +1,27 @@ +// 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; +using System.Globalization.Tests; +using Xunit; + +namespace GB18030.Tests; + +public class CharUnicodeInfoTests +{ + [Theory] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + public void GetUnicodeCategory(CharUnicodeInfoTestCase testCase) + { + if (testCase.Utf32CodeValue.Length == 1) + { + Assert.Equal(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue[0])); + } + + Assert.Equal(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue, 0)); +#if !NETFRAMEWORK + Assert.Equal(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.CodePoint)); +#endif + Assert.Equal(UnicodeCategory.OtherLetter, testCase.GeneralCategory); + } +} diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs index 38ae3b3b55649a..80fd1bebc3a25b 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -10,7 +10,7 @@ namespace GB18030.Tests; public class FileTests : FileTestBase { - private static readonly byte[] s_expectedBytes = File.ReadAllBytes(TestHelper.s_testDataFilePath); + 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(); @@ -21,7 +21,7 @@ public class FileTests : FileTestBase [Fact] public void ReadAllText() { - Assert.Equal(s_expectedText, File.ReadAllText(TestHelper.s_testDataFilePath, TestHelper.GB18030Encoding)); + Assert.Equal(s_expectedText, File.ReadAllText(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); } [Fact] @@ -29,7 +29,7 @@ public void ReadAllLines() { Assert.Equal( s_expectedText.Split([Environment.NewLine], StringSplitOptions.None), - File.ReadAllLines(TestHelper.s_testDataFilePath, TestHelper.GB18030Encoding)); + File.ReadAllLines(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); } [Fact] diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs index 725d8b38ff0392..b2df87604c439a 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -7,7 +7,6 @@ using System.Linq; using System.Text.RegularExpressions; using System.Text.RegularExpressions.Tests; -using System.Text.Unicode; using System.Threading.Tasks; using Xunit; @@ -18,23 +17,23 @@ namespace GB18030.Tests; /// public class RegexTests { - // Ranges added in GB18030-2020 - private static readonly UnicodeRange s_cjkNewRange = UnicodeRange.Create((char)0x9FF0, (char)0x9FFF); - private static readonly UnicodeRange s_cjkExtensionANewRange = UnicodeRange.Create((char)0x4DB6, (char)0x4DBF); + public enum RegexNamedBlock + { + IsCJKUnifiedIdeographs, + IsCJKUnifiedIdeographsExtensionA, + } - private static readonly IEnumerable s_cjkNewCharacters = Enumerable.Range(s_cjkNewRange.FirstCodePoint, s_cjkNewRange.Length).Select(c => ((char)c).ToString()); - private static readonly IEnumerable s_cjkExtensionANewCharacters = Enumerable.Range(s_cjkExtensionANewRange.FirstCodePoint, s_cjkExtensionANewRange.Length).Select(c => ((char)c).ToString()); - private static readonly IEnumerable s_allNewCharacters = s_cjkNewCharacters.Union(s_cjkExtensionANewCharacters); + private static readonly IEnumerable s_cjkAndCjkExtensionANewChars = TestHelper.CjkNewCodePoints.Union(TestHelper.CjkExtensionANewCodePoints).Select(c => ((char)c).ToString()); - private static readonly Dictionary s_rangeToRegexMap = new() + private static readonly List<(RegexNamedBlock, string[])> s_namedBlocks = new() { - { s_cjkNewRange, ("IsCJKUnifiedIdeographs", s_cjkNewCharacters.ToArray()) }, - { s_cjkExtensionANewRange, ("IsCJKUnifiedIdeographsExtensionA", s_cjkExtensionANewCharacters.ToArray()) } + (RegexNamedBlock.IsCJKUnifiedIdeographs, TestHelper.CjkNewCodePoints.Select(c => ((char)c).ToString()).ToArray()), + (RegexNamedBlock.IsCJKUnifiedIdeographsExtensionA, TestHelper.CjkExtensionANewCodePoints.Select(c => ((char)c).ToString()).ToArray()) }; public static IEnumerable UnicodeCategories_TestData() => RegexHelpers.AvailableEngines.SelectMany(engine => - TestHelper.s_cultures.Select(culture => new object[] { engine, culture })); + TestHelper.Cultures.Select(culture => new object[] { engine, culture })); [Theory] [MemberData(nameof(UnicodeCategories_TestData))] @@ -42,19 +41,19 @@ public static IEnumerable UnicodeCategories_TestData() => 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_allNewCharacters) + 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_allNewCharacters) + 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_allNewCharacters) + 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_allNewCharacters) + foreach (string element in s_cjkAndCjkExtensionANewChars) Assert.Matches(r, element); } @@ -64,58 +63,50 @@ public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo 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_allNewCharacters) + 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_allNewCharacters) + 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_allNewCharacters) + 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_allNewCharacters) + foreach (string element in s_cjkAndCjkExtensionANewChars) Assert.DoesNotMatch(r, element); } public static IEnumerable NamedBlock_TestData() => - s_rangeToRegexMap.SelectMany(rangeKvp => + s_namedBlocks.SelectMany(namedBlock => RegexHelpers.AvailableEngines.SelectMany(engine => - TestHelper.s_cultures.Select(culture => new object[] { rangeKvp.Key, engine, culture }))); + TestHelper.Cultures.Select(culture => new object[] { namedBlock.Item2, namedBlock.Item1, engine, culture }))); [Theory] [MemberData(nameof(NamedBlock_TestData))] - public async Task NamedBlock_InclusionAsync(UnicodeRange range, RegexEngine engine, CultureInfo culture) + public async Task NamedBlock_InclusionAsync(string[]characters, RegexNamedBlock namedBlock, RegexEngine engine, CultureInfo culture) { - (string namedBlock, string[] charactersInRange) = s_rangeToRegexMap[range]; - Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\p{{{namedBlock}}}", RegexOptions.None, culture); - foreach (string element in charactersInRange) - Assert.Matches(r, element); + foreach (string c in characters) + Assert.Matches(r, c); r = await RegexHelpers.GetRegexAsync(engine, $@"[\p{{{namedBlock}}}]", RegexOptions.None, culture); - foreach (string element in charactersInRange) - Assert.Matches(r, element); + foreach (string c in characters) + Assert.Matches(r, c); } [Theory] [MemberData(nameof(NamedBlock_TestData))] - public async Task NamedBlock_ExclusionAsync(UnicodeRange range, RegexEngine engine, CultureInfo culture) + public async Task NamedBlock_ExclusionAsync(string[] characters, RegexNamedBlock namedBlock, RegexEngine engine, CultureInfo culture) { - (string namedBlock, string[] charactersInRange) = s_rangeToRegexMap[range]; - Regex r = await RegexHelpers.GetRegexAsync(engine, $@"\P{{{namedBlock}}}", RegexOptions.None, culture); - foreach (string element in charactersInRange) - { - Assert.DoesNotMatch(r, element); - } + foreach (string c in characters) + Assert.DoesNotMatch(r, c); r = await RegexHelpers.GetRegexAsync(engine, $@"[^\p{{{namedBlock}}}]", RegexOptions.None, culture); - foreach (string element in charactersInRange) - { - Assert.DoesNotMatch(r, element); - } + 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 index 7bf1544218c371..d8d0c55b0b7a75 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -28,8 +28,8 @@ public unsafe void Ctor(byte[] encoded) public static IEnumerable Compare_TestData() => TestHelper.s_decodedTestData.SelectMany(testData => - TestHelper.s_cultures.SelectMany(culture => - TestHelper.s_compareOptions.Select(option => new object[] { testData, culture, option }))); + TestHelper.Cultures.SelectMany(culture => + TestHelper.CompareOptions.Select(option => new object[] { testData, culture, option }))); [Theory] [MemberData(nameof(Compare_TestData))] @@ -43,7 +43,7 @@ public void Compare(string decoded, CultureInfo culture, CompareOptions option) public static IEnumerable Contains_TestData() => TestHelper.s_decodedTestData.SelectMany(testData => - TestHelper.s_nonOrdinalStringComparisons.Select(comparison => new object[] { testData, comparison })); + TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { testData, comparison })); [Theory] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] @@ -67,7 +67,7 @@ public void Contains(string decoded, StringComparison comparison) public static IEnumerable StringComparison_TestData() => TestHelper.s_decodedTestData.SelectMany(decoded => - TestHelper.s_nonOrdinalStringComparisons.Select(comparison => new object[] { decoded, comparison })); + TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { decoded, comparison })); [Theory] [MemberData(nameof(StringComparison_TestData))] @@ -91,7 +91,7 @@ public void String_Equals(string decoded, StringComparison comparison) public static IEnumerable EndsStartsWith_TestData() => TestHelper.s_decodedTestData.SelectMany(testData => - TestHelper.s_cultures.Select(culture => new object[] { testData, culture })); + TestHelper.Cultures.Select(culture => new object[] { testData, culture })); [Theory] [MemberData(nameof(EndsStartsWith_TestData))] @@ -255,7 +255,7 @@ public void Replace(string decoded) public static IEnumerable Replace_NetCore_TestData() => TestHelper.s_decodedTestData.SelectMany(testData => - TestHelper.s_cultures.Select(culture => new object[] { testData, culture })); + TestHelper.Cultures.Select(culture => new object[] { testData, culture })); #if NETCOREAPP [Theory] 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; } From 0b6baf1b60dba65d8254ab499a7fa5c01bb1b988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 11 Aug 2025 11:49:23 -0500 Subject: [PATCH 08/21] Rename fields and properties --- .../ComplianceTests/GB18030/TestHelper.cs | 16 ++++--- .../GB18030/Tests/CharTests.cs | 10 ++-- .../GB18030/Tests/CharUnicodeInfoTests.cs | 2 +- .../GB18030/Tests/ConsoleTests.cs | 6 +-- .../GB18030/Tests/DirectoryInfoTests.cs | 4 +- .../GB18030/Tests/DirectoryTestBase.cs | 10 ++-- .../GB18030/Tests/DirectoryTests.cs | 2 +- .../GB18030/Tests/EncodingTests.cs | 2 +- .../GB18030/Tests/FileTestBase.cs | 8 ++-- .../GB18030/Tests/RegexTests.cs | 12 ++--- .../GB18030/Tests/StringTests.cs | 46 +++++++++---------- 11 files changed, 60 insertions(+), 58 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 200ca4b58bbb21..210d204453c303 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -81,11 +81,13 @@ internal static Encoding GB18030Encoding } private static readonly IEnumerable s_encodedTestData = GetTestData(); - internal static readonly IEnumerable s_decodedTestData = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)); - private static readonly IEnumerable s_splitNewLineDecodedTestData = s_decodedTestData.SelectMany( + + 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 readonly IEnumerable s_nonExceedingPathNameMaxDecodedTestData = + internal static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_splitNewLineDecodedTestData.SelectMany( (data) => { @@ -110,10 +112,10 @@ internal static Encoding GB18030Encoding return result; }); - public static IEnumerable EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data }); - public static IEnumerable DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data }); - public static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_nonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }); - public static IEnumerable GB18030CharUnicodeInfoTestData { get; } = s_gb18030CharUnicodeInfo.Select(data => new object[] { data }); + 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() { diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs index a8a1c1e4e77e0a..364a49e273aa25 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -14,7 +14,7 @@ namespace GB18030.Tests; public class CharTests { [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void Convert(CharUnicodeInfoTestCase testCase) { Assert.Equal(testCase.CodePoint, char.ConvertToUtf32(char.ConvertFromUtf32(testCase.CodePoint), 0)); @@ -28,7 +28,7 @@ public void Convert(CharUnicodeInfoTestCase testCase) } [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void Parse(CharUnicodeInfoTestCase testCase) { string utf32String = testCase.Utf32CodeValue; @@ -47,7 +47,7 @@ public void Parse(CharUnicodeInfoTestCase testCase) } [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void IsSurrogate(CharUnicodeInfoTestCase testCase) { string utf32String = testCase.Utf32CodeValue; @@ -73,7 +73,7 @@ public void IsSurrogate(CharUnicodeInfoTestCase testCase) } [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void IsLetter(CharUnicodeInfoTestCase testCase) { string utf32String = testCase.Utf32CodeValue; @@ -89,7 +89,7 @@ public void IsLetter(CharUnicodeInfoTestCase testCase) [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void IsNonLetter_False(CharUnicodeInfoTestCase testCase) { string utf32String = testCase.Utf32CodeValue; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs index 90e9100731436e..3aaf687c0be355 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs @@ -10,7 +10,7 @@ namespace GB18030.Tests; public class CharUnicodeInfoTests { [Theory] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void GetUnicodeCategory(CharUnicodeInfoTestCase testCase) { if (testCase.Utf32CodeValue.Length == 1) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs index d96e586a14ce92..cd9a681ad47e9d 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs @@ -12,7 +12,7 @@ public class ConsoleTests protected static readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardOutput(string decodedText) { var remoteOptions = new RemoteInvokeOptions(); @@ -33,7 +33,7 @@ public void StandardOutput(string decodedText) } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardInput(string decodedText) { var remoteOptions = new RemoteInvokeOptions(); @@ -67,7 +67,7 @@ public void StandardInput(string decodedText) } [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardError(string decodedText) { var remoteOptions = new RemoteInvokeOptions(); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs index bf35b8e96771bc..27fcfd69c677ee 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs @@ -15,7 +15,7 @@ public class DirectoryInfoTests : DirectoryTestBase protected override void MoveDirectory(string source, string destination) => new DirectoryInfo(source).MoveTo(destination); [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void CreateSubdirectory(string gb18030Line) { var subDirInfo = TempDirectory.CreateSubdirectory(gb18030Line); @@ -26,7 +26,7 @@ public void CreateSubdirectory(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void EnumerateFileSystemInfos(string gb18030Line) { string rootDir = TempDirectory.FullName; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs index c62f85f53245a6..d922db43d988ad 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -23,7 +23,7 @@ public DirectoryTestBase() } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Create(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -34,12 +34,12 @@ public void Create(string gb18030Line) Assert.Equal(gb18030Line, dirInfo.Name); } - public static IEnumerable Delete_TestData() => - TestHelper.s_nonExceedingPathNameMaxDecodedTestData.SelectMany(testData => + public static IEnumerable Delete_MemberData() => + TestHelper.NonExceedingPathNameMaxDecodedTestData.SelectMany(testData => new int[] { 0, 2, 8 }.Select(recurseLevel => new object[] { testData, recurseLevel })); [Theory] - [MemberData(nameof(Delete_TestData))] + [MemberData(nameof(Delete_MemberData))] public void Delete(string gb18030Line, int recurseLevel) { string firstPath = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -55,7 +55,7 @@ public void Delete(string gb18030Line, int recurseLevel) } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Move(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs index 0132ba27bb1d62..5f7a85186285b5 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs @@ -15,7 +15,7 @@ public class DirectoryTests : DirectoryTestBase protected override void MoveDirectory(string source, string destination) => Directory.Move(source, destination); [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void EnumerateFileSystemEntries(string gb18030Line) { string rootDir = TempDirectory.FullName; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs index f12ac3292e48a9..d285eadb9daa48 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/EncodingTests.cs @@ -9,7 +9,7 @@ namespace GB18030.Tests; public class EncodingTests { [Theory] - [MemberData(nameof(TestHelper.EncodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.EncodedMemberData), MemberType = typeof(TestHelper))] public void Roundtrips(byte[] testData) { Assert.True(testData.AsSpan().SequenceEqual( diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs index e1f45beef92e2c..981246bf7433d9 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs @@ -27,7 +27,7 @@ public void Dispose() } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Create(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -39,7 +39,7 @@ public void Create(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Delete(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -52,7 +52,7 @@ public void Delete(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Move(string gb18030Line) { string gb18030Path = Path.Combine(TempDirectory.FullName, gb18030Line); @@ -69,7 +69,7 @@ public void Move(string gb18030Line) } [Theory] - [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.NonExceedingPathNameMaxDecodedMemberData), MemberType = typeof(TestHelper))] public void Copy(string gb18030Line) { ReadOnlySpan sampleContent = "File_Copy"u8; diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs index b2df87604c439a..3f1602479944c0 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -31,12 +31,12 @@ public enum RegexNamedBlock (RegexNamedBlock.IsCJKUnifiedIdeographsExtensionA, TestHelper.CjkExtensionANewCodePoints.Select(c => ((char)c).ToString()).ToArray()) }; - public static IEnumerable UnicodeCategories_TestData() => + public static IEnumerable UnicodeCategories_MemberData() => RegexHelpers.AvailableEngines.SelectMany(engine => TestHelper.Cultures.Select(culture => new object[] { engine, culture })); [Theory] - [MemberData(nameof(UnicodeCategories_TestData))] + [MemberData(nameof(UnicodeCategories_MemberData))] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo culture) { @@ -58,7 +58,7 @@ public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo } [Theory] - [MemberData(nameof(UnicodeCategories_TestData))] + [MemberData(nameof(UnicodeCategories_MemberData))] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] public async Task UnicodeCategory_ExclusionAsync(RegexEngine engine, CultureInfo culture) { @@ -79,13 +79,13 @@ public async Task UnicodeCategory_ExclusionAsync(RegexEngine engine, CultureInfo Assert.DoesNotMatch(r, element); } - public static IEnumerable NamedBlock_TestData() => + 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_TestData))] + [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); @@ -98,7 +98,7 @@ public async Task NamedBlock_InclusionAsync(string[]characters, RegexNamedBlock } [Theory] - [MemberData(nameof(NamedBlock_TestData))] + [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); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index d8d0c55b0b7a75..0e8280016637a7 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -16,7 +16,7 @@ public class StringTests private const string Dummy = "\uFFFF"; [Theory] - [MemberData(nameof(TestHelper.EncodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.EncodedMemberData), MemberType = typeof(TestHelper))] public unsafe void Ctor(byte[] encoded) { fixed (sbyte* p = (sbyte[])(object)encoded) @@ -26,13 +26,13 @@ public unsafe void Ctor(byte[] encoded) } } - public static IEnumerable Compare_TestData() => - TestHelper.s_decodedTestData.SelectMany(testData => + 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_TestData))] + [MemberData(nameof(Compare_MemberData))] public void Compare(string decoded, CultureInfo culture, CompareOptions option) { #pragma warning disable 0618 // suppress obsolete warning for String.Copy @@ -41,13 +41,13 @@ public void Compare(string decoded, CultureInfo culture, CompareOptions option) Assert.True(string.Compare(decoded, copy, culture, option) == 0); } - public static IEnumerable Contains_TestData() => - TestHelper.s_decodedTestData.SelectMany(testData => + public static IEnumerable Contains_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { testData, comparison })); [Theory] [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] - [MemberData(nameof(Contains_TestData))] + [MemberData(nameof(Contains_MemberData))] public void Contains(string decoded, StringComparison comparison) { string current = string.Empty; @@ -65,12 +65,12 @@ public void Contains(string decoded, StringComparison comparison) } } - public static IEnumerable StringComparison_TestData() => - TestHelper.s_decodedTestData.SelectMany(decoded => + public static IEnumerable StringComparison_MemberData() => + TestHelper.DecodedTestData.SelectMany(decoded => TestHelper.NonOrdinalStringComparisons.Select(comparison => new object[] { decoded, comparison })); [Theory] - [MemberData(nameof(StringComparison_TestData))] + [MemberData(nameof(StringComparison_MemberData))] public void String_Equals(string decoded, StringComparison comparison) { #pragma warning disable 0618 // suppress obsolete warning for String.Copy @@ -89,12 +89,12 @@ public void String_Equals(string decoded, StringComparison comparison) } } - public static IEnumerable EndsStartsWith_TestData() => - TestHelper.s_decodedTestData.SelectMany(testData => + public static IEnumerable EndsStartsWith_MemberData() => + TestHelper.DecodedTestData.SelectMany(testData => TestHelper.Cultures.Select(culture => new object[] { testData, culture })); [Theory] - [MemberData(nameof(EndsStartsWith_TestData))] + [MemberData(nameof(EndsStartsWith_MemberData))] public void EndsWith(string decoded, CultureInfo culture) { string suffix = string.Empty; @@ -106,7 +106,7 @@ public void EndsWith(string decoded, CultureInfo culture) } [Theory] - [MemberData(nameof(EndsStartsWith_TestData))] + [MemberData(nameof(EndsStartsWith_MemberData))] public void StartsWith(string decoded, CultureInfo culture) { string prefix = string.Empty; @@ -118,7 +118,7 @@ public void StartsWith(string decoded, CultureInfo culture) } [Theory] - [MemberData(nameof(StringComparison_TestData))] + [MemberData(nameof(StringComparison_MemberData))] public void IndexOf_MultipleElements(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); @@ -137,7 +137,7 @@ public void IndexOf_MultipleElements(string decoded, StringComparison comparison } [Theory] - [MemberData(nameof(StringComparison_TestData))] + [MemberData(nameof(StringComparison_MemberData))] public void IndexOf_SingleElement(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); @@ -174,7 +174,7 @@ void IndexOf_SingleElement_Slow(string current, int expectedIndex) } [Theory] - [MemberData(nameof(StringComparison_TestData))] + [MemberData(nameof(StringComparison_MemberData))] public void LastIndexOf_MultipleElements(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); @@ -197,7 +197,7 @@ public void LastIndexOf_MultipleElements(string decoded, StringComparison compar } [Theory] - [MemberData(nameof(StringComparison_TestData))] + [MemberData(nameof(StringComparison_MemberData))] public void LastIndexOf_SingleElement(string decoded, StringComparison comparison) { Assert.NotEqual(StringComparison.Ordinal, comparison); @@ -234,7 +234,7 @@ void LastIndexOf_SingleElement_Slow(string current, int expectedIndex) } [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void Replace(string decoded) { Assert.False(decoded.Contains(Dummy)); @@ -253,13 +253,13 @@ public void Replace(string decoded) } } - public static IEnumerable Replace_NetCore_TestData() => - TestHelper.s_decodedTestData.SelectMany(testData => + 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_TestData))] + [MemberData(nameof(Replace_NetCore_MemberData))] public void Replace_CultureInfo(string decoded, CultureInfo culture) { Assert.False(decoded.Contains(Dummy)); @@ -278,7 +278,7 @@ public void Replace_CultureInfo(string decoded, CultureInfo culture) #endif [Theory] - [MemberData(nameof(TestHelper.DecodedTestData), MemberType = typeof(TestHelper))] + [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void Split(string decoded) { string[] textElements = TestHelper.GetTextElements(decoded).ToArray(); From 126ff40bce7e894b5c760ab52248eb0cad790b72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 11 Aug 2025 12:16:11 -0500 Subject: [PATCH 09/21] Make adjustments for linux and netfx --- .../ComplianceTests/GB18030/TestHelper.cs | 2 +- .../GB18030/Tests/CharTests.cs | 20 ++++++++++++++++++- .../GB18030/Tests/CharUnicodeInfoTests.cs | 11 ++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 210d204453c303..c1d04bfff98641 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -91,7 +91,7 @@ internal static Encoding GB18030Encoding s_splitNewLineDecodedTestData.SelectMany( (data) => { - const int MaxPathSegmentName = 255; + const int MaxPathSegmentName = 128; Encoding fileSystemEncoding = PlatformDetection.IsWindows ? Encoding.Unicode : Encoding.UTF8; if (fileSystemEncoding.GetByteCount(data) <= MaxPathSegmentName) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs index 364a49e273aa25..fb237e300edc0c 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -1,6 +1,7 @@ // 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 System.Linq; @@ -72,7 +73,8 @@ public void IsSurrogate(CharUnicodeInfoTestCase testCase) } } - [Theory] + [ConditionalTheory] + [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void IsLetter(CharUnicodeInfoTestCase testCase) { @@ -87,6 +89,22 @@ public void IsLetter(CharUnicodeInfoTestCase testCase) } } + [ConditionalTheory] + [SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)] + [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] + public void IsLetter_NetFramework(CharUnicodeInfoTestCase testCase) + { + string utf32String = testCase.Utf32CodeValue; + Assert.False(char.IsLetter(utf32String, 0)); + Assert.False(char.IsLetterOrDigit(utf32String, 0)); + + if (utf32String.Length < 2) + { + Assert.False(char.IsLetter(utf32String[0])); + Assert.False(char.IsLetterOrDigit(utf32String[0])); + } + } + [Theory] [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs index 3aaf687c0be355..f8d3f10754f3e8 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs @@ -1,6 +1,7 @@ // 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; @@ -13,15 +14,17 @@ public class CharUnicodeInfoTests [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(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue[0])); + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue[0])); } - Assert.Equal(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue, 0)); + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.Utf32CodeValue, 0)); #if !NETFRAMEWORK - Assert.Equal(UnicodeCategory.OtherLetter, CharUnicodeInfo.GetUnicodeCategory(testCase.CodePoint)); + Assert.Equal(expected, CharUnicodeInfo.GetUnicodeCategory(testCase.CodePoint)); #endif - Assert.Equal(UnicodeCategory.OtherLetter, testCase.GeneralCategory); + Assert.True(PlatformDetection.IsNetFramework || testCase.GeneralCategory == expected); } } From ba384e52b752ff8632dbf8727ed34b182fc82b57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Mon, 11 Aug 2025 13:34:54 -0500 Subject: [PATCH 10/21] Skip NetFramework at testclass level --- .../GB18030/Tests/CharTests.cs | 26 ++----------------- .../GB18030/Tests/CharUnicodeInfoTests.cs | 1 + .../GB18030/Tests/RegexTests.cs | 3 +-- .../RegexGeneratorHelper.netcoreapp.cs | 2 +- 4 files changed, 5 insertions(+), 27 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs index fb237e300edc0c..502c94612b0def 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharTests.cs @@ -1,17 +1,12 @@ // 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 System.Linq; -using System.Xml.Linq; -using Microsoft.VisualStudio.TestPlatform.ObjectModel; using Xunit; -using Xunit.Sdk; namespace GB18030.Tests; +[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] public class CharTests { [Theory] @@ -73,8 +68,7 @@ public void IsSurrogate(CharUnicodeInfoTestCase testCase) } } - [ConditionalTheory] - [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] + [Theory] [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] public void IsLetter(CharUnicodeInfoTestCase testCase) { @@ -89,22 +83,6 @@ public void IsLetter(CharUnicodeInfoTestCase testCase) } } - [ConditionalTheory] - [SkipOnTargetFramework(TargetFrameworkMonikers.Netcoreapp)] - [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] - public void IsLetter_NetFramework(CharUnicodeInfoTestCase testCase) - { - string utf32String = testCase.Utf32CodeValue; - Assert.False(char.IsLetter(utf32String, 0)); - Assert.False(char.IsLetterOrDigit(utf32String, 0)); - - if (utf32String.Length < 2) - { - Assert.False(char.IsLetter(utf32String[0])); - Assert.False(char.IsLetterOrDigit(utf32String[0])); - } - } - [Theory] [MemberData(nameof(TestHelper.GB18030CharUnicodeInfoMemberData), MemberType = typeof(TestHelper))] diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs index f8d3f10754f3e8..530879e48023ee 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/CharUnicodeInfoTests.cs @@ -8,6 +8,7 @@ namespace GB18030.Tests; +[SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] public class CharUnicodeInfoTests { [Theory] diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs index 3f1602479944c0..620dfba0103bff 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -15,6 +15,7 @@ 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 @@ -37,7 +38,6 @@ public static IEnumerable UnicodeCategories_MemberData() => [Theory] [MemberData(nameof(UnicodeCategories_MemberData))] - [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo culture) { Regex r = await RegexHelpers.GetRegexAsync(engine, @"\p{Lo}", RegexOptions.None, culture); @@ -59,7 +59,6 @@ public async Task UnicodeCategory_InclusionAsync(RegexEngine engine, CultureInfo [Theory] [MemberData(nameof(UnicodeCategories_MemberData))] - [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework, "https://devdiv.visualstudio.com/DevDiv/_workitems/edit/2522617")] public async Task UnicodeCategory_ExclusionAsync(RegexEngine engine, CultureInfo culture) { Regex r = await RegexHelpers.GetRegexAsync(engine, @"\P{Lo}", RegexOptions.None, culture); 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 21db5608cd2168..cbf143eb3b3cb3 100644 --- a/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs +++ b/src/libraries/System.Text.RegularExpressions/tests/FunctionalTests/RegexGeneratorHelper.netcoreapp.cs @@ -18,7 +18,7 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Text; -using Xunit; +using Xunit; namespace System.Text.RegularExpressions.Tests { From f119767b8215d283869539c27f5b6657e03b1884 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sat, 25 Jul 2026 17:42:43 +0000 Subject: [PATCH 11/21] Fix Compliance.Tests build against current main Merging main pulled in two changes that broke the GB18030 compliance test project: - Trim/AOT analyzers are now enabled by default on libraries test projects (#126863). The shared RegexGeneratorHelper.netcoreapp.cs trips IL2026/IL2075, so disable EnableTrimAnalyzer/EnableAotAnalyzer in the project, matching System.Text.RegularExpressions.Tests. - Microsoft.DotNet.XUnitAssert was bumped to 3.2.2-beta, which marks Assert.Equivalent obsolete-as-error. Replace its two usages with an order-independent AssertExtensions.CollectionEqual comparison. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Common/tests/ComplianceTests/Compliance.Tests.csproj | 2 ++ .../ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs | 6 +++++- .../tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs | 6 +++++- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj index 386a669efe325c..d55c61ac215667 100644 --- a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -5,6 +5,8 @@ true true 16.0 + false + false diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs index 27fcfd69c677ee..4d96d3376f0ac2 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryInfoTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Xunit; namespace GB18030.Tests; @@ -42,6 +43,9 @@ public void EnumerateFileSystemInfos(string gb18030Line) fileInfo.Create().Dispose(); expected.Add(fileInfo); - Assert.Equivalent(expected, new DirectoryInfo(rootDir).EnumerateFileSystemInfos()); + AssertExtensions.CollectionEqual( + expected.Select(i => i.FullName), + new DirectoryInfo(rootDir).EnumerateFileSystemInfos().Select(i => i.FullName), + StringComparer.Ordinal); } } diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs index 5f7a85186285b5..f7dab1b80ff37f 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTests.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using Xunit; namespace GB18030.Tests; @@ -29,6 +30,9 @@ public void EnumerateFileSystemEntries(string gb18030Line) File.Create(gb18030File).Dispose(); expected.Add(gb18030File); - Assert.Equivalent(expected, Directory.EnumerateFileSystemEntries(rootDir)); + AssertExtensions.CollectionEqual( + expected, + Directory.EnumerateFileSystemEntries(rootDir), + StringComparer.Ordinal); } } From 3d2bc3869978686f67092af1f38d7d8d4c3b199b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sat, 25 Jul 2026 17:43:32 +0000 Subject: [PATCH 12/21] Address Copilot review feedback - TestHelper.GetTestData: throw InvalidDataException when a start delimiter is missing so an unintended change to the test data file fails loudly instead of silently running fewer cases. - FileTestBase.Create: fix reversed Assert.Equal argument order so the expected value comes first, matching DirectoryTestBase.Create. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Common/tests/ComplianceTests/GB18030/TestHelper.cs | 7 +++++++ .../tests/ComplianceTests/GB18030/Tests/FileTestBase.cs | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index c1d04bfff98641..c7a3dcff6df2c3 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -128,6 +128,13 @@ private static IEnumerable GetTestData() while (testFileBytes.Length > 0) { int start = testFileBytes.Span.IndexOf(startDelimiter); + if (start == -1) + { + // Every record is introduced by its start delimiter, so anything left here means the + // data file was modified unexpectedly. Fail loudly rather than silently running fewer cases. + throw new InvalidDataException($"Unexpected trailing content in '{TestDataFilePath}'; expected a '{{label}}:' record delimiter."); + } + testFileBytes = testFileBytes.Slice(start + startDelimiter.Length); int end = testFileBytes.Span.IndexOf(endDelimiter); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs index 981246bf7433d9..eaee867b9ea463 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTestBase.cs @@ -35,7 +35,7 @@ public void Create(string gb18030Line) var fileInfo = new FileInfo(gb18030Path); Assert.True(fileInfo.Exists); - Assert.Equal(fileInfo.Name, gb18030Line); + Assert.Equal(gb18030Line, fileInfo.Name); } [Theory] From c7d7821fb55865ade56fc6240eaff3f2ef3d12ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sun, 26 Jul 2026 05:33:35 +0000 Subject: [PATCH 13/21] Embed GB18030 test data to fix WASM test runs The GB18030 compliance tests failed on browser-wasm (and would fail on any sandboxed-VFS target such as wasi/mobile) with: System.IO.DirectoryNotFoundException : Could not find a part of the path '/GB18030/Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt' The data file was included only via , which places it next to the assembly on desktop but is not mounted into the browser's in-memory filesystem, so File.ReadAllBytes threw during the static initialization of TestHelper (surfacing directly or as a TypeInitializationException in FileTests). Embed the data file as a resource and read it via GetManifestResourceStream instead of from disk, matching the existing UnicodeData.txt handling in this project. This makes the data travel inside the assembly and removes the filesystem dependency on all platforms. FileTests.ReadAllText/ReadAllLines now write the embedded bytes to a temp file and read it back, preserving the File API under test without depending on an on-disk asset. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../ComplianceTests/Compliance.Tests.csproj | 4 +++- .../ComplianceTests/GB18030/TestHelper.cs | 19 +++++++++++++++---- .../GB18030/Tests/FileTests.cs | 12 +++++++++--- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj index d55c61ac215667..d4df2264548363 100644 --- a/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj +++ b/src/libraries/Common/tests/ComplianceTests/Compliance.Tests.csproj @@ -40,7 +40,9 @@ - + + Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt + diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index c7a3dcff6df2c3..de10b7c112a8cb 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -61,7 +61,18 @@ static bool IsInGB18030Range(int codePoint) 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"); + internal static string TestDataResourceName { get; } = "Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt"; + + internal static byte[] TestDataFileBytes { get; } = ReadTestDataFileBytes(); + + private static byte[] ReadTestDataFileBytes() + { + using Stream stream = typeof(TestHelper).Assembly.GetManifestResourceStream(TestDataResourceName) + ?? throw new InvalidOperationException($"Could not find embedded resource '{TestDataResourceName}'."); + using MemoryStream memoryStream = new(); + stream.CopyTo(memoryStream); + return memoryStream.ToArray(); + } private static Encoding? s_gb18030Encoding; internal static Encoding GB18030Encoding @@ -122,8 +133,8 @@ 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); + // Instead of inlining the data in source, parse the test data from an embedded resource to prevent encoding issues. + ReadOnlyMemory testFileBytes = TestDataFileBytes; while (testFileBytes.Length > 0) { @@ -132,7 +143,7 @@ private static IEnumerable GetTestData() { // Every record is introduced by its start delimiter, so anything left here means the // data file was modified unexpectedly. Fail loudly rather than silently running fewer cases. - throw new InvalidDataException($"Unexpected trailing content in '{TestDataFilePath}'; expected a '{{label}}:' record delimiter."); + throw new InvalidDataException($"Unexpected trailing content in '{TestDataResourceName}'; expected a '{{label}}:' record delimiter."); } testFileBytes = testFileBytes.Slice(start + startDelimiter.Length); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs index 80fd1bebc3a25b..1041e834de0644 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -10,7 +10,7 @@ namespace GB18030.Tests; public class FileTests : FileTestBase { - private static readonly byte[] s_expectedBytes = File.ReadAllBytes(TestHelper.TestDataFilePath); + private static readonly byte[] s_expectedBytes = TestHelper.TestDataFileBytes; private static readonly string s_expectedText = TestHelper.GB18030Encoding.GetString(s_expectedBytes); protected override void CreateFile(string path) => File.Create(path).Dispose(); @@ -21,15 +21,21 @@ public class FileTests : FileTestBase [Fact] public void ReadAllText() { - Assert.Equal(s_expectedText, File.ReadAllText(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); + string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + File.WriteAllBytes(tempFile, s_expectedBytes); + + Assert.Equal(s_expectedText, File.ReadAllText(tempFile, TestHelper.GB18030Encoding)); } [Fact] public void ReadAllLines() { + string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); + File.WriteAllBytes(tempFile, s_expectedBytes); + Assert.Equal( s_expectedText.Split([Environment.NewLine], StringSplitOptions.None), - File.ReadAllLines(TestHelper.TestDataFilePath, TestHelper.GB18030Encoding)); + File.ReadAllLines(tempFile, TestHelper.GB18030Encoding)); } [Fact] From 7d8d3924d37fb23903832ae91d0c9f1694663ee9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sun, 26 Jul 2026 05:46:44 +0000 Subject: [PATCH 14/21] Address Copilot review feedback - TestHelper: materialize the encoded/decoded test-data pipeline and the GB18030 CharUnicodeInfo sequence with ToArray() so the data is parsed once instead of re-running the iterators on every MemberData enumeration, keeping discovery deterministic and avoiding repeated work. - FileTests: compare lines in a newline-agnostic way. The checked-in data file is LF-only, so splitting the expected text on Environment.NewLine mismatched File.ReadAllLines on Windows. Add a SplitLines helper that splits on '\n' and trims an optional trailing '\r', and reconstruct the WriteAllLines expectation from those lines joined with Environment.NewLine. --- .../ComplianceTests/GB18030/TestHelper.cs | 20 +++++++++---------- .../GB18030/Tests/FileTests.cs | 15 +++++++++----- 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index de10b7c112a8cb..1e47f6ec04d54c 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -32,8 +32,8 @@ 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()); + var ret = CharUnicodeInfoTestData.TestCases.Where(tc => IsInGB18030Range(tc.CodePoint)).ToArray(); + Assert.Equal(CodePointsTotal, ret.Length); return ret; static bool IsInGB18030Range(int codePoint) @@ -91,12 +91,12 @@ internal static Encoding GB18030Encoding } } - private static readonly IEnumerable s_encodedTestData = GetTestData(); + private static readonly IEnumerable s_encodedTestData = GetTestData().ToArray(); - internal static IEnumerable DecodedTestData { get; } = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)); + internal static IEnumerable DecodedTestData { get; } = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)).ToArray(); private static readonly IEnumerable s_splitNewLineDecodedTestData = DecodedTestData.SelectMany( - data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)); + data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)).ToArray(); internal static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_splitNewLineDecodedTestData.SelectMany( @@ -121,12 +121,12 @@ internal static Encoding GB18030Encoding } result.Add(current); return result; - }); + }).ToArray(); - 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 }); + public static IEnumerable EncodedMemberData { get; } = s_encodedTestData.Select(data => new object[] { data }).ToArray(); + public static IEnumerable DecodedMemberData { get; } = DecodedTestData.Select(data => new object[] { data }).ToArray(); + public static IEnumerable NonExceedingPathNameMaxDecodedMemberData { get; } = NonExceedingPathNameMaxDecodedTestData.Select(data => new object[] { data }).ToArray(); + public static IEnumerable GB18030CharUnicodeInfoMemberData { get; } = s_gb18030CharUnicodeInfo.Select(data => new object[] { data }).ToArray(); private static IEnumerable GetTestData() { diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs index 1041e834de0644..1a59f3557d3f89 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/FileTests.cs @@ -34,7 +34,7 @@ public void ReadAllLines() File.WriteAllBytes(tempFile, s_expectedBytes); Assert.Equal( - s_expectedText.Split([Environment.NewLine], StringSplitOptions.None), + SplitLines(s_expectedText), File.ReadAllLines(tempFile, TestHelper.GB18030Encoding)); } @@ -51,12 +51,12 @@ public void WriteAllText() public void WriteAllLines() { string tempFile = Path.Combine(TempDirectory.FullName, Path.GetRandomFileName()); - string[] lines = s_expectedText.Split([Environment.NewLine], StringSplitOptions.None); + string[] lines = SplitLines(s_expectedText); 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); + // WriteAllLines uses TextWriter.WriteLine which concats Environment.NewLine to each provided line, + // so the expected content is the lines rejoined with Environment.NewLine plus a trailing one. + byte[] expected = TestHelper.GB18030Encoding.GetBytes(string.Join(Environment.NewLine, lines) + Environment.NewLine); Assert.True(expected.AsSpan().SequenceEqual(File.ReadAllBytes(tempFile))); } @@ -71,4 +71,9 @@ public void AppendAllText() byte[] expected = TestHelper.GB18030Encoding.GetBytes(initialContent + s_expectedText); Assert.True(expected.AsSpan().SequenceEqual(File.ReadAllBytes(tempFile))); } + + // Splits text the same way File.ReadAllLines does: on '\n', trimming an optional trailing '\r', + // so comparisons are robust regardless of whether the data uses LF or CRLF line endings. + private static string[] SplitLines(string text) => + text.Split('\n').Select(line => line.TrimEnd('\r')).ToArray(); } From aa37d99e01fc713011ec31c4e030a5259874811f Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sat, 25 Jul 2026 23:06:48 -0500 Subject: [PATCH 15/21] reduce nesting to avoid MAX_PATH errors on netfx --- .../tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs | 2 +- src/native/external/libunwind/README.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) mode change 120000 => 100644 src/native/external/libunwind/README.md diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs index d922db43d988ad..e9d179752e77c0 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/DirectoryTestBase.cs @@ -36,7 +36,7 @@ public void Create(string gb18030Line) public static IEnumerable Delete_MemberData() => TestHelper.NonExceedingPathNameMaxDecodedTestData.SelectMany(testData => - new int[] { 0, 2, 8 }.Select(recurseLevel => new object[] { testData, recurseLevel })); + new int[] { 0, 1 }.Select(recurseLevel => new object[] { testData, recurseLevel })); [Theory] [MemberData(nameof(Delete_MemberData))] diff --git a/src/native/external/libunwind/README.md b/src/native/external/libunwind/README.md deleted file mode 120000 index 100b93820ade4c..00000000000000 --- a/src/native/external/libunwind/README.md +++ /dev/null @@ -1 +0,0 @@ -README \ No newline at end of file diff --git a/src/native/external/libunwind/README.md b/src/native/external/libunwind/README.md new file mode 100644 index 00000000000000..100b93820ade4c --- /dev/null +++ b/src/native/external/libunwind/README.md @@ -0,0 +1 @@ +README \ No newline at end of file From f50bfb195511a95c100d239ce494e9013e46ddaf Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 26 Jul 2026 00:07:01 -0500 Subject: [PATCH 16/21] Skip GB18030 Console/String tests on platforms without GB18030 support Windows Nano Server lacks up-to-date GB18030 data: it has no collation weights for GB18030-2022 code points and can't set the GB18030 console code page. Detect this behaviorally via a probe (U+4DBC is ignorable in linguistic comparison there) and gate the affected culture-sensitive String tests and the Console tests on it. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: de19db8a-98f8-48f3-8090-e2059e5f5aad --- .../Common/tests/ComplianceTests/GB18030/TestHelper.cs | 8 ++++++++ .../ComplianceTests/GB18030/Tests/ConsoleTests.cs | 10 +++++++--- .../tests/ComplianceTests/GB18030/Tests/StringTests.cs | 10 +++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 1e47f6ec04d54c..c78f9854b7f487 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -61,6 +61,14 @@ static bool IsInGB18030Range(int codePoint) StringComparison.InvariantCulture, StringComparison.InvariantCultureIgnoreCase]; + // Detects whether the current platform has up-to-date GB18030 support. Some older + // platforms (e.g. Windows Nano Server) lack it: they don't carry collation weights for + // the GB18030-2022 code points and can't set the GB18030 console code page. On such + // platforms U+4DBC (a CJK Extension A code point that GB18030-2022 covers) is treated as + // ignorable in linguistic comparisons, so IndexOf finds it at index 0 instead of -1. + public static bool IsGB18030Supported { get; } = + "abc".IndexOf("\u4DBC", StringComparison.InvariantCulture) < 0; + internal static string TestDataResourceName { get; } = "Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt"; internal static byte[] TestDataFileBytes { get; } = ReadTestDataFileBytes(); diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs index cd9a681ad47e9d..9ba2507b1f87cb 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/ConsoleTests.cs @@ -11,7 +11,11 @@ public class ConsoleTests { protected static readonly int WaitInMS = 30 * 1000 * PlatformDetection.SlowRuntimeTimeoutModifier; - [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + // GB18030 console tests require a platform whose console subsystem supports the GB18030 + // code page. Older platforms (e.g. Windows Nano Server) don't, so gate on GB18030 support. + public static bool IsSupported => RemoteExecutor.IsSupported && TestHelper.IsGB18030Supported; + + [ConditionalTheory(typeof(ConsoleTests), nameof(IsSupported))] [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardOutput(string decodedText) { @@ -32,7 +36,7 @@ public void StandardOutput(string decodedText) Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); } - [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [ConditionalTheory(typeof(ConsoleTests), nameof(IsSupported))] [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardInput(string decodedText) { @@ -66,7 +70,7 @@ public void StandardInput(string decodedText) Assert.True(remoteHandle.Process.WaitForExit(WaitInMS)); } - [ConditionalTheory(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [ConditionalTheory(typeof(ConsoleTests), nameof(IsSupported))] [MemberData(nameof(TestHelper.DecodedMemberData), MemberType = typeof(TestHelper))] public void StandardError(string decodedText) { diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index 0e8280016637a7..4758b335468d88 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -117,7 +117,7 @@ public void StartsWith(string decoded, CultureInfo culture) } } - [Theory] + [ConditionalTheory(typeof(TestHelper), nameof(TestHelper.IsGB18030Supported))] [MemberData(nameof(StringComparison_MemberData))] public void IndexOf_MultipleElements(string decoded, StringComparison comparison) { @@ -136,7 +136,7 @@ public void IndexOf_MultipleElements(string decoded, StringComparison comparison } } - [Theory] + [ConditionalTheory(typeof(TestHelper), nameof(TestHelper.IsGB18030Supported))] [MemberData(nameof(StringComparison_MemberData))] public void IndexOf_SingleElement(string decoded, StringComparison comparison) { @@ -173,7 +173,7 @@ void IndexOf_SingleElement_Slow(string current, int expectedIndex) } } - [Theory] + [ConditionalTheory(typeof(TestHelper), nameof(TestHelper.IsGB18030Supported))] [MemberData(nameof(StringComparison_MemberData))] public void LastIndexOf_MultipleElements(string decoded, StringComparison comparison) { @@ -196,7 +196,7 @@ public void LastIndexOf_MultipleElements(string decoded, StringComparison compar } } - [Theory] + [ConditionalTheory(typeof(TestHelper), nameof(TestHelper.IsGB18030Supported))] [MemberData(nameof(StringComparison_MemberData))] public void LastIndexOf_SingleElement(string decoded, StringComparison comparison) { @@ -258,7 +258,7 @@ public static IEnumerable Replace_NetCore_MemberData() => TestHelper.Cultures.Select(culture => new object[] { testData, culture })); #if NETCOREAPP - [Theory] + [ConditionalTheory(typeof(TestHelper), nameof(TestHelper.IsGB18030Supported))] [MemberData(nameof(Replace_NetCore_MemberData))] public void Replace_CultureInfo(string decoded, CultureInfo culture) { From 289af03b096f6d9ccb548f98b7ea54f35c2f9ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sun, 26 Jul 2026 05:52:19 +0000 Subject: [PATCH 17/21] Restore libunwind README.md symlink Commit aa37d99 accidentally converted src/native/external/libunwind/README.md from a symlink (git mode 120000, pointing at README) into a regular file with mode 100644. The blob content was unchanged (a symlink's content is its target path, "README"), so the change was easy to miss, but the mode flip is unrelated to the GB18030 compliance tests and diverges from upstream. Restore the symlink. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/native/external/libunwind/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 120000 src/native/external/libunwind/README.md diff --git a/src/native/external/libunwind/README.md b/src/native/external/libunwind/README.md deleted file mode 100644 index 100b93820ade4c..00000000000000 --- a/src/native/external/libunwind/README.md +++ /dev/null @@ -1 +0,0 @@ -README \ No newline at end of file diff --git a/src/native/external/libunwind/README.md b/src/native/external/libunwind/README.md new file mode 120000 index 00000000000000..100b93820ade4c --- /dev/null +++ b/src/native/external/libunwind/README.md @@ -0,0 +1 @@ +README \ No newline at end of file From cce6f646cfc79a6670d6c8328f95bd23a1284473 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 26 Jul 2026 13:49:33 -0500 Subject: [PATCH 18/21] Decouple GB18030 test data parsing from newline conventions Normalize the embedded test-data resource to LF at read time and parse records against literal LF delimiters instead of Environment.NewLine, so results are identical regardless of the OS that built or runs the tests. This fixes the WASM legs, where a Windows-built (CRLF) resource was parsed on mono/WASM (LF), throwing from TestHelper's static constructor and failing every test. Use the char[] Split overload so it also compiles on .NET Framework. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 744f142e-00de-4b24-a591-1646cbd71a11 --- .../ComplianceTests/GB18030/TestHelper.cs | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index c78f9854b7f487..0b07eee81112a4 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -77,9 +77,24 @@ private static byte[] ReadTestDataFileBytes() { using Stream stream = typeof(TestHelper).Assembly.GetManifestResourceStream(TestDataResourceName) ?? throw new InvalidOperationException($"Could not find embedded resource '{TestDataResourceName}'."); - using MemoryStream memoryStream = new(); - stream.CopyTo(memoryStream); - return memoryStream.ToArray(); + + byte[] bytes = new byte[stream.Length]; + int totalRead = 0; + while (totalRead < bytes.Length) + { + int read = stream.Read(bytes, totalRead, bytes.Length - totalRead); + if (read == 0) + break; + totalRead += read; + } + + // The data file is checked in as LF, but 'text=auto' materializes it as CRLF on Windows + // checkouts and it is then embedded verbatim, so the embedded bytes depend on the build OS. + // This broke the WASM legs: built on Windows (CRLF) but run on mono/WASM where the parser's + // LF record delimiters weren't found, throwing from the static constructor and failing every + // test. Normalize to LF so the data is identical regardless of build/run OS. Dropping every + // 0x0D is safe: it can only be a CR, as it never appears inside a multi-byte GB18030 sequence. + return bytes.Where(b => b != (byte)'\r').ToArray(); } private static Encoding? s_gb18030Encoding; @@ -104,7 +119,7 @@ internal static Encoding GB18030Encoding internal static IEnumerable DecodedTestData { get; } = s_encodedTestData.Select(data => GB18030Encoding.GetString(data)).ToArray(); private static readonly IEnumerable s_splitNewLineDecodedTestData = DecodedTestData.SelectMany( - data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries)).ToArray(); + data => data.Split(['\n'], StringSplitOptions.RemoveEmptyEntries)).ToArray(); internal static IEnumerable NonExceedingPathNameMaxDecodedTestData { get; } = s_splitNewLineDecodedTestData.SelectMany( @@ -138,8 +153,11 @@ internal static Encoding GB18030Encoding private static IEnumerable GetTestData() { - byte[] startDelimiter = GB18030Encoding.GetBytes($":{Environment.NewLine}"); - byte[] endDelimiter = GB18030Encoding.GetBytes($"{Environment.NewLine}{Environment.NewLine}"); + // The test data uses LF as its record separator (see ReadTestDataFileBytes, which normalizes + // the embedded resource to LF). Parse against '\n' explicitly so the record boundaries never + // depend on Environment.NewLine, which varies by runtime (LF on Unix/WASM, CRLF on Windows). + byte[] startDelimiter = GB18030Encoding.GetBytes(":\n"); + byte[] endDelimiter = GB18030Encoding.GetBytes("\n\n"); // Instead of inlining the data in source, parse the test data from an embedded resource to prevent encoding issues. ReadOnlyMemory testFileBytes = TestDataFileBytes; From 915749aeda2aca0a1c1a28d6f8667b61da195688 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 26 Jul 2026 13:56:09 -0500 Subject: [PATCH 19/21] Tolerate '0'/'U+3007' zh-CN collation equivalence on browser in Replace_CultureInfo The browser's ICU collation shard treats '0' (U+0030) and the ideographic zero '\u3007' as equal even under zh-CN, unlike full ICU where zh-CN keeps them distinct. Relax the existing non-zh-CN special case to also apply on browser so Replace_CultureInfo doesn't fail on the WASM legs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 744f142e-00de-4b24-a591-1646cbd71a11 --- .../tests/ComplianceTests/GB18030/Tests/StringTests.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs index 4758b335468d88..40527bfc7fb19d 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/StringTests.cs @@ -268,10 +268,14 @@ public void Replace_CultureInfo(string decoded, CultureInfo culture) { int expected = SplitHelper(decoded, textElement).Length; string replaced = decoded.Replace(textElement, Dummy, ignoreCase: false, culture); + + // '0' (U+0030) and '〇' (U+3007) collate as equal under most cultures. zh-CN normally keeps + // them distinct, but the browser's ICU collation shard treats them as equal there too, so + // tolerate the resulting count mismatch wherever that equivalence applies. + bool zeroIdeographicZeroEquivalence = culture.Name != "zh-CN" || PlatformDetection.IsBrowser; 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')), + (zeroIdeographicZeroEquivalence && textElement == "\u3007" && decoded.Contains('0')) || + (zeroIdeographicZeroEquivalence && textElement == "0" && decoded.Contains('\u3007')), $"Values differ for text element {textElement}"); } } From 999b7a25d3157375daaf3c283d6ebb284f1a0449 Mon Sep 17 00:00:00 2001 From: David Cantu Date: Sun, 26 Jul 2026 14:06:45 -0500 Subject: [PATCH 20/21] Throw when embedded test data stream cannot be fully read Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 744f142e-00de-4b24-a591-1646cbd71a11 --- .../Common/tests/ComplianceTests/GB18030/TestHelper.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs index 0b07eee81112a4..f1b08331ab80b3 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/TestHelper.cs @@ -78,13 +78,15 @@ private static byte[] ReadTestDataFileBytes() using Stream stream = typeof(TestHelper).Assembly.GetManifestResourceStream(TestDataResourceName) ?? throw new InvalidOperationException($"Could not find embedded resource '{TestDataResourceName}'."); + // The manifest resource stream is seekable and reports the exact resource size, so read the + // whole thing into a right-sized buffer instead of growing a MemoryStream. byte[] bytes = new byte[stream.Length]; int totalRead = 0; while (totalRead < bytes.Length) { int read = stream.Read(bytes, totalRead, bytes.Length - totalRead); if (read == 0) - break; + throw new EndOfStreamException($"Expected {bytes.Length} bytes from '{TestDataResourceName}' but only read {totalRead}."); totalRead += read; } From a485dd7119d86b518c121c7707705401d8765d51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20Cant=C3=BA?= Date: Sun, 26 Jul 2026 12:10:26 -0700 Subject: [PATCH 21/21] Fix missing space in param --- .../Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs index 620dfba0103bff..19ce6ddac069f3 100644 --- a/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs +++ b/src/libraries/Common/tests/ComplianceTests/GB18030/Tests/RegexTests.cs @@ -85,7 +85,7 @@ public static IEnumerable NamedBlock_MemberData() => [Theory] [MemberData(nameof(NamedBlock_MemberData))] - public async Task NamedBlock_InclusionAsync(string[]characters, RegexNamedBlock namedBlock, RegexEngine engine, CultureInfo culture) + 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)