diff options
Diffstat (limited to 'test/objspace')
| -rw-r--r-- | test/objspace/test_objspace.rb | 1085 | ||||
| -rw-r--r-- | test/objspace/test_ractor.rb | 83 |
2 files changed, 1168 insertions, 0 deletions
diff --git a/test/objspace/test_objspace.rb b/test/objspace/test_objspace.rb new file mode 100644 index 0000000000..a9b902ed45 --- /dev/null +++ b/test/objspace/test_objspace.rb @@ -0,0 +1,1085 @@ +# frozen_string_literal: false +require "test/unit" +require "objspace" +begin + require "json" +rescue LoadError +end + +class TestObjSpace < Test::Unit::TestCase + def test_memsize_of + assert_equal(0, ObjectSpace.memsize_of(true)) + assert_equal(0, ObjectSpace.memsize_of(nil)) + assert_equal(0, ObjectSpace.memsize_of(1)) + assert_kind_of(Integer, ObjectSpace.memsize_of(Object.new)) + assert_kind_of(Integer, ObjectSpace.memsize_of(Class)) + assert_kind_of(Integer, ObjectSpace.memsize_of("")) + assert_kind_of(Integer, ObjectSpace.memsize_of([])) + assert_kind_of(Integer, ObjectSpace.memsize_of({})) + assert_kind_of(Integer, ObjectSpace.memsize_of(//)) + f = File.new(__FILE__) + assert_kind_of(Integer, ObjectSpace.memsize_of(f)) + f.close + assert_kind_of(Integer, ObjectSpace.memsize_of(/a/.match("a"))) + assert_kind_of(Integer, ObjectSpace.memsize_of(Struct.new(:a))) + + assert_operator(ObjectSpace.memsize_of(Regexp.new("(a)"*1000).match("a"*1000)), + :>, + ObjectSpace.memsize_of(//.match(""))) + end + + def test_memsize_of_root_shared_string + a = "a" * GC::INTERNAL_CONSTANTS[:RVARGC_MAX_ALLOCATE_SIZE] + b = a.dup + c = nil + ObjectSpace.each_object(String) {|x| break c = x if a == x and x.frozen?} + rv_size = Integer(ObjectSpace.dump(a)[/"slot_size":(\d+)/, 1]) + assert_equal([rv_size, rv_size, a.length + 1 + rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)}) + end + + def test_argf_memsize + size = ObjectSpace.memsize_of(ARGF) + assert_kind_of(Integer, size) + assert_operator(size, :>, 0) + argf = ARGF.dup + argf.inplace_mode = nil + size = ObjectSpace.memsize_of(argf) + argf.inplace_mode = "inplace_mode_suffix" + assert_equal(size, ObjectSpace.memsize_of(argf)) + end + + def test_memsize_of_all + assert_kind_of(Integer, a = ObjectSpace.memsize_of_all) + assert_kind_of(Integer, b = ObjectSpace.memsize_of_all(String)) + assert_operator(a, :>, b) + assert_operator(a, :>, 0) + assert_operator(b, :>, 0) + assert_kind_of(Integer, ObjectSpace.memsize_of_all(Enumerable)) + end + + def test_memsize_of_all_with_wrong_type + assert_raise(TypeError) { ObjectSpace.memsize_of_all(Object.new) } + end + + def test_count_objects_size + res = ObjectSpace.count_objects_size + assert_not_empty(res) + assert_operator(res[:TOTAL], :>, 0) + end + + def test_count_objects_size_with_hash + arg = {} + ObjectSpace.count_objects_size(arg) + assert_not_empty(arg) + arg = {:TOTAL => 1 } + ObjectSpace.count_objects_size(arg) + assert_not_empty(arg) + end + + def test_count_objects_size_with_wrong_type + assert_raise(TypeError) { ObjectSpace.count_objects_size(0) } + end + + def test_count_tdata_objects + res = ObjectSpace.count_tdata_objects + assert_not_empty(res) + arg = {} + ObjectSpace.count_tdata_objects(arg) + assert_not_empty(arg) + end + + def test_count_imemo_objects + res = ObjectSpace.count_imemo_objects + assert_not_empty(res) + assert_not_nil(res[:imemo_cref]) + assert_not_empty res.inspect + + arg = {} + res = ObjectSpace.count_imemo_objects(arg) + assert_not_empty(res) + end + + def test_memsize_of_iseq + iseqw = RubyVM::InstructionSequence.compile('def a; a = :b; a; end') + # Use anonymous class as a basic object size because size of Object.new can be increased + base_obj_size = ObjectSpace.memsize_of(Class.new.new) + assert_operator(ObjectSpace.memsize_of(iseqw), :>, base_obj_size) + end + + def test_reachable_objects_from + opts = %w[--disable-gem --disable=frozen-string-literal -robjspace] + assert_separately opts, "#{<<-"begin;"}\n#{<<-'end;'}" + begin; + assert_equal(nil, ObjectSpace.reachable_objects_from(nil)) + assert_equal([Array, 'a', 'b', 'c'], ObjectSpace.reachable_objects_from(['a', 'b', 'c'])) + + assert_equal([Array, 'a', 'a', 'a'], ObjectSpace.reachable_objects_from(['a', 'a', 'a'])) + assert_equal([Array, 'a', 'a'], ObjectSpace.reachable_objects_from(['a', v = 'a', v])) + assert_equal([Array, 'a'], ObjectSpace.reachable_objects_from([v = 'a', v, v])) + + long_ary = Array.new(1_000){''} + max = 0 + + ObjectSpace.each_object{|o| + refs = ObjectSpace.reachable_objects_from(o) + max = [refs.size, max].max + + unless refs.nil? + refs.each_with_index {|ro, i| + assert_not_nil(ro, "#{i}: this referenced object is internal object") + } + end + } + assert_operator(max, :>=, long_ary.size+1, "1000 elems + Array class") + 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_ruby_status 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 + + assert_operator(root_objects.size, :>, 0) + + root_objects.each{|category, objects| + assert_kind_of(String, category) + assert_kind_of(Array, objects) + assert_operator(objects.size, :>, 0) + } + end + + def test_reachable_objects_size + assert_separately %w[--disable-gem -robjspace], "#{<<~"begin;"}\n#{<<~'end;'}" + begin; + ObjectSpace.each_object{|o| + ObjectSpace.reachable_objects_from(o).each{|reached_obj| + size = ObjectSpace.memsize_of(reached_obj) + assert_kind_of(Integer, size) + assert_operator(size, :>=, 0) + } + } + end; + end + + def test_trace_object_allocations_stop_first + assert_ruby_status([], "#{<<~"begin;"}\n#{<<~'end;'}") + begin; + require "objspace" + # Make sure stopping before the tracepoints are initialized doesn't raise. See [Bug #17020] + ObjectSpace.trace_object_allocations_stop + end; + end + + def test_trace_object_allocations + ObjectSpace.trace_object_allocations_clear # clear object_table to get rid of erroneous detection for c0 + Class.name + o0 = Object.new + ObjectSpace.trace_object_allocations{ + o1 = Object.new; line1 = __LINE__; c1 = GC.count + o2 = "xyzzy" ; line2 = __LINE__; c2 = GC.count + o3 = [1, 2] ; line3 = __LINE__; c3 = GC.count + + assert_equal(nil, ObjectSpace.allocation_sourcefile(o0)) + assert_equal(nil, ObjectSpace.allocation_sourceline(o0)) + assert_equal(nil, ObjectSpace.allocation_generation(o0)) + + assert_equal(line1, ObjectSpace.allocation_sourceline(o1)) + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o1)) + assert_equal(c1, ObjectSpace.allocation_generation(o1)) + # These assertions fail under coverage measurement: https://bugs.ruby-lang.org/issues/21298 + #assert_equal(self.class.name, ObjectSpace.allocation_class_path(o1)) + #assert_equal(__method__, ObjectSpace.allocation_method_id(o1)) + + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o2)) + assert_equal(line2, ObjectSpace.allocation_sourceline(o2)) + assert_equal(c2, ObjectSpace.allocation_generation(o2)) + assert_equal(self.class.name, ObjectSpace.allocation_class_path(o2)) + assert_equal(__method__, ObjectSpace.allocation_method_id(o2)) + + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o3)) + assert_equal(line3, ObjectSpace.allocation_sourceline(o3)) + 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 + + def test_trace_object_allocations_start_stop_clear + ObjectSpace.trace_object_allocations_clear # clear object_table to get rid of erroneous detection for obj3 + EnvUtil.without_gc do # suppress potential object reuse. see [Bug #11271] + begin + ObjectSpace.trace_object_allocations_start + begin + ObjectSpace.trace_object_allocations_start + begin + ObjectSpace.trace_object_allocations_start + obj0 = Object.new + ensure + ObjectSpace.trace_object_allocations_stop + obj1 = Object.new + end + ensure + ObjectSpace.trace_object_allocations_stop + obj2 = Object.new + end + ensure + ObjectSpace.trace_object_allocations_stop + obj3 = Object.new + end + + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj0)) + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj1)) + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj2)) + assert_equal(nil , ObjectSpace.allocation_sourcefile(obj3)) # after tracing + + ObjectSpace.trace_object_allocations_clear + assert_equal(nil, ObjectSpace.allocation_sourcefile(obj0)) + assert_equal(nil, ObjectSpace.allocation_sourcefile(obj1)) + assert_equal(nil, ObjectSpace.allocation_sourcefile(obj2)) + assert_equal(nil, ObjectSpace.allocation_sourcefile(obj3)) + end + end + + def test_trace_object_allocations_gc_stress + EnvUtil.under_gc_stress do + ObjectSpace.trace_object_allocations{ + proc{} + } + end + assert true # success + end + + def test_trace_object_allocations_with_other_tracepoint + # Test that ObjectSpace.trace_object_allocations isn't changed by changes + # to another tracepoint + line_tp = TracePoint.new(:line) { } + + ObjectSpace.trace_object_allocations_start + + obj1 = Object.new; line1 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj1) + assert_equal line1, ObjectSpace.allocation_sourceline(obj1) + + line_tp.enable + + obj2 = Object.new; line2 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj2) + assert_equal line2, ObjectSpace.allocation_sourceline(obj2) + + line_tp.disable + + obj3 = Object.new; line3 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj3) + assert_equal line3, ObjectSpace.allocation_sourceline(obj3) + ensure + ObjectSpace.trace_object_allocations_stop + ObjectSpace.trace_object_allocations_clear + end + + def test_trace_object_allocations_compaction + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + assert_separately(%w(-robjspace), <<~RUBY) + ObjectSpace.trace_object_allocations do + objs = 100.times.map do + Object.new + end + + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(objs[0])) + + GC.verify_compaction_references(expand_heap: true, toward: :empty) + + assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(objs[0])) + end + RUBY + end + + def test_trace_object_allocations_compaction_freed_pages + omit "compaction is not supported on this platform" unless GC.respond_to?(:compact) + + assert_normal_exit(<<~RUBY, timeout: 60) + require "objspace" + + objs = [] + ObjectSpace.trace_object_allocations do + 1_000_000.times do + objs << Object.new + end + end + + objs = nil + + # Free pages that the objs were on + GC.start + + # Run compaction and check that it doesn't crash + GC.compact + RUBY + end + + def test_trace_object_allocations_does_not_reuse_freed_allocation_info + assert_separately(%w(-robjspace), <<~RUBY) + ObjectSpace.trace_object_allocations do + 1_000_000.times.map { Object.new } + end + + GC.start + + objs = 1_000_000.times.map { Object.new } + + leaked = objs.count { |obj| ObjectSpace.allocation_sourcefile(obj) } + assert_equal 0, leaked + RUBY + end + + def test_dump_flags + # Ensure that the fstring is promoted to old generation + 4.times { GC.start } + info = ObjectSpace.dump("foo".freeze) + assert_include(info, '"wb_protected":true') + assert_include(info, '"age":3') + assert_include(info, '"old":true') + assert_match(/"fstring":true/, info) + JSON.parse(info) if defined?(JSON) + end + + def test_dump_flag_age + EnvUtil.without_gc do + o = Object.new + + assert_include(ObjectSpace.dump(o), '"age":0') + + GC.start + + assert_include(ObjectSpace.dump(o), '"age":1') + end + end + + if defined?(RubyVM::Shape) + class TooComplex; end + + def test_dump_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(/"complex_shape"/, info) + tc.instance_variable_set(:@new_ivar, 1) + info = ObjectSpace.dump(tc) + assert_match(/"complex_shape":true/, info) + if defined?(JSON) + assert_true(JSON.parse(info)["complex_shape"]) + end + end + end + + class NotTooComplex ; end + + def test_dump_not_complex_shape + tc = NotTooComplex.new + tc.instance_variable_set(:@new_ivar, 1) + info = ObjectSpace.dump(tc) + + assert_not_match(/"complex_shape"/, info) + if defined?(JSON) + assert_nil(JSON.parse(info)["complex_shape"]) + end + end + + def test_dump_to_default + line = nil + info = nil + ObjectSpace.trace_object_allocations do + line = __LINE__ + 1 + str = "hello w" + info = ObjectSpace.dump(str) + end + assert_dump_object(info, line) + end + + def test_dump_to_io + line = nil + info = IO.pipe do |r, w| + th = Thread.start {r.read} + ObjectSpace.trace_object_allocations do + line = __LINE__ + 1 + str = "hello w" + ObjectSpace.dump(str, output: w) + end + w.close + th.value + end + assert_dump_object(info, line) + end + + def assert_dump_object(info, line) + loc = caller_locations(1, 1)[0] + assert_match(/"type":"STRING"/, 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 (needs > 6 ivars to exceed pool 0 embed capacity) + obj = klass.new + 7.times { |i| obj.instance_variable_set("@ivar#{i}", 0) } + info = ObjectSpace.dump(obj) + assert_not_include(info, '"embedded":true') + assert_include(info, '"ivars":7') + end + + def test_dump_control_char + assert_include(ObjectSpace.dump("\x0f"), '"value":"\u000f"') + assert_include(ObjectSpace.dump("\C-?"), '"value":"\u007f"') + end + + def test_dump_special_consts + # [ruby-core:69692] [Bug #11291] + assert_equal('null', ObjectSpace.dump(nil)) + assert_equal('true', ObjectSpace.dump(true)) + assert_equal('false', ObjectSpace.dump(false)) + assert_equal('0', ObjectSpace.dump(0)) + assert_equal('{"type":"SYMBOL", "value":"test_dump_special_consts"}', ObjectSpace.dump(:test_dump_special_consts)) + end + + def test_dump_singleton_class + assert_include(ObjectSpace.dump(Object), '"name":"Object"') + assert_include(ObjectSpace.dump(Kernel), '"name":"Kernel"') + assert_include(ObjectSpace.dump(Object.new.singleton_class), '"real_class_name":"Object"') + + singleton = Object.new.singleton_class + singleton_dump = ObjectSpace.dump(singleton) + assert_include(singleton_dump, '"singleton":true') + if defined?(JSON) + assert_equal(Object, singleton.superclass) + superclass_address = JSON.parse(ObjectSpace.dump(Object)).fetch('address') + assert_equal(superclass_address, JSON.parse(singleton_dump).fetch('superclass')) + end + end + + def test_dump_special_floats + assert_match(/"value":"NaN"/, ObjectSpace.dump(Float::NAN)) + assert_match(/"value":"Inf"/, ObjectSpace.dump(Float::INFINITY)) + assert_match(/"value":"\-Inf"/, ObjectSpace.dump(-Float::INFINITY)) + end + + def test_dump_dynamic_symbol + dump = ObjectSpace.dump(("foobar%x" % rand(0x10000)).to_sym) + assert_match(/"type":"SYMBOL"/, dump) + assert_match(/"value":"foobar\h+"/, dump) + end + + def test_dump_outputs_object_id + obj = Object.new + + # Doesn't output object_id when it has not been seen + dump = ObjectSpace.dump(obj) + assert_not_include(dump, "\"object_id\"") + + id = obj.object_id + + # Outputs object_id when it has been seen + dump = ObjectSpace.dump(obj) + assert_include(dump, "\"object_id\":#{id}") + end + + def test_dump_includes_imemo_type + 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 dump_my_heap_please + end; + assert_equal 'nil', output.pop + heap = output.find_all { |l| + obj = JSON.parse(l) + obj['type'] == "IMEMO" && obj['imemo_type'] + } + assert_operator heap.length, :>, 0 + end + end + + def test_dump_all_full + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + ObjectSpace.dump_all(output: :stdout, full: true) + end + + p dump_my_heap_please + end; + assert_equal 'nil', output.pop + heap = output.find_all { |l| JSON.parse(l)['type'] == "NONE" } + assert_operator heap.length, :>, 0 + end + end + + def test_dump_all_single_generation + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + GC.start + ObjectSpace.trace_object_allocations_start + gc_gen = GC.count + puts gc_gen + @obj1 = Object.new + GC.start + @obj2 = Object.new + ObjectSpace.dump_all(output: :stdout, since: gc_gen, shapes: false) + end + + p dump_my_heap_please + end; + assert_equal 'nil', output.pop + since = output.shift.to_i + assert_operator output.size, :>, 0 + generations = output.map { |l| JSON.parse(l) }.map { |o| o["generation"] }.uniq.sort + assert_equal [since, since + 1], generations + end + end + + def test_dump_addresses_match_dump_all_addresses + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + obj = Object.new + puts ObjectSpace.dump(obj) + ObjectSpace.dump_all(output: $stdout) + end + + p $stdout == dump_my_heap_please + end; + assert_equal 'true', output.pop + needle = JSON.parse(output.first) + addr = needle['address'] + found = output.drop(1).find { |l| JSON.parse(l)['address'] == addr } + assert found, "object #{addr} should be findable in full heap dump" + end + end + + def test_dump_class_addresses_match_dump_all_addresses + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + obj = Object.new + puts ObjectSpace.dump(obj) + ObjectSpace.dump_all(output: $stdout) + end + + p $stdout == dump_my_heap_please + end; + assert_equal 'true', output.pop + needle = JSON.parse(output.first) + addr = needle['class'] + found = output.drop(1).find { |l| JSON.parse(l)['address'] == addr } + assert found, "object #{addr} should be findable in full heap dump" + 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"] + slot_sizes = GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times.map { |i| GC.stat_heap(i, :slot_size) } + assert_include slot_sizes, obj["slot_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 + define_method(method_name) { "TEST STRING" } + end + ObjectSpace.trace_object_allocations_start + + obj = klass.new.send(method_name) + + dump = ObjectSpace.dump(obj) + assert_includes dump, '"method":"foo\"bar"' + + parsed = JSON.parse(dump) + assert_equal "foo\"bar", parsed["method"] + ensure + ObjectSpace.trace_object_allocations_stop + end + + def test_dump_includes_slot_size + klass = Class.new + obj = klass.new + dump = ObjectSpace.dump(obj) + + assert_includes dump, "\"slot_size\":#{GC.stat_heap(0, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]}" + end + + def test_dump_reference_addresses_match_dump_all_addresses + assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error| + begin; + def dump_my_heap_please + obj = Object.new + obj2 = Object.new + obj2.instance_variable_set(:@ref, obj) + puts ObjectSpace.dump(obj) + ObjectSpace.dump_all(output: $stdout) + end + + p $stdout == dump_my_heap_please + end; + assert_equal 'true', output.pop + needle = JSON.parse(output.first) + addr = needle['address'] + found = output.drop(1).find { |l| (JSON.parse(l)['references'] || []).include? addr } + assert found, "object #{addr} should be findable in full heap dump" + 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 + 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 = "TEST2".force_encoding("UTF-8") + ObjectSpace.dump_all(output: :stdout) + end + + p dump_my_heap_please + end; + + 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 = "TEST2").force_encoding("UTF-8") + ObjectSpace.dump_all().path + end + + puts dump_my_heap_please + end; + assert_nil(error) + dump = File.readlines(output) + File.unlink(output) + + assert_test_string_entry_correct_in_dump_all(dump) + end + + if defined?(JSON) + args = [ + "-rjson", "-", + EnvUtil.rubybin, + "--disable=gems", "-robjspace", "-eObjectSpace.dump_all(output: :stdout)", + ] + assert_ruby_status(args, "#{<<~"begin;"}\n#{<<~"end;"}") + begin; + IO.popen(ARGV) do |f| + f.each_line.map { |x| JSON.load(x) } + end + end; + end + end + + def test_dump_all_with_ractors + assert_ractor("#{<<-"begin;"}#{<<-'end;'}") + begin; + require "objspace" + require "tempfile" + require "json" + rs = 4.times.map do + Ractor.new do + Tempfile.create do |f| + ObjectSpace.dump_all(output: f) + f.close + File.readlines(f.path).each do |line| + JSON.parse(line) + end + end + end + end + rs.each(&:join) + end; + end + + def test_dump_uninitialized_file + assert_in_out_err(%[-robjspace], <<-RUBY) do |(output), (error)| + puts ObjectSpace.dump(File.allocate) + RUBY + assert_nil error + assert_match(/"type":"FILE"/, output) + assert_not_match(/"fd":/, output) + end + end + + def traverse_classes klass + h = {} + while klass && !h.has_key?(klass) + h[klass] = true + klass = ObjectSpace.internal_class_of(klass) + end + end + + def test_internal_class_of + i = 0 + ObjectSpace.each_object{|o| + traverse_classes ObjectSpace.internal_class_of(o) + i += 1 + } + assert_operator i, :>, 0 + end + + def test_internal_class_of_on_ast + children = ObjectSpace.reachable_objects_from(RubyVM::AbstractSyntaxTree.parse("kadomatsu")) + children.each {|child| ObjectSpace.internal_class_of(child).itself} # this used to crash + end + + def test_name_error_message + begin + bar + rescue => err + _, m = ObjectSpace.reachable_objects_from(err) + end + assert_equal(m, m.clone) + end + + def traverse_super_classes klass + while klass + klass = ObjectSpace.internal_super_of(klass) + end + end + + def all_super_classes klass + klasses = [] + while klass + klasses << klass + klass = ObjectSpace.internal_super_of(klass) + end + klasses + end + + def test_internal_super_of + klasses = all_super_classes(String) + String.ancestors.each{|k| + case k + when Class + assert_equal(true, klasses.include?(k), k.inspect) + when Module + assert_equal(false, klasses.include?(k), k.inspect) # Internal object (T_ICLASS) + end + } + + i = 0 + ObjectSpace.each_object(Module){|o| + traverse_super_classes ObjectSpace.internal_super_of(o) + i += 1 + } + assert_operator i, :>, 0 + end + + def test_count_symbols + assert_separately(%w[-robjspace], "#{<<~';;;'}") + h0 = ObjectSpace.count_symbols + + syms = (1..128).map{|i| ("xyzzy#{i}_#{Process.pid}_#{rand(1_000_000)}_" * 128).to_sym} + syms << Class.new{define_method(syms[-1]){}} + + h = ObjectSpace.count_symbols + m = proc {h0.inspect + "\n" + h.inspect} + assert_equal 127, h[:mortal_dynamic_symbol] - h0[:mortal_dynamic_symbol], m + assert_equal 1, h[:immortal_dynamic_symbol] - h0[:immortal_dynamic_symbol], m + assert_operator h[:immortal_static_symbol], :>=, Object.methods.size, m + assert_equal h[:immortal_symbol], h[:immortal_dynamic_symbol] + h[:immortal_static_symbol], m + ;;; + end + + def test_anonymous_class_name + assert_not_include ObjectSpace.dump(Class.new), '"name"' + assert_not_include ObjectSpace.dump(Module.new), '"name"' + end + + 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 + p a, b, c + end; + assert_equal ["objspace/trace is enabled"], err + assert_equal 3, out.size + 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_escape_class_name + class_name = '" little boby table [Bug #20892]' + json = ObjectSpace.dump(Class.new.tap { |c| c.set_temporary_name(class_name) }) + assert_equal class_name, JSON.parse(json)["name"] + end + + def test_dump_free_immediately + require '-test-/typeddata' + + # Bug::TypedData has flags=0 (no FREE_IMMEDIATELY) + info = ObjectSpace.dump(Bug::TypedData.new) + assert_include(info, '"struct":"typed_data"') + assert_include(info, '"free_immediately":false') + + # Most typed data objects have FREE_IMMEDIATELY, so the field should be absent + info = ObjectSpace.dump(Thread.current.group) + assert_include(info, '"struct":"thgroup"') + assert_not_include(info, '"free_immediately"') + end + + def test_dump_include_shareable + omit 'Not provided by mmtk' if RUBY_DESCRIPTION.include?("+GC[mmtk]") + + assert_include(ObjectSpace.dump(ENV), '"shareable":true') + assert_not_include(ObjectSpace.dump([]), '"shareable":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..fb6432a827 --- /dev/null +++ b/test/objspace/test_ractor.rb @@ -0,0 +1,83 @@ +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 + end + + r.join + end + RUBY + end + + def test_undefine_finalizer + assert_ractor(<<~'RUBY', timeout: 20, require: 'objspace', signal: :SEGV) + def fin + ->(id) { } + end + ractors = 5.times.map do + Ractor.new do + 10_000.times do + o = Object.new + ObjectSpace.define_finalizer(o, fin) + ObjectSpace.undefine_finalizer(o) + end + end + end + + ractors.each(&:join) + RUBY + end + + def test_copy_finalizer + assert_ractor(<<~'RUBY', require: 'objspace') + def fin + ->(id) { } + end + OBJ = Object.new + ObjectSpace.define_finalizer(OBJ, fin) + OBJ.freeze + + ractors = 5.times.map do + Ractor.new do + 10_000.times do + OBJ.clone + end + end + end + + ractors.each(&:join) + RUBY + end + + def test_trace_object_allocations_with_ractor_tracepoint + # Test that ObjectSpace.trace_object_allocations works globally across all Ractors + assert_ractor(<<~'RUBY', require: 'objspace') + ObjectSpace.trace_object_allocations do + obj1 = Object.new; line1 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj1) + assert_equal line1, ObjectSpace.allocation_sourceline(obj1) + + r = Ractor.new { + obj = Object.new; line = __LINE__ + [line, obj] + } + + obj2 = Object.new; line2 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj2) + assert_equal line2, ObjectSpace.allocation_sourceline(obj2) + + expected_line, ractor_obj = r.value + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(ractor_obj) + assert_equal expected_line, ObjectSpace.allocation_sourceline(ractor_obj) + + obj3 = Object.new; line3 = __LINE__ + assert_equal __FILE__, ObjectSpace.allocation_sourcefile(obj3) + assert_equal line3, ObjectSpace.allocation_sourceline(obj3) + end + RUBY + end +end |
