This commit is contained in:
David Cantú 2025-07-30 22:31:30 +08:00 committed by GitHub
commit 76891ec859
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 997 additions and 0 deletions

View File

@ -5,6 +5,7 @@
</Folder>
<Folder Name="/tests/">
<Project Path="tests/Common.Tests.csproj" />
<Project Path="tests/ComplianceTests/Compliance.Tests.csproj" />
<Project Path="tests/StreamConformanceTests/StreamConformanceTests.csproj" />
<Project Path="tests/TestUtilities/TestUtilities.csproj" />
</Folder>

View File

@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeRemoteExecutor>true</IncludeRemoteExecutor>
</PropertyGroup>
<ItemGroup>
<Compile Include="GB18030\TestHelper.cs" />
<Compile Include="GB18030\Tests\ConsoleTests.cs" />
<Compile Include="GB18030\Tests\DirectoryInfoTests.cs" />
<Compile Include="GB18030\Tests\DirectoryTestBase.cs" />
<Compile Include="GB18030\Tests\DirectoryTests.cs" />
<Compile Include="GB18030\Tests\EncodingTests.cs" />
<Compile Include="GB18030\Tests\FileInfoTests.cs" />
<Compile Include="GB18030\Tests\FileTestBase.cs" />
<Compile Include="GB18030\Tests\FileTests.cs" />
<Compile Include="GB18030\Tests\StringTests.cs" />
</ItemGroup>
<ItemGroup>
<None Include="GB18030\Level3+Amendment_Test_Data_for_Mid_to_High_Volume_cases.txt">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,45 @@
Short sample strings for inputs with length limitation:
龭唉𫓧G㐁A𫟦D𠳐ⅷ𫇭C丂𬣙𪛗⼀<EFBFBD>𱍐𰀀䶶㎎𮯰9𫜵荳鿰𬺰
鬃𮚮Q鿃𫦠d㐇𬉼𪨰C鍲𫠊棗𪛘𮯳g𱍓⿕𫜶C𰀇㎡䶷𠁠<EFBFBD>
麥𬴂c𮮐d堃𬣙𨙸αa苘𫠋龨䡫𫜷⽦<EFBFBD>𪛙𰀑G𱍝㏒䶸A鿲𮯽𪾢
䒚d𫠆鿀𨓌遫𮟮C𫠜𫥹ǒ𪣻蜒𪛚𫜸<EFBFBD>㏄䶹⼵枛鿳s𱐺𰂼𮲱
𫚭齅䶱5𮯠灋𬘭r𫟼蝌龯𪛒<EFBFBD>𪛛㊣𫜹⾢𱔟𫍲𮴋䶺𰆬a
A龬𮧄r𫖳G𡿯𪢧隣𫞫崸𬺡䶻𫜵⽭𪛜㒹𰇻馈<EFBFBD>𱖄𮵦鿵
怐𪧂䶮𢃾鿂磷㘥N𫟰D𮓇𬸚㎏𫜶𮶍㈢⽥<EFBFBD>䶼𬺠鿶𪛝2𰉖𱘍
霹褷㦬A𬹼鿁𫝦D扏𬒇6𪛞𢳂ǒ㏑⽆<EFBFBD>𫜷𰋵𱙁𮷎鿷𭸌G䶽𪴈
蘏龿蔚P汘㰀O𥫔Ω𫇢v𫟄𬯎𰌸q𫜸⿃<EFBFBD>𪛟𬂪𱚃𮷼𭇋鿸䶾
龩胤獫㰜𫚻0狛𫯪𬭳𫠓D𨰾㎝𫜹<EFBFBD>𲎯𪛘𱍊⾁𮹝鿿䶿𬺲
Long sample strings for inputs with extended length limitation
Group0:
鿀鿃啊齄𬉼𫚭狜﨩㐀䶵8e𠀀𪛖丂狛𫠠𬺡⿻𬺰𮯠𫝀𫠝rP𪛗𪛟𮯰𮹝<EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3𱍐𲎯①ㄩ𫜵𫜹⼀⿕鿰鿿䶶䶿𰀀𱍊𪜀𫜴6n
Group1:
啊唉𬉼𠳐0u㐀㐇A𫝏𫝕狧龍丏丒丟𠀀𠀅ⅶǒ𪾢𫘝i𪛗𪛘龦龪𬺰𮯙𮨴8鿰鿱𫠠𫠣𫠥㎎㎡𫜵𫜶𮯰𮯱𮲇𮲅䶶䶷𰀀𰁒𰃋𰃌𱍐𱍑𱎄𱎅⼀⽴<EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3
Group2:
癌隘鼾𫚭𫓯丣妺圵㐈㫒䀄狜珵𫓧𫄨𫓯8A𬺞𬷻𬶆𫍣𫛸𫖳FOC𱎒𱎓𱏱𱓃𠀈𠀉𤴠⒚𪛙𪛚𫜷𫜸𮲭𮲮𮴡𮴢𮴥㏎鿲鿳⼈⽍𰃖𰄩𰄱𰆆𰆇𰆈<EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3䶸䶹𫠆𫟹𫞩𮨸𮡬𮓶
Group3:
𫓧𬬮舰齄D屌囍囧垔囒珸珹䲟珺陫cz㩹㩽䀃𫟼𫟅𫠜ao鿀龬ɑ𤴤𤴨𮴽𮶆𮶇𮶻𬸈𬴌㏄㏑⿻𫜹𫜵𪛛𪛜𮚸𮄄𰆊𰆋𰆌𰆍<EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3𱐎𱐏𱑛𱑯𱗄E9z𫍯𫍲𫐐鿴鿵⽬⾒䶺䶻C
Group4:
龫龭姜江疆蒋𬴂𫘦琀琂琇琌50R𫖮𫓶𫓹𨸠𪎑𪛒𫠊𫠆𫟹8U5㸰䒯䢀𬝄𬰼𬩹𪛝𪛞㏒﹫𫜸𫜹8U5𮷀𮷁𮷄𮷅𮄃𭾁𮇏<EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3鿶鿷⾔⾧䶼䶽Ⅷξ𰉣𰉩𰎙𰐼𱘕𱖬𱙼𱙵凃喛囘
Group5:
纮肣繤魴𫠆𨐈龲龳䦼䦽䦾8b亘兀丐廿𪨊𪞝b𫠆𫞩𫠜怋怐怓怗𭮂𭦬𱚹𱛢𱪥𱜢𮷏𮷐𮷑𮷒鿸鿹𪛓𩚳𪛟𪛗䶾䶿⾨⾲𫜵𫜷<EFBFBD><EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3𬩫𬬪㎝⿻𰑇𰑈58b
Group6:
䶭㑆㢶龼龮孬噩澧澹𪎄𪎇Ω羓缽翋豖𫌀𫖳𫘪𫚕0wL𭡆𭨬𫟅𫞩𫟦𰠄𰠅𰡪𰤌𬢝𬢙𫜸𫜶𱝋𱝌𱝍𱝎𱝏𮷕𮷖𮷀𮵩A鿺鿻𫄧𪟝<EFBFBD><EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3䶶䶹㏕〒𪛘𪛚g9⾽⿅怘怞怢怣妢
Group7:
龫龯澶濂濡濞濠瀚𫔶𫌀pW5㿻㧜㗝䦸氿崑汈汋嫾𪩘𫗴𫔍𫄸@pU䀅𤴩𤴡𤴣ěγ㎎㎞𫞩𫟦𫠜𭁾𭁽𬺳馬鴻齊镸閉𪛛𪛜𮷿𮸧𮸨𮸬e𫜸𫜷𱝻𱝼𱠺𱡟鿲鿺䶷䶼⿆⿌𰧬𰧭𰧮𰩎𰪥𰪦𬚺𬘐𬖱<F0AC9890><F0AC96B1>U1<55><31><EFBFBD>U2<55><32><EFBFBD>U3
Group8:
窣甪療裏闫囡猬嗦𩾃𬭯龾龽𫍣𫑡yG㚉㟰㕩㓺A𠃉𠃊Ⅱ𫟹𫟼𫟦𫠜y0R𮸰𮸱𮸹𮸺汎汑汒汖狆𬓮𬓯𬔙𬔵㎎㏒𪛘𪛟𫜸𫜷鿼鿽<EFBFBD><EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>U3䶾䶿𬺹𬺺𬺻y0R𰮤𰯴𰰛𰰜𰱺𱡦𱡻𱤽𱥮𱧓⿋⾿
Group9:
狇狉狑狓㝎㝒㝓Qa隣﨤﨧A3c鿃龿黩黧黥黯鼢𠗥𠕤𠔌𦈡𫄸𫠊𫞩𫟦𫠜V9𬻱𬻴𬻵㏑﹫𮸄𮸾𮸿𮹀𫜸𫜹𰸛𰹇𰹭𰺨𰻦βě𪛟𪛛𱯄𱪡𱫦𱫧𱮴𪾢𪟝𫐄𫘜m0⾣⾋鿾鿿𬓭𬅝<EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3䶶䶷
Group10:
鿁鿂鼬鼯鼹鼷𬕂𡎚㖞䎱䲣狽䴙䀅㩶㪷a4𬅁𫽵𫶟𫟼𫟅𫟦𫠜𤴩𤴪𤴣𤴫⑶ū︔𫜶𫓯𫘦𫐐䶼䶿鿱鿽𭀛𭀜𭀞㎎﹦𪛝𪛟𮹙𮹚𮹛𮵠𮹝牘狕狖狘丗𲎪𲎫𲎬𲎭𲎮𲎯𱍆𱍇𱍈𱍉𱍊⽏⽐<EFBFBD><EFBFBD><EFBFBD>U1<EFBFBD><EFBFBD><EFBFBD><EFBFBD>U2<EFBFBD><EFBFBD><EFBFBD>U3iv

View File

@ -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<byte[]> s_encodedTestData = GetTestData();
internal static readonly IEnumerable<string> s_decodedTestData = s_encodedTestData.Select(data => GB18030Encoding.GetString(data));
internal static readonly IEnumerable<string> s_splitNewLineDecodedTestData = s_decodedTestData.SelectMany(
data => data.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries));
public static IEnumerable<object[]> EncodedTestData { get; } = s_encodedTestData.Select(data => new object[] { data });
public static IEnumerable<object[]> DecodedTestData { get; } = s_decodedTestData.Select(data => new object[] { data });
public static IEnumerable<object[]> SplitNewLineDecodedTestData { get; } = s_splitNewLineDecodedTestData.Select(data => new object[] { data });
private static IEnumerable<byte[]> 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<byte> 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<string> GetTextElements(string input)
{
TextElementEnumerator enumerator = StringInfo.GetTextElementEnumerator(input);
while (enumerator.MoveNext())
{
yield return enumerator.GetTextElement();
}
}
}

View File

@ -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));
}
}

View File

@ -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<FileSystemInfo> 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());
}
}

View File

@ -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<object[]> 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);
}
}

View File

@ -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<string> 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));
}
}

View File

@ -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))));
}
}

View File

@ -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);
}

View File

@ -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<byte> 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)));
}
}

View File

@ -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)));
}
}

View File

@ -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<object[]> 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<object[]> 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<object[]> 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<object[]> 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<T>(string str, params T[] separators) =>
separators switch
{
char[] chars => str.Split(chars, StringSplitOptions.None),
string[] strings => str.Split(strings, StringSplitOptions.None),
_ => throw new ArgumentException()
};
}
}