mirror of
https://github.com/ruby/ruby.git
synced 2025-08-15 13:39:04 +02:00

If the previous instruction is not a leaf instruction, then the PC was incremented before the instruction was ran (meaning the currently executing instruction is actually the previous instruction), so we should not increment the PC otherwise we will calculate the source line for the next instruction. This bug can be reproduced in the following script: ``` require "objspace" ObjectSpace.trace_object_allocations_start a = 1.0 / 0.0 p [ObjectSpace.allocation_sourceline(a), ObjectSpace.allocation_sourcefile(a)] ``` Which outputs: [4, "test.rb"] This is incorrect because the object was allocated on line 10 and not line 4. The behaviour is correct when we use a leaf instruction (e.g. if we replaced `1.0 / 0.0` with `"hello"`), then the output is: [10, "test.rb"]. [Bug #19456]
877 lines
27 KiB
Ruby
877 lines
27 KiB
Ruby
# 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 x == a and x.frozen?}
|
||
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
|
||
|
||
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_raise(TypeError) {ObjectSpace.memsize_of_all('error')}
|
||
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_nodes
|
||
res = ObjectSpace.count_nodes
|
||
assert_not_empty(res)
|
||
arg = {}
|
||
ObjectSpace.count_nodes(arg)
|
||
assert_not_empty(arg)
|
||
bug8014 = '[ruby-core:53130] [Bug #8014]'
|
||
assert_empty(arg.select {|k, v| !(Symbol === k && Integer === v)}, bug8014)
|
||
end if false
|
||
|
||
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
|
||
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
|
||
|
||
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_separately([], "#{<<~"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))
|
||
assert_equal(Class.name, ObjectSpace.allocation_class_path(o1))
|
||
assert_equal(:new, 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))
|
||
}
|
||
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
|
||
GC.disable # 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))
|
||
ensure
|
||
GC.enable
|
||
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_dump_flags
|
||
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
|
||
|
||
def test_dump_too_complex_shape
|
||
if defined?(RubyVM::Shape)
|
||
RubyVM::Shape::SHAPE_MAX_VARIATIONS.times do
|
||
Object.new.instance_variable_set(:"@a#{_1}", 1)
|
||
end
|
||
|
||
tc = Object.new
|
||
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 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
|
||
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"')
|
||
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":"foo"}', ObjectSpace.dump(:foo))
|
||
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_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"]
|
||
assert_equal 0, obj["slot_size"] % GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
|
||
}
|
||
end
|
||
end
|
||
|
||
def test_dump_string_coderange
|
||
assert_includes ObjectSpace.dump("TEST STRING"), '"coderange":"7bit"'
|
||
unknown = "TEST STRING".dup.force_encoding(Encoding::BINARY)
|
||
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
|
||
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;
|
||
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
|
||
|
||
entry_hash = JSON.parse(test_string_in_dump_all[1])
|
||
|
||
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(4, 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|
|
||
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;
|
||
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_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;
|
||
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" @ -:2', out[0]
|
||
assert_equal '"bar" @ -:3', 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
|