mirror of
https://github.com/ruby/ruby.git
synced 2026-10-28 16:36:20 +08:00
1508 lines
47 KiB
Ruby
Executable File
1508 lines
47 KiB
Ruby
Executable File
# frozen_string_literal: true
|
|
|
|
require_relative 'test_helper'
|
|
|
|
class JSONGeneratorTest < Test::Unit::TestCase
|
|
include JSON
|
|
|
|
def setup
|
|
@hash = {
|
|
'a' => 2,
|
|
'b' => 3.141,
|
|
'c' => 'c',
|
|
'd' => [ 1, "b", 3.14 ],
|
|
'e' => { 'foo' => 'bar' },
|
|
'g' => "\"\0\037",
|
|
'h' => 1000.0,
|
|
'i' => 0.001
|
|
}
|
|
@json2 = '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' +
|
|
'"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}'
|
|
@json3 = <<~'JSON'.chomp
|
|
{
|
|
"a": 2,
|
|
"b": 3.141,
|
|
"c": "c",
|
|
"d": [
|
|
1,
|
|
"b",
|
|
3.14
|
|
],
|
|
"e": {
|
|
"foo": "bar"
|
|
},
|
|
"g": "\"\u0000\u001f",
|
|
"h": 1000.0,
|
|
"i": 0.001
|
|
}
|
|
JSON
|
|
end
|
|
|
|
def test_generate
|
|
json = generate(@hash)
|
|
assert_equal(parse(@json2), parse(json))
|
|
json = JSON[@hash]
|
|
assert_equal(parse(@json2), parse(json))
|
|
parsed_json = parse(json)
|
|
assert_equal(@hash, parsed_json)
|
|
json = generate({1=>2})
|
|
assert_equal('{"1":2}', json)
|
|
parsed_json = parse(json)
|
|
assert_equal({"1"=>2}, parsed_json)
|
|
assert_equal '666', generate(666)
|
|
end
|
|
|
|
def test_dump_unenclosed_hash
|
|
assert_equal '{"a":1,"b":2}', dump(a: 1, b: 2)
|
|
end
|
|
|
|
def test_dump_strict
|
|
assert_equal '{}', dump({}, strict: true)
|
|
|
|
assert_equal '{"array":[42,4.2,"forty-two",true,false,null]}', dump({
|
|
"array" => [42, 4.2, "forty-two", true, false, nil]
|
|
}, strict: true)
|
|
|
|
assert_equal '{"int":42,"float":4.2,"string":"forty-two","true":true,"false":false,"nil":null,"hash":{}}', dump({
|
|
"int" => 42,
|
|
"float" => 4.2,
|
|
"string" => "forty-two",
|
|
"true" => true,
|
|
"false" => false,
|
|
"nil" => nil,
|
|
"hash" => {},
|
|
}, strict: true)
|
|
|
|
assert_equal '[]', dump([], strict: true)
|
|
|
|
assert_equal '42', dump(42, strict: true)
|
|
assert_equal 'true', dump(true, strict: true)
|
|
|
|
assert_equal '"hello"', dump(:hello, strict: true)
|
|
assert_equal '"hello"', :hello.to_json(strict: true)
|
|
assert_equal '"World"', "World".to_json(strict: true)
|
|
assert_equal '["hello"]', dump([:hello], strict: true)
|
|
assert_equal '{"hello":"world"}', dump({ hello: :world }, strict: true)
|
|
end
|
|
|
|
def test_dump_deprecated_limit
|
|
io = StringIO.new
|
|
JSON.dump([1], io, 0)
|
|
assert_equal '[1]', io.string
|
|
end
|
|
|
|
def test_dump_string_mutated_during_io_write
|
|
[{}, {ascii_only: true}, {script_safe: true}, {buffer_initial_length: 1}].each do |options|
|
|
['a', "a\né/"].each do |pattern|
|
|
string = pattern * 100_000
|
|
assert_dump_preserves_string_during_io_write(string, string, options)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_dump_fragment_mutated_during_io_write
|
|
string = JSON.generate('a' * 100_000)
|
|
assert_dump_preserves_string_during_io_write(JSON::Fragment.new(string), string)
|
|
end
|
|
|
|
def test_dump_to_json_result_mutated_during_io_write
|
|
string = JSON.generate('a' * 100_000)
|
|
object = Object.new
|
|
object.define_singleton_method(:to_json) { |*| string }
|
|
assert_dump_preserves_string_during_io_write(object, string)
|
|
end
|
|
|
|
def assert_dump_preserves_string_during_io_write(object, string, options = {})
|
|
expected = JSON.dump([object], options)
|
|
io = StringIO.new
|
|
mutated = false
|
|
io.define_singleton_method(:write) do |chunk|
|
|
unless mutated
|
|
string.setbyte(0, 'b'.ord)
|
|
string.replace('changed')
|
|
mutated = true
|
|
GC.start
|
|
end
|
|
super(chunk)
|
|
end
|
|
assert_same io, JSON.dump([object], io, options)
|
|
assert_equal 'changed', string
|
|
assert_equal true, expected == io.string, 'IO output must preserve the original string'
|
|
end
|
|
|
|
def test_not_frozen
|
|
[
|
|
[[], '[]'],
|
|
[{}, '{}'],
|
|
["string", '"string"'],
|
|
[:sym, '"sym"'],
|
|
[1, '1'],
|
|
[1.0, '1.0'],
|
|
[true, 'true'],
|
|
[false, 'false'],
|
|
[nil, 'null'],
|
|
].each do |(obj, exp)|
|
|
dumped = dump(obj, strict: true)
|
|
assert_equal exp, dumped
|
|
refute_predicate dumped, :frozen?
|
|
end
|
|
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)
|
|
|
|
json = pretty_generate({1=>{}, 2=>[], 3=>4})
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
"1": {},
|
|
"2": [],
|
|
"3": 4
|
|
}
|
|
JSON
|
|
|
|
json = pretty_generate(@hash)
|
|
# hashes aren't (insertion) ordered on every ruby implementation
|
|
# assert_equal(@json3, json)
|
|
assert_equal(parse(@json3), parse(json))
|
|
parsed_json = parse(json)
|
|
assert_equal(@hash, parsed_json)
|
|
json = pretty_generate({1=>2})
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
"1": 2
|
|
}
|
|
JSON
|
|
parsed_json = parse(json)
|
|
assert_equal({"1"=>2}, parsed_json)
|
|
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_sort_keys
|
|
json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: true)
|
|
assert_equal('{"1":"b","2":"a","3":"c"}', json)
|
|
|
|
json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: false)
|
|
assert_equal('{"2":"a","1":"b","3":"c"}', json)
|
|
|
|
json = pretty_generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: true)
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
"1": "b",
|
|
"2": "a",
|
|
"3": "c"
|
|
}
|
|
JSON
|
|
|
|
json = pretty_generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: false)
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
"2": "a",
|
|
"1": "b",
|
|
"3": "c"
|
|
}
|
|
JSON
|
|
|
|
json = pretty_generate({2=>"a", 1=>"b", 3=>"c"})
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
"2": "a",
|
|
"1": "b",
|
|
"3": "c"
|
|
}
|
|
JSON
|
|
end
|
|
|
|
def test_generate_sort_keys_with_proc
|
|
reverse = ->(hash) { hash.sort.reverse.to_h }
|
|
json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: reverse)
|
|
assert_equal('{"3":"c","2":"a","1":"b"}', json)
|
|
|
|
by_value = ->(hash) { hash.sort_by { |_k, v| v }.to_h }
|
|
json = generate({2=>"c", 1=>"a", 3=>"b"}, sort_keys: by_value)
|
|
assert_equal('{"1":"a","3":"b","2":"c"}', json)
|
|
|
|
state = State.new(sort_keys: reverse)
|
|
assert_same reverse, state.to_h[:sort_keys]
|
|
assert_equal('{"3":"c","2":"a","1":"b"}', state.generate({2=>"a", 1=>"b", 3=>"c"}))
|
|
|
|
# A truthy sort_keys is normalized to the default sorting proc.
|
|
state = State.new(sort_keys: true)
|
|
assert_instance_of Proc, state.sort_keys
|
|
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)
|
|
assert_equal(<<~'JSON'.chomp, json)
|
|
{
|
|
<i>"1" : {
|
|
<i><i>"2" : 3,
|
|
<i><i>"4" : [<a_nl><i><i><i>5,<a_nl><i><i><i>6<a_nl><i><i>]
|
|
<i>}
|
|
}
|
|
JSON
|
|
end
|
|
|
|
def test_own_state
|
|
state = State.new
|
|
json = generate(@hash, state)
|
|
assert_equal(parse(@json2), parse(json))
|
|
parsed_json = parse(json)
|
|
assert_equal(@hash, parsed_json)
|
|
json = generate({1=>2}, state)
|
|
assert_equal('{"1":2}', json)
|
|
parsed_json = parse(json)
|
|
assert_equal({"1"=>2}, parsed_json)
|
|
assert_equal '666', generate(666, state)
|
|
end
|
|
|
|
def test_states
|
|
json = generate({1=>2}, nil)
|
|
assert_equal('{"1":2}', json)
|
|
s = JSON.state.new
|
|
h = { 1=>2 }
|
|
h[3] = h
|
|
assert_raise(JSON::NestingError) { generate(h) }
|
|
assert_raise(JSON::NestingError) { generate(h, s) }
|
|
s = JSON.state.new
|
|
a = [ 1, 2 ]
|
|
a << a
|
|
assert_raise(JSON::NestingError) { generate(a, s) }
|
|
end
|
|
|
|
def test_falsy_state
|
|
object = { foo: [1, 2], bar: { egg: :spam }}
|
|
expected_json = JSON.generate(
|
|
object,
|
|
array_nl: "",
|
|
indent: "",
|
|
object_nl: "",
|
|
space: "",
|
|
space_before: "",
|
|
)
|
|
|
|
assert_equal expected_json, JSON.generate(
|
|
object,
|
|
array_nl: nil,
|
|
indent: nil,
|
|
object_nl: nil,
|
|
space: nil,
|
|
space_before: nil,
|
|
)
|
|
end
|
|
|
|
def test_state_defaults
|
|
state = JSON::State.new
|
|
assert_equal({
|
|
allow_duplicate_key: false,
|
|
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_keys: false,
|
|
rfc8785: false,
|
|
}.sort_by { |n,| n.to_s }.to_h, state.to_h.sort_by { |n,| n.to_s }.to_h)
|
|
|
|
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_keys: false,
|
|
rfc8785: false,
|
|
}.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s })
|
|
end
|
|
|
|
def test_allow_nan
|
|
error = assert_raise(GeneratorError) { generate([JSON::NaN]) }
|
|
assert_same JSON::NaN, error.invalid_object
|
|
assert_equal '[NaN]', generate([JSON::NaN], allow_nan: true)
|
|
assert_raise(GeneratorError) { generate([JSON::NaN]) }
|
|
assert_raise(GeneratorError) { pretty_generate([JSON::NaN]) }
|
|
assert_equal "[\n NaN\n]", pretty_generate([JSON::NaN], allow_nan: true)
|
|
error = assert_raise(GeneratorError) { generate([JSON::Infinity]) }
|
|
assert_same JSON::Infinity, error.invalid_object
|
|
assert_equal '[Infinity]', generate([JSON::Infinity], allow_nan: true)
|
|
assert_raise(GeneratorError) { generate([JSON::Infinity]) }
|
|
assert_raise(GeneratorError) { pretty_generate([JSON::Infinity]) }
|
|
assert_equal "[\n Infinity\n]", pretty_generate([JSON::Infinity], allow_nan: true)
|
|
error = assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) }
|
|
assert_same JSON::MinusInfinity, error.invalid_object
|
|
assert_equal '[-Infinity]', generate([JSON::MinusInfinity], allow_nan: true)
|
|
assert_raise(GeneratorError) { generate([JSON::MinusInfinity]) }
|
|
assert_raise(GeneratorError) { pretty_generate([JSON::MinusInfinity]) }
|
|
assert_equal "[\n -Infinity\n]", pretty_generate([JSON::MinusInfinity], allow_nan: true)
|
|
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) }
|
|
end
|
|
|
|
def test_nesting_error_reports_attempted_depth
|
|
[[[]], { a: {} }].each do |object|
|
|
[0, 1].each do |depth|
|
|
state = JSON.state.new(depth: depth, max_nesting: depth + 1)
|
|
error = assert_raise(JSON::NestingError) { state.generate(object) }
|
|
assert_match(/\Anesting of #{depth + 2} is too deep\./, error.message)
|
|
assert_equal depth, state.depth
|
|
assert_equal '[]', state.generate([])
|
|
end
|
|
end
|
|
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 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
|
|
s = JSON.state.new
|
|
assert_equal 1024, s.buffer_initial_length
|
|
s.buffer_initial_length = 0
|
|
assert_equal 1024, s.buffer_initial_length
|
|
s.buffer_initial_length = -1
|
|
assert_equal 1024, s.buffer_initial_length
|
|
s.buffer_initial_length = 128
|
|
assert_equal 128, s.buffer_initial_length
|
|
end
|
|
|
|
def test_gc
|
|
pid = fork do
|
|
bignum_too_long_to_embed_as_string = 1234567890123456789012345
|
|
expect = bignum_too_long_to_embed_as_string.to_s
|
|
GC.stress = true
|
|
|
|
10.times do |i|
|
|
tmp = bignum_too_long_to_embed_as_string.to_json
|
|
raise "#{expect}' is expected, but '#{tmp}'" unless tmp == expect
|
|
end
|
|
end
|
|
_, status = Process.waitpid2(pid)
|
|
assert_predicate status, :success?
|
|
end if GC.respond_to?(:stress=) && Process.respond_to?(:fork)
|
|
|
|
def test_configure_using_configure_and_merge
|
|
numbered_state = {
|
|
indent: "1",
|
|
space: '2',
|
|
space_before: '3',
|
|
object_nl: '4',
|
|
array_nl: '5'
|
|
}
|
|
state1 = JSON.state.new
|
|
state1.merge(numbered_state)
|
|
assert_equal '1', state1.indent
|
|
assert_equal '2', state1.space
|
|
assert_equal '3', state1.space_before
|
|
assert_equal '4', state1.object_nl
|
|
assert_equal '5', state1.array_nl
|
|
state2 = JSON.state.new
|
|
state2.configure(numbered_state)
|
|
assert_equal '1', state2.indent
|
|
assert_equal '2', state2.space
|
|
assert_equal '3', state2.space_before
|
|
assert_equal '4', state2.object_nl
|
|
assert_equal '5', state2.array_nl
|
|
end
|
|
|
|
def test_configure_only_writes_the_string_options_it_is_given
|
|
state = JSON.state.new(indent: '1', space: '2', space_before: '3', object_nl: '4', array_nl: '5')
|
|
state.configure(space: '9')
|
|
assert_equal '1', state.indent
|
|
assert_equal '9', state.space
|
|
assert_equal '3', state.space_before
|
|
assert_equal '4', state.object_nl
|
|
assert_equal '5', state.array_nl
|
|
state.merge(array_nl: '8')
|
|
assert_equal '1', state.indent
|
|
assert_equal '9', state.space
|
|
assert_equal '3', state.space_before
|
|
assert_equal '4', state.object_nl
|
|
assert_equal '8', state.array_nl
|
|
end
|
|
|
|
def test_configure_keeps_the_layout_of_a_pretty_state
|
|
state = JSON.state.new(indent: ' ', object_nl: "\n", array_nl: "\n")
|
|
state.configure(depth: 0)
|
|
assert_equal %({\n "foo":[\n 1\n ]\n}), state.generate({ 'foo' => [1] })
|
|
end
|
|
|
|
def test_configure_only_writes_the_other_options_it_is_given
|
|
state = JSON.state.new(max_nesting: 3, allow_nan: true, ascii_only: true, script_safe: true,
|
|
strict: true, buffer_initial_length: 32)
|
|
state.configure(indent: '1')
|
|
assert_equal '1', state.indent
|
|
assert_equal 3, state.max_nesting
|
|
assert_equal true, state.allow_nan?
|
|
assert_equal true, state.ascii_only?
|
|
assert_equal true, state.script_safe?
|
|
assert_equal true, state.strict?
|
|
assert_equal 32, state.buffer_initial_length
|
|
end
|
|
|
|
def test_configure_keeps_sort_keys
|
|
state = JSON.state.new(sort_keys: true)
|
|
state.configure(depth: 0)
|
|
assert_equal '{"a":2,"b":1}', state.generate({ 'b' => 1, 'a' => 2 })
|
|
end
|
|
|
|
def test_configure_keeps_as_json
|
|
as_json = ->(object, _is_key) { object.to_s }
|
|
state = JSON.state.new(strict: true, as_json: as_json)
|
|
state.configure(depth: 0)
|
|
assert_equal as_json, state.as_json
|
|
end
|
|
|
|
def test_configure_writes_a_string_option_given_as_nil
|
|
state = JSON.state.new(indent: '1', space: '2')
|
|
state.configure(indent: nil)
|
|
assert_equal '', state.indent
|
|
assert_equal '2', state.space
|
|
end
|
|
|
|
def test_configure_hash_conversion
|
|
state = JSON.state.new
|
|
state.configure(indent: '1')
|
|
assert_equal '1', state.indent
|
|
state = JSON.state.new
|
|
foo = 'foo'.dup
|
|
assert_raise(TypeError) do
|
|
state.configure(foo)
|
|
end
|
|
def foo.to_h
|
|
{ indent: '2' }
|
|
end
|
|
state.configure(foo)
|
|
assert_equal '2', state.indent
|
|
end
|
|
|
|
def test_broken_bignum # [Bug #5173]
|
|
bignum = 1 << 64
|
|
bignum_to_s = bignum.to_s
|
|
|
|
original_to_s = bignum.class.instance_method(:to_s)
|
|
bignum.class.class_eval do
|
|
def to_s
|
|
nil
|
|
end
|
|
alias_method :to_s, :to_s
|
|
end
|
|
case RUBY_ENGINE
|
|
when "jruby"
|
|
assert_equal bignum_to_s, JSON.generate(bignum)
|
|
when "truffleruby"
|
|
assert_raise(NoMethodError) do
|
|
JSON.generate(bignum)
|
|
end
|
|
when "ruby"
|
|
assert_raise(TypeError) do
|
|
JSON.generate(bignum)
|
|
end
|
|
end
|
|
ensure
|
|
bignum.class.define_method(:to_s, original_to_s) if original_to_s
|
|
end
|
|
|
|
def test_json_state_to_h_roundtrip
|
|
state = JSON.state.new
|
|
assert_equal state.to_h, JSON.state.new(state.to_h).to_h
|
|
end
|
|
|
|
def test_json_state_to_h_ignores_instance_variables
|
|
state = JSON.state.new(indent: ' ')
|
|
expected = state.to_h
|
|
state.instance_variable_set(:@custom, 42)
|
|
state.instance_variable_set(:@enabled, false)
|
|
|
|
assert_equal expected, state.to_h
|
|
assert_equal expected, state.to_hash
|
|
end
|
|
|
|
def test_json_generate
|
|
assert_raise JSON::GeneratorError do
|
|
generate(["\xea"])
|
|
end
|
|
end
|
|
|
|
def test_json_generate_error_detailed_message
|
|
error = assert_raise JSON::GeneratorError do
|
|
generate(["\xea"])
|
|
end
|
|
|
|
assert_not_nil(error.detailed_message)
|
|
end
|
|
|
|
def test_json_generate_unsupported_types
|
|
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
|
|
too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
|
|
too_deep_ary = eval too_deep
|
|
assert_raise(JSON::NestingError) { generate too_deep_ary }
|
|
assert_raise(JSON::NestingError) { generate too_deep_ary, max_nesting: 100 }
|
|
ok = generate too_deep_ary, max_nesting: 101
|
|
assert_equal too_deep, ok
|
|
ok = generate too_deep_ary, max_nesting: nil
|
|
assert_equal too_deep, ok
|
|
ok = generate too_deep_ary, max_nesting: false
|
|
assert_equal too_deep, ok
|
|
ok = generate too_deep_ary, max_nesting: 0
|
|
assert_equal too_deep, ok
|
|
|
|
assert_raise(TypeError) { generate too_deep_ary, max_nesting: "garbage" }
|
|
end
|
|
|
|
def test_backslash
|
|
data = [ '\\.(?i:gif|jpe?g|png)$' ]
|
|
json = '["\\\\.(?i:gif|jpe?g|png)$"]'
|
|
assert_equal json, generate(data)
|
|
#
|
|
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)
|
|
#
|
|
data = [ "ABC \u2028 DEF \u2029 GHI" ]
|
|
json = '["ABC \u2028 DEF \u2029 GHI"]'
|
|
assert_equal json, generate(data, script_safe: true)
|
|
#
|
|
data = [ "/\u2028\u2029" ]
|
|
json = '["\/\u2028\u2029"]'
|
|
assert_equal json, generate(data, script_safe: true)
|
|
#
|
|
data = ['"']
|
|
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)
|
|
#
|
|
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
|
|
s = Class.new(String) do
|
|
def to_s; self; end
|
|
undef to_json
|
|
end
|
|
assert_nothing_raised(SystemStackError) do
|
|
assert_equal '["foo"]', JSON.generate([s.new('foo')])
|
|
end
|
|
end
|
|
|
|
def test_invalid_encoding_string
|
|
error = assert_raise(JSON::GeneratorError) do
|
|
"\x82\xAC\xEF".to_json
|
|
end
|
|
assert_includes error.message, "source sequence is illegal/malformed utf-8"
|
|
|
|
error = assert_raise(JSON::GeneratorError) do
|
|
JSON.dump("\x82\xAC\xEF")
|
|
end
|
|
assert_includes error.message, "source sequence is illegal/malformed utf-8"
|
|
|
|
assert_raise(JSON::GeneratorError) do
|
|
JSON.dump("\x82\xAC\xEF".b)
|
|
end
|
|
|
|
assert_raise(JSON::GeneratorError) do
|
|
"\x82\xAC\xEF".b.to_json
|
|
end
|
|
|
|
assert_raise(JSON::GeneratorError) do
|
|
["\x82\xAC\xEF".b].to_json
|
|
end
|
|
|
|
badly_encoded = "\x82\xAC\xEF".b
|
|
exception = assert_raise(JSON::GeneratorError) do
|
|
{ foo: badly_encoded }.to_json
|
|
end
|
|
|
|
assert_kind_of EncodingError, exception.cause
|
|
assert_same badly_encoded, exception.invalid_object
|
|
end
|
|
|
|
class MyCustomString < String
|
|
def to_json(_state = nil)
|
|
'"my_custom_key"'
|
|
end
|
|
|
|
def to_s
|
|
self
|
|
end
|
|
end
|
|
|
|
def test_string_subclass_as_keys
|
|
# Ref: https://github.com/ruby/json/issues/667
|
|
# if key.to_s doesn't return a bare string, we call `to_json` on it.
|
|
key = MyCustomString.new("won't be used")
|
|
assert_equal '{"my_custom_key":1}', JSON.generate(key => 1)
|
|
end
|
|
|
|
class FakeString
|
|
def to_json(_state = nil)
|
|
raise "Shouldn't be called"
|
|
end
|
|
|
|
def to_s
|
|
self
|
|
end
|
|
end
|
|
|
|
def test_custom_object_as_keys
|
|
key = FakeString.new
|
|
error = assert_raise(TypeError) do
|
|
JSON.generate(key => 1)
|
|
end
|
|
assert_match "FakeString", error.message
|
|
end
|
|
|
|
def test_to_json_called_with_state_object
|
|
object = Object.new
|
|
called = false
|
|
argument = nil
|
|
object.singleton_class.define_method(:to_json) do |state|
|
|
called = true
|
|
argument = state
|
|
"<hello>"
|
|
end
|
|
|
|
assert_equal "<hello>", JSON.dump(object)
|
|
assert called, "#to_json wasn't called"
|
|
assert_instance_of JSON::State, argument
|
|
end
|
|
|
|
module CustomToJSON
|
|
def to_json(*)
|
|
%{"#{self.class.name}#to_json"}
|
|
end
|
|
end
|
|
|
|
module CustomToS
|
|
def to_s
|
|
"#{self.class.name}#to_s"
|
|
end
|
|
end
|
|
|
|
class ArrayWithToJSON < Array
|
|
include CustomToJSON
|
|
end
|
|
|
|
def test_array_subclass_with_to_json
|
|
assert_equal '["JSONGeneratorTest::ArrayWithToJSON#to_json"]', JSON.generate([ArrayWithToJSON.new])
|
|
assert_equal '{"[]":1}', JSON.generate(ArrayWithToJSON.new => 1)
|
|
end
|
|
|
|
class ArrayWithToS < Array
|
|
include CustomToS
|
|
end
|
|
|
|
def test_array_subclass_with_to_s
|
|
assert_equal '[[]]', JSON.generate([ArrayWithToS.new])
|
|
assert_equal '{"JSONGeneratorTest::ArrayWithToS#to_s":1}', JSON.generate(ArrayWithToS.new => 1)
|
|
end
|
|
|
|
class HashWithToJSON < Hash
|
|
include CustomToJSON
|
|
end
|
|
|
|
def test_hash_subclass_with_to_json
|
|
assert_equal '["JSONGeneratorTest::HashWithToJSON#to_json"]', JSON.generate([HashWithToJSON.new])
|
|
assert_equal '{"{}":1}', JSON.generate(HashWithToJSON.new => 1)
|
|
end
|
|
|
|
class HashWithToS < Hash
|
|
include CustomToS
|
|
end
|
|
|
|
def test_hash_subclass_with_to_s
|
|
assert_equal '[{}]', JSON.generate([HashWithToS.new])
|
|
assert_equal '{"JSONGeneratorTest::HashWithToS#to_s":1}', JSON.generate(HashWithToS.new => 1)
|
|
end
|
|
|
|
class StringWithToJSON < String
|
|
include CustomToJSON
|
|
end
|
|
|
|
def test_string_subclass_with_to_json
|
|
assert_equal '["JSONGeneratorTest::StringWithToJSON#to_json"]', JSON.generate([StringWithToJSON.new])
|
|
assert_equal '{"":1}', JSON.generate(StringWithToJSON.new => 1)
|
|
end
|
|
|
|
class StringWithToS < String
|
|
include CustomToS
|
|
end
|
|
|
|
def test_string_subclass_with_to_s
|
|
assert_equal '[""]', JSON.generate([StringWithToS.new])
|
|
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
|
|
|
|
def test_valid_utf8_in_binary_encoding
|
|
string = "€™".b.freeze
|
|
assert_raise(JSON::GeneratorError) { string.to_json }
|
|
assert_raise(JSON::GeneratorError) { JSON.dump(string) }
|
|
[string, [string], { string => 1 }, { value: string }].each do |object|
|
|
error = assert_raise(JSON::GeneratorError) { JSON.generate(object) }
|
|
assert_same string, error.invalid_object
|
|
assert_kind_of Encoding::UndefinedConversionError, error.cause
|
|
end
|
|
end
|
|
|
|
def test_ascii_in_binary_encoding
|
|
string = "ascii".b
|
|
assert_equal '"ascii"', string.to_json
|
|
assert_equal '"ascii"', JSON.dump(string)
|
|
assert_equal '["ascii"]', JSON.generate([string])
|
|
assert_equal '{"ascii":1}', JSON.generate(string => 1)
|
|
end
|
|
|
|
def test_nonutf8_encoding
|
|
assert_equal("\"5\u{b0}\"", "5\xb0".dup.force_encoding(Encoding::ISO_8859_1).to_json)
|
|
end
|
|
|
|
def test_utf8_multibyte
|
|
assert_equal('["foßbar"]', JSON.generate(["foßbar"]))
|
|
assert_equal('"n€ßt€ð2"', JSON.generate("n€ßt€ð2"))
|
|
assert_equal('"\"\u0000\u001f"', JSON.generate("\"\u0000\u001f"))
|
|
end
|
|
|
|
def test_fragment
|
|
fragment = JSON::Fragment.new(" 42")
|
|
assert_equal '{"number": 42}', JSON.generate({ number: fragment })
|
|
assert_equal '{"number": 42}', JSON.generate({ number: fragment }, strict: true)
|
|
end
|
|
|
|
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: -> (o, is_key) { o.object_id })
|
|
end
|
|
|
|
def test_json_generate_as_json_method
|
|
object = Object.new
|
|
as_json = -> (o, is_key) { o.object_id }.method(:call)
|
|
assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: as_json)
|
|
end
|
|
|
|
def test_state_as_json_method
|
|
object = Object.new
|
|
state = JSON.state.new(strict: true)
|
|
state.as_json = -> (o, is_key) { o.object_id }.method(:call)
|
|
assert_kind_of Proc, state.as_json
|
|
assert_equal object.object_id.to_json, state.generate(object)
|
|
end
|
|
|
|
def test_json_generate_as_json_invalid_type
|
|
[Object.new, Time.now].each do |as_json|
|
|
assert_raise(TypeError) { JSON.generate(Object.new, strict: true, as_json: as_json) }
|
|
end
|
|
end
|
|
|
|
def test_state_as_json_invalid_type
|
|
state = JSON.state.new(strict: true)
|
|
[Object.new, Time.now].each do |as_json|
|
|
assert_raise(TypeError) { state.as_json = as_json }
|
|
assert_raise(TypeError) { state.configure(as_json: as_json) }
|
|
end
|
|
end
|
|
|
|
def test_as_json_to_proc_returns_invalid_type
|
|
as_json = Object.new
|
|
def as_json.to_proc
|
|
method(:to_proc)
|
|
end
|
|
assert_raise(TypeError) { JSON.generate(Object.new, strict: true, as_json: as_json) }
|
|
assert_raise(TypeError) { JSON.state.new.as_json = as_json }
|
|
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
|
|
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
|
|
|
|
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
|
|
numbers = [
|
|
0, 1, -1, 9, -9, 13, -13, 91, -91, 513, -513, 7513, -7513,
|
|
17591, -17591, -4611686018427387904, 4611686018427387903,
|
|
2**62, 2**63, 2**64, -(2**62), -(2**63), -(2**64)
|
|
]
|
|
|
|
numbers.each do |number|
|
|
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_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
|
|
|
|
def test_adversary_encode
|
|
fake_string = Class.new(String) do
|
|
def to_s
|
|
self
|
|
end
|
|
|
|
def encode(*)
|
|
42
|
|
end
|
|
end
|
|
|
|
assert_raise TypeError, JSON::GeneratorError do
|
|
JSON.generate({fake_string.new("\xff".b) => 1})
|
|
end
|
|
|
|
no_to_json = Class.new do
|
|
undef_method :to_json
|
|
|
|
def initialize(str)
|
|
@str = str
|
|
end
|
|
|
|
def to_s
|
|
@str
|
|
end
|
|
end
|
|
|
|
assert_raise TypeError, JSON::GeneratorError do
|
|
JSON.generate([no_to_json.new(fake_string.new("\xff".b))])
|
|
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
|
|
|
|
def test_negative_depth_raises
|
|
assert_raise(ArgumentError) do
|
|
JSON.generate({"a" => 1}, depth: -1)
|
|
end
|
|
assert_raise(ArgumentError) do
|
|
JSON.state.new(depth: -1)
|
|
end
|
|
end
|
|
|
|
def test_large_depth_raises
|
|
assert_raise(RangeError, ArgumentError) do
|
|
JSON.generate([[1]],
|
|
indent: " " * 5,
|
|
array_nl: "\n",
|
|
depth: 3_689_348_814_741_910_324,
|
|
max_nesting: 0
|
|
)
|
|
end
|
|
end
|
|
|
|
def test_rfc8785_key_order
|
|
assert_rfc8785 '{"a":1,"a b":2}', { 'a' => 1, 'a b' => 2 }
|
|
assert_rfc8785 '{"a":2,"a!":1}', { 'a!' => 1, 'a' => 2 }
|
|
assert_rfc8785 '{"\n":1,"A":2}', { "\n" => 1, 'A' => 2 }
|
|
assert_rfc8785 '[{"a":1,"a b":2}]', [{ 'a b' => 2, 'a' => 1 }]
|
|
end
|
|
|
|
def test_rfc8785_mixed_key_order
|
|
assert_rfc8785 '{"10":2,"9":1}', { '9' => 1, 10 => 2 }
|
|
assert_rfc8785 '{"a":1,"b":2}', { a: 1, 'b' => 2 }
|
|
end
|
|
|
|
def test_rfc8785_key_order_with_as_json
|
|
key = Object.new
|
|
def key.to_s
|
|
raise 'must not sort the original key'
|
|
end
|
|
as_json = ->(object, is_key) { is_key ? 'a' : object }
|
|
options = { rfc8785: true, strict: true, as_json: as_json }
|
|
object = [{ 'b' => 2, key => 1 }]
|
|
expected = '[{"a":1,"b":2}]'
|
|
assert_equal expected, JSON.generate(object, options)
|
|
assert_equal expected, object.to_json(options)
|
|
state = JSON::State.new(options)
|
|
io = StringIO.new
|
|
state.generate(object, io)
|
|
assert_equal expected, io.string
|
|
|
|
assert_equal '{"1":1,"b":2}', JSON.generate({ 'b' => 2, 1 => 1 }, options.merge(strict: false))
|
|
end
|
|
|
|
def test_rfc8785_key_order_with_default_external_encoding
|
|
# https://www.rfc-editor.org/rfc/rfc8785.html#section-3.2.3
|
|
hash = { "\u20ac" => 1, "\r" => 2, "\ufb33" => 3, '1' => 4,
|
|
"\u{1f600}" => 5, "\u0080" => 6, "\u00f6" => 7 }
|
|
expected = "{\"\\r\":2,\"1\":4,\"\u0080\":6,\"\u00f6\":7,\"\u20ac\":1,\"\u{1f600}\":5,\"\ufb33\":3}"
|
|
verbose = $VERBOSE
|
|
previous_encoding = Encoding.default_external
|
|
$VERBOSE = nil
|
|
outputs = [Encoding::UTF_8, Encoding::US_ASCII].map do |encoding|
|
|
Encoding.default_external = encoding
|
|
JSON.generate(hash, rfc8785: true)
|
|
end
|
|
assert_equal [expected, expected], outputs
|
|
ensure
|
|
Encoding.default_external = previous_encoding
|
|
$VERBOSE = verbose
|
|
end
|
|
|
|
{
|
|
0 => '0',
|
|
1 => '1',
|
|
2**53 - 1 => '9007199254740991',
|
|
2**53 => '9007199254740992',
|
|
2**53 + 1 => '9007199254740992',
|
|
2**53 + 3 => '9007199254740996',
|
|
2**63 - 1 => '9223372036854776000',
|
|
2**68 => '295147905179352830000',
|
|
10**20 => '100000000000000000000',
|
|
10**21 => '1e+21',
|
|
Float::MAX.to_i => '1.7976931348623157e+308',
|
|
}.each do |integer, expected|
|
|
define_method("test_rfc8785_integer_#{integer}") do
|
|
[1, -1].each do |sign|
|
|
number = sign * integer
|
|
json = sign < 0 && integer != 0 ? "-#{expected}" : expected
|
|
assert_rfc8785 json, number
|
|
assert_rfc8785 %({"n":[#{json}]}), { 'n' => [number] }
|
|
assert_equal json, number.to_json(rfc8785: true)
|
|
assert_equal json, JSON.generate(number, rfc8785: true, max_nesting: false)
|
|
assert_equal json, JSON::Coder.new(rfc8785: true).dump(number)
|
|
io = StringIO.new
|
|
JSON.dump(number, io, rfc8785: true)
|
|
assert_equal json, io.string
|
|
assert_equal number.to_s, JSON.generate(number)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_rfc8785_rejects_out_of_range_integers
|
|
[Float::MAX.to_i + 1, 10**400].each do |integer|
|
|
[integer, -integer].each do |number|
|
|
error = assert_raise(JSON::GeneratorError) { JSON.generate(number, rfc8785: true) }
|
|
assert_same number, error.invalid_object
|
|
assert_equal 'Integer out of range for RFC 8785', error.message
|
|
assert_raise(JSON::GeneratorError) { JSON.dump([number], rfc8785: true) }
|
|
assert_equal number.to_s, JSON.generate(number)
|
|
end
|
|
end
|
|
end
|
|
|
|
{
|
|
indent: ' ', space: ' ', space_before: ' ', object_nl: "\n", array_nl: "\n",
|
|
ascii_only: true, script_safe: true, allow_nan: true,
|
|
}.each do |option, value|
|
|
define_method("test_rfc8785_rejects_#{option}") do
|
|
options = { rfc8785: true, option => value }
|
|
error = assert_raise(ArgumentError) { JSON.generate({ 'a' => [1] }, options) }
|
|
assert_equal "#{option} cannot be used with rfc8785", error.message
|
|
assert_raise(ArgumentError) { JSON.generate(nil, options.to_a.reverse.to_h) }
|
|
assert_raise(ArgumentError) { [1].to_json(options) }
|
|
assert_raise(ArgumentError) { { 'a' => 1 }.to_json(options) }
|
|
assert_raise(ArgumentError) { JSON::Coder.new(**options).dump([1]) }
|
|
|
|
state = JSON::State.new(option => value)
|
|
state.rfc8785 = true
|
|
io = StringIO.new
|
|
assert_raise(ArgumentError) { state.generate([1], io) }
|
|
assert_equal '', io.string
|
|
assert_raise(ArgumentError) { [1].to_json(state) }
|
|
assert_equal 0, state.depth
|
|
assert_raise(ArgumentError) { state.freeze.generate([1]) }
|
|
end
|
|
end
|
|
|
|
def test_rfc8785_accepts_disabled_options
|
|
options = { rfc8785: true, indent: '', space: '', space_before: '',
|
|
object_nl: '', array_nl: '', ascii_only: false,
|
|
script_safe: false, allow_nan: false }
|
|
assert_equal '["é/",1]', JSON.generate(['é/', 1], options)
|
|
end
|
|
|
|
def test_rfc8785_rejects_pretty_generation
|
|
assert_raise(ArgumentError) { JSON.pretty_generate({ 'a' => 1 }, rfc8785: true) }
|
|
end
|
|
|
|
def test_rfc8785_dump_disables_allow_nan_by_default
|
|
assert_equal '[1]', JSON.dump([1], rfc8785: true)
|
|
[Float::NAN, Float::INFINITY, -Float::INFINITY].each do |number|
|
|
assert_raise(JSON::GeneratorError) { JSON.dump([number], rfc8785: true) }
|
|
end
|
|
assert_raise(ArgumentError) { JSON.dump([1], rfc8785: true, allow_nan: true) }
|
|
end
|
|
|
|
def test_rfc8785_fragments
|
|
fragment = JSON::Fragment.new('[1,2]')
|
|
assert_rfc8785 '[1,2]', fragment
|
|
assert_rfc8785 '[[1,2]]', [fragment]
|
|
assert_rfc8785 '{"a":[1,2]}', { 'a' => fragment }
|
|
assert_equal '[[1,2]]', JSON.generate([fragment], rfc8785: true, max_nesting: false)
|
|
assert_equal '[1,2]', JSON::Coder.new(rfc8785: true).dump(fragment)
|
|
assert_equal '[1,2]', fragment.to_json(rfc8785: true)
|
|
|
|
# Fragment contents are the caller's responsibility and are inserted as is.
|
|
assert_rfc8785 '[1, 2]', JSON::Fragment.new('[1, 2]')
|
|
end
|
|
|
|
def test_rfc8785_numbers
|
|
assert_rfc8785 '-9007199254740992', -9007199254740992
|
|
assert_rfc8785 '0', 0
|
|
assert_rfc8785 '0.000001', 0.000001
|
|
assert_rfc8785 '1e+21', 1e+21
|
|
assert_rfc8785 '9.999999999999997e+22', 9.999999999999997e+22
|
|
assert_rfc8785 '9.999999999999997e-7', 9.999999999999997e-7
|
|
assert_rfc8785 '9007199254740992', 9007199254740992
|
|
assert_rfc8785 '9007199254740994', 9007199254740994
|
|
assert_rfc8785 '9007199254740996', 9007199254740996
|
|
assert_rfc8785 '999999999999999700000', 999999999999999700000
|
|
assert_rfc8785 '999999999999999900000', 999999999999999900000
|
|
assert_rfc8785 '333333333.3333333', 333333333.33333329
|
|
assert_rfc8785 '0.30000000000000004', 0.1 + 0.2
|
|
end
|
|
|
|
# https://www.rfc-editor.org/rfc/rfc8785.html#appendix-B
|
|
{
|
|
'0000000000000000' => '0',
|
|
'8000000000000000' => '0',
|
|
'0000000000000001' => '5e-324',
|
|
'8000000000000001' => '-5e-324',
|
|
'7fefffffffffffff' => '1.7976931348623157e+308',
|
|
'ffefffffffffffff' => '-1.7976931348623157e+308',
|
|
'4340000000000000' => '9007199254740992',
|
|
'c340000000000000' => '-9007199254740992',
|
|
'4430000000000000' => '295147905179352830000',
|
|
'7fffffffffffffff' => nil,
|
|
'7ff0000000000000' => nil,
|
|
'44b52d02c7e14af5' => '9.999999999999997e+22',
|
|
'44b52d02c7e14af6' => '1e+23',
|
|
'44b52d02c7e14af7' => '1.0000000000000001e+23',
|
|
'444b1ae4d6e2ef4e' => '999999999999999700000',
|
|
'444b1ae4d6e2ef4f' => '999999999999999900000',
|
|
'444b1ae4d6e2ef50' => '1e+21',
|
|
'3eb0c6f7a0b5ed8c' => '9.999999999999997e-7',
|
|
'3eb0c6f7a0b5ed8d' => '0.000001',
|
|
'41b3de4355555553' => '333333333.3333332',
|
|
'41b3de4355555554' => '333333333.33333325',
|
|
'41b3de4355555555' => '333333333.3333333',
|
|
'41b3de4355555556' => '333333333.3333334',
|
|
'41b3de4355555557' => '333333333.33333343',
|
|
'becbf647612f3696' => '-0.0000033333333333333333',
|
|
'43143ff3c1cb0959' => '1424953923781206.2',
|
|
}.each do |bits, expected|
|
|
define_method("test_rfc8785_number_#{bits}") do
|
|
omit "Float#to_s is not always shortest on this engine" unless RUBY_ENGINE == "ruby"
|
|
number = [bits].pack('H*').unpack('G').first
|
|
if expected
|
|
assert_rfc8785 expected, number
|
|
assert_equal number, JSON.parse(JSON.generate(number, rfc8785: true)).to_f
|
|
else
|
|
assert_raise(JSON::GeneratorError) { JSON.generate(number, rfc8785: true) }
|
|
end
|
|
end
|
|
end
|
|
|
|
fixtures_path = File.expand_path('../fixtures/rfc8785/', __FILE__)
|
|
Dir[File.join(fixtures_path, "input/*.json", __FILE__)].each do |input|
|
|
filename = File.basename(input)
|
|
name, _ = File.basename(filename, ".json")
|
|
expected = File.join(fixtures_path, 'output', filename)
|
|
define_method("test_rfc8785_#{name}") do
|
|
assert_rfc8785(File.read(output), JSON.load_file(input))
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def assert_rfc8785(expected, value)
|
|
assert_equal(expected, JSON.generate(value, rfc8785: true))
|
|
end
|
|
end
|