Skip to content

Commit bdd9166

Browse files
committed
Improvements in unit tests
- Move encoding unit tests to separate project (where in StringBuilder one which was hindeing discovery). - Add new enconding/deconding unit tests (following #237)
1 parent a6c59a5 commit bdd9166

File tree

7 files changed

+342
-1
lines changed

7 files changed

+342
-1
lines changed

Tests/NFUnitTestStringBuilder/EncodingTests.cs renamed to Tests/NFUnitTestEncoding/EncodingTests.cs

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,5 +275,233 @@ public void Utf8EncodingTests_TestFullASCIIRange()
275275
byte[] reencoded = Encoding.UTF8.GetBytes(decoded);
276276
RoundtripUtf8(reencoded, input, 127);
277277
}
278+
279+
// NEW TESTS FOR FIXES
280+
281+
[TestMethod]
282+
public void Utf8EncodingTests_TestIncrementalDecodingExactBuffer()
283+
{
284+
// Test the fix for exact buffer size (no space for null terminator)
285+
// This was the original issue where iMaxChars=1, outputUTF16_size=1
286+
string testString = "AB";
287+
byte[] utf8Bytes = Encoding.UTF8.GetBytes(testString);
288+
289+
// Decode one character at a time with exact buffer
290+
char[] outputChars = new char[1];
291+
int bytesUsed, charsUsed;
292+
bool completed;
293+
294+
var decoder = Encoding.UTF8.GetDecoder();
295+
decoder.Convert(utf8Bytes, 0, 1, outputChars, 0, 1, false, out bytesUsed, out charsUsed, out completed);
296+
297+
Assert.AreEqual(1, bytesUsed);
298+
Assert.AreEqual(1, charsUsed);
299+
Assert.AreEqual('A', outputChars[0]);
300+
}
301+
302+
[TestMethod]
303+
public void Utf8EncodingTests_TestInvalidSurrogatePairHandling()
304+
{
305+
// Test UTF-16 to UTF-8 conversion with invalid surrogate pairs
306+
// High surrogate (0xD800) followed by a regular character 'A' (0x41)
307+
// The high surrogate should be replaced with U+FFFD and 'A' should be preserved
308+
309+
// Create string with high surrogate followed by 'A'
310+
char[] chars = new char[] { (char)0xD800, 'A', 'B' };
311+
string testString = new string(chars);
312+
313+
byte[] encoded = Encoding.UTF8.GetBytes(testString);
314+
315+
// Expect: U+FFFD (0xEF 0xBF 0xBD) + 'A' (0x41) + 'B' (0x42)
316+
byte[] expected = new byte[] { 0xEF, 0xBF, 0xBD, 0x41, 0x42 };
317+
CollectionAssert.AreEqual(expected, encoded);
318+
}
319+
320+
[TestMethod]
321+
public void Utf8EncodingTests_TestInvalidSurrogatePairMiddle()
322+
{
323+
// High surrogate followed by another high surrogate
324+
char[] chars = new char[] { 'A', (char)0xD800, (char)0xD801, 'B' };
325+
string testString = new string(chars);
326+
327+
byte[] encoded = Encoding.UTF8.GetBytes(testString);
328+
329+
// Expect: 'A' (0x41) + U+FFFD (0xEF 0xBF 0xBD) + U+FFFD (0xEF 0xBF 0xBD) + 'B' (0x42)
330+
byte[] expected = new byte[] { 0x41, 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD, 0x42 };
331+
CollectionAssert.AreEqual(expected, encoded);
332+
}
333+
334+
[TestMethod]
335+
public void Utf8EncodingTests_TestUnpairedLowSurrogate()
336+
{
337+
// Low surrogate without preceding high surrogate
338+
char[] chars = new char[] { 'A', (char)0xDC00, 'B' };
339+
string testString = new string(chars);
340+
341+
byte[] encoded = Encoding.UTF8.GetBytes(testString);
342+
343+
// Expect: 'A' (0x41) + U+FFFD (0xEF 0xBF 0xBD) + 'B' (0x42)
344+
byte[] expected = new byte[] { 0x41, 0xEF, 0xBF, 0xBD, 0x42 };
345+
CollectionAssert.AreEqual(expected, encoded);
346+
}
347+
348+
[TestMethod]
349+
public void Utf8EncodingTests_TestUnpairedHighSurrogateAtEnd()
350+
{
351+
// High surrogate at the end of input
352+
char[] chars = new char[] { 'A', 'B', (char)0xD800 };
353+
string testString = new string(chars);
354+
355+
byte[] encoded = Encoding.UTF8.GetBytes(testString);
356+
357+
// Expect: 'A' (0x41) + 'B' (0x42) + U+FFFD (0xEF 0xBF 0xBD)
358+
byte[] expected = new byte[] { 0x41, 0x42, 0xEF, 0xBF, 0xBD };
359+
CollectionAssert.AreEqual(expected, encoded);
360+
}
361+
362+
[TestMethod]
363+
public void Utf8EncodingTests_TestPartial2ByteSequence()
364+
{
365+
// Start of 2-byte sequence without continuation byte
366+
byte[] input = new byte[] { 0x41, 0xC2 }; // 'A' followed by incomplete 2-byte sequence
367+
byte[] expected = new byte[] { 0x41, 0xEF, 0xBF, 0xBD };
368+
RoundtripUtf8(input, expected, 2);
369+
}
370+
371+
[TestMethod]
372+
public void Utf8EncodingTests_TestPartial3ByteSequence()
373+
{
374+
// Start of 3-byte sequence with only 1 continuation byte
375+
byte[] input = new byte[] { 0x41, 0xE2, 0x82 }; // 'A' followed by incomplete 3-byte sequence
376+
byte[] expected = new byte[] { 0x41, 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD };
377+
RoundtripUtf8(input, expected, 3);
378+
}
379+
380+
[TestMethod]
381+
public void Utf8EncodingTests_TestPartial4ByteSequence()
382+
{
383+
// Start of 4-byte sequence with only 2 continuation bytes
384+
byte[] input = new byte[] { 0x41, 0xF0, 0x9F, 0x98 }; // 'A' followed by incomplete 4-byte sequence
385+
byte[] expected = new byte[] { 0x41, 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD };
386+
RoundtripUtf8(input, expected, 4);
387+
}
388+
389+
[TestMethod]
390+
public void Utf8EncodingTests_TestMixedValidAndInvalidSequences()
391+
{
392+
// Mix of valid and invalid sequences
393+
byte[] input = new byte[]
394+
{
395+
0x41, // 'A' - valid ASCII
396+
0xC2, 0xA9, // © - valid 2-byte
397+
0xE2, 0x82, // incomplete 3-byte
398+
0x42, // 'B' - valid ASCII
399+
0xF0, 0x9F, 0x98, 0x80, // 😀 - valid 4-byte
400+
0xED, 0xA0, 0x80// invalid surrogate
401+
};
402+
403+
string decoded = Encoding.UTF8.GetString(input, 0, input.Length);
404+
Assert.IsNotNull(decoded);
405+
Assert.IsTrue(decoded.Contains("A"));
406+
Assert.IsTrue(decoded.Contains("©"));
407+
Assert.IsTrue(decoded.Contains("B"));
408+
}
409+
410+
[TestMethod]
411+
public void Utf8EncodingTests_TestValidSurrogatePair()
412+
{
413+
// Test proper handling of valid surrogate pairs
414+
// 😀 (U+1F600) should encode to F0 9F 98 80 and decode back correctly
415+
string emoji = "😀";
416+
byte[] encoded = Encoding.UTF8.GetBytes(emoji);
417+
418+
byte[] expected = new byte[] { 0xF0, 0x9F, 0x98, 0x80 };
419+
CollectionAssert.AreEqual(expected, encoded);
420+
421+
string decoded = Encoding.UTF8.GetString(encoded, 0, encoded.Length);
422+
Assert.AreEqual(emoji, decoded);
423+
}
424+
425+
[TestMethod]
426+
public void Utf8EncodingTests_TestMultipleSurrogatePairs()
427+
{
428+
// Multiple emoji/surrogate pairs in sequence
429+
string emojis = "😀😁😂";
430+
byte[] encoded = Encoding.UTF8.GetBytes(emojis);
431+
string decoded = Encoding.UTF8.GetString(encoded, 0, encoded.Length);
432+
Assert.AreEqual(emojis, decoded);
433+
}
434+
435+
[TestMethod]
436+
public void Utf8EncodingTests_TestIncrementalDecodingMultiByte()
437+
{
438+
// Test incremental decoding of multi-byte sequences
439+
byte[] utf8 = new byte[] { 0xE2, 0x82, 0xAC }; // €
440+
441+
char[] output = new char[1];
442+
int bytesUsed, charsUsed;
443+
bool completed;
444+
445+
var decoder = Encoding.UTF8.GetDecoder();
446+
decoder.Convert(utf8, 0, 3, output, 0, 1, false, out bytesUsed, out charsUsed, out completed);
447+
448+
Assert.AreEqual(3, bytesUsed);
449+
Assert.AreEqual(1, charsUsed);
450+
Assert.AreEqual('€', output[0]);
451+
}
452+
453+
[TestMethod]
454+
public void Utf8EncodingTests_TestOverlongEncodingRejection()
455+
{
456+
// Ensure overlong encodings are rejected and replaced with U+FFFD
457+
// Overlong encoding of 'A' (should be 0x41, not C1 81)
458+
byte[] input = new byte[] { 0xC1, 0x81 };
459+
string decoded = Encoding.UTF8.GetString(input, 0, input.Length);
460+
byte[] reencoded = Encoding.UTF8.GetBytes(decoded);
461+
462+
// Should produce replacement characters
463+
Assert.AreNotEqual(input.Length, reencoded.Length);
464+
Assert.IsTrue(decoded.Contains("\uFFFD"));
465+
}
466+
467+
[TestMethod]
468+
public void Utf8EncodingTests_TestSequentialInvalidBytes()
469+
{
470+
// Multiple sequential invalid bytes
471+
byte[] input = new byte[] { 0xFE, 0xFF, 0xFE };
472+
string decoded = Encoding.UTF8.GetString(input, 0, input.Length);
473+
byte[] reencoded = Encoding.UTF8.GetBytes(decoded);
474+
475+
// Each invalid byte should become one replacement character
476+
byte[] expected = new byte[] { 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD, 0xEF, 0xBF, 0xBD };
477+
CollectionAssert.AreEqual(expected, reencoded);
478+
Assert.AreEqual(3, decoded.Length);
479+
}
480+
481+
[TestMethod]
482+
public void Utf8EncodingTests_TestBoundaryCodepoints()
483+
{
484+
// Test boundary values for different UTF-8 sequence lengths
485+
486+
// U+007F - last 1-byte character
487+
byte[] input1 = new byte[] { 0x7F };
488+
RoundtripUtf8(input1, input1, 1);
489+
490+
// U+0080 - first 2-byte character
491+
byte[] input2 = new byte[] { 0xC2, 0x80 };
492+
RoundtripUtf8(input2, input2, 1);
493+
494+
// U+07FF - last 2-byte character
495+
byte[] input3 = new byte[] { 0xDF, 0xBF };
496+
RoundtripUtf8(input3, input3, 1);
497+
498+
// U+0800 - first 3-byte character
499+
byte[] input4 = new byte[] { 0xE0, 0xA0, 0x80 };
500+
RoundtripUtf8(input4, input4, 1);
501+
502+
// U+FFFF - last 3-byte character (excluding surrogates)
503+
byte[] input5 = new byte[] { 0xEF, 0xBF, 0xBF };
504+
RoundtripUtf8(input5, input5, 1);
505+
}
278506
}
279507
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project ToolsVersion="Current" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
3+
<PropertyGroup Label="Globals">
4+
<NanoFrameworkProjectSystemPath>$(MSBuildExtensionsPath)\nanoFramework\v1.0\</NanoFrameworkProjectSystemPath>
5+
</PropertyGroup>
6+
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.Default.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.Default.props')" />
7+
<ItemGroup>
8+
<ProjectCapability Include="TestContainer" />
9+
</ItemGroup>
10+
<PropertyGroup>
11+
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
12+
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
13+
<ProjectTypeGuids>{11A8DD76-328B-46DF-9F39-F559912D0360};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
14+
<ProjectGuid>5cc54ee8-8145-4b94-adf0-cd0879fcdf7d</ProjectGuid>
15+
<OutputType>Library</OutputType>
16+
<AppDesignerFolder>Properties</AppDesignerFolder>
17+
<FileAlignment>512</FileAlignment>
18+
<RootNamespace>NFUnitTestEncoding</RootNamespace>
19+
<AssemblyName>NFUnitTest</AssemblyName>
20+
<IsCodedUITest>False</IsCodedUITest>
21+
<IsTestProject>true</IsTestProject>
22+
<TestProjectType>UnitTest</TestProjectType>
23+
<TargetFrameworkVersion>v1.0</TargetFrameworkVersion>
24+
</PropertyGroup>
25+
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.props')" />
26+
<PropertyGroup>
27+
<RunSettingsFilePath>$(MSBuildProjectDirectory)\nano.runsettings</RunSettingsFilePath>
28+
</PropertyGroup>
29+
<ItemGroup>
30+
<Compile Include="EncodingTests.cs" />
31+
<Compile Include="Properties\AssemblyInfo.cs" />
32+
</ItemGroup>
33+
<ItemGroup>
34+
<Reference Include="mscorlib">
35+
<HintPath>..\..\packages\nanoFramework.CoreLibrary.1.17.11\lib\mscorlib.dll</HintPath>
36+
</Reference>
37+
<Reference Include="nanoFramework.TestFramework">
38+
<HintPath>..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.TestFramework.dll</HintPath>
39+
</Reference>
40+
<Reference Include="nanoFramework.UnitTestLauncher">
41+
<HintPath>..\..\packages\nanoFramework.TestFramework.3.0.77\lib\nanoFramework.UnitTestLauncher.exe</HintPath>
42+
</Reference>
43+
</ItemGroup>
44+
<ItemGroup>
45+
<None Include="nano.runsettings" />
46+
<None Include="packages.config" />
47+
</ItemGroup>
48+
<ItemGroup>
49+
<ProjectReference Include="..\..\nanoFramework.System.Text\nanoFramework.System.Text.nfproj" />
50+
</ItemGroup>
51+
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.CSharp.targets')" />
52+
</Project>
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System.Reflection;
2+
using System.Runtime.CompilerServices;
3+
using System.Runtime.InteropServices;
4+
5+
// General Information about an assembly is controlled through the following
6+
// set of attributes. Change these attribute values to modify the information
7+
// associated with an assembly.
8+
[assembly: AssemblyDescription("")]
9+
[assembly: AssemblyConfiguration("")]
10+
[assembly: AssemblyCompany("")]
11+
[assembly: AssemblyCopyright("Copyright (c) 2021 nanoFramework contributors")]
12+
[assembly: AssemblyTrademark("")]
13+
[assembly: AssemblyCulture("")]
14+
15+
// Setting ComVisible to false makes the types in this assembly not visible
16+
// to COM components. If you need to access a type in this assembly from
17+
// COM, set the ComVisible attribute to true on that type.
18+
[assembly: ComVisible(false)]
19+
20+
// Version information for an assembly consists of the following four values:
21+
//
22+
// Major Version
23+
// Minor Version
24+
// Build Number
25+
// Revision
26+
//
27+
// You can specify all the values or you can default the Build and Revision Numbers
28+
// by using the '*' as shown below:
29+
// [assembly: AssemblyVersion("1.0.*")]
30+
[assembly: AssemblyVersion("1.0.0.0")]
31+
[assembly: AssemblyFileVersion("1.0.0.0")]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<RunSettings>
3+
<!-- Configurations that affect the Test Framework -->
4+
<RunConfiguration>
5+
<ResultsDirectory>.\TestResults</ResultsDirectory><!-- Path relative to solution directory -->
6+
<TestSessionTimeout>120000</TestSessionTimeout><!-- Milliseconds -->
7+
<TargetFrameworkVersion>net48</TargetFrameworkVersion>
8+
<TargetPlatform>x64</TargetPlatform>
9+
</RunConfiguration>
10+
<nanoFrameworkAdapter>
11+
<Logging>None</Logging> <!--Set to the desired level of logging for Unit Test execution. Possible values are: None, Detailed, Verbose, Error. -->
12+
<IsRealHardware>False</IsRealHardware><!--Set to true to run tests on real hardware. -->
13+
<RealHardwarePort>COM3</RealHardwarePort><!--Specify the COM port to use to connect to a nanoDevice. If none is specified, a device detection is performed and the 1st available one will be used. -->
14+
<CLRVersion></CLRVersion><!--Specify the nanoCLR version to use. If not specified, the latest available will be used. -->
15+
<PathToLocalCLRInstance></PathToLocalCLRInstance><!--Specify the path to a local nanoCLR instance. If not specified, the default one installed with nanoclr CLR witll be used. -->
16+
</nanoFrameworkAdapter>
17+
</RunSettings>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<packages>
3+
<package id="nanoFramework.CoreLibrary" version="1.17.11" targetFramework="netnano1.0" />
4+
<package id="nanoFramework.TestFramework" version="3.0.77" targetFramework="netnano1.0" developmentDependency="true" />
5+
</packages>

Tests/NFUnitTestStringBuilder/NFUnitTestStringBuilder.nfproj

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
</PropertyGroup>
2727
<Import Project="$(NanoFrameworkProjectSystemPath)NFProjectSystem.props" Condition="Exists('$(NanoFrameworkProjectSystemPath)NFProjectSystem.props')" />
2828
<ItemGroup>
29-
<Compile Include="EncodingTests.cs" />
3029
<Compile Include="Properties\AssemblyInfo.cs" />
3130
<Compile Include="StringBuilderTests.cs" />
3231
</ItemGroup>

nanoFramework.System.Text.sln

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "NFUnitTestStringBuilder", "
1818
EndProject
1919
Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "nanoFramework.System.Text.Benchmark", "nanoFramework.System.Text.Benchmark\nanoFramework.System.Text.Benchmark.nfproj", "{3ADD8949-B5FB-4116-B4E0-5A218A0D8045}"
2020
EndProject
21+
Project("{11A8DD76-328B-46DF-9F39-F559912D0360}") = "NFUnitTestEncoding", "Tests\NFUnitTestEncoding\NFUnitTestEncoding.nfproj", "{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}"
22+
EndProject
2123
Global
2224
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2325
Debug|Any CPU = Debug|Any CPU
@@ -42,12 +44,19 @@ Global
4244
{3ADD8949-B5FB-4116-B4E0-5A218A0D8045}.Release|Any CPU.ActiveCfg = Release|Any CPU
4345
{3ADD8949-B5FB-4116-B4E0-5A218A0D8045}.Release|Any CPU.Build.0 = Release|Any CPU
4446
{3ADD8949-B5FB-4116-B4E0-5A218A0D8045}.Release|Any CPU.Deploy.0 = Release|Any CPU
47+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
48+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Debug|Any CPU.Build.0 = Debug|Any CPU
49+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
50+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Release|Any CPU.ActiveCfg = Release|Any CPU
51+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Release|Any CPU.Build.0 = Release|Any CPU
52+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D}.Release|Any CPU.Deploy.0 = Release|Any CPU
4553
EndGlobalSection
4654
GlobalSection(SolutionProperties) = preSolution
4755
HideSolutionNode = FALSE
4856
EndGlobalSection
4957
GlobalSection(NestedProjects) = preSolution
5058
{C5331952-B712-4C77-939B-27589CED5A24} = {1A9B4D93-29BC-4314-ABF9-E4BC2A755032}
59+
{5CC54EE8-8145-4B94-ADF0-CD0879FCDF7D} = {1A9B4D93-29BC-4314-ABF9-E4BC2A755032}
5160
EndGlobalSection
5261
GlobalSection(ExtensibilityGlobals) = postSolution
5362
SolutionGuid = {863DB2A2-5555-49B7-A921-C2EF51A150B1}

0 commit comments

Comments
 (0)