Skip to content

Instantly share code, notes, and snippets.

@simi
Created October 27, 2025 10:59
Show Gist options
  • Select an option

  • Save simi/d5b4800e9eb050dbefb34f218c8e49b4 to your computer and use it in GitHub Desktop.

Select an option

Save simi/d5b4800e9eb050dbefb34f218c8e49b4 to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
# Fuzzer for JSON float parsing - generates random numbers and verifies parsing
# Tests edge cases, precision limits, and ensures Ryu implementation is robust
require 'json'
require 'set'
class JSONFloatFuzzer
attr_reader :stats
def initialize
@stats = {
total_numbers: 0,
total_batches: 0,
integers: 0,
floats: 0,
scientific: 0,
negative: 0,
zero: 0,
tiny: 0,
huge: 0,
errors: [],
start_time: Time.now
}
end
# Generate random integer
def random_integer
case rand(10)
when 0..3 # Small integers
rand(-1000..1000)
when 4..6 # Medium integers
rand(-1_000_000..1_000_000)
when 7..8 # Large integers
rand(-1_000_000_000_000..1_000_000_000_000)
else # Very large (may hit precision limits)
rand(-10**17..10**17)
end
end
# Generate random decimal number
def random_decimal
# Generate mantissa with varying digit counts
digit_count = case rand(10)
when 0..4 # Short decimals (1-5 digits)
rand(1..5)
when 5..7 # Medium decimals (6-12 digits)
rand(6..12)
else # Long decimals (13-17 digits, tests Ryu limits)
rand(13..17)
end
mantissa = rand(10**(digit_count-1)..10**digit_count - 1)
decimal_places = rand(1..10)
value = mantissa.to_f / (10 ** decimal_places)
rand < 0.3 ? -value : value
end
# Generate random scientific notation number
def random_scientific
mantissa = rand(1.0..10.0).round(rand(1..15))
exponent = case rand(10)
when 0..3 # Small exponents
rand(-10..10)
when 4..6 # Medium exponents
rand(-100..100)
when 7..8 # Large exponents
rand(-307..307) # Near double precision limits
else # Extreme (may overflow/underflow)
rand(-400..400)
end
value = mantissa * (10 ** exponent)
rand < 0.3 ? -value : value
end
# Generate special edge cases
def random_edge_case
cases = [
0.0,
-0.0,
0.1,
0.01,
0.001,
0.0001,
1.0,
10.0,
100.0,
1000.0,
1.7976931348623157e+308, # Max double
2.2250738585072014e-308, # Min positive normalized
5e-324, # Min positive subnormal
-1.7976931348623157e+308, # Min double (most negative)
1e-10,
1e-20,
1e-100,
1e-200,
1e+10,
1e+100,
1e+200,
3.141592653589793, # π
2.718281828459045, # e
1.4142135623730951, # √2
]
cases.sample
end
# Generate a random number based on distribution
def generate_number
case rand(100)
when 0..25 # 25% integers
@stats[:integers] += 1
random_integer
when 26..55 # 30% decimals
@stats[:floats] += 1
random_decimal
when 56..80 # 25% scientific notation
@stats[:scientific] += 1
random_scientific
else # 20% edge cases
random_edge_case
end
end
# Generate a batch of random numbers
def generate_batch(size = 1000)
numbers = []
size.times do
num = generate_number
numbers << num
# Update stats
@stats[:total_numbers] += 1
@stats[:negative] += 1 if num < 0
@stats[:zero] += 1 if num == 0
@stats[:tiny] += 1 if num.abs > 0 && num.abs < 1e-100
@stats[:huge] += 1 if num.abs > 1e100
end
numbers
end
# Test a batch of numbers
def test_batch(numbers, batch_num)
# Filter out special values that JSON doesn't support
valid_numbers = numbers.map do |n|
if n.is_a?(Float) && (n.infinite? || n.nan?)
0.0 # Replace with 0.0 for JSON serialization
else
n
end
end
json_array = valid_numbers.to_json
begin
parsed = JSON.parse(json_array)
# Verify round-trip for integers and simple floats
valid_numbers.each_with_index do |original, i|
parsed_value = parsed[i]
# For integers, should match exactly
if original.is_a?(Integer)
unless original == parsed_value
@stats[:errors] << {
batch: batch_num,
index: i,
original: original,
parsed: parsed_value,
type: :integer_mismatch
}
end
# For floats, check they're close (within floating point precision)
elsif original.is_a?(Float)
# Special handling for zero
if original == 0.0 && parsed_value == 0.0
next
end
# For normal numbers, check relative error
if original.finite? && parsed_value.finite?
relative_error = ((parsed_value - original).abs / original.abs)
if relative_error > 1e-14 # Allow small precision differences
@stats[:errors] << {
batch: batch_num,
index: i,
original: original,
parsed: parsed_value,
relative_error: relative_error,
type: :precision_error
}
end
# Check special values match
elsif original.infinite? && parsed_value.infinite?
unless (original <=> parsed_value) == 0
@stats[:errors] << {
batch: batch_num,
index: i,
original: original,
parsed: parsed_value,
type: :infinity_sign_mismatch
}
end
elsif original.nan? != parsed_value.nan?
@stats[:errors] << {
batch: batch_num,
index: i,
original: original,
parsed: parsed_value,
type: :nan_mismatch
}
end
end
end
true
rescue JSON::ParserError => e
@stats[:errors] << {
batch: batch_num,
error: e.message,
json_size: json_array.size,
type: :parse_error
}
false
rescue => e
@stats[:errors] << {
batch: batch_num,
error: "#{e.class}: #{e.message}",
type: :unexpected_error
}
false
end
end
# Print statistics
def print_stats
elapsed = Time.now - @stats[:start_time]
numbers_per_sec = (@stats[:total_numbers] / elapsed).round(1)
puts "\n" + "=" * 80
puts "Fuzzer Statistics"
puts "=" * 80
puts "Batches: #{@stats[:total_batches]}"
puts "Total numbers: #{@stats[:total_numbers]} (#{numbers_per_sec}/sec)"
puts "Elapsed time: #{elapsed.round(2)}s"
puts ""
puts "Number types:"
puts " Integers: #{@stats[:integers]} (#{pct(@stats[:integers])}%)"
puts " Floats: #{@stats[:floats]} (#{pct(@stats[:floats])}%)"
puts " Scientific: #{@stats[:scientific]} (#{pct(@stats[:scientific])}%)"
puts ""
puts "Special values:"
puts " Negative: #{@stats[:negative]} (#{pct(@stats[:negative])}%)"
puts " Zero: #{@stats[:zero]}"
puts " Tiny (<1e-100): #{@stats[:tiny]}"
puts " Huge (>1e100): #{@stats[:huge]}"
puts ""
puts "Errors: #{@stats[:errors].size}"
if @stats[:errors].any?
puts "\nError breakdown:"
error_types = @stats[:errors].group_by { |e| e[:type] }
error_types.each do |type, errors|
puts " #{type}: #{errors.size}"
end
puts "\nFirst 5 errors:"
@stats[:errors].first(5).each_with_index do |err, i|
puts "\n Error #{i + 1}:"
err.each do |k, v|
puts " #{k}: #{v}"
end
end
end
puts "=" * 80
end
# Calculate percentage
def pct(count)
return 0 if @stats[:total_numbers] == 0
((count.to_f / @stats[:total_numbers]) * 100).round(1)
end
# Run the fuzzer
def run(batch_size: 1000, batch_count: nil, continuous: false)
puts "JSON Float Fuzzer"
puts "=" * 80
puts "Batch size: #{batch_size} numbers per batch"
puts "Mode: #{continuous ? 'Continuous (Ctrl-C to stop)' : "#{batch_count} batches"}"
puts "=" * 80
puts ""
batch_num = 0
last_print_time = Time.now
last_stats_time = Time.now
loop do
batch_num += 1
@stats[:total_batches] = batch_num
# Generate and test batch
numbers = generate_batch(batch_size)
success = test_batch(numbers, batch_num)
current_time = Time.now
# Print progress line every 5 seconds
if current_time - last_print_time >= 5.0
status = success ? "✓" : "✗"
elapsed = (current_time - @stats[:start_time]).round(2)
rate = (@stats[:total_numbers] / (current_time - @stats[:start_time])).round(0)
print "\r[#{elapsed}s] Batch #{batch_num} #{status} | "
print "#{@stats[:total_numbers]} numbers | "
print "#{rate}/sec | "
print "Errors: #{@stats[:errors].size}"
print " " * 20 # Clear any leftover characters
$stdout.flush
last_print_time = current_time
end
# Print detailed stats every 100 batches (but not more often than every 30 seconds)
if batch_num % 100 == 0 && (current_time - last_stats_time >= 30.0 || batch_num == 100)
puts "" # New line before stats
print_stats
last_print_time = current_time # Reset timer after printing stats
last_stats_time = current_time
end
# Stop if not continuous and reached batch count
break if !continuous && batch_count && batch_num >= batch_count
end
puts "\n"
print_stats
if @stats[:errors].empty?
puts "\n✓ SUCCESS! All numbers parsed correctly!"
exit 0
else
puts "\n✗ ERRORS FOUND! See details above."
exit 1
end
end
end
# Parse command line arguments
require 'optparse'
options = {
batch_size: 1000,
batch_count: 10,
continuous: false
}
OptionParser.new do |opts|
opts.banner = "Usage: ruby fuzz.rb [options]"
opts.on("-s", "--size SIZE", Integer, "Numbers per batch (default: 1000)") do |s|
options[:batch_size] = s
end
opts.on("-n", "--batches COUNT", Integer, "Number of batches (default: 10)") do |n|
options[:batch_count] = n
end
opts.on("-c", "--continuous", "Run continuously until Ctrl-C") do
options[:continuous] = true
end
opts.on("-h", "--help", "Show this help") do
puts opts
exit
end
end.parse!
# Run fuzzer
fuzzer = JSONFloatFuzzer.new
begin
fuzzer.run(
batch_size: options[:batch_size],
batch_count: options[:batch_count],
continuous: options[:continuous]
)
rescue Interrupt
puts "\n\nInterrupted by user"
fuzzer.print_stats
exit 0
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment