diff options
Diffstat (limited to 'test/objspace')
-rw-r--r-- | test/objspace/test_objspace.rb | 325 | ||||
-rw-r--r-- | test/objspace/test_ractor.rb | 17 |
2 files changed, 327 insertions, 15 deletions
diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb index a1954d56a9..17f73e4433 100644 --- a/test/objspace/test_objspace.rb +++ b/test/objspace/test_objspace.rb @@ -33,7 +33,7 @@ class TestObjSpace < Test::Unit::TestCase b = a.dup c = nil ObjectSpace.each_object(String) {|x| break c = x if x == a and x.frozen?} - rv_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + rv_size = GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] assert_equal([rv_size, rv_size, a.length + 1 + rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)}) end @@ -140,6 +140,19 @@ class TestObjSpace < Test::Unit::TestCase end; end + def test_reachable_objects_during_iteration + omit 'flaky on Visual Studio with: [BUG] Unnormalized Fixnum value' if /mswin/ =~ RUBY_PLATFORM + opts = %w[--disable-gem --disable=frozen-string-literal -robjspace] + assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}" + begin; + ObjectSpace.each_object{|o| + o.inspect + ObjectSpace.reachable_objects_from(Class) + } + end; + end + + def test_reachable_objects_from_root root_objects = ObjectSpace.reachable_objects_from_root @@ -204,6 +217,28 @@ class TestObjSpace < Test::Unit::TestCase assert_equal(c3, ObjectSpace.allocation_generation(o3)) assert_equal(self.class.name, ObjectSpace.allocation_class_path(o3)) assert_equal(__method__, ObjectSpace.allocation_method_id(o3)) + + # [Bug #19456] + o4 = + # This line intentionally left blank + # This line intentionally left blank + 1.0 / 0.0; line4 = __LINE__; _c4 = GC.count + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o4)) + assert_equal(line4, ObjectSpace.allocation_sourceline(o4)) + + # The line number should be based on newarray instead of getinstancevariable. + line5 = __LINE__; o5 = [ # newarray (leaf) + @ivar, # getinstancevariable (not leaf) + ] + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o5)) + assert_equal(line5, ObjectSpace.allocation_sourceline(o5)) + + # [Bug #19482] + EnvUtil.under_gc_stress do + 100.times do + Class.new + end + end } end @@ -254,18 +289,55 @@ class TestObjSpace < Test::Unit::TestCase end def test_dump_flags + # Ensure that the fstring is promoted to old generation + 4.times { GC.start } info = ObjectSpace.dump("foo".freeze) assert_match(/"wb_protected":true, "old":true/, info) assert_match(/"fstring":true/, info) JSON.parse(info) if defined?(JSON) end + if defined?(RubyVM::Shape) + class TooComplex; end + + def test_dump_too_complex_shape + omit "flaky test" + + RubyVM::Shape::SHAPE_MAX_VARIATIONS.times do + TooComplex.new.instance_variable_set(:"@a#{_1}", 1) + end + + tc = TooComplex.new + info = ObjectSpace.dump(tc) + assert_not_match(/"too_complex_shape"/, info) + tc.instance_variable_set(:@new_ivar, 1) + info = ObjectSpace.dump(tc) + assert_match(/"too_complex_shape":true/, info) + if defined?(JSON) + assert_true(JSON.parse(info)["too_complex_shape"]) + end + end + end + + class NotTooComplex ; end + + def test_dump_not_too_complex_shape + tc = NotTooComplex.new + tc.instance_variable_set(:@new_ivar, 1) + info = ObjectSpace.dump(tc) + + assert_not_match(/"too_complex_shape"/, info) + if defined?(JSON) + assert_nil(JSON.parse(info)["too_complex_shape"]) + end + end + def test_dump_to_default line = nil info = nil ObjectSpace.trace_object_allocations do line = __LINE__ + 1 - str = "hello world" + str = "hello w" info = ObjectSpace.dump(str) end assert_dump_object(info, line) @@ -277,7 +349,7 @@ class TestObjSpace < Test::Unit::TestCase th = Thread.start {r.read} ObjectSpace.trace_object_allocations do line = __LINE__ + 1 - str = "hello world" + str = "hello w" ObjectSpace.dump(str, output: w) end w.close @@ -289,12 +361,50 @@ class TestObjSpace < Test::Unit::TestCase def assert_dump_object(info, line) loc = caller_locations(1, 1)[0] assert_match(/"type":"STRING"/, info) - assert_match(/"embedded":true, "bytesize":11, "value":"hello world", "encoding":"UTF-8"/, info) + assert_match(/"embedded":true, "bytesize":7, "value":"hello w", "encoding":"UTF-8"/, info) assert_match(/"file":"#{Regexp.escape __FILE__}", "line":#{line}/, info) assert_match(/"method":"#{loc.base_label}"/, info) JSON.parse(info) if defined?(JSON) end + def test_dump_array + # Empty array + info = ObjectSpace.dump([]) + assert_include(info, '"length":0, "embedded":true') + assert_not_include(info, '"shared":true') + + # Non-embed array + arr = (1..10).to_a + info = ObjectSpace.dump(arr) + assert_include(info, '"length":10') + assert_not_include(info, '"embedded":true') + assert_not_include(info, '"shared":true') + + # Shared array + arr1 = (1..10).to_a + arr = [] + arr.replace(arr1) + info = ObjectSpace.dump(arr) + assert_include(info, '"length":10, "shared":true') + assert_not_include(info, '"embedded":true') + end + + def test_dump_object + klass = Class.new + + # Empty object + info = ObjectSpace.dump(klass.new) + assert_include(info, '"embedded":true') + assert_include(info, '"ivars":0') + + # Non-embed object + obj = klass.new + 5.times { |i| obj.instance_variable_set("@ivar#{i}", 0) } + info = ObjectSpace.dump(obj) + assert_not_include(info, '"embedded":true') + assert_include(info, '"ivars":5') + end + def test_dump_control_char assert_include(ObjectSpace.dump("\x0f"), '"value":"\u000f"') assert_include(ObjectSpace.dump("\C-?"), '"value":"\u007f"') @@ -306,7 +416,7 @@ class TestObjSpace < Test::Unit::TestCase assert_equal('true', ObjectSpace.dump(true)) assert_equal('false', ObjectSpace.dump(false)) assert_equal('0', ObjectSpace.dump(0)) - assert_equal('{"type":"SYMBOL", "value":"foo"}', ObjectSpace.dump(:foo)) + assert_equal('{"type":"SYMBOL", "value":"test_dump_special_consts"}', ObjectSpace.dump(:test_dump_special_consts)) end def test_dump_singleton_class @@ -380,7 +490,7 @@ class TestObjSpace < Test::Unit::TestCase @obj1 = Object.new GC.start @obj2 = Object.new - ObjectSpace.dump_all(output: :stdout, since: gc_gen) + ObjectSpace.dump_all(output: :stdout, since: gc_gen, shapes: false) end p dump_my_heap_please @@ -388,7 +498,7 @@ class TestObjSpace < Test::Unit::TestCase assert_equal 'nil', output.pop since = output.shift.to_i assert_operator output.size, :>, 0 - generations = output.map { |l| JSON.parse(l)["generation"] }.uniq.sort + generations = output.map { |l| JSON.parse(l) }.map { |o| o["generation"] }.uniq.sort assert_equal [since, since + 1], generations end end @@ -431,6 +541,59 @@ class TestObjSpace < Test::Unit::TestCase end end + def test_dump_objects_dumps_page_slot_sizes + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + ObjectSpace.dump_all(output: $stdout) + end + + p $stdout == dump_my_heap_please + end; + assert_equal 'true', output.pop + assert(output.count > 1) + output.each { |l| + obj = JSON.parse(l) + next if obj["type"] == "ROOT" + next if obj["type"] == "SHAPE" + + assert_not_nil obj["slot_size"] + assert_equal 0, obj["slot_size"] % GC::INTERNAL_CONSTANTS[:RVALUE_SIZE] + } + end + end + + def test_dump_callinfo_includes_mid + assert_in_out_err(%w[-robjspace --disable-gems], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + class Foo + def foo + super(bar: 123) # should not crash on 0 mid + end + + def bar + baz(bar: 123) # mid: baz + end + end + + ObjectSpace.dump_all(output: $stdout) + end; + assert_empty error + assert(output.count > 1) + assert_includes output.grep(/"imemo_type":"callinfo"/).join("\n"), '"mid":"baz"' + end + end + + def test_dump_string_coderange + assert_includes ObjectSpace.dump("TEST STRING"), '"coderange":"7bit"' + unknown = "TEST STRING".dup.force_encoding(Encoding::UTF_16BE) + 2.times do # ensure that dumping the string doesn't mutate it + assert_includes ObjectSpace.dump(unknown), '"coderange":"unknown"' + end + assert_includes ObjectSpace.dump("Fée"), '"coderange":"valid"' + assert_includes ObjectSpace.dump("\xFF"), '"coderange":"broken"' + end + def test_dump_escapes_method_name method_name = "foo\"bar" klass = Class.new do @@ -449,6 +612,13 @@ class TestObjSpace < Test::Unit::TestCase ObjectSpace.trace_object_allocations_stop end + def test_dump_includes_slot_size + str = "TEST" + dump = ObjectSpace.dump(str) + + assert_includes dump, "\"slot_size\":#{GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]}" + end + def test_dump_reference_addresses_match_dump_all_addresses assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| begin; @@ -470,31 +640,70 @@ class TestObjSpace < Test::Unit::TestCase end end + def assert_test_string_entry_correct_in_dump_all(output) + # `TEST STRING` appears twice in the output of `ObjectSpace.dump_all` + # 1. To create the T_STRING object for the literal string "TEST STRING" + # 2. When it is assigned to the `str` variable with a new encoding + # + # This test makes assertions on the assignment to `str`, so we look for + # the second appearance of /TEST STRING/ in the output + test_string_in_dump_all = output.grep(/TEST2/) + + begin + assert_equal(2, test_string_in_dump_all.size, "number of strings") + rescue Test::Unit::AssertionFailedError => e + STDERR.puts e.inspect + STDERR.puts test_string_in_dump_all + if test_string_in_dump_all.size == 3 + STDERR.puts "This test is skipped because it seems hard to fix." + else + raise + end + end + + strs = test_string_in_dump_all.reject do |s| + s.include?("fstring") + end + + assert_equal(1, strs.length) + + entry_hash = JSON.parse(strs[0]) + + assert_equal(5, entry_hash["bytesize"], "bytesize is wrong") + assert_equal("TEST2", entry_hash["value"], "value is wrong") + assert_equal("UTF-8", entry_hash["encoding"], "encoding is wrong") + assert_equal("-", entry_hash["file"], "file is wrong") + assert_equal(5, entry_hash["line"], "line is wrong") + assert_equal("dump_my_heap_please", entry_hash["method"], "method is wrong") + assert_not_nil(entry_hash["generation"]) + end + def test_dump_all - entry = /"bytesize":11, "value":"TEST STRING", "encoding":"UTF-8", "file":"-", "line":4, "method":"dump_my_heap_please", "generation":/ opts = %w[--disable-gem --disable=frozen-string-literal -robjspace] assert_in_out_err(opts, "#{<<-"begin;"}#{<<-'end;'}") do |output, error| + # frozen_string_literal: false begin; def dump_my_heap_please ObjectSpace.trace_object_allocations_start GC.start - str = "TEST STRING".force_encoding("UTF-8") + str = "TEST2".force_encoding("UTF-8") ObjectSpace.dump_all(output: :stdout) end p dump_my_heap_please end; - assert_equal 'nil', output.pop - assert_match(entry, output.grep(/TEST STRING/).join("\n")) + + assert_test_string_entry_correct_in_dump_all(output) end assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}#{<<-'end;'}") do |(output), (error)| begin; + # frozen_string_literal: false def dump_my_heap_please ObjectSpace.trace_object_allocations_start GC.start - (str = "TEST STRING").force_encoding("UTF-8") + (str = "TEST2").force_encoding("UTF-8") ObjectSpace.dump_all().path end @@ -503,7 +712,8 @@ class TestObjSpace < Test::Unit::TestCase assert_nil(error) dump = File.readlines(output) File.unlink(output) - assert_match(entry, dump.grep(/TEST STRING/).join("\n")) + + assert_test_string_entry_correct_in_dump_all(dump) end if defined?(JSON) @@ -620,6 +830,7 @@ class TestObjSpace < Test::Unit::TestCase def test_objspace_trace assert_in_out_err(%w[-robjspace/trace], "#{<<-"begin;"}\n#{<<-'end;'}") do |out, err| begin; + # frozen_string_literal: false a = "foo" b = "b" + "a" + "r" c = 42 @@ -627,9 +838,93 @@ class TestObjSpace < Test::Unit::TestCase end; assert_equal ["objspace/trace is enabled"], err assert_equal 3, out.size - assert_equal '"foo" @ -:2', out[0] - assert_equal '"bar" @ -:3', out[1] + assert_equal '"foo" @ -:3', out[0] + assert_equal '"bar" @ -:4', out[1] assert_equal '42', out[2] end end + + def load_allocation_path_helper method, to_binary: false + + Tempfile.create(["test_ruby_load_allocation_path", ".rb"]) do |t| + path = t.path + str = "#{Time.now.to_f.to_s}_#{rand.to_s}" + t.puts script = <<~RUBY + # frozen-string-literal: true + return if Time.now.to_i > 0 + $gv = 'rnd-#{str}' # unreachable, but the string literal was written + RUBY + + t.close + + if to_binary + bin = RubyVM::InstructionSequence.compile_file(t.path).to_binary + bt = Tempfile.new(['test_ruby_load_allocation_path', '.yarb'], mode: File::Constants::WRONLY) + bt.write bin + bt.close + + path = bt.path + end + + assert_separately(%w[-robjspace -rtempfile], <<~RUBY) + GC.disable + path = "#{path}" + ObjectSpace.trace_object_allocations do + #{method} + end + + n = 0 + dump = ObjectSpace.dump_all(output: :string) + dump.each_line do |line| + if /"value":"rnd-#{str}"/ =~ line && /"frozen":true/ =~ line + assert Regexp.new('"file":"' + "#{path}") =~ line + assert Regexp.new('"line":') !~ line + n += 1 + end + rescue ArgumentError + end + + assert_equal(1, n) + RUBY + ensure + bt.unlink if bt + end + end + + def test_load_allocation_path_load + load_allocation_path_helper 'load(path)' + end + + def test_load_allocation_path_compile_file + load_allocation_path_helper 'RubyVM::InstructionSequence.compile_file(path)' + end + + def test_load_allocation_path_load_from_binary + # load_allocation_path_helper 'iseq = RubyVM::InstructionSequence.load_from_binary(File.binread(path))', to_binary: true + end + + def test_utf8_method_names + name = "utf8_❨╯°□°❩╯︵┻━┻" + obj = ObjectSpace.trace_object_allocations do + __send__(name) + end + dump = ObjectSpace.dump(obj) + assert_equal name, JSON.parse(dump)["method"], dump + end + + def test_dump_shapes + json = ObjectSpace.dump_shapes(output: :string) + json.each_line do |line| + assert_include(line, '"type":"SHAPE"') + end + + assert_empty ObjectSpace.dump_shapes(output: :string, since: RubyVM.stat(:next_shape_id)) + assert_equal 2, ObjectSpace.dump_shapes(output: :string, since: RubyVM.stat(:next_shape_id) - 2).lines.size + end + + private + + def utf8_❨╯°□°❩╯︵┻━┻ + "1#{2}" + end end diff --git a/test/objspace/test_ractor.rb b/test/objspace/test_ractor.rb new file mode 100644 index 0000000000..4901eeae2e --- /dev/null +++ b/test/objspace/test_ractor.rb @@ -0,0 +1,17 @@ +require "test/unit" + +class TestObjSpaceRactor < Test::Unit::TestCase + def test_tracing_does_not_crash + assert_ractor(<<~RUBY, require: 'objspace') + ObjectSpace.trace_object_allocations do + r = Ractor.new do + obj = 'a' * 1024 + Ractor.yield obj + end + + r.take + r.take + end + RUBY + end +end |