summaryrefslogtreecommitdiff
path: root/test/json/json_ryu_fallback_test.rb
diff options
context:
space:
mode:
Diffstat (limited to 'test/json/json_ryu_fallback_test.rb')
-rw-r--r--test/json/json_ryu_fallback_test.rb191
1 files changed, 191 insertions, 0 deletions
diff --git a/test/json/json_ryu_fallback_test.rb b/test/json/json_ryu_fallback_test.rb
new file mode 100644
index 0000000000..a61b3e668d
--- /dev/null
+++ b/test/json/json_ryu_fallback_test.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+require_relative 'test_helper'
+begin
+ require 'bigdecimal'
+rescue LoadError
+end
+
+class JSONRyuFallbackTest < Test::Unit::TestCase
+ include JSON
+
+ # Test that numbers with more than 17 significant digits fall back to rb_cstr_to_dbl
+ def test_more_than_17_significant_digits
+ # These numbers have > 17 significant digits and should use fallback path
+ # They should still parse correctly, just not via the Ryu optimization
+
+ test_cases = [
+ # input, expected (rounded to double precision)
+ ["1.23456789012345678901234567890", 1.2345678901234567],
+ ["123456789012345678.901234567890", 1.2345678901234568e+17],
+ ["0.123456789012345678901234567890", 0.12345678901234568],
+ ["9999999999999999999999999999.9", 1.0e+28],
+ # Edge case: exactly 18 digits
+ ["123456789012345678", 123456789012345680.0],
+ # Many fractional digits
+ ["0.12345678901234567890123456789", 0.12345678901234568],
+ ]
+
+ test_cases.each do |input, expected|
+ result = JSON.parse(input)
+ assert_in_delta(expected, result, 1e-10,
+ "Failed to parse #{input} correctly (>17 digits, fallback path)")
+ end
+ end
+
+ # Test decimal_class option forces fallback
+ def test_decimal_class_option
+ input = "3.141"
+
+ # Without decimal_class: uses Ryu, returns Float
+ result_float = JSON.parse(input)
+ assert_instance_of(Float, result_float)
+ assert_equal(3.141, result_float)
+
+ # With decimal_class: uses fallback, returns BigDecimal
+ result_bigdecimal = JSON.parse(input, decimal_class: BigDecimal)
+ assert_instance_of(BigDecimal, result_bigdecimal)
+ assert_equal(BigDecimal("3.141"), result_bigdecimal)
+ end if defined?(::BigDecimal)
+
+ # Test that numbers with <= 17 digits use Ryu optimization
+ def test_ryu_optimization_used_for_normal_numbers
+ test_cases = [
+ ["3.141", 3.141],
+ ["1.23456789012345e100", 1.23456789012345e100],
+ ["0.00000000000001", 1.0e-14],
+ ["123456789012345.67", 123456789012345.67],
+ ["-1.7976931348623157e+308", -1.7976931348623157e+308],
+ ["2.2250738585072014e-308", 2.2250738585072014e-308],
+ # Exactly 17 significant digits
+ ["12345678901234567", 12345678901234567.0],
+ ["1.2345678901234567", 1.2345678901234567],
+ ]
+
+ test_cases.each do |input, expected|
+ result = JSON.parse(input)
+ assert_in_delta(expected, result, expected.abs * 1e-15,
+ "Failed to parse #{input} correctly (<=17 digits, Ryu path)")
+ end
+ end
+
+ # Test edge cases at the boundary (17 digits)
+ def test_seventeen_digit_boundary
+ # Exactly 17 significant digits should use Ryu
+ input_17 = "12345678901234567.0" # Force it to be a float with .0
+ result = JSON.parse(input_17)
+ assert_in_delta(12345678901234567.0, result, 1e-10)
+
+ # 18 significant digits should use fallback
+ input_18 = "123456789012345678.0"
+ result = JSON.parse(input_18)
+ # Note: This will be rounded to double precision
+ assert_in_delta(123456789012345680.0, result, 1e-10)
+ end
+
+ # Test that leading zeros don't count toward the 17-digit limit
+ def test_leading_zeros_dont_count
+ test_cases = [
+ ["0.00012345678901234567", 0.00012345678901234567], # 17 significant digits
+ ["0.000000000000001234567890123456789", 1.234567890123457e-15], # >17 significant
+ ]
+
+ test_cases.each do |input, expected|
+ result = JSON.parse(input)
+ assert_in_delta(expected, result, expected.abs * 1e-10,
+ "Failed to parse #{input} correctly")
+ end
+ end
+
+ # Test that Ryu handles special values correctly
+ def test_special_double_values
+ test_cases = [
+ ["1.7976931348623157e+308", Float::MAX], # Largest finite double
+ ["2.2250738585072014e-308", Float::MIN], # Smallest normalized double
+ ]
+
+ test_cases.each do |input, expected|
+ result = JSON.parse(input)
+ assert_in_delta(expected, result, expected.abs * 1e-10,
+ "Failed to parse #{input} correctly")
+ end
+
+ # Test zero separately
+ result_pos_zero = JSON.parse("0.0")
+ assert_equal(0.0, result_pos_zero)
+
+ # Note: JSON.parse doesn't preserve -0.0 vs +0.0 distinction in standard mode
+ result_neg_zero = JSON.parse("-0.0")
+ assert_equal(0.0, result_neg_zero.abs)
+ end
+
+ # Test subnormal numbers that caused precision issues before fallback was added
+ # These are extreme edge cases discovered by fuzzing (4 in 6 billion numbers tested)
+ def test_subnormal_edge_cases_round_trip
+ # These subnormal numbers (~1e-310) had 1 ULP rounding errors in original Ryu
+ # They now use rb_cstr_to_dbl fallback for exact precision
+ test_cases = [
+ "-3.2652630314355e-310",
+ "3.9701623107025e-310",
+ "-3.6607772435415e-310",
+ "2.9714076801985e-310",
+ ]
+
+ test_cases.each do |input|
+ # Parse the number
+ result = JSON.parse(input)
+
+ # Should be bit-identical
+ assert_equal(result, JSON.parse(result.to_s),
+ "Subnormal #{input} failed round-trip test")
+
+ # Should be bit-identical
+ assert_equal(result, JSON.parse(JSON.dump(result)),
+ "Subnormal #{input} failed round-trip test")
+
+ # Verify the value is in the expected subnormal range
+ assert(result.abs < 2.225e-308,
+ "#{input} should be subnormal (< 2.225e-308)")
+ end
+ end
+
+ # Test invalid numbers are properly rejected
+ def test_invalid_numbers_rejected
+ invalid_cases = [
+ "-",
+ ".",
+ "-.",
+ "-.e10",
+ "1.2.3",
+ "1e",
+ "1e+",
+ ]
+
+ invalid_cases.each do |input|
+ assert_raise(JSON::ParserError, "Should reject invalid number: #{input}") do
+ JSON.parse(input)
+ end
+ end
+ end
+
+ def test_large_exponent_numbers
+ assert_equal Float::INFINITY, JSON.parse("1e4294967296")
+ assert_equal 0.0, JSON.parse("1e-4294967296")
+ assert_equal 0.0, JSON.parse("99999999999999999e-4294967296")
+ assert_equal Float::INFINITY, JSON.parse("1e4294967295")
+ assert_equal Float::INFINITY, JSON.parse("1e4294967297")
+
+ assert_equal(-Float::INFINITY, JSON.parse("-1e4294967296"))
+ assert_equal(-0.0, JSON.parse("-1e-4294967296"))
+ assert_equal(-0.0, JSON.parse("-99999999999999999e-4294967296"))
+ assert_equal(-Float::INFINITY, JSON.parse("-1e4294967295"))
+ assert_equal(-Float::INFINITY, JSON.parse("-1e4294967297"))
+
+ assert_equal(Float::INFINITY, JSON.parse("1e9223372036854775808"))
+ assert_equal(Float::INFINITY, JSON.parse("1e9999999999999999999"))
+ assert_equal(Float::INFINITY, JSON.parse("1e18446744073709551616"))
+ assert_equal(Float::INFINITY, JSON.parse("1e10000000000000000000"))
+ assert_equal(Float::INFINITY, JSON.parse("1e184467440737095516160"))
+ assert_equal 0.0, JSON.parse("1e-18446744073709551615")
+ assert_equal 0.0, JSON.parse("1e-9223372036854775809")
+ end
+end