mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
189 lines
6.5 KiB
Ruby
189 lines
6.5 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative 'test_helper'
|
|
|
|
class JSONFloatParserFallbackTest < Test::Unit::TestCase
|
|
include JSON
|
|
|
|
# Test that numbers with more than 18 significant digits fall back to rb_cstr_to_dbl
|
|
def test_more_than_18_significant_digits
|
|
# These numbers have > 18 significant digits and should use fallback path.
|
|
# They should still parse correctly, just not via the Fast Float optimization
|
|
|
|
# input, expected (rounded to double precision)
|
|
assert_float_parsing "1.23456789012345678901234567890"
|
|
assert_float_parsing "123456789012345678.901234567890"
|
|
assert_float_parsing "0.123456789012345678901234567890"
|
|
assert_float_parsing "9999999999999999999999999999.9"
|
|
|
|
# Edge case: exactly 18 digits
|
|
assert_float_parsing "123456789012345678.0"
|
|
|
|
# Many fractional digits
|
|
assert_float_parsing "0.12345678901234567890123456789"
|
|
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
|
|
|
|
private
|
|
|
|
def assert_float_parsing(number)
|
|
assert_equal(Float(number), JSON.parse(number), "Failed to parse #{number} correctly")
|
|
end
|
|
end
|