From 483009dd8a6e9f2a19bce8f77a9541531f4a3eec Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Sun, 20 Dec 2020 12:17:32 +0900 Subject: [PATCH 01/13] [json] Stop using prototype objects --- ext/json/ext/generator/generator.c | 7 ++---- lib/json/common.rb | 21 ++++++++++------ tests/json_generator_test.rb | 39 +++--------------------------- 3 files changed, 18 insertions(+), 49 deletions(-) diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 80d1ca7b..2e802c8e 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -15,8 +15,7 @@ static VALUE mJSON, mExt, mGenerator, cState, mGeneratorMethods, mObject, #endif mFloat, mString, mString_Extend, mTrueClass, mFalseClass, mNilClass, eGeneratorError, - eNestingError, - i_SAFE_STATE_PROTOTYPE; + eNestingError; static ID i_to_s, i_to_json, i_new, i_indent, i_space, i_space_before, i_object_nl, i_array_nl, i_max_nesting, i_allow_nan, i_ascii_only, @@ -1166,8 +1165,7 @@ static VALUE cState_from_state_s(VALUE self, VALUE opts) } else if (rb_obj_is_kind_of(opts, rb_cHash)) { return rb_funcall(self, i_new, 1, opts); } else { - VALUE prototype = rb_const_get(mJSON, i_SAFE_STATE_PROTOTYPE); - return rb_funcall(prototype, i_dup, 0); + return rb_class_new_instance(0, NULL, cState); } } @@ -1608,5 +1606,4 @@ void Init_generator(void) i_encoding = rb_intern("encoding"); i_encode = rb_intern("encode"); #endif - i_SAFE_STATE_PROTOTYPE = rb_intern("SAFE_STATE_PROTOTYPE"); } diff --git a/lib/json/common.rb b/lib/json/common.rb index 111d70c3..c34fa61e 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -71,22 +71,27 @@ def generator=(generator) # :nodoc: end self.state = generator::State const_set :State, self.state - const_set :SAFE_STATE_PROTOTYPE, State.new - const_set :FAST_STATE_PROTOTYPE, State.new( + ensure + $VERBOSE = old + end + + def create_fast_state + State.new( :indent => '', :space => '', :object_nl => "", :array_nl => "", :max_nesting => false ) - const_set :PRETTY_STATE_PROTOTYPE, State.new( + end + + def create_pretty_state + State.new( :indent => ' ', :space => ' ', :object_nl => "\n", :array_nl => "\n" ) - ensure - $VERBOSE = old end # Returns the JSON generator module that is used by JSON. This is @@ -276,7 +281,7 @@ def generate(obj, opts = nil) if State === opts state, opts = opts, nil else - state = SAFE_STATE_PROTOTYPE.dup + state = State.new end if opts if opts.respond_to? :to_hash @@ -315,7 +320,7 @@ def fast_generate(obj, opts = nil) if State === opts state, opts = opts, nil else - state = FAST_STATE_PROTOTYPE.dup + state = JSON.create_fast_state end if opts if opts.respond_to? :to_hash @@ -370,7 +375,7 @@ def pretty_generate(obj, opts = nil) if State === opts state, opts = opts, nil else - state = PRETTY_STATE_PROTOTYPE.dup + state = JSON.create_pretty_state end if opts if opts.respond_to? :to_hash diff --git a/tests/json_generator_test.rb b/tests/json_generator_test.rb index 2ecdc972..5bafc3ea 100644 --- a/tests/json_generator_test.rb +++ b/tests/json_generator_test.rb @@ -48,35 +48,6 @@ def silence $VERBOSE = v end - def test_remove_const_segv - stress = GC.stress - const = JSON::SAFE_STATE_PROTOTYPE.dup - - 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 - - silence do - JSON.const_set :SAFE_STATE_PROTOTYPE, nil - end - - 10.times do |i| - assert_raise TypeError do - bignum_too_long_to_embed_as_string.to_json - end - end - ensure - GC.stress = stress - silence do - JSON.const_set :SAFE_STATE_PROTOTYPE, const - end - end if JSON.const_defined?("Ext") && RUBY_ENGINE != 'jruby' - def test_generate json = generate(@hash) assert_equal(parse(@json2), parse(json)) @@ -171,7 +142,7 @@ def test_states end def test_pretty_state - state = PRETTY_STATE_PROTOTYPE.dup + state = JSON.create_pretty_state assert_equal({ :allow_nan => false, :array_nl => "\n", @@ -188,7 +159,7 @@ def test_pretty_state end def test_safe_state - state = SAFE_STATE_PROTOTYPE.dup + state = JSON::State.new assert_equal({ :allow_nan => false, :array_nl => "", @@ -205,7 +176,7 @@ def test_safe_state end def test_fast_state - state = FAST_STATE_PROTOTYPE.dup + state = JSON.create_fast_state assert_equal({ :allow_nan => false, :array_nl => "", @@ -241,12 +212,8 @@ def test_allow_nan def test_depth ary = []; ary << ary - assert_equal 0, JSON::SAFE_STATE_PROTOTYPE.depth assert_raise(JSON::NestingError) { generate(ary) } - assert_equal 0, JSON::SAFE_STATE_PROTOTYPE.depth - assert_equal 0, JSON::PRETTY_STATE_PROTOTYPE.depth assert_raise(JSON::NestingError) { JSON.pretty_generate(ary) } - assert_equal 0, JSON::PRETTY_STATE_PROTOTYPE.depth s = JSON.state.new assert_equal 0, s.depth assert_raise(JSON::NestingError) { ary.to_json(s) } From 4d6482bb16e1214927c962c363bb765a4954fdc4 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 21 Dec 2020 15:45:50 +0900 Subject: [PATCH 02/13] [json] Make JSON.create_id thread-safe --- lib/json/common.rb | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/lib/json/common.rb b/lib/json/common.rb index c34fa61e..747effee 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -109,7 +109,20 @@ def create_pretty_state # JSON.create_id # => 'json_class' attr_accessor :create_id end - self.create_id = 'json_class' + + DEFAULT_CREATE_ID = 'json_class'.freeze + private_constant :DEFAULT_CREATE_ID + + CREATE_ID_TLS_KEY = "JSON.create_id".freeze + private_constant :CREATE_ID_TLS_KEY + + def self.create_id + Thread.current[CREATE_ID_TLS_KEY] || DEFAULT_CREATE_ID + end + + def self.create_id=(new_value) + Thread.current[CREATE_ID_TLS_KEY] = new_value.dup.freeze + end NaN = 0.0/0 From ae5ef25af52b2b92d7ecf40feeca09c324c0d777 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 21 Dec 2020 15:50:04 +0900 Subject: [PATCH 03/13] [json] JSON_parse_float: Fix how to convert number Stop BigDecimal-specific optimization. Instead, it tries the conversion methods in the following order: 1. `try_convert`, 2. `new`, and 3. class-named function, e.g. `Foo::Bar.Baz` function for `Foo::Bar::Baz` class If all the above candidates are unavailable, it fallbacks to Float. --- ext/json/ext/parser/parser.c | 61 +++++++++++++++++++++-------------- ext/json/ext/parser/parser.rl | 61 +++++++++++++++++++++-------------- 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index aaef53aa..f9972956 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -91,13 +91,12 @@ static int convert_UTF32_to_UTF8(char *buf, UTF32 ch) static VALUE mJSON, mExt, cParser, eParserError, eNestingError; static VALUE CNaN, CInfinity, CMinusInfinity; -static VALUE cBigDecimal = Qundef; static ID i_json_creatable_p, i_json_create, i_create_id, i_create_additions, i_chr, i_max_nesting, i_allow_nan, i_symbolize_names, i_object_class, i_array_class, i_decimal_class, i_key_p, i_deep_const_get, i_match, i_match_string, i_aset, i_aref, - i_leftshift, i_new, i_BigDecimal, i_freeze, i_uminus; + i_leftshift, i_new, i_try_convert, i_freeze, i_uminus; #line 126 "parser.rl" @@ -991,19 +990,6 @@ enum {JSON_float_en_main = 1}; #line 346 "parser.rl" -static int is_bigdecimal_class(VALUE obj) -{ - if (cBigDecimal == Qundef) { - if (rb_const_defined(rb_cObject, i_BigDecimal)) { - cBigDecimal = rb_const_get_at(rb_cObject, i_BigDecimal); - } - else { - return 0; - } - } - return obj == cBigDecimal; -} - static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *result) { int cs = EVIL; @@ -1146,21 +1132,46 @@ case 7: #line 368 "parser.rl" if (cs >= JSON_float_first_final) { + VALUE mod = Qnil; + ID method_id = 0; + if (rb_respond_to(json->decimal_class, i_try_convert)) { + mod = json->decimal_class; + method_id = i_try_convert; + } else if (rb_respond_to(json->decimal_class, i_new)) { + mod = json->decimal_class; + method_id = i_new; + } else if (RB_TYPE_P(json->decimal_class, T_CLASS)) { + VALUE name = rb_class_name(json->decimal_class); + const char *name_cstr = RSTRING_PTR(name); + const char *last_colon = strrchr(name_cstr, ':'); + if (last_colon) { + const char *mod_path_end = last_colon - 1; + VALUE mod_path = rb_str_substr(name, 0, mod_path_end - name_cstr); + mod = rb_path_to_class(mod_path); + + const char *method_name_beg = last_colon + 1; + long before_len = method_name_beg - name_cstr; + long len = RSTRING_LEN(name) - before_len; + VALUE method_name = rb_str_substr(name, before_len, len); + method_id = SYM2ID(rb_str_intern(method_name)); + } else { + mod = rb_mKernel; + method_id = SYM2ID(rb_str_intern(name)); + } + } + long len = p - json->memo; fbuffer_clear(json->fbuffer); fbuffer_append(json->fbuffer, json->memo, len); fbuffer_append_char(json->fbuffer, '\0'); - if (NIL_P(json->decimal_class)) { - *result = rb_float_new(rb_cstr_to_dbl(FBUFFER_PTR(json->fbuffer), 1)); + + if (method_id) { + VALUE text = rb_str_new2(FBUFFER_PTR(json->fbuffer)); + *result = rb_funcallv(mod, method_id, 1, &text); } else { - VALUE text; - text = rb_str_new2(FBUFFER_PTR(json->fbuffer)); - if (is_bigdecimal_class(json->decimal_class)) { - *result = rb_funcall(Qnil, i_BigDecimal, 1, text); - } else { - *result = rb_funcall(json->decimal_class, i_new, 1, text); - } + *result = DBL2NUM(rb_cstr_to_dbl(FBUFFER_PTR(json->fbuffer), 1)); } + return p + 1; } else { return NULL; @@ -2150,7 +2161,7 @@ void Init_parser(void) i_aref = rb_intern("[]"); i_leftshift = rb_intern("<<"); i_new = rb_intern("new"); - i_BigDecimal = rb_intern("BigDecimal"); + i_try_convert = rb_intern("try_convert"); i_freeze = rb_intern("freeze"); i_uminus = rb_intern("-@"); } diff --git a/ext/json/ext/parser/parser.rl b/ext/json/ext/parser/parser.rl index 46290520..24aed600 100644 --- a/ext/json/ext/parser/parser.rl +++ b/ext/json/ext/parser/parser.rl @@ -89,13 +89,12 @@ static int convert_UTF32_to_UTF8(char *buf, UTF32 ch) static VALUE mJSON, mExt, cParser, eParserError, eNestingError; static VALUE CNaN, CInfinity, CMinusInfinity; -static VALUE cBigDecimal = Qundef; static ID i_json_creatable_p, i_json_create, i_create_id, i_create_additions, i_chr, i_max_nesting, i_allow_nan, i_symbolize_names, i_object_class, i_array_class, i_decimal_class, i_key_p, i_deep_const_get, i_match, i_match_string, i_aset, i_aref, - i_leftshift, i_new, i_BigDecimal, i_freeze, i_uminus; + i_leftshift, i_new, i_try_convert, i_freeze, i_uminus; %%{ machine JSON_common; @@ -345,19 +344,6 @@ static char *JSON_parse_integer(JSON_Parser *json, char *p, char *pe, VALUE *res ) (^[0-9Ee.\-]? @exit ); }%% -static int is_bigdecimal_class(VALUE obj) -{ - if (cBigDecimal == Qundef) { - if (rb_const_defined(rb_cObject, i_BigDecimal)) { - cBigDecimal = rb_const_get_at(rb_cObject, i_BigDecimal); - } - else { - return 0; - } - } - return obj == cBigDecimal; -} - static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *result) { int cs = EVIL; @@ -367,21 +353,46 @@ static char *JSON_parse_float(JSON_Parser *json, char *p, char *pe, VALUE *resul %% write exec; if (cs >= JSON_float_first_final) { + VALUE mod = Qnil; + ID method_id = 0; + if (rb_respond_to(json->decimal_class, i_try_convert)) { + mod = json->decimal_class; + method_id = i_try_convert; + } else if (rb_respond_to(json->decimal_class, i_new)) { + mod = json->decimal_class; + method_id = i_new; + } else if (RB_TYPE_P(json->decimal_class, T_CLASS)) { + VALUE name = rb_class_name(json->decimal_class); + const char *name_cstr = RSTRING_PTR(name); + const char *last_colon = strrchr(name_cstr, ':'); + if (last_colon) { + const char *mod_path_end = last_colon - 1; + VALUE mod_path = rb_str_substr(name, 0, mod_path_end - name_cstr); + mod = rb_path_to_class(mod_path); + + const char *method_name_beg = last_colon + 1; + long before_len = method_name_beg - name_cstr; + long len = RSTRING_LEN(name) - before_len; + VALUE method_name = rb_str_substr(name, before_len, len); + method_id = SYM2ID(rb_str_intern(method_name)); + } else { + mod = rb_mKernel; + method_id = SYM2ID(rb_str_intern(name)); + } + } + long len = p - json->memo; fbuffer_clear(json->fbuffer); fbuffer_append(json->fbuffer, json->memo, len); fbuffer_append_char(json->fbuffer, '\0'); - if (NIL_P(json->decimal_class)) { - *result = rb_float_new(rb_cstr_to_dbl(FBUFFER_PTR(json->fbuffer), 1)); + + if (method_id) { + VALUE text = rb_str_new2(FBUFFER_PTR(json->fbuffer)); + *result = rb_funcallv(mod, method_id, 1, &text); } else { - VALUE text; - text = rb_str_new2(FBUFFER_PTR(json->fbuffer)); - if (is_bigdecimal_class(json->decimal_class)) { - *result = rb_funcall(Qnil, i_BigDecimal, 1, text); - } else { - *result = rb_funcall(json->decimal_class, i_new, 1, text); - } + *result = DBL2NUM(rb_cstr_to_dbl(FBUFFER_PTR(json->fbuffer), 1)); } + return p + 1; } else { return NULL; @@ -910,7 +921,7 @@ void Init_parser(void) i_aref = rb_intern("[]"); i_leftshift = rb_intern("<<"); i_new = rb_intern("new"); - i_BigDecimal = rb_intern("BigDecimal"); + i_try_convert = rb_intern("try_convert"); i_freeze = rb_intern("freeze"); i_uminus = rb_intern("-@"); } From f1d5fb030ce758b96e13817290964d92e3516d82 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 21 Dec 2020 15:57:42 +0900 Subject: [PATCH 04/13] [json] Make json Ractor safe --- ext/json/ext/generator/generator.c | 11 +++++++++- ext/json/ext/parser/parser.c | 4 ++++ ext/json/ext/parser/parser.rl | 4 ++++ tests/ractor_test.rb | 34 ++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 tests/ractor_test.rb diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 2e802c8e..407c1af4 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -619,13 +619,18 @@ static size_t State_memsize(const void *ptr) return size; } +#ifndef HAVE_RB_EXT_RACTOR_SAFE +# undef RUBY_TYPED_FROZEN_SHAREABLE +# define RUBY_TYPED_FROZEN_SHAREABLE 0 +#endif + #ifdef NEW_TYPEDDATA_WRAPPER static const rb_data_type_t JSON_Generator_State_type = { "JSON/Generator/State", {NULL, State_free, State_memsize,}, #ifdef RUBY_TYPED_FREE_IMMEDIATELY 0, 0, - RUBY_TYPED_FREE_IMMEDIATELY, + RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE, #endif }; #endif @@ -1497,6 +1502,10 @@ static VALUE cState_buffer_initial_length_set(VALUE self, VALUE buffer_initial_l */ void Init_generator(void) { +#ifdef HAVE_RB_EXT_RACTOR_SAFE + rb_ext_ractor_safe(true); +#endif + #undef rb_intern rb_require("json/common"); diff --git a/ext/json/ext/parser/parser.c b/ext/json/ext/parser/parser.c index f9972956..29b5674d 100644 --- a/ext/json/ext/parser/parser.c +++ b/ext/json/ext/parser/parser.c @@ -2119,6 +2119,10 @@ static VALUE cParser_source(VALUE self) void Init_parser(void) { +#ifdef HAVE_RB_EXT_RACTOR_SAFE + rb_ext_ractor_safe(true); +#endif + #undef rb_intern rb_require("json/common"); mJSON = rb_define_module("JSON"); diff --git a/ext/json/ext/parser/parser.rl b/ext/json/ext/parser/parser.rl index 24aed600..1da70c54 100644 --- a/ext/json/ext/parser/parser.rl +++ b/ext/json/ext/parser/parser.rl @@ -879,6 +879,10 @@ static VALUE cParser_source(VALUE self) void Init_parser(void) { +#ifdef HAVE_RB_EXT_RACTOR_SAFE + rb_ext_ractor_safe(true); +#endif + #undef rb_intern rb_require("json/common"); mJSON = rb_define_module("JSON"); diff --git a/tests/ractor_test.rb b/tests/ractor_test.rb new file mode 100644 index 00000000..96d1528c --- /dev/null +++ b/tests/ractor_test.rb @@ -0,0 +1,34 @@ +# encoding: utf-8 +# frozen_string_literal: false + +require 'test_helper' + +class JSONInRactorTest < Test::Unit::TestCase + def setup + skip unless defined? Ractor + end + + def test_generate + assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + $VERBOSE = nil + require "json" + r = Ractor.new do + json = JSON.generate({ + 'a' => 2, + 'b' => 3.141, + 'c' => 'c', + 'd' => [ 1, "b", 3.14 ], + 'e' => { 'foo' => 'bar' }, + 'g' => "\"\0\037", + 'h' => 1000.0, + 'i' => 0.001 + }) + JSON.parse(json) + end + expected_json = '{"a":2,"b":3.141,"c":"c","d":[1,"b",3.14],"e":{"foo":"bar"},' + + '"g":"\\"\\u0000\\u001f","h":1000.0,"i":0.001}' + assert_equal(JSON.parse(expected_json), r.take) + end; + end +end From f35ab5ee4cf6e3f1855885f86e8ed72d16752401 Mon Sep 17 00:00:00 2001 From: Kenta Murata Date: Mon, 21 Dec 2020 22:40:38 +0900 Subject: [PATCH 05/13] [json] Avoid method redefinition --- lib/json/common.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/json/common.rb b/lib/json/common.rb index 747effee..d58c7eae 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -103,11 +103,6 @@ def create_pretty_state # either JSON::Ext::Generator::State or JSON::Pure::Generator::State: # JSON.state # => JSON::Ext::Generator::State attr_accessor :state - - # Sets or returns create identifier, which is used to decide if the _json_create_ - # hook of a class should be called; initial value is +json_class+: - # JSON.create_id # => 'json_class' - attr_accessor :create_id end DEFAULT_CREATE_ID = 'json_class'.freeze @@ -116,14 +111,19 @@ def create_pretty_state CREATE_ID_TLS_KEY = "JSON.create_id".freeze private_constant :CREATE_ID_TLS_KEY - def self.create_id - Thread.current[CREATE_ID_TLS_KEY] || DEFAULT_CREATE_ID - end - + # Sets create identifier, which is used to decide if the _json_create_ + # hook of a class should be called; initial value is +json_class+: + # JSON.create_id # => 'json_class' def self.create_id=(new_value) Thread.current[CREATE_ID_TLS_KEY] = new_value.dup.freeze end + # Returns the current create identifier. + # See also JSON.create_id=. + def self.create_id + Thread.current[CREATE_ID_TLS_KEY] || DEFAULT_CREATE_ID + end + NaN = 0.0/0 Infinity = 1.0/0 From fc7bd74843d73f8f65d0ab2f48e1c512f556da48 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 14:21:33 +0900 Subject: [PATCH 06/13] Import test assertions from ruby/ruby --- Rakefile | 6 +- tests/lib/core_assertions.rb | 763 +++++++++++++++++++++++++++++++++++ tests/lib/envutil.rb | 365 +++++++++++++++++ tests/lib/find_executable.rb | 22 + tests/lib/helper.rb | 4 + 5 files changed, 1158 insertions(+), 2 deletions(-) create mode 100644 tests/lib/core_assertions.rb create mode 100644 tests/lib/envutil.rb create mode 100644 tests/lib/find_executable.rb create mode 100644 tests/lib/helper.rb diff --git a/Rakefile b/Rakefile index f48c377b..de330b10 100644 --- a/Rakefile +++ b/Rakefile @@ -99,7 +99,8 @@ task(:set_env_pure) { ENV['JSON'] = 'pure' } UndocumentedTestTask.new do |t| t.name = 'do_test_pure' - t.libs << 'lib' << 'tests' + t.libs << 'lib' << 'tests' << 'tests/lib' + t.ruby_opts << "-rhelper" t.test_files = FileList['tests/*_test.rb'] t.verbose = true t.options = '-v' @@ -252,7 +253,8 @@ else UndocumentedTestTask.new do |t| t.name = 'do_test_ext' - t.libs << 'ext' << 'lib' << 'tests' + t.libs << 'lib' << 'tests' << "tests/lib" + t.ruby_opts << '-rhelper' t.test_files = FileList['tests/*_test.rb'] t.verbose = true t.options = '-v' diff --git a/tests/lib/core_assertions.rb b/tests/lib/core_assertions.rb new file mode 100644 index 00000000..abd0e450 --- /dev/null +++ b/tests/lib/core_assertions.rb @@ -0,0 +1,763 @@ +# frozen_string_literal: true + +module Test + module Unit + module Assertions + def _assertions= n # :nodoc: + @_assertions = n + end + + def _assertions # :nodoc: + @_assertions ||= 0 + end + + ## + # Returns a proc that will output +msg+ along with the default message. + + def message msg = nil, ending = nil, &default + proc { + msg = msg.call.chomp(".") if Proc === msg + custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? + "#{custom_message}#{default.call}#{ending || "."}" + } + end + end + + module CoreAssertions + if defined?(MiniTest) + require_relative '../../envutil' + # for ruby core testing + include MiniTest::Assertions + + # Compatibility hack for assert_raise + Test::Unit::AssertionFailedError = MiniTest::Assertion + else + module MiniTest + class Assertion < Exception; end + class Skip < Assertion; end + end + + require 'pp' + require_relative 'envutil' + include Test::Unit::Assertions + end + + def mu_pp(obj) #:nodoc: + obj.pretty_inspect.chomp + end + + def assert_file + AssertFile + end + + FailDesc = proc do |status, message = "", out = ""| + now = Time.now + proc do + EnvUtil.failure_description(status, now, message, out) + end + end + + def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, + success: nil, **opt) + args = Array(args).dup + args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') + stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) + desc = FailDesc[status, message, stderr] + if block_given? + raise "test_stdout ignored, use block only or without block" if test_stdout != [] + raise "test_stderr ignored, use block only or without block" if test_stderr != [] + yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) + else + all_assertions(desc) do |a| + [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| + a.for(key) do + if exp.is_a?(Regexp) + assert_match(exp, act) + elsif exp.all? {|e| String === e} + assert_equal(exp, act.lines.map {|l| l.chomp }) + else + assert_pattern_list(exp, act) + end + end + end + unless success.nil? + a.for("success?") do + if success + assert_predicate(status, :success?) + else + assert_not_predicate(status, :success?) + end + end + end + end + status + end + end + + if defined?(RubyVM::InstructionSequence) + def syntax_check(code, fname, line) + code = code.dup.force_encoding(Encoding::UTF_8) + RubyVM::InstructionSequence.compile(code, fname, fname, line) + :ok + ensure + raise if SyntaxError === $! + end + else + def syntax_check(code, fname, line) + code = code.b + code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { + "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" + } + code = code.force_encoding(Encoding::UTF_8) + catch {|tag| eval(code, binding, fname, line - 1)} + end + end + + def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) + # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail + pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? + + require_relative '../../memory_status' + raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) + + token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" + token_dump = token.dump + token_re = Regexp.quote(token) + envs = args.shift if Array === args and Hash === args.first + args = [ + "--disable=gems", + "-r", File.expand_path("../../../memory_status", __FILE__), + *args, + "-v", "-", + ] + if defined? Memory::NO_MEMORY_LEAK_ENVS then + envs ||= {} + newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } + envs = newenvs if newenvs + end + args.unshift(envs) if envs + cmd = [ + 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', + prepare, + 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', + '$initial_size = $initial_status.size', + code, + 'GC.start', + ].join("\n") + _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) + before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) + after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) + assert(status.success?, FailDesc[status, message, err]) + ([:size, (rss && :rss)] & after.members).each do |n| + b = before[n] + a = after[n] + next unless a > 0 and b > 0 + assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) + end + rescue LoadError + pend + end + + # :call-seq: + # assert_nothing_raised( *args, &block ) + # + #If any exceptions are given as arguments, the assertion will + #fail if one of those exceptions are raised. Otherwise, the test fails + #if any exceptions are raised. + # + #The final argument may be a failure message. + # + # assert_nothing_raised RuntimeError do + # raise Exception #Assertion passes, Exception is not a RuntimeError + # end + # + # assert_nothing_raised do + # raise Exception #Assertion fails + # end + def assert_nothing_raised(*args) + self._assertions += 1 + if Module === args.last + msg = nil + else + msg = args.pop + end + begin + line = __LINE__; yield + rescue MiniTest::Skip + raise + rescue Exception => e + bt = e.backtrace + as = e.instance_of?(MiniTest::Assertion) + if as + ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o + bt.reject! {|ln| ans =~ ln} + end + if ((args.empty? && !as) || + args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) + msg = message(msg) { + "Exception raised:\n<#{mu_pp(e)}>\n" + + "Backtrace:\n" + + e.backtrace.map{|frame| " #{frame}"}.join("\n") + } + raise MiniTest::Assertion, msg.call, bt + else + raise + end + end + end + + def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) + fname ||= caller_locations(2, 1)[0] + mesg ||= fname.to_s + verbose, $VERBOSE = $VERBOSE, verbose + case + when Array === fname + fname, line = *fname + when defined?(fname.path) && defined?(fname.lineno) + fname, line = fname.path, fname.lineno + else + line = 1 + end + yield(code, fname, line, message(mesg) { + if code.end_with?("\n") + "```\n#{code}```\n" + else + "```\n#{code}\n```\n""no-newline" + end + }) + ensure + $VERBOSE = verbose + end + + def assert_valid_syntax(code, *args, **opt) + prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| + yield if defined?(yield) + assert_nothing_raised(SyntaxError, mesg) do + assert_equal(:ok, syntax_check(src, fname, line), mesg) + end + end + end + + def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) + assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) + if child_env + child_env = [child_env] + else + child_env = [] + end + out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) + assert !status.signaled?, FailDesc[status, message, out] + end + + def assert_ruby_status(args, test_stdin="", message=nil, **opt) + out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) + desc = FailDesc[status, message, out] + assert(!status.signaled?, desc) + message ||= "ruby exit status is not success:" + assert(status.success?, desc) + end + + ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") + + def separated_runner(out = nil) + out = out ? IO.new(out, 'w') : STDOUT + at_exit { + out.puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}" + } + Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) if defined?(Test::Unit::Runner) + end + + def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) + unless file and line + loc, = caller_locations(1,1) + file ||= loc.path + line ||= loc.lineno + end + capture_stdout = true + unless /mswin|mingw/ =~ RUBY_PLATFORM + capture_stdout = false + opt[:out] = MiniTest::Unit.output if defined?(MiniTest::Unit) + res_p, res_c = IO.pipe + opt[res_c.fileno] = res_c.fileno + end + src = < marshal_error + ignore_stderr = nil + res = nil + end + if res and !(SystemExit === res) + if bt = res.backtrace + bt.each do |l| + l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} + end + bt.concat(caller) + else + res.set_backtrace(caller) + end + raise res + end + + # really is it succeed? + unless ignore_stderr + # the body of assert_separately must not output anything to detect error + assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) + end + assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) + raise marshal_error if marshal_error + end + + # Run Ractor-related test without influencing the main test suite + def assert_ractor(src, args: [], require: nil, require_relative: nil, file: nil, line: nil, ignore_stderr: nil, **opt) + return unless defined?(Ractor) + + require = "require #{require.inspect}" if require + if require_relative + dir = File.dirname(caller_locations[0,1][0].absolute_path) + full_path = File.expand_path(require_relative, dir) + require = "#{require}; require #{full_path.inspect}" + end + + assert_separately(args, file, line, <<~RUBY, ignore_stderr: ignore_stderr, **opt) + #{require} + previous_verbose = $VERBOSE + $VERBOSE = nil + Ractor.new {} # trigger initial warning + $VERBOSE = previous_verbose + #{src} + RUBY + end + + # :call-seq: + # assert_throw( tag, failure_message = nil, &block ) + # + #Fails unless the given block throws +tag+, returns the caught + #value otherwise. + # + #An optional failure message may be provided as the final argument. + # + # tag = Object.new + # assert_throw(tag, "#{tag} was not thrown!") do + # throw tag + # end + def assert_throw(tag, msg = nil) + ret = catch(tag) do + begin + yield(tag) + rescue UncaughtThrowError => e + thrown = e.tag + end + msg = message(msg) { + "Expected #{mu_pp(tag)} to have been thrown"\ + "#{%Q[, not #{thrown}] if thrown}" + } + assert(false, msg) + end + assert(true) + ret + end + + # :call-seq: + # assert_raise( *args, &block ) + # + #Tests if the given block raises an exception. Acceptable exception + #types may be given as optional arguments. If the last argument is a + #String, it will be used as the error message. + # + # assert_raise do #Fails, no Exceptions are raised + # end + # + # assert_raise NameError do + # puts x #Raises NameError, so assertion succeeds + # end + def assert_raise(*exp, &b) + case exp.last + when String, Proc + msg = exp.pop + end + + begin + yield + rescue MiniTest::Skip => e + return e if exp.include? MiniTest::Skip + raise e + rescue Exception => e + expected = exp.any? { |ex| + if ex.instance_of? Module then + e.kind_of? ex + else + e.instance_of? ex + end + } + + assert expected, proc { + flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) + } + + return e + ensure + unless e + exp = exp.first if exp.size == 1 + + flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) + end + end + end + + # :call-seq: + # assert_raise_with_message(exception, expected, msg = nil, &block) + # + #Tests if the given block raises an exception with the expected + #message. + # + # assert_raise_with_message(RuntimeError, "foo") do + # nil #Fails, no Exceptions are raised + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise ArgumentError, "foo" #Fails, different Exception is raised + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise "bar" #Fails, RuntimeError is raised but the message differs + # end + # + # assert_raise_with_message(RuntimeError, "foo") do + # raise "foo" #Raises RuntimeError with the message, so assertion succeeds + # end + def assert_raise_with_message(exception, expected, msg = nil, &block) + case expected + when String + assert = :assert_equal + when Regexp + assert = :assert_match + else + raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" + end + + ex = m = nil + EnvUtil.with_default_internal(expected.encoding) do + ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do + yield + end + m = ex.message + end + msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} + + if assert == :assert_equal + assert_equal(expected, m, msg) + else + msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } + assert expected =~ m, msg + block.binding.eval("proc{|_|$~=_}").call($~) + end + ex + end + + MINI_DIR = File.join(File.dirname(File.dirname(File.expand_path(__FILE__))), "minitest") #:nodoc: + + # :call-seq: + # assert(test, [failure_message]) + # + #Tests if +test+ is true. + # + #+msg+ may be a String or a Proc. If +msg+ is a String, it will be used + #as the failure message. Otherwise, the result of calling +msg+ will be + #used as the message if the assertion fails. + # + #If no +msg+ is given, a default message will be used. + # + # assert(false, "This was expected to be true") + def assert(test, *msgs) + case msg = msgs.first + when String, Proc + when nil + msgs.shift + else + bt = caller.reject { |s| s.start_with?(MINI_DIR) } + raise ArgumentError, "assertion message must be String or Proc, but #{msg.class} was given.", bt + end unless msgs.empty? + super + end + + # :call-seq: + # assert_respond_to( object, method, failure_message = nil ) + # + #Tests if the given Object responds to +method+. + # + #An optional failure message may be provided as the final argument. + # + # assert_respond_to("hello", :reverse) #Succeeds + # assert_respond_to("hello", :does_not_exist) #Fails + def assert_respond_to(obj, (meth, *priv), msg = nil) + unless priv.empty? + msg = message(msg) { + "Expected #{mu_pp(obj)} (#{obj.class}) to respond to ##{meth}#{" privately" if priv[0]}" + } + return assert obj.respond_to?(meth, *priv), msg + end + #get rid of overcounting + if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + return if obj.respond_to?(meth) + end + super(obj, meth, msg) + end + + # :call-seq: + # assert_not_respond_to( object, method, failure_message = nil ) + # + #Tests if the given Object does not respond to +method+. + # + #An optional failure message may be provided as the final argument. + # + # assert_not_respond_to("hello", :reverse) #Fails + # assert_not_respond_to("hello", :does_not_exist) #Succeeds + def assert_not_respond_to(obj, (meth, *priv), msg = nil) + unless priv.empty? + msg = message(msg) { + "Expected #{mu_pp(obj)} (#{obj.class}) to not respond to ##{meth}#{" privately" if priv[0]}" + } + return assert !obj.respond_to?(meth, *priv), msg + end + #get rid of overcounting + if caller_locations(1, 1)[0].path.start_with?(MINI_DIR) + return unless obj.respond_to?(meth) + end + refute_respond_to(obj, meth, msg) + end + + # pattern_list is an array which contains regexp and :*. + # :* means any sequence. + # + # pattern_list is anchored. + # Use [:*, regexp, :*] for non-anchored match. + def assert_pattern_list(pattern_list, actual, message=nil) + rest = actual + anchored = true + pattern_list.each_with_index {|pattern, i| + if pattern == :* + anchored = false + else + if anchored + match = /\A#{pattern}/.match(rest) + else + match = pattern.match(rest) + end + unless match + msg = message(msg) { + expect_msg = "Expected #{mu_pp pattern}\n" + if /\n[^\n]/ =~ rest + actual_mesg = +"to match\n" + rest.scan(/.*\n+/) { + actual_mesg << ' ' << $&.inspect << "+\n" + } + actual_mesg.sub!(/\+\n\z/, '') + else + actual_mesg = "to match " + mu_pp(rest) + end + actual_mesg << "\nafter #{i} patterns with #{actual.length - rest.length} characters" + expect_msg + actual_mesg + } + assert false, msg + end + rest = match.post_match + anchored = true + end + } + if anchored + assert_equal("", rest) + end + end + + def assert_warning(pat, msg = nil) + result = nil + stderr = EnvUtil.with_default_internal(pat.encoding) { + EnvUtil.verbose_warning { + result = yield + } + } + msg = message(msg) {diff pat, stderr} + assert(pat === stderr, msg) + result + end + + def assert_warn(*args) + assert_warning(*args) {$VERBOSE = false; yield} + end + + def assert_deprecated_warning(mesg = /deprecated/) + assert_warning(mesg) do + Warning[:deprecated] = true + yield + end + end + + def assert_deprecated_warn(mesg = /deprecated/) + assert_warn(mesg) do + Warning[:deprecated] = true + yield + end + end + + class << (AssertFile = Struct.new(:failure_message).new) + include CoreAssertions + def assert_file_predicate(predicate, *args) + if /\Anot_/ =~ predicate + predicate = $' + neg = " not" + end + result = File.__send__(predicate, *args) + result = !result if neg + mesg = "Expected file ".dup << args.shift.inspect + mesg << "#{neg} to be #{predicate}" + mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? + mesg << " #{failure_message}" if failure_message + assert(result, mesg) + end + alias method_missing assert_file_predicate + + def for(message) + clone.tap {|a| a.failure_message = message} + end + end + + class AllFailures + attr_reader :failures + + def initialize + @count = 0 + @failures = {} + end + + def for(key) + @count += 1 + yield + rescue Exception => e + @failures[key] = [@count, e] + end + + def foreach(*keys) + keys.each do |key| + @count += 1 + begin + yield key + rescue Exception => e + @failures[key] = [@count, e] + end + end + end + + def message + i = 0 + total = @count.to_s + fmt = "%#{total.size}d" + @failures.map {|k, (n, v)| + v = v.message + "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" + }.join("\n") + end + + def pass? + @failures.empty? + end + end + + # threads should respond to shift method. + # Array can be used. + def assert_join_threads(threads, message = nil) + errs = [] + values = [] + while th = threads.shift + begin + values << th.value + rescue Exception + errs << [th, $!] + th = nil + end + end + values + ensure + if th&.alive? + th.raise(Timeout::Error.new) + th.join rescue errs << [th, $!] + end + if !errs.empty? + msg = "exceptions on #{errs.length} threads:\n" + + errs.map {|t, err| + "#{t.inspect}:\n" + + RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message + }.join("\n---\n") + if message + msg = "#{message}\n#{msg}" + end + raise MiniTest::Assertion, msg + end + end + + def assert_all_assertions(msg = nil) + all = AllFailures.new + yield all + ensure + assert(all.pass?, message(msg) {all.message.chomp(".")}) + end + alias all_assertions assert_all_assertions + + def message(msg = nil, *args, &default) # :nodoc: + if Proc === msg + super(nil, *args) do + ary = [msg.call, (default.call if default)].compact.reject(&:empty?) + if 1 < ary.length + ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? Date: Tue, 22 Dec 2020 15:03:22 +0900 Subject: [PATCH 07/13] skip ractor_test with JRuby --- tests/ractor_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/ractor_test.rb b/tests/ractor_test.rb index 96d1528c..cbca9d22 100644 --- a/tests/ractor_test.rb +++ b/tests/ractor_test.rb @@ -5,6 +5,7 @@ class JSONInRactorTest < Test::Unit::TestCase def setup + skip if RUBY_PLATFORM =~ /java/ skip unless defined? Ractor end From 795b274d1ce497172913ec31ab6ecb671ee9e766 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 15:25:56 +0900 Subject: [PATCH 08/13] Change the condition for Ractor --- tests/ractor_test.rb | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/tests/ractor_test.rb b/tests/ractor_test.rb index cbca9d22..71105e55 100644 --- a/tests/ractor_test.rb +++ b/tests/ractor_test.rb @@ -4,11 +4,6 @@ require 'test_helper' class JSONInRactorTest < Test::Unit::TestCase - def setup - skip if RUBY_PLATFORM =~ /java/ - skip unless defined? Ractor - end - def test_generate assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}") begin; @@ -32,4 +27,4 @@ def test_generate assert_equal(JSON.parse(expected_json), r.take) end; end -end +end if defined?(Ractor) From 147cbfa499f411851cb509a28b3f093eb838a08c Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 15:57:19 +0900 Subject: [PATCH 09/13] Use 2.7 in Travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 0aaa2fbb..3824394a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ rvm: - 2.4 - 2.5 - 2.6 - - 2.7.0-preview3 + - 2.7 - ruby-head - jruby-9.1 # Ruby 2.3 - jruby-9.2 # Ruby 2.5 From dece2379dcc8d8ebbeaf59d6a4fd7eb97b2c3cde Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 16:09:52 +0900 Subject: [PATCH 10/13] Guard for Ruby 3.0 --- tests/lib/envutil.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/envutil.rb b/tests/lib/envutil.rb index 323d7100..63e23ba9 100644 --- a/tests/lib/envutil.rb +++ b/tests/lib/envutil.rb @@ -53,7 +53,7 @@ def capture_global_values @original_internal_encoding = Encoding.default_internal @original_external_encoding = Encoding.default_external @original_verbose = $VERBOSE - @original_warning = %i[deprecated experimental].to_h {|i| [i, Warning[i]]} + @original_warning = %i[deprecated experimental].to_h {|i| [i, Warning[i]]} if RUBY_VERSION > "2.7" end end From d1a341fafdf86202acfce6b95aed7a3e65b63ac8 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 16:56:41 +0900 Subject: [PATCH 11/13] Skip tests with JRuby platform --- tests/json_generator_test.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/json_generator_test.rb b/tests/json_generator_test.rb index 5bafc3ea..f31b6b29 100644 --- a/tests/json_generator_test.rb +++ b/tests/json_generator_test.rb @@ -232,7 +232,7 @@ def test_buffer_initial_length end def test_gc - if respond_to?(:assert_in_out_err) + if respond_to?(:assert_in_out_err) && !(RUBY_PLATFORM =~ /java/) assert_in_out_err(%w[-rjson --disable-gems], <<-EOS, [], []) bignum_too_long_to_embed_as_string = 1234567890123456789012345 expect = bignum_too_long_to_embed_as_string.to_s From 951c7d108af91612b81790e7b31e831998c91b92 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 16:57:00 +0900 Subject: [PATCH 12/13] Drop to tests with too old Ruby --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3824394a..38a85841 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ language: ruby # Specify which ruby versions you wish to run your tests on, each version will be used rvm: - - 2.1 - - 2.2 - 2.3 - 2.4 - 2.5 From 0c446fa9b7c57647b11085304a532da70a64e231 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Tue, 22 Dec 2020 17:41:47 +0900 Subject: [PATCH 13/13] Workaround for JRuby --- lib/json/common.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/json/common.rb b/lib/json/common.rb index d58c7eae..3e390f2d 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -71,6 +71,7 @@ def generator=(generator) # :nodoc: end self.state = generator::State const_set :State, self.state + const_set :SAFE_STATE_PROTOTYPE, State.new # for JRuby ensure $VERBOSE = old end