Skip to content

Commit 6f0aa99

Browse files
Add tests for NumberStyles.AllowTrailingInvalidCharacters (#124054)
## Description Added comprehensive test coverage for the `NumberStyles.AllowTrailingInvalidCharacters` mode across all numeric types. This style allows Parse/TryParse APIs to parse up to the first invalid character and return the number of characters consumed, similar to Utf8Parser behavior. ## Changes **Test Coverage Added:** - All integer types: Int32, Int64, Int16, SByte, UInt32, UInt64, UInt16, Byte - All floating point types: Double, Single, Half, Decimal - Arbitrary precision: BigInteger **Test Scenarios:** - Basic parsing with trailing invalid characters (e.g., `"123abc"` → value: 123, consumed: 3) - Combined with NumberStyles: Integer, HexNumber, BinaryNumber, Currency, Float - Edge cases: max/min values, empty strings, overflow, special values (Infinity, NaN) - All API surface: string, ReadOnlySpan\<char\>, ReadOnlySpan\<byte\> overloads - Both success and failure paths with proper validation of characters/bytes consumed Tests follow existing patterns in System.Runtime.Tests and mirror the ValidateParser approach used in System.Memory Utf8Parser tests. ## Testing Tests build successfully. Full baseline build required for test execution would exceed time constraints. <!-- START COPILOT CODING AGENT TIPS --> --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: tannergooding <10487869+tannergooding@users.noreply.github.com>
1 parent 68a36cd commit 6f0aa99

13 files changed

Lines changed: 1060 additions & 0 deletions

File tree

src/libraries/System.Runtime.Numerics/tests/BigInteger/parse.cs

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1407,5 +1407,91 @@ public static void RegressionIssueRuntime94610(string text)
14071407
parseTest.RegressionIssueRuntime94610(text);
14081408
}));
14091409
}
1410+
1411+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_TestData()
1412+
{
1413+
// Basic BigInteger parsing with trailing invalid characters
1414+
yield return new object[] { "123abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(123), 3 };
1415+
yield return new object[] { "456789xyz", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(456789), 6 };
1416+
yield return new object[] { "0abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, BigInteger.Zero, 1 };
1417+
1418+
// With leading whitespace
1419+
yield return new object[] { " 123abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(123), 5 };
1420+
1421+
// With signs
1422+
yield return new object[] { "+123abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(123), 4 };
1423+
yield return new object[] { "-456xyz", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(-456), 4 };
1424+
1425+
// HexNumber with trailing invalid characters
1426+
yield return new object[] { "ABCDEFxyz", NumberStyles.HexNumber | NumberStyles.AllowTrailingInvalidCharacters, null, BigInteger.Parse("ABCDEF", NumberStyles.HexNumber), 6 };
1427+
yield return new object[] { "FFGHxyz", NumberStyles.HexNumber | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(0xFF), 2 };
1428+
1429+
// BinaryNumber with trailing invalid characters
1430+
yield return new object[] { "101010abc", NumberStyles.BinaryNumber | NumberStyles.AllowTrailingInvalidCharacters, null, BigInteger.Parse("101010", NumberStyles.BinaryNumber), 6 };
1431+
yield return new object[] { "1112", NumberStyles.BinaryNumber | NumberStyles.AllowTrailingInvalidCharacters, null, BigInteger.Parse("111", NumberStyles.BinaryNumber), 3 };
1432+
1433+
// Large numbers with trailing characters
1434+
yield return new object[] { "123456789012345678901234567890abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, BigInteger.Parse("123456789012345678901234567890"), 30 };
1435+
1436+
// Valid number without trailing characters
1437+
yield return new object[] { "123456", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, new BigInteger(123456), 6 };
1438+
}
1439+
1440+
[Theory]
1441+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_TestData))]
1442+
public static void Parse_AllowTrailingInvalidCharacters(string value, NumberStyles style, IFormatProvider provider, BigInteger expectedValue, int expectedCharsConsumed)
1443+
{
1444+
BigInteger result;
1445+
int charsConsumed;
1446+
1447+
// Test string overload with charsConsumed
1448+
Assert.True(BigInteger.TryParse(value, style, provider, out result, out charsConsumed));
1449+
Assert.Equal(expectedValue, result);
1450+
Assert.Equal(expectedCharsConsumed, charsConsumed);
1451+
1452+
// Test ReadOnlySpan<char> overload with charsConsumed
1453+
Assert.True(BigInteger.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
1454+
Assert.Equal(expectedValue, result);
1455+
Assert.Equal(expectedCharsConsumed, charsConsumed);
1456+
1457+
// Test UTF-8 overload with bytesConsumed
1458+
byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
1459+
int bytesConsumed;
1460+
Assert.True(BigInteger.TryParse(utf8Bytes.AsSpan(), style, provider, out result, out bytesConsumed));
1461+
Assert.Equal(expectedValue, result);
1462+
// For ASCII characters, bytes consumed should equal chars consumed
1463+
if (value.All(c => c < 128))
1464+
{
1465+
Assert.Equal(expectedCharsConsumed, bytesConsumed);
1466+
}
1467+
}
1468+
1469+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_Invalid_TestData()
1470+
{
1471+
// Empty string
1472+
yield return new object[] { "", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null };
1473+
1474+
// Only invalid characters (no valid number)
1475+
yield return new object[] { "abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null };
1476+
yield return new object[] { "xyz", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null };
1477+
}
1478+
1479+
[Theory]
1480+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_Invalid_TestData))]
1481+
public static void Parse_AllowTrailingInvalidCharacters_Invalid(string value, NumberStyles style, IFormatProvider provider)
1482+
{
1483+
BigInteger result;
1484+
int charsConsumed;
1485+
1486+
// Test string overload with charsConsumed
1487+
Assert.False(BigInteger.TryParse(value, style, provider, out result, out charsConsumed));
1488+
Assert.Equal(BigInteger.Zero, result);
1489+
Assert.Equal(0, charsConsumed);
1490+
1491+
// Test ReadOnlySpan<char> overload with charsConsumed
1492+
Assert.False(BigInteger.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
1493+
Assert.Equal(BigInteger.Zero, result);
1494+
Assert.Equal(0, charsConsumed);
1495+
}
14101496
}
14111497
}

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/ByteTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Collections.Generic;
55
using System.Globalization;
6+
using System.Linq;
67
using System.Text;
78
using Xunit;
89

@@ -429,5 +430,40 @@ public static void Parse_Utf8Span_InvalidUtf8()
429430
[MemberData(nameof(ToString_TestData))]
430431
public static void TryFormat(byte i, string format, IFormatProvider provider, string expected) =>
431432
NumberFormatTestHelper.TryFormatNumberTest(i, format, provider, expected);
433+
434+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_TestData()
435+
{
436+
yield return new object[] { "123abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)123, 3 };
437+
yield return new object[] { "12xyz", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)12, 2 };
438+
yield return new object[] { "0abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)0, 1 };
439+
yield return new object[] { "255abc", NumberStyles.Integer | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)255, 3 };
440+
yield return new object[] { "FFxyz", NumberStyles.HexNumber | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)0xFF, 2 };
441+
yield return new object[] { "ABGxyz", NumberStyles.HexNumber | NumberStyles.AllowTrailingInvalidCharacters, null, (byte)0xAB, 2 };
442+
}
443+
444+
[Theory]
445+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_TestData))]
446+
public static void Parse_AllowTrailingInvalidCharacters(string value, NumberStyles style, IFormatProvider provider, byte expectedValue, int expectedCharsConsumed)
447+
{
448+
byte result;
449+
int charsConsumed;
450+
451+
Assert.True(byte.TryParse(value, style, provider, out result, out charsConsumed));
452+
Assert.Equal(expectedValue, result);
453+
Assert.Equal(expectedCharsConsumed, charsConsumed);
454+
455+
Assert.True(byte.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
456+
Assert.Equal(expectedValue, result);
457+
Assert.Equal(expectedCharsConsumed, charsConsumed);
458+
459+
byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
460+
int bytesConsumed;
461+
Assert.True(byte.TryParse(utf8Bytes.AsSpan(), style, provider, out result, out bytesConsumed));
462+
Assert.Equal(expectedValue, result);
463+
if (value.All(c => c < 128))
464+
{
465+
Assert.Equal(expectedCharsConsumed, bytesConsumed);
466+
}
467+
}
432468
}
433469
}

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DecimalTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,5 +2434,93 @@ public static void TryFormat()
24342434
}
24352435
}
24362436
}
2437+
2438+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_TestData()
2439+
{
2440+
// Basic decimal parsing with trailing invalid characters
2441+
yield return new object[] { "123.45abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45m, 6 };
2442+
yield return new object[] { "456.78xyz", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 456.78m, 6 };
2443+
yield return new object[] { "0.123abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 0.123m, 5 };
2444+
2445+
// With leading whitespace
2446+
yield return new object[] { " 123.45abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45m, 8 };
2447+
2448+
// With signs
2449+
yield return new object[] { "+123.45abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45m, 7 };
2450+
yield return new object[] { "-456.78xyz", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, -456.78m, 7 };
2451+
2452+
// Integer without decimal point
2453+
yield return new object[] { "123abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 123m, 3 };
2454+
2455+
// With thousands separator
2456+
NumberFormatInfo customFormat = new NumberFormatInfo() { NumberGroupSeparator = "," };
2457+
yield return new object[] { "1,234abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, customFormat, 1234m, 5 };
2458+
2459+
// Max and min values with trailing characters
2460+
yield return new object[] { "79228162514264337593543950335abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 79228162514264337593543950335m, 29 };
2461+
yield return new object[] { "-79228162514264337593543950335xyz", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, -79228162514264337593543950335m, 30 };
2462+
2463+
// Valid number without trailing characters
2464+
yield return new object[] { "123.45", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45m, 6 };
2465+
}
2466+
2467+
[Theory]
2468+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_TestData))]
2469+
public static void Parse_AllowTrailingInvalidCharacters(string value, NumberStyles style, IFormatProvider provider, decimal expectedValue, int expectedCharsConsumed)
2470+
{
2471+
decimal result;
2472+
int charsConsumed;
2473+
2474+
// Test string overload with charsConsumed
2475+
Assert.True(decimal.TryParse(value, style, provider, out result, out charsConsumed));
2476+
Assert.Equal(expectedValue, result);
2477+
Assert.Equal(expectedCharsConsumed, charsConsumed);
2478+
2479+
// Test ReadOnlySpan<char> overload with charsConsumed
2480+
Assert.True(decimal.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
2481+
Assert.Equal(expectedValue, result);
2482+
Assert.Equal(expectedCharsConsumed, charsConsumed);
2483+
2484+
// Test UTF-8 overload with bytesConsumed
2485+
byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
2486+
int bytesConsumed;
2487+
Assert.True(decimal.TryParse(utf8Bytes.AsSpan(), style, provider, out result, out bytesConsumed));
2488+
Assert.Equal(expectedValue, result);
2489+
// For ASCII characters, bytes consumed should equal chars consumed
2490+
if (value.All(c => c < 128))
2491+
{
2492+
Assert.Equal(expectedCharsConsumed, bytesConsumed);
2493+
}
2494+
}
2495+
2496+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_Invalid_TestData()
2497+
{
2498+
// Empty string
2499+
yield return new object[] { "", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null };
2500+
2501+
// Only invalid characters (no valid number)
2502+
yield return new object[] { "abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null };
2503+
2504+
// Overflow
2505+
yield return new object[] { "79228162514264337593543950336abc", NumberStyles.Number | NumberStyles.AllowTrailingInvalidCharacters, null };
2506+
}
2507+
2508+
[Theory]
2509+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_Invalid_TestData))]
2510+
public static void Parse_AllowTrailingInvalidCharacters_Invalid(string value, NumberStyles style, IFormatProvider provider)
2511+
{
2512+
decimal result;
2513+
int charsConsumed;
2514+
2515+
// Test string overload with charsConsumed
2516+
Assert.False(decimal.TryParse(value, style, provider, out result, out charsConsumed));
2517+
Assert.Equal(0m, result);
2518+
Assert.Equal(0, charsConsumed);
2519+
2520+
// Test ReadOnlySpan<char> overload with charsConsumed
2521+
Assert.False(decimal.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
2522+
Assert.Equal(0m, result);
2523+
Assert.Equal(0, charsConsumed);
2524+
}
24372525
}
24382526
}

src/libraries/System.Runtime/tests/System.Runtime.Tests/System/DoubleTests.cs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1856,5 +1856,120 @@ public static void RadiansToDegreesTest(double value, double expectedResult, dou
18561856
AssertExtensions.Equal(-expectedResult, double.RadiansToDegrees(-value), allowedVariance);
18571857
AssertExtensions.Equal(+expectedResult, double.RadiansToDegrees(+value), allowedVariance);
18581858
}
1859+
1860+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_TestData()
1861+
{
1862+
// Basic floating point parsing with trailing invalid characters
1863+
yield return new object[] { "123.45abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45, 6 };
1864+
yield return new object[] { "456.78xyz", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 456.78, 6 };
1865+
yield return new object[] { "0.123abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 0.123, 5 };
1866+
1867+
// With leading whitespace
1868+
yield return new object[] { " 123.45abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45, 8 };
1869+
1870+
// With signs
1871+
yield return new object[] { "+123.45abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45, 7 };
1872+
yield return new object[] { "-456.78xyz", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, -456.78, 7 };
1873+
1874+
// With exponent
1875+
yield return new object[] { "1.23e10abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 1.23e10, 7 };
1876+
yield return new object[] { "4.56E-5xyz", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 4.56E-5, 7 };
1877+
yield return new object[] { "1E10abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 1E10, 4 };
1878+
1879+
// Integer without decimal point
1880+
yield return new object[] { "123abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.0, 3 };
1881+
yield return new object[] { "456xyz", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 456.0, 3 };
1882+
1883+
// Special values
1884+
yield return new object[] { "Infinityabc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, double.PositiveInfinity, 8 };
1885+
yield return new object[] { "-Infinityxyz", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, double.NegativeInfinity, 9 };
1886+
yield return new object[] { "NaNabc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, double.NaN, 3 };
1887+
1888+
// Valid number without trailing characters
1889+
yield return new object[] { "123.45", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.45, 6 };
1890+
1891+
// Stop at invalid exponent
1892+
yield return new object[] { "123eabc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null, 123.0, 3 };
1893+
}
1894+
1895+
[Theory]
1896+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_TestData))]
1897+
public static void Parse_AllowTrailingInvalidCharacters(string value, NumberStyles style, IFormatProvider provider, double expectedValue, int expectedCharsConsumed)
1898+
{
1899+
double result;
1900+
int charsConsumed;
1901+
1902+
// Test string overload with charsConsumed
1903+
Assert.True(double.TryParse(value, style, provider, out result, out charsConsumed));
1904+
if (double.IsNaN(expectedValue))
1905+
{
1906+
Assert.True(double.IsNaN(result));
1907+
}
1908+
else
1909+
{
1910+
Assert.Equal(expectedValue, result);
1911+
}
1912+
Assert.Equal(expectedCharsConsumed, charsConsumed);
1913+
1914+
// Test ReadOnlySpan<char> overload with charsConsumed
1915+
Assert.True(double.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
1916+
if (double.IsNaN(expectedValue))
1917+
{
1918+
Assert.True(double.IsNaN(result));
1919+
}
1920+
else
1921+
{
1922+
Assert.Equal(expectedValue, result);
1923+
}
1924+
Assert.Equal(expectedCharsConsumed, charsConsumed);
1925+
1926+
// Test UTF-8 overload with bytesConsumed
1927+
byte[] utf8Bytes = Encoding.UTF8.GetBytes(value);
1928+
int bytesConsumed;
1929+
Assert.True(double.TryParse(utf8Bytes.AsSpan(), style, provider, out result, out bytesConsumed));
1930+
if (double.IsNaN(expectedValue))
1931+
{
1932+
Assert.True(double.IsNaN(result));
1933+
}
1934+
else
1935+
{
1936+
Assert.Equal(expectedValue, result);
1937+
}
1938+
// For ASCII characters, bytes consumed should equal chars consumed
1939+
if (value.All(c => c < 128))
1940+
{
1941+
Assert.Equal(expectedCharsConsumed, bytesConsumed);
1942+
}
1943+
}
1944+
1945+
public static IEnumerable<object[]> Parse_AllowTrailingInvalidCharacters_Invalid_TestData()
1946+
{
1947+
// Empty string
1948+
yield return new object[] { "", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null };
1949+
1950+
// Only invalid characters (no valid number)
1951+
yield return new object[] { "abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null };
1952+
1953+
// Invalid start characters
1954+
yield return new object[] { ".abc", NumberStyles.Float | NumberStyles.AllowTrailingInvalidCharacters, null };
1955+
}
1956+
1957+
[Theory]
1958+
[MemberData(nameof(Parse_AllowTrailingInvalidCharacters_Invalid_TestData))]
1959+
public static void Parse_AllowTrailingInvalidCharacters_Invalid(string value, NumberStyles style, IFormatProvider provider)
1960+
{
1961+
double result;
1962+
int charsConsumed;
1963+
1964+
// Test string overload with charsConsumed
1965+
Assert.False(double.TryParse(value, style, provider, out result, out charsConsumed));
1966+
Assert.Equal(0.0, result);
1967+
Assert.Equal(0, charsConsumed);
1968+
1969+
// Test ReadOnlySpan<char> overload with charsConsumed
1970+
Assert.False(double.TryParse(value.AsSpan(), style, provider, out result, out charsConsumed));
1971+
Assert.Equal(0.0, result);
1972+
Assert.Equal(0, charsConsumed);
1973+
}
18591974
}
18601975
}

0 commit comments

Comments
 (0)