summaryrefslogtreecommitdiff
path: root/test/json
diff options
context:
space:
mode:
Diffstat (limited to 'test/json')
-rw-r--r--test/json/fixtures/fail15.json (renamed from test/json/fixtures/pass15.json)0
-rw-r--r--test/json/fixtures/fail16.json (renamed from test/json/fixtures/pass16.json)0
-rw-r--r--test/json/fixtures/fail17.json (renamed from test/json/fixtures/pass17.json)0
-rw-r--r--test/json/fixtures/fail26.json (renamed from test/json/fixtures/pass26.json)0
-rw-r--r--test/json/fixtures/pass1.json2
-rwxr-xr-xtest/json/json_coder_test.rb102
-rw-r--r--test/json/json_common_interface_test.rb112
-rw-r--r--test/json/json_encoding_test.rb188
-rw-r--r--test/json/json_ext_parser_test.rb31
-rw-r--r--test/json/json_fixtures_test.rb2
-rwxr-xr-xtest/json/json_generator_test.rb455
-rw-r--r--test/json/json_generic_object_test.rb24
-rw-r--r--test/json/json_parser_test.rb206
-rw-r--r--test/json/json_ryu_fallback_test.rb169
-rw-r--r--test/json/ractor_test.rb74
-rw-r--r--test/json/test_helper.rb24
16 files changed, 1307 insertions, 82 deletions
diff --git a/test/json/fixtures/pass15.json b/test/json/fixtures/fail15.json
index fc8376b605..fc8376b605 100644
--- a/test/json/fixtures/pass15.json
+++ b/test/json/fixtures/fail15.json
diff --git a/test/json/fixtures/pass16.json b/test/json/fixtures/fail16.json
index c43ae3c286..c43ae3c286 100644
--- a/test/json/fixtures/pass16.json
+++ b/test/json/fixtures/fail16.json
diff --git a/test/json/fixtures/pass17.json b/test/json/fixtures/fail17.json
index 62b9214aed..62b9214aed 100644
--- a/test/json/fixtures/pass17.json
+++ b/test/json/fixtures/fail17.json
diff --git a/test/json/fixtures/pass26.json b/test/json/fixtures/fail26.json
index 845d26a6a5..845d26a6a5 100644
--- a/test/json/fixtures/pass26.json
+++ b/test/json/fixtures/fail26.json
diff --git a/test/json/fixtures/pass1.json b/test/json/fixtures/pass1.json
index 7828fcc137..fa9058b136 100644
--- a/test/json/fixtures/pass1.json
+++ b/test/json/fixtures/pass1.json
@@ -12,7 +12,7 @@
"real": -9876.543210,
"e": 0.123456789e-12,
"E": 1.234567890E+34,
- "": 23456789012E666,
+ "": 23456789012E66,
"zero": 0,
"one": 1,
"space": " ",
diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb
index 9861181910..47e12ff919 100755
--- a/test/json/json_coder_test.rb
+++ b/test/json/json_coder_test.rb
@@ -12,12 +12,28 @@ class JSONCoderTest < Test::Unit::TestCase
end
def test_json_coder_with_proc_with_unsupported_value
- coder = JSON::Coder.new do |object|
+ coder = JSON::Coder.new do |object, is_key|
+ assert_equal false, is_key
Object.new
end
assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) }
end
+ def test_json_coder_hash_key
+ obj = Object.new
+ coder = JSON::Coder.new do |obj, is_key|
+ assert_equal true, is_key
+ obj.to_s
+ end
+ assert_equal %({#{obj.to_s.inspect}:1}), coder.dump({ obj => 1 })
+
+ coder = JSON::Coder.new { 42 }
+ error = assert_raise JSON::GeneratorError do
+ coder.dump({ obj => 1 })
+ end
+ assert_equal "Integer not allowed as object key in JSON", error.message
+ end
+
def test_json_coder_options
coder = JSON::Coder.new(array_nl: "\n") do |object|
42
@@ -37,17 +53,97 @@ class JSONCoderTest < Test::Unit::TestCase
end
def test_json_coder_dump_NaN_or_Infinity
- coder = JSON::Coder.new(&:inspect)
+ coder = JSON::Coder.new { |o| o.inspect }
assert_equal "NaN", coder.load(coder.dump(Float::NAN))
assert_equal "Infinity", coder.load(coder.dump(Float::INFINITY))
assert_equal "-Infinity", coder.load(coder.dump(-Float::INFINITY))
end
def test_json_coder_dump_NaN_or_Infinity_loop
- coder = JSON::Coder.new(&:itself)
+ coder = JSON::Coder.new { |o| o.itself }
error = assert_raise JSON::GeneratorError do
coder.dump(Float::NAN)
end
assert_include error.message, "NaN not allowed in JSON"
end
+
+ def test_json_coder_string_invalid_encoding
+ calls = 0
+ coder = JSON::Coder.new do |object, is_key|
+ calls += 1
+ object
+ end
+
+ error = assert_raise JSON::GeneratorError do
+ coder.dump("\xFF")
+ end
+ assert_equal "source sequence is illegal/malformed utf-8", error.message
+ assert_equal 1, calls
+
+ error = assert_raise JSON::GeneratorError do
+ coder.dump({ "\xFF" => 1 })
+ end
+ assert_equal "source sequence is illegal/malformed utf-8", error.message
+ assert_equal 2, calls
+
+ calls = 0
+ coder = JSON::Coder.new do |object, is_key|
+ calls += 1
+ object.dup
+ end
+
+ error = assert_raise JSON::GeneratorError do
+ coder.dump("\xFF")
+ end
+ assert_equal "source sequence is illegal/malformed utf-8", error.message
+ assert_equal 1, calls
+
+ error = assert_raise JSON::GeneratorError do
+ coder.dump({ "\xFF" => 1 })
+ end
+ assert_equal "source sequence is illegal/malformed utf-8", error.message
+ assert_equal 2, calls
+
+ calls = 0
+ coder = JSON::Coder.new do |object, is_key|
+ calls += 1
+ object.bytes
+ end
+
+ assert_equal "[255]", coder.dump("\xFF")
+ assert_equal 1, calls
+
+ error = assert_raise JSON::GeneratorError do
+ coder.dump({ "\xFF" => 1 })
+ end
+ assert_equal "Array not allowed as object key in JSON", error.message
+ assert_equal 2, calls
+
+ calls = 0
+ coder = JSON::Coder.new do |object, is_key|
+ calls += 1
+ [object].pack("m")
+ end
+
+ assert_equal '"/w==\\n"', coder.dump("\xFF")
+ assert_equal 1, calls
+
+ assert_equal '{"/w==\\n":1}', coder.dump({ "\xFF" => 1 })
+ assert_equal 2, calls
+ end
+
+ def test_depth
+ coder = JSON::Coder.new(object_nl: "\n", array_nl: "\n", space: " ", indent: " ", depth: 1)
+ assert_equal %({\n "foo": 42\n }), coder.dump(foo: 42)
+ end
+
+ def test_nesting_recovery
+ coder = JSON::Coder.new
+ ary = []
+ ary << ary
+ assert_raise JSON::NestingError do
+ coder.dump(ary)
+ end
+ assert_equal '{"a":1}', coder.dump({ a: 1 })
+ end
end
diff --git a/test/json/json_common_interface_test.rb b/test/json/json_common_interface_test.rb
index 9c12551300..3dfd0623cd 100644
--- a/test/json/json_common_interface_test.rb
+++ b/test/json/json_common_interface_test.rb
@@ -68,11 +68,6 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
JSON.create_id = 'json_class'
end
- def test_deep_const_get
- assert_raise(ArgumentError) { JSON.deep_const_get('Nix::Da') }
- assert_equal File::SEPARATOR, JSON.deep_const_get('File::SEPARATOR')
- end
-
def test_parse
assert_equal [ 1, 2, 3, ], JSON.parse('[ 1, 2, 3 ]')
end
@@ -91,6 +86,30 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
def test_pretty_generate
assert_equal "[\n 1,\n 2,\n 3\n]", JSON.pretty_generate([ 1, 2, 3 ])
+ assert_equal <<~JSON.strip, JSON.pretty_generate({ a: { b: "f"}, c: "d"})
+ {
+ "a": {
+ "b": "f"
+ },
+ "c": "d"
+ }
+ JSON
+
+ # Cause the state to be spilled on the heap.
+ o = Object.new
+ def o.to_s
+ "Object"
+ end
+ actual = JSON.pretty_generate({ a: { b: o}, c: "d", e: "f"})
+ assert_equal <<~JSON.strip, actual
+ {
+ "a": {
+ "b": "Object"
+ },
+ "c": "d",
+ "e": "f"
+ }
+ JSON
end
def test_load
@@ -130,6 +149,7 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
def test_load_with_options
json = '{ "foo": NaN }'
assert JSON.load(json, nil, :allow_nan => true)['foo'].nan?
+ assert JSON.load(json, :allow_nan => true)['foo'].nan?
end
def test_load_null
@@ -138,6 +158,88 @@ class JSONCommonInterfaceTest < Test::Unit::TestCase
assert_raise(JSON::ParserError) { JSON.load('', nil, :allow_blank => false) }
end
+ def test_unsafe_load
+ string_able_klass = Class.new do
+ def initialize(str)
+ @str = str
+ end
+
+ def to_str
+ @str
+ end
+ end
+
+ io_able_klass = Class.new do
+ def initialize(str)
+ @str = str
+ end
+
+ def to_io
+ StringIO.new(@str)
+ end
+ end
+
+ assert_equal @hash, JSON.unsafe_load(@json)
+ tempfile = Tempfile.open('@json')
+ tempfile.write @json
+ tempfile.rewind
+ assert_equal @hash, JSON.unsafe_load(tempfile)
+ stringio = StringIO.new(@json)
+ stringio.rewind
+ assert_equal @hash, JSON.unsafe_load(stringio)
+ string_able = string_able_klass.new(@json)
+ assert_equal @hash, JSON.unsafe_load(string_able)
+ io_able = io_able_klass.new(@json)
+ assert_equal @hash, JSON.unsafe_load(io_able)
+ assert_equal nil, JSON.unsafe_load(nil)
+ assert_equal nil, JSON.unsafe_load('')
+ ensure
+ tempfile.close!
+ end
+
+ def test_unsafe_load_with_proc
+ visited = []
+ JSON.unsafe_load('{"foo": [1, 2, 3], "bar": {"baz": "plop"}}', proc { |o| visited << JSON.dump(o); o })
+
+ expected = [
+ '"foo"',
+ '1',
+ '2',
+ '3',
+ '[1,2,3]',
+ '"bar"',
+ '"baz"',
+ '"plop"',
+ '{"baz":"plop"}',
+ '{"foo":[1,2,3],"bar":{"baz":"plop"}}',
+ ]
+ assert_equal expected, visited
+ end
+
+ def test_unsafe_load_default_options
+ too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
+ assert JSON.unsafe_load(too_deep, nil).is_a?(Array)
+ nan_json = '{ "foo": NaN }'
+ assert JSON.unsafe_load(nan_json, nil)['foo'].nan?
+ assert_equal nil, JSON.unsafe_load(nil, nil)
+ t = Time.new(2025, 9, 3, 14, 50, 0)
+ assert_equal t.to_s, JSON.unsafe_load(JSON(t)).to_s
+ end
+
+ def test_unsafe_load_with_options
+ nan_json = '{ "foo": NaN }'
+ assert_raise(JSON::ParserError) { JSON.unsafe_load(nan_json, nil, :allow_nan => false)['foo'].nan? }
+ # make sure it still uses the defaults when something is provided
+ assert JSON.unsafe_load(nan_json, nil, :allow_blank => true)['foo'].nan?
+ assert JSON.unsafe_load(nan_json, :allow_nan => true)['foo'].nan?
+ end
+
+ def test_unsafe_load_null
+ assert_equal nil, JSON.unsafe_load(nil, nil, :allow_blank => true)
+ assert_raise(TypeError) { JSON.unsafe_load(nil, nil, :allow_blank => false) }
+ assert_raise(JSON::ParserError) { JSON.unsafe_load('', nil, :allow_blank => false) }
+ end
+
def test_dump
too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
obj = eval(too_deep)
diff --git a/test/json/json_encoding_test.rb b/test/json/json_encoding_test.rb
index afffd8976a..7ac06b2a7b 100644
--- a/test/json/json_encoding_test.rb
+++ b/test/json/json_encoding_test.rb
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+
require_relative 'test_helper'
class JSONEncodingTest < Test::Unit::TestCase
@@ -30,6 +31,18 @@ class JSONEncodingTest < Test::Unit::TestCase
assert_equal @generated, JSON.generate(@utf_16_data, ascii_only: true)
end
+ def test_generate_shared_string
+ # Ref: https://github.com/ruby/json/issues/859
+ s = "01234567890"
+ assert_equal '"234567890"', JSON.dump(s[2..-1])
+ s = '01234567890123456789"a"b"c"d"e"f"g"h'
+ assert_equal '"\"a\"b\"c\"d\"e\"f\"g\""', JSON.dump(s[20, 15])
+ s = "0123456789001234567890012345678900123456789001234567890"
+ assert_equal '"23456789001234567890012345678900123456789001234567890"', JSON.dump(s[2..-1])
+ s = "0123456789001234567890012345678900123456789001234567890"
+ assert_equal '"567890012345678900123456789001234567890012345678"', JSON.dump(s[5..-3])
+ end
+
def test_unicode
assert_equal '""', ''.to_json
assert_equal '"\\b"', "\b".to_json
@@ -37,7 +50,7 @@ class JSONEncodingTest < Test::Unit::TestCase
assert_equal '"\u001f"', 0x1f.chr.to_json
assert_equal '" "', ' '.to_json
assert_equal "\"#{0x7f.chr}\"", 0x7f.chr.to_json
- utf8 = [ "© ≠ €! \01" ]
+ utf8 = ["© ≠ €! \01"]
json = '["© ≠ €! \u0001"]'
assert_equal json, utf8.to_json(ascii_only: false)
assert_equal utf8, parse(json)
@@ -78,10 +91,10 @@ class JSONEncodingTest < Test::Unit::TestCase
json = '"\u%04x"' % i
i = i.chr
assert_equal i, parse(json)[0]
- if i == ?\b
+ if i == "\b"
generated = generate(i)
- assert '"\b"' == generated || '"\10"' == generated
- elsif [?\n, ?\r, ?\t, ?\f].include?(i)
+ assert ['"\b"', '"\10"'].include?(generated)
+ elsif ["\n", "\r", "\t", "\f"].include?(i)
assert_equal i.dump, generate(i)
elsif i.chr < 0x20.chr
assert_equal json, generate(i)
@@ -92,4 +105,171 @@ class JSONEncodingTest < Test::Unit::TestCase
end
assert_equal "\302\200", parse('"\u0080"')
end
+
+ def test_deeply_nested_structures
+ # Test for deeply nested arrays
+ nesting_level = 100
+ deeply_nested = []
+ current = deeply_nested
+
+ (nesting_level - 1).times do
+ current << []
+ current = current[0]
+ end
+
+ json = generate(deeply_nested)
+ assert_equal deeply_nested, parse(json)
+
+ # Test for deeply nested objects/hashes
+ deeply_nested_hash = {}
+ current_hash = deeply_nested_hash
+
+ (nesting_level - 1).times do |i|
+ current_hash["key#{i}"] = {}
+ current_hash = current_hash["key#{i}"]
+ end
+
+ json = generate(deeply_nested_hash)
+ assert_equal deeply_nested_hash, parse(json)
+ end
+
+ def test_very_large_json_strings
+ # Create a large array with repeated elements
+ large_array = Array.new(10_000) { |i| "item#{i}" }
+
+ json = generate(large_array)
+ parsed = parse(json)
+
+ assert_equal large_array.size, parsed.size
+ assert_equal large_array.first, parsed.first
+ assert_equal large_array.last, parsed.last
+
+ # Create a large hash
+ large_hash = {}
+ 10_000.times { |i| large_hash["key#{i}"] = "value#{i}" }
+
+ json = generate(large_hash)
+ parsed = parse(json)
+
+ assert_equal large_hash.size, parsed.size
+ assert_equal large_hash["key0"], parsed["key0"]
+ assert_equal large_hash["key9999"], parsed["key9999"]
+ end
+
+ def test_invalid_utf8_sequences
+ invalid_utf8 = "\xFF\xFF"
+ error = assert_raise(JSON::GeneratorError) do
+ generate(invalid_utf8)
+ end
+ assert_match(%r{source sequence is illegal/malformed utf-8}, error.message)
+ end
+
+ def test_surrogate_pair_handling
+ # Test valid surrogate pairs
+ assert_equal "\u{10000}", parse('"\ud800\udc00"')
+ assert_equal "\u{10FFFF}", parse('"\udbff\udfff"')
+
+ # The existing test already checks for orphaned high surrogate
+ assert_raise(JSON::ParserError) { parse('"\ud800"') }
+
+ # Test generating surrogate pairs
+ utf8_string = "\u{10437}"
+ generated = generate(utf8_string, ascii_only: true)
+ assert_match(/\\ud801\\udc37/, generated)
+ end
+
+ def test_json_escaping_edge_cases
+ # Test escaping forward slashes
+ assert_equal "/", parse('"\/"')
+
+ # Test escaping backslashes
+ assert_equal "\\", parse('"\\\\"')
+
+ # Test escaping quotes
+ assert_equal '"', parse('"\\""')
+
+ # Multiple escapes in sequence - different JSON parsers might handle escaped forward slashes differently
+ # Some parsers preserve the escaping, others don't
+ escaped_result = parse('"\\\\\\"\\/"')
+ assert_match(/\\"/, escaped_result)
+ assert_match(%r{/}, escaped_result)
+
+ # Generate string with all special characters
+ special_chars = "\b\f\n\r\t\"\\"
+ escaped_json = generate(special_chars)
+ assert_equal special_chars, parse(escaped_json)
+ end
+
+ def test_empty_objects_and_arrays
+ # Test empty objects with different encodings
+ assert_equal({}, parse('{}'))
+ assert_equal({}, parse('{}'.encode(Encoding::UTF_16BE)))
+ assert_equal({}, parse('{}'.encode(Encoding::UTF_16LE)))
+ assert_equal({}, parse('{}'.encode(Encoding::UTF_32BE)))
+ assert_equal({}, parse('{}'.encode(Encoding::UTF_32LE)))
+
+ # Test empty arrays with different encodings
+ assert_equal([], parse('[]'))
+ assert_equal([], parse('[]'.encode(Encoding::UTF_16BE)))
+ assert_equal([], parse('[]'.encode(Encoding::UTF_16LE)))
+ assert_equal([], parse('[]'.encode(Encoding::UTF_32BE)))
+ assert_equal([], parse('[]'.encode(Encoding::UTF_32LE)))
+
+ # Test generating empty objects and arrays
+ assert_equal '{}', generate({})
+ assert_equal '[]', generate([])
+ end
+
+ def test_null_character_handling
+ # Test parsing null character
+ assert_equal "\u0000", parse('"\u0000"')
+
+ # Test generating null character
+ string_with_null = "\u0000"
+ generated = generate(string_with_null)
+ assert_equal '"\u0000"', generated
+
+ # Test null characters in middle of string
+ mixed_string = "before\u0000after"
+ generated = generate(mixed_string)
+ assert_equal mixed_string, parse(generated)
+ end
+
+ def test_whitespace_handling
+ # Test parsing with various whitespace patterns
+ assert_equal({}, parse(' { } '))
+ assert_equal({}, parse("{\r\n}"))
+ assert_equal([], parse(" [ \n ] "))
+ assert_equal(["a", "b"], parse(" [ \n\"a\",\r\n \"b\"\n ] "))
+ assert_equal({ "a" => "b" }, parse(" { \n\"a\" \r\n: \t\"b\"\n } "))
+
+ # Test with excessive whitespace
+ excessive_whitespace = " \n\r\t" * 10 + "{}" + " \n\r\t" * 10
+ assert_equal({}, parse(excessive_whitespace))
+
+ # Mixed whitespace in keys and values
+ mixed_json = '{"a \n b":"c \r\n d"}'
+ assert_equal({ "a \n b" => "c \r\n d" }, parse(mixed_json))
+ end
+
+ def test_control_character_handling
+ # Test all control characters (U+0000 to U+001F)
+ (0..0x1F).each do |i|
+ # Skip already tested ones
+ next if [0x08, 0x0A, 0x0D, 0x0C, 0x09].include?(i)
+
+ control_char = i.chr('UTF-8')
+ escaped_json = '"' + "\\u%04x" % i + '"'
+ assert_equal control_char, parse(escaped_json)
+
+ # Check that the character is properly escaped when generating
+ assert_match(/\\u00[0-1][0-9a-f]/, generate(control_char))
+ end
+
+ # Test string with multiple control characters
+ control_str = "\u0001\u0002\u0003\u0004"
+ generated = generate(control_str)
+ assert_equal control_str, parse(generated)
+ assert_match(/\\u0001\\u0002\\u0003\\u0004/, generated)
+ end
end
diff --git a/test/json/json_ext_parser_test.rb b/test/json/json_ext_parser_test.rb
index 8aa626257e..e610f642f1 100644
--- a/test/json/json_ext_parser_test.rb
+++ b/test/json/json_ext_parser_test.rb
@@ -14,16 +14,35 @@ class JSONExtParserTest < Test::Unit::TestCase
end
def test_error_messages
- ex = assert_raise(ParserError) { parse('Infinity') }
- assert_equal "unexpected token at 'Infinity'", ex.message
+ ex = assert_raise(ParserError) { parse('Infinity something') }
+ unless RUBY_PLATFORM =~ /java/
+ assert_equal "unexpected token 'Infinity' at line 1 column 1", ex.message
+ end
+ ex = assert_raise(ParserError) { parse('foo bar') }
unless RUBY_PLATFORM =~ /java/
- ex = assert_raise(ParserError) { parse('-Infinity') }
- assert_equal "unexpected token at '-Infinity'", ex.message
+ assert_equal "unexpected token 'foo' at line 1 column 1", ex.message
end
- ex = assert_raise(ParserError) { parse('NaN') }
- assert_equal "unexpected token at 'NaN'", ex.message
+ ex = assert_raise(ParserError) { parse('-Infinity something') }
+ unless RUBY_PLATFORM =~ /java/
+ assert_equal "unexpected token '-Infinity' at line 1 column 1", ex.message
+ end
+
+ ex = assert_raise(ParserError) { parse('NaN something') }
+ unless RUBY_PLATFORM =~ /java/
+ assert_equal "unexpected token 'NaN' at line 1 column 1", ex.message
+ end
+
+ ex = assert_raise(ParserError) { parse(' ') }
+ unless RUBY_PLATFORM =~ /java/
+ assert_equal "unexpected end of input at line 1 column 4", ex.message
+ end
+
+ ex = assert_raise(ParserError) { parse('{ ') }
+ unless RUBY_PLATFORM =~ /java/
+ assert_equal "expected object key, got EOF at line 1 column 5", ex.message
+ end
end
if GC.respond_to?(:stress=)
diff --git a/test/json/json_fixtures_test.rb b/test/json/json_fixtures_test.rb
index c153ebef7c..c0d1037939 100644
--- a/test/json/json_fixtures_test.rb
+++ b/test/json/json_fixtures_test.rb
@@ -10,6 +10,8 @@ class JSONFixturesTest < Test::Unit::TestCase
source = File.read(f)
define_method("test_#{name}") do
assert JSON.parse(source), "Did not pass for fixture '#{File.basename(f)}': #{source.inspect}"
+ rescue JSON::ParserError
+ raise "#{File.basename(f)} parsing failure"
end
end
diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb
index f87006ac7b..d7c4173e8e 100755
--- a/test/json/json_generator_test.rb
+++ b/test/json/json_generator_test.rb
@@ -92,6 +92,46 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal '"World"', "World".to_json(strict: true)
end
+ def test_state_depth_to_json
+ depth = Object.new
+ def depth.to_json(state)
+ JSON::State.from_state(state).depth.to_s
+ end
+
+ assert_equal "0", JSON.generate(depth)
+ assert_equal "[1]", JSON.generate([depth])
+ assert_equal %({"depth":1}), JSON.generate(depth: depth)
+ assert_equal "[[2]]", JSON.generate([[depth]])
+ assert_equal %([{"depth":2}]), JSON.generate([{depth: depth}])
+
+ state = JSON::State.new
+ assert_equal "0", state.generate(depth)
+ assert_equal "[1]", state.generate([depth])
+ assert_equal %({"depth":1}), state.generate(depth: depth)
+ assert_equal "[[2]]", state.generate([[depth]])
+ assert_equal %([{"depth":2}]), state.generate([{depth: depth}])
+ end
+
+ def test_state_depth_to_json_recursive
+ recur = Object.new
+ def recur.to_json(state = nil, *)
+ state = JSON::State.from_state(state)
+ if state.depth < 3
+ state.generate([state.depth, self])
+ else
+ state.generate([state.depth])
+ end
+ end
+
+ assert_raise(NestingError) { JSON.generate(recur, max_nesting: 3) }
+ assert_equal "[0,[1,[2,[3]]]]", JSON.generate(recur, max_nesting: 4)
+
+ state = JSON::State.new(max_nesting: 3)
+ assert_raise(NestingError) { state.generate(recur) }
+ state.max_nesting = 4
+ assert_equal "[0,[1,[2,[3]]]]", JSON.generate(recur, max_nesting: 4)
+ end
+
def test_generate_pretty
json = pretty_generate({})
assert_equal('{}', json)
@@ -122,6 +162,22 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal '666', pretty_generate(666)
end
+ def test_generate_pretty_custom
+ state = State.new(:space_before => "<psb>", :space => "<ps>", :indent => "<pi>", :object_nl => "\n<po_nl>\n", :array_nl => "<pa_nl>")
+ json = pretty_generate({1=>{}, 2=>['a','b'], 3=>4}, state)
+ assert_equal(<<~'JSON'.chomp, json)
+ {
+ <po_nl>
+ <pi>"1"<psb>:<ps>{},
+ <po_nl>
+ <pi>"2"<psb>:<ps>[<pa_nl><pi><pi>"a",<pa_nl><pi><pi>"b"<pa_nl><pi>],
+ <po_nl>
+ <pi>"3"<psb>:<ps>4
+ <po_nl>
+ }
+ JSON
+ end
+
def test_generate_custom
state = State.new(:space_before => " ", :space => " ", :indent => "<i>", :object_nl => "\n", :array_nl => "<a_nl>")
json = generate({1=>{2=>3,4=>[5,6]}}, state)
@@ -167,7 +223,9 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal('{"1":2}', json)
s = JSON.state.new
assert s.check_circular?
- assert s[:check_circular?]
+ assert_deprecated_warning(/JSON::State/) do
+ assert s[:check_circular?]
+ end
h = { 1=>2 }
h[3] = h
assert_raise(JSON::NestingError) { generate(h) }
@@ -177,7 +235,9 @@ class JSONGeneratorTest < Test::Unit::TestCase
a << a
assert_raise(JSON::NestingError) { generate(a, s) }
assert s.check_circular?
- assert s[:check_circular?]
+ assert_deprecated_warning(/JSON::State/) do
+ assert s[:check_circular?]
+ end
end
def test_falsy_state
@@ -218,6 +278,24 @@ class JSONGeneratorTest < Test::Unit::TestCase
:space => "",
:space_before => "",
}.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s })
+
+ state = JSON::State.new(allow_duplicate_key: true)
+ assert_equal({
+ :allow_duplicate_key => true,
+ :allow_nan => false,
+ :array_nl => "",
+ :as_json => false,
+ :ascii_only => false,
+ :buffer_initial_length => 1024,
+ :depth => 0,
+ :script_safe => false,
+ :strict => false,
+ :indent => "",
+ :max_nesting => 100,
+ :object_nl => "",
+ :space => "",
+ :space_before => "",
+ }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s })
end
def test_allow_nan
@@ -243,14 +321,100 @@ class JSONGeneratorTest < Test::Unit::TestCase
end
end
+ # An object that changes state.depth when it receives to_json(state)
+ def bad_to_json
+ obj = Object.new
+ def obj.to_json(state)
+ state.depth += 1
+ "{#{state.object_nl}"\
+ "#{state.indent * state.depth}\"foo\":#{state.space}1#{state.object_nl}"\
+ "#{state.indent * (state.depth - 1)}}"
+ end
+ obj
+ end
+
+ def test_depth_restored_bad_to_json
+ state = JSON::State.new
+ state.generate(bad_to_json)
+ assert_equal 0, state.depth
+ end
+
+ def test_depth_restored_bad_to_json_in_Array
+ assert_equal <<~JSON.chomp, JSON.pretty_generate([bad_to_json] * 2)
+ [
+ {
+ "foo": 1
+ },
+ {
+ "foo": 1
+ }
+ ]
+ JSON
+ state = JSON::State.new
+ state.generate([bad_to_json])
+ assert_equal 0, state.depth
+ end
+
+ def test_depth_restored_bad_to_json_in_Hash
+ assert_equal <<~JSON.chomp, JSON.pretty_generate(a: bad_to_json, b: bad_to_json)
+ {
+ "a": {
+ "foo": 1
+ },
+ "b": {
+ "foo": 1
+ }
+ }
+ JSON
+ state = JSON::State.new
+ state.generate(a: bad_to_json)
+ assert_equal 0, state.depth
+ end
+
def test_depth
+ pretty = { object_nl: "\n", array_nl: "\n", space: " ", indent: " " }
+ state = JSON.state.new(**pretty)
+ assert_equal %({\n "foo": 42\n}), JSON.generate({ foo: 42 }, pretty)
+ assert_equal %({\n "foo": 42\n}), state.generate(foo: 42)
+ state.depth = 1
+ assert_equal %({\n "foo": 42\n }), JSON.generate({ foo: 42 }, pretty.merge(depth: 1))
+ assert_equal %({\n "foo": 42\n }), state.generate(foo: 42)
+ end
+
+ def test_depth_nesting_error
ary = []; ary << ary
assert_raise(JSON::NestingError) { generate(ary) }
assert_raise(JSON::NestingError) { JSON.pretty_generate(ary) }
- s = JSON.state.new
- assert_equal 0, s.depth
+ end
+
+ def test_depth_nesting_error_to_json
+ ary = []; ary << ary
+ s = JSON.state.new(depth: 1)
assert_raise(JSON::NestingError) { ary.to_json(s) }
- assert_equal 100, s.depth
+ assert_equal 1, s.depth
+ end
+
+ def test_depth_nesting_error_Hash_to_json
+ hash = {}; hash[:a] = hash
+ s = JSON.state.new(depth: 1)
+ assert_raise(JSON::NestingError) { hash.to_json(s) }
+ assert_equal 1, s.depth
+ end
+
+ def test_depth_nesting_error_generate
+ ary = []; ary << ary
+ s = JSON.state.new(depth: 1)
+ assert_raise(JSON::NestingError) { s.generate(ary) }
+ assert_equal 1, s.depth
+ end
+
+ def test_depth_exception_calling_to_json
+ def (obj = Object.new).to_json(*)
+ raise
+ end
+ s = JSON.state.new(depth: 1).freeze
+ assert_raise(RuntimeError) { s.generate([{ hash: obj }]) }
+ assert_equal 1, s.depth
end
def test_buffer_initial_length
@@ -341,28 +505,32 @@ class JSONGeneratorTest < Test::Unit::TestCase
end
def test_hash_likeness_set_symbol
- state = JSON.state.new
- assert_equal nil, state[:foo]
- assert_equal nil.class, state[:foo].class
- assert_equal nil, state['foo']
- state[:foo] = :bar
- assert_equal :bar, state[:foo]
- assert_equal :bar, state['foo']
- state_hash = state.to_hash
- assert_kind_of Hash, state_hash
- assert_equal :bar, state_hash[:foo]
+ assert_deprecated_warning(/JSON::State/) do
+ state = JSON.state.new
+ assert_equal nil, state[:foo]
+ assert_equal nil.class, state[:foo].class
+ assert_equal nil, state['foo']
+ state[:foo] = :bar
+ assert_equal :bar, state[:foo]
+ assert_equal :bar, state['foo']
+ state_hash = state.to_hash
+ assert_kind_of Hash, state_hash
+ assert_equal :bar, state_hash[:foo]
+ end
end
def test_hash_likeness_set_string
- state = JSON.state.new
- assert_equal nil, state[:foo]
- assert_equal nil, state['foo']
- state['foo'] = :bar
- assert_equal :bar, state[:foo]
- assert_equal :bar, state['foo']
- state_hash = state.to_hash
- assert_kind_of Hash, state_hash
- assert_equal :bar, state_hash[:foo]
+ assert_deprecated_warning(/JSON::State/) do
+ state = JSON.state.new
+ assert_equal nil, state[:foo]
+ assert_equal nil, state['foo']
+ state['foo'] = :bar
+ assert_equal :bar, state[:foo]
+ assert_equal :bar, state['foo']
+ state_hash = state.to_hash
+ assert_kind_of Hash, state_hash
+ assert_equal :bar, state_hash[:foo]
+ end
end
def test_json_state_to_h_roundtrip
@@ -388,6 +556,18 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_raise JSON::GeneratorError do
generate(Object.new, strict: true)
end
+
+ assert_raise JSON::GeneratorError do
+ generate([Object.new], strict: true)
+ end
+
+ assert_raise JSON::GeneratorError do
+ generate({ "key" => Object.new }, strict: true)
+ end
+
+ assert_raise JSON::GeneratorError do
+ generate({ Object.new => "value" }, strict: true)
+ end
end
def test_nesting
@@ -410,18 +590,34 @@ class JSONGeneratorTest < Test::Unit::TestCase
json = '["\\\\.(?i:gif|jpe?g|png)$"]'
assert_equal json, generate(data)
#
- data = [ '\\"' ]
- json = '["\\\\\""]'
+ data = [ '\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$\\.(?i:gif|jpe?g|png)$' ]
+ json = '["\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$\\\\.(?i:gif|jpe?g|png)$"]'
+ assert_equal json, generate(data)
+ #
+ data = [ '\\"\\"\\"\\"\\"\\"\\"\\"\\"\\"\\"' ]
+ json = '["\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\"\\\\\""]'
assert_equal json, generate(data)
#
data = [ '/' ]
json = '["/"]'
assert_equal json, generate(data)
#
+ data = [ '////////////////////////////////////////////////////////////////////////////////////' ]
+ json = '["////////////////////////////////////////////////////////////////////////////////////"]'
+ assert_equal json, generate(data)
+ #
data = [ '/' ]
json = '["\/"]'
assert_equal json, generate(data, :script_safe => true)
#
+ data = [ '///////////' ]
+ json = '["\/\/\/\/\/\/\/\/\/\/\/"]'
+ assert_equal json, generate(data, :script_safe => true)
+ #
+ data = [ '///////////////////////////////////////////////////////' ]
+ json = '["\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/"]'
+ assert_equal json, generate(data, :script_safe => true)
+ #
data = [ "\u2028\u2029" ]
json = '["\u2028\u2029"]'
assert_equal json, generate(data, :script_safe => true)
@@ -438,6 +634,38 @@ class JSONGeneratorTest < Test::Unit::TestCase
json = '["\""]'
assert_equal json, generate(data)
#
+ data = ['"""""""""""""""""""""""""']
+ json = '["\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\""]'
+ assert_equal json, generate(data)
+ #
+ data = '"""""'
+ json = '"\"\"\"\"\""'
+ assert_equal json, generate(data)
+ #
+ data = "abc\n"
+ json = '"abc\\n"'
+ assert_equal json, generate(data)
+ #
+ data = "\nabc"
+ json = '"\\nabc"'
+ assert_equal json, generate(data)
+ #
+ data = "\n"
+ json = '"\\n"'
+ assert_equal json, generate(data)
+ #
+ (0..16).each do |i|
+ data = ('a' * i) + "\n"
+ json = '"' + ('a' * i) + '\\n"'
+ assert_equal json, generate(data)
+ end
+ #
+ (0..16).each do |i|
+ data = "\n" + ('a' * i)
+ json = '"' + '\\n' + ('a' * i) + '"'
+ assert_equal json, generate(data)
+ end
+ #
data = ["'"]
json = '["\\\'"]'
assert_equal '["\'"]', generate(data)
@@ -445,6 +673,72 @@ class JSONGeneratorTest < Test::Unit::TestCase
data = ["倩", "瀨"]
json = '["倩","瀨"]'
assert_equal json, generate(data, script_safe: true)
+ #
+ data = '["This is a "test" of the emergency broadcast system."]'
+ json = "\"[\\\"This is a \\\"test\\\" of the emergency broadcast system.\\\"]\""
+ assert_equal json, generate(data)
+ #
+ data = '\tThis is a test of the emergency broadcast system.'
+ json = "\"\\\\tThis is a test of the emergency broadcast system.\""
+ assert_equal json, generate(data)
+ #
+ data = 'This\tis a test of the emergency broadcast system.'
+ json = "\"This\\\\tis a test of the emergency broadcast system.\""
+ assert_equal json, generate(data)
+ #
+ data = 'This is\ta test of the emergency broadcast system.'
+ json = "\"This is\\\\ta test of the emergency broadcast system.\""
+ assert_equal json, generate(data)
+ #
+ data = 'This is a test of the emergency broadcast\tsystem.'
+ json = "\"This is a test of the emergency broadcast\\\\tsystem.\""
+ assert_equal json, generate(data)
+ #
+ data = 'This is a test of the emergency broadcast\tsystem.\n'
+ json = "\"This is a test of the emergency broadcast\\\\tsystem.\\\\n\""
+ assert_equal json, generate(data)
+ data = '"' * 15
+ json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\""
+ assert_equal json, generate(data)
+ data = "\"\"\"\"\"\"\"\"\"\"\"\"\"\"a"
+ json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"a\""
+ assert_equal json, generate(data)
+ data = "\u0001\u0001\u0001\u0001"
+ json = "\"\\u0001\\u0001\\u0001\\u0001\""
+ assert_equal json, generate(data)
+ data = "\u0001a\u0001a\u0001a\u0001a"
+ json = "\"\\u0001a\\u0001a\\u0001a\\u0001a\""
+ assert_equal json, generate(data)
+ data = "\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\""
+ assert_equal json, generate(data)
+ data = "\u0001aa\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\\u0001aa\""
+ assert_equal json, generate(data)
+ data = "\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\""
+ assert_equal json, generate(data)
+ data = "\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002"
+ json = "\"\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\""
+ assert_equal json, generate(data)
+ data = "ab\u0002c"
+ json = "\"ab\\u0002c\""
+ assert_equal json, generate(data)
+ data = "ab\u0002cab\u0002cab\u0002cab\u0002c"
+ json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
+ assert_equal json, generate(data)
+ data = "ab\u0002cab\u0002cab\u0002cab\u0002cab\u0002cab\u0002c"
+ json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
+ assert_equal json, generate(data)
+ data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f"
+ json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\""
+ assert_equal json, generate(data)
+ data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f\b"
+ json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\""
+ assert_equal json, generate(data)
+ data = "a\n\t\f\b\n\t\f\b\n\t\f\b\n\t"
+ json = "\"a\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\""
+ assert_equal json, generate(data)
end
def test_string_subclass
@@ -605,6 +899,22 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal '{"JSONGeneratorTest::StringWithToS#to_s":1}', JSON.generate(StringWithToS.new => 1)
end
+ def test_string_subclass_with_broken_to_s
+ klass = Class.new(String) do
+ def to_s
+ false
+ end
+ end
+ s = klass.new("test")
+ assert_equal '["test"]', JSON.generate([s])
+
+ omit("Can't figure out how to match behavior in java code") if RUBY_PLATFORM == "java"
+
+ assert_raise TypeError do
+ JSON.generate(s => 1)
+ end
+ end
+
if defined?(JSON::Ext::Generator) and RUBY_PLATFORM != "java"
def test_valid_utf8_in_different_encoding
utf8_string = "€™"
@@ -662,16 +972,53 @@ class JSONGeneratorTest < Test::Unit::TestCase
def test_json_generate_as_json_convert_to_proc
object = Object.new
- assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id)
+ assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: -> (o, is_key) { o.object_id })
+ end
+
+ def test_as_json_nan_does_not_call_to_json
+ def (obj = Object.new).to_json(*)
+ "null"
+ end
+ assert_raise(JSON::GeneratorError) do
+ JSON.generate(Float::NAN, strict: true, as_json: proc { obj })
+ end
+ end
+
+ def assert_float_roundtrip(expected, actual)
+ assert_equal(expected, JSON.generate(actual))
+ assert_equal(actual, JSON.parse(JSON.generate(actual)), "JSON: #{JSON.generate(actual)}")
end
def test_json_generate_float
- values = [-1.0, 1.0, 0.0, 12.2, 7.5 / 3.2, 12.0, 100.0, 1000.0]
- expecteds = ["-1.0", "1.0", "0.0", "12.2", "2.34375", "12.0", "100.0", "1000.0"]
+ assert_float_roundtrip "-1.0", -1.0
+ assert_float_roundtrip "1.0", 1.0
+ assert_float_roundtrip "0.0", 0.0
+ assert_float_roundtrip "12.2", 12.2
+ assert_float_roundtrip "2.34375", 7.5 / 3.2
+ assert_float_roundtrip "12.0", 12.0
+ assert_float_roundtrip "100.0", 100.0
+ assert_float_roundtrip "1000.0", 1000.0
+
+ if RUBY_ENGINE == "jruby"
+ assert_float_roundtrip "1.7468619377842371E9", 1746861937.7842371
+ else
+ assert_float_roundtrip "1746861937.7842371", 1746861937.7842371
+ end
- values.zip(expecteds).each do |value, expected|
- assert_equal expected, value.to_json
- end
+ if RUBY_ENGINE == "ruby"
+ assert_float_roundtrip "100000000000000.0", 100000000000000.0
+ assert_float_roundtrip "1e+15", 1e+15
+ assert_float_roundtrip "-100000000000000.0", -100000000000000.0
+ assert_float_roundtrip "-1e+15", -1e+15
+ assert_float_roundtrip "1111111111111111.1", 1111111111111111.1
+ assert_float_roundtrip "1.1111111111111112e+16", 11111111111111111.1
+ assert_float_roundtrip "-1111111111111111.1", -1111111111111111.1
+ assert_float_roundtrip "-1.1111111111111112e+16", -11111111111111111.1
+
+ assert_float_roundtrip "-0.000000022471348024634545", -2.2471348024634545e-08
+ assert_float_roundtrip "-0.0000000022471348024634545", -2.2471348024634545e-09
+ assert_float_roundtrip "-2.2471348024634546e-10", -2.2471348024634545e-10
+ end
end
def test_numbers_of_various_sizes
@@ -685,4 +1032,48 @@ class JSONGeneratorTest < Test::Unit::TestCase
assert_equal "[#{number}]", JSON.generate([number])
end
end
+
+ def test_generate_duplicate_keys_allowed
+ hash = { foo: 1, "foo" => 2 }
+ assert_equal %({"foo":1,"foo":2}), JSON.generate(hash, allow_duplicate_key: true)
+ end
+
+ def test_generate_duplicate_keys_deprecated
+ hash = { foo: 1, "foo" => 2 }
+ assert_deprecated_warning(/allow_duplicate_key/) do
+ assert_equal %({"foo":1,"foo":2}), JSON.generate(hash)
+ end
+ end
+
+ def test_generate_duplicate_keys_disallowed
+ hash = { foo: 1, "foo" => 2 }
+ error = assert_raise JSON::GeneratorError do
+ JSON.generate(hash, allow_duplicate_key: false)
+ end
+ assert_equal %(detected duplicate key "foo" in #{hash.inspect}), error.message
+ end
+
+ def test_frozen
+ state = JSON::State.new.freeze
+ assert_raise(FrozenError) do
+ state.configure(max_nesting: 1)
+ end
+ setters = state.methods.grep(/\w=$/)
+ assert_not_empty setters
+ setters.each do |setter|
+ assert_raise(FrozenError) do
+ state.send(setter, 1)
+ end
+ end
+ end
+
+ # The case when the State is frozen is tested in JSONCoderTest#test_nesting_recovery
+ def test_nesting_recovery
+ state = JSON::State.new
+ ary = []
+ ary << ary
+ assert_raise(JSON::NestingError) { state.generate(ary) }
+ assert_equal 0, state.depth
+ assert_equal '{"a":1}', state.generate({ a: 1 })
+ end
end
diff --git a/test/json/json_generic_object_test.rb b/test/json/json_generic_object_test.rb
index 471534192e..57e3bf3c52 100644
--- a/test/json/json_generic_object_test.rb
+++ b/test/json/json_generic_object_test.rb
@@ -1,10 +1,16 @@
# frozen_string_literal: true
require_relative 'test_helper'
-class JSONGenericObjectTest < Test::Unit::TestCase
+# ostruct is required to test JSON::GenericObject
+begin
+ require "ostruct"
+rescue LoadError
+ return
+end
+class JSONGenericObjectTest < Test::Unit::TestCase
def setup
- if defined?(GenericObject)
+ if defined?(JSON::GenericObject)
@go = JSON::GenericObject[ :a => 1, :b => 2 ]
else
omit("JSON::GenericObject is not available")
@@ -40,10 +46,10 @@ class JSONGenericObjectTest < Test::Unit::TestCase
)
assert_equal 1, l.a
assert_equal @go,
- l = JSON('{ "a": 1, "b": 2 }', :object_class => GenericObject)
+ l = JSON('{ "a": 1, "b": 2 }', :object_class => JSON::GenericObject)
assert_equal 1, l.a
- assert_equal GenericObject[:a => GenericObject[:b => 2]],
- l = JSON('{ "a": { "b": 2 } }', :object_class => GenericObject)
+ assert_equal JSON::GenericObject[:a => JSON::GenericObject[:b => 2]],
+ l = JSON('{ "a": { "b": 2 } }', :object_class => JSON::GenericObject)
assert_equal 2, l.a.b
end
end
@@ -51,12 +57,12 @@ class JSONGenericObjectTest < Test::Unit::TestCase
def test_from_hash
result = JSON::GenericObject.from_hash(
:foo => { :bar => { :baz => true }, :quux => [ { :foobar => true } ] })
- assert_kind_of GenericObject, result.foo
- assert_kind_of GenericObject, result.foo.bar
+ assert_kind_of JSON::GenericObject, result.foo
+ assert_kind_of JSON::GenericObject, result.foo.bar
assert_equal true, result.foo.bar.baz
- assert_kind_of GenericObject, result.foo.quux.first
+ assert_kind_of JSON::GenericObject, result.foo.quux.first
assert_equal true, result.foo.quux.first.foobar
- assert_equal true, GenericObject.from_hash(true)
+ assert_equal true, JSON::GenericObject.from_hash(true)
end
def test_json_generic_object_load
diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb
index 87b78fb0ca..ac53ba9f0c 100644
--- a/test/json/json_parser_test.rb
+++ b/test/json/json_parser_test.rb
@@ -128,6 +128,13 @@ class JSONParserTest < Test::Unit::TestCase
assert_equal(1.0/0, parse('Infinity', :allow_nan => true))
assert_raise(ParserError) { parse('-Infinity') }
assert_equal(-1.0/0, parse('-Infinity', :allow_nan => true))
+ capture_output { assert_equal(Float::INFINITY, parse("23456789012E666")) }
+ end
+
+ def test_parse_bignum
+ bignum = Integer('1234567890' * 10)
+ assert_equal(bignum, JSON.parse(bignum.to_s))
+ assert_equal(bignum.to_f, JSON.parse(bignum.to_s + ".0"))
end
def test_parse_bigdecimals
@@ -157,6 +164,20 @@ class JSONParserTest < Test::Unit::TestCase
end
end
+ def test_parse_control_chars_in_string
+ 0.upto(31) do |ord|
+ assert_raise JSON::ParserError do
+ parse(%("#{ord.chr}"))
+ end
+ end
+ end
+
+ def test_parse_allowed_control_chars_in_string
+ 0.upto(31) do |ord|
+ assert_equal ord.chr, parse(%("#{ord.chr}"), allow_control_characters: true)
+ end
+ end
+
def test_parse_arrays
assert_equal([1,2,3], parse('[1,2,3]'))
assert_equal([1.2,2,3], parse('[1.2,2,3]'))
@@ -318,6 +339,20 @@ class JSONParserTest < Test::Unit::TestCase
assert_raise(JSON::ParserError) { parse('"\u111___"') }
end
+ def test_unicode_followed_by_newline
+ # Ref: https://github.com/ruby/json/issues/912
+ assert_equal "🌌\n".bytes, JSON.parse('"\ud83c\udf0c\n"').bytes
+ assert_equal "🌌\n", JSON.parse('"\ud83c\udf0c\n"')
+ assert_predicate JSON.parse('"\ud83c\udf0c\n"'), :valid_encoding?
+ end
+
+ def test_invalid_surogates
+ assert_raise(JSON::ParserError) { parse('"\\uD800"') }
+ assert_raise(JSON::ParserError) { parse('"\\uD800_________________"') }
+ assert_raise(JSON::ParserError) { parse('"\\uD800\\u0041"') }
+ assert_raise(JSON::ParserError) { parse('"\\uD800\\u004') }
+ end
+
def test_parse_big_integers
json1 = JSON(orig = (1 << 31) - 1)
assert_equal orig, parse(json1)
@@ -331,6 +366,52 @@ class JSONParserTest < Test::Unit::TestCase
assert_equal orig, parse(json5)
end
+ def test_parse_escaped_key
+ doc = {
+ "test\r1" => 1,
+ "entries" => [
+ "test\t2" => 2,
+ "test\n3" => 3,
+ ]
+ }
+
+ assert_equal doc, parse(JSON.generate(doc))
+ end
+
+ def test_parse_duplicate_key
+ expected = {"a" => 2}
+ expected_sym = {a: 2}
+
+ assert_equal expected, parse('{"a": 1, "a": 2}', allow_duplicate_key: true)
+ assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false) }
+ assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false, symbolize_names: true) }
+
+ assert_deprecated_warning(/duplicate key "a"/) do
+ assert_equal expected, parse('{"a": 1, "a": 2}')
+ end
+ assert_deprecated_warning(/duplicate key "a"/) do
+ assert_equal expected_sym, parse('{"a": 1, "a": 2}', symbolize_names: true)
+ end
+
+ if RUBY_ENGINE == 'ruby'
+ assert_deprecated_warning(/#{File.basename(__FILE__)}\:#{__LINE__ + 1}/) do
+ assert_equal expected, parse('{"a": 1, "a": 2}')
+ end
+ end
+
+ unless RUBY_ENGINE == 'jruby'
+ assert_raise(ParserError) do
+ fake_key = Object.new
+ JSON.load('{"a": 1, "a": 2}', -> (obj) { obj == "a" ? fake_key : obj }, allow_duplicate_key: false)
+ end
+
+ assert_deprecated_warning(/duplicate key #<Object:0x/) do
+ fake_key = Object.new
+ JSON.load('{"a": 1, "a": 2}', -> (obj) { obj == "a" ? fake_key : obj })
+ end
+ end
+ end
+
def test_some_wrong_inputs
assert_raise(ParserError) { parse('[] bla') }
assert_raise(ParserError) { parse('[] 1') }
@@ -362,10 +443,8 @@ class JSONParserTest < Test::Unit::TestCase
assert_predicate parse('[]', :freeze => true), :frozen?
assert_predicate parse('"foo"', :freeze => true), :frozen?
- if string_deduplication_available?
- assert_same(-'foo', parse('"foo"', :freeze => true))
- assert_same(-'foo', parse('{"foo": 1}', :freeze => true).keys.first)
- end
+ assert_same(-'foo', parse('"foo"', :freeze => true))
+ assert_same(-'foo', parse('{"foo": 1}', :freeze => true).keys.first)
end
def test_parse_comments
@@ -453,13 +532,97 @@ class JSONParserTest < Test::Unit::TestCase
data = ['"']
assert_equal data, parse(json)
#
- json = '["\\\'"]'
- data = ["'"]
+ json = '["\\/"]'
+ data = ["/"]
assert_equal data, parse(json)
json = '["\/"]'
data = [ '/' ]
assert_equal data, parse(json)
+
+ data = ['"""""""""""""""""""""""""']
+ json = '["\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\""]'
+ assert_equal data, parse(json)
+
+ data = '["This is a "test" of the emergency broadcast system."]'
+ json = "\"[\\\"This is a \\\"test\\\" of the emergency broadcast system.\\\"]\""
+ assert_equal data, parse(json)
+
+ data = '\tThis is a test of the emergency broadcast system.'
+ json = "\"\\\\tThis is a test of the emergency broadcast system.\""
+ assert_equal data, parse(json)
+
+ data = 'This\tis a test of the emergency broadcast system.'
+ json = "\"This\\\\tis a test of the emergency broadcast system.\""
+ assert_equal data, parse(json)
+
+ data = 'This is\ta test of the emergency broadcast system.'
+ json = "\"This is\\\\ta test of the emergency broadcast system.\""
+ assert_equal data, parse(json)
+
+ data = 'This is a test of the emergency broadcast\tsystem.'
+ json = "\"This is a test of the emergency broadcast\\\\tsystem.\""
+ assert_equal data, parse(json)
+
+ data = 'This is a test of the emergency broadcast\tsystem.\n'
+ json = "\"This is a test of the emergency broadcast\\\\tsystem.\\\\n\""
+ assert_equal data, parse(json)
+
+ data = '"' * 15
+ json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\""
+ assert_equal data, parse(json)
+
+ data = "\"\"\"\"\"\"\"\"\"\"\"\"\"\"a"
+ json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"a\""
+ assert_equal data, parse(json)
+
+ data = "\u0001\u0001\u0001\u0001"
+ json = "\"\\u0001\\u0001\\u0001\\u0001\""
+ assert_equal data, parse(json)
+
+ data = "\u0001a\u0001a\u0001a\u0001a"
+ json = "\"\\u0001a\\u0001a\\u0001a\\u0001a\""
+ assert_equal data, parse(json)
+
+ data = "\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\""
+ assert_equal data, parse(json)
+
+ data = "\u0001aa\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\\u0001aa\""
+ assert_equal data, parse(json)
+
+ data = "\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa"
+ json = "\"\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\""
+ assert_equal data, parse(json)
+
+ data = "\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002"
+ json = "\"\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\""
+ assert_equal data, parse(json)
+
+ data = "ab\u0002c"
+ json = "\"ab\\u0002c\""
+ assert_equal data, parse(json)
+
+ data = "ab\u0002cab\u0002cab\u0002cab\u0002c"
+ json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
+ assert_equal data, parse(json)
+
+ data = "ab\u0002cab\u0002cab\u0002cab\u0002cab\u0002cab\u0002c"
+ json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
+ assert_equal data, parse(json)
+
+ data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f"
+ json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\""
+ assert_equal data, parse(json)
+
+ data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f\b"
+ json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\""
+ assert_equal data, parse(json)
+
+ data = "a\n\t\f\b\n\t\f\b\n\t\f\b\n\t"
+ json = "\"a\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\""
+ assert_equal data, parse(json)
end
class SubArray < Array
@@ -518,6 +681,7 @@ class JSONParserTest < Test::Unit::TestCase
def test_parse_array_custom_non_array_derived_class
res = parse('[1,2]', :array_class => SubArrayWrapper)
assert_equal([1,2], res.data)
+ assert_equal(1, res[0])
assert_equal(SubArrayWrapper, res.class)
assert res.shifted?
end
@@ -579,6 +743,7 @@ class JSONParserTest < Test::Unit::TestCase
def test_parse_object_custom_non_hash_derived_class
res = parse('{"foo":"bar"}', :object_class => SubOpenStruct)
assert_equal "bar", res.foo
+ assert_equal "bar", res[:foo]
assert_equal(SubOpenStruct, res.class)
assert res.item_set?
end
@@ -638,7 +803,7 @@ class JSONParserTest < Test::Unit::TestCase
error = assert_raise(JSON::ParserError) do
JSON.parse('{"foo": ' + ('A' * 500) + '}')
end
- assert_operator 60, :>, error.message.bytesize
+ assert_operator 80, :>, error.message.bytesize
end
def test_parse_error_incomplete_hash
@@ -646,7 +811,7 @@ class JSONParserTest < Test::Unit::TestCase
JSON.parse('{"input":{"firstName":"Bob","lastName":"Mob","email":"bob@example.com"}')
end
if RUBY_ENGINE == "ruby"
- assert_equal %(expected ',' or '}' after object value, got: ''), error.message
+ assert_equal %(expected ',' or '}' after object value, got: EOF at line 1 column 72), error.message
end
end
@@ -654,16 +819,16 @@ class JSONParserTest < Test::Unit::TestCase
omit "C ext only test" unless RUBY_ENGINE == "ruby"
error = assert_raise(JSON::ParserError) { JSON.parse("あああああああああああああああああああああああ") }
- assert_equal "unexpected character: 'ああああああああああ'", error.message
+ assert_equal "unexpected character: 'ああああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("aあああああああああああああああああああああああ") }
- assert_equal "unexpected character: 'aああああああああああ'", error.message
+ assert_equal "unexpected character: 'aああああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("abあああああああああああああああああああああああ") }
- assert_equal "unexpected character: 'abあああああああああ'", error.message
+ assert_equal "unexpected character: 'abあああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("abcあああああああああああああああああああああああ") }
- assert_equal "unexpected character: 'abcあああああああああ'", error.message
+ assert_equal "unexpected character: 'abcあああああああああ' at line 1 column 1", error.message
end
def test_parse_leading_slash
@@ -673,18 +838,19 @@ class JSONParserTest < Test::Unit::TestCase
end
end
- private
+ def test_parse_whitespace_after_newline
+ assert_equal [], JSON.parse("[\n#{' ' * (8 + 8 + 4 + 3)}]")
+ end
- def string_deduplication_available?
- r1 = rand.to_s
- r2 = r1.dup
- begin
- (-r1).equal?(-r2)
- rescue NoMethodError
- false # No String#-@
+ def test_frozen
+ parser_config = JSON::Parser::Config.new({}).freeze
+ assert_raise FrozenError do
+ parser_config.send(:initialize, {})
end
end
+ private
+
def assert_equal_float(expected, actual, delta = 1e-2)
Array === expected and expected = expected.first
Array === actual and actual = actual.first
diff --git a/test/json/json_ryu_fallback_test.rb b/test/json/json_ryu_fallback_test.rb
new file mode 100644
index 0000000000..59ba76d392
--- /dev/null
+++ b/test/json/json_ryu_fallback_test.rb
@@ -0,0 +1,169 @@
+# 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
+end
diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb
index f857c9a8bf..e53c405a74 100644
--- a/test/json/ractor_test.rb
+++ b/test/json/ractor_test.rb
@@ -8,8 +8,19 @@ rescue LoadError
end
class JSONInRactorTest < Test::Unit::TestCase
+ unless Ractor.method_defined?(:value)
+ module RactorBackport
+ refine Ractor do
+ alias_method :value, :take
+ end
+ end
+
+ using RactorBackport
+ end
+
def test_generate
pid = fork do
+ Warning[:experimental] = false
r = Ractor.new do
json = JSON.generate({
'a' => 2,
@@ -25,14 +36,14 @@ class JSONInRactorTest < Test::Unit::TestCase
end
expected_json = JSON.parse('{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' +
'"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}')
- actual_json = r.take
+ actual_json = r.value
if expected_json == actual_json
exit 0
else
puts "Expected:"
puts expected_json
- puts "Acutual:"
+ puts "Actual:"
puts actual_json
puts
exit 1
@@ -41,4 +52,63 @@ class JSONInRactorTest < Test::Unit::TestCase
_, status = Process.waitpid2(pid)
assert_predicate status, :success?
end
+
+ def test_coder
+ coder = JSON::Coder.new.freeze
+ assert Ractor.shareable?(coder)
+ pid = fork do
+ Warning[:experimental] = false
+ r = Ractor.new(coder) do |coder|
+ json = coder.dump({
+ 'a' => 2,
+ 'b' => 3.141,
+ 'c' => 'c',
+ 'd' => [ 1, "b", 3.14 ],
+ 'e' => { 'foo' => 'bar' },
+ 'g' => "\"\0\037",
+ 'h' => 1000.0,
+ 'i' => 0.001
+ })
+ coder.load(json)
+ end
+ expected_json = JSON.parse('{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' +
+ '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}')
+ actual_json = r.value
+
+ if expected_json == actual_json
+ exit 0
+ else
+ puts "Expected:"
+ puts expected_json
+ puts "Actual:"
+ puts actual_json
+ puts
+ exit 1
+ end
+ end
+ _, status = Process.waitpid2(pid)
+ assert_predicate status, :success?
+ end
+
+ class NonNative
+ def initialize(value)
+ @value = value
+ end
+ end
+
+ def test_coder_proc
+ block = Ractor.shareable_proc { |value| value.as_json }
+ coder = JSON::Coder.new(&block).freeze
+ assert Ractor.shareable?(coder)
+
+ pid = fork do
+ Warning[:experimental] = false
+ assert_equal [{}], Ractor.new(coder) { |coder|
+ coder.load('[{}]')
+ }.value
+ end
+
+ _, status = Process.waitpid2(pid)
+ assert_predicate status, :success?
+ end if Ractor.respond_to?(:shareable_proc)
end if defined?(Ractor) && Process.respond_to?(:fork)
diff --git a/test/json/test_helper.rb b/test/json/test_helper.rb
index d849e28b9b..24cde4348c 100644
--- a/test/json/test_helper.rb
+++ b/test/json/test_helper.rb
@@ -1,5 +1,29 @@
$LOAD_PATH.unshift(File.expand_path('../../../ext', __FILE__), File.expand_path('../../../lib', __FILE__))
+if ENV["JSON_COVERAGE"]
+ # This test helper is loaded inside Ruby's own test suite, so we try to not mess it up.
+ require 'coverage'
+
+ branches_supported = Coverage.respond_to?(:supported?) && Coverage.supported?(:branches)
+
+ # Coverage module must be started before SimpleCov to work around the cyclic require order.
+ # Track both branches and lines, or else SimpleCov misleadingly reports 0/0 = 100% for non-branching files.
+ Coverage.start(lines: true,
+ branches: branches_supported)
+
+ require 'simplecov'
+ SimpleCov.start do
+ # Enabling both coverage types to let SimpleCov know to output them together in reports
+ enable_coverage :line
+ enable_coverage :branch if branches_supported
+
+ # Can't always trust SimpleCov to find files implicitly
+ track_files 'lib/**/*.rb'
+
+ add_filter 'lib/json/truffle_ruby' unless RUBY_ENGINE == 'truffleruby'
+ end
+end
+
require 'json'
require 'test/unit'