1# frozen_string_literal: false
2require "test/unit"
3require "objspace"
4begin
5  require "json"
6rescue LoadError
7end
8
9class TestObjSpace < Test::Unit::TestCase
10  def test_memsize_of
11    assert_equal(0, ObjectSpace.memsize_of(true))
12    assert_equal(0, ObjectSpace.memsize_of(nil))
13    assert_equal(0, ObjectSpace.memsize_of(1))
14    assert_kind_of(Integer, ObjectSpace.memsize_of(Object.new))
15    assert_kind_of(Integer, ObjectSpace.memsize_of(Class))
16    assert_kind_of(Integer, ObjectSpace.memsize_of(""))
17    assert_kind_of(Integer, ObjectSpace.memsize_of([]))
18    assert_kind_of(Integer, ObjectSpace.memsize_of({}))
19    assert_kind_of(Integer, ObjectSpace.memsize_of(//))
20    f = File.new(__FILE__)
21    assert_kind_of(Integer, ObjectSpace.memsize_of(f))
22    f.close
23    assert_kind_of(Integer, ObjectSpace.memsize_of(/a/.match("a")))
24    assert_kind_of(Integer, ObjectSpace.memsize_of(Struct.new(:a)))
25
26    assert_operator(ObjectSpace.memsize_of(Regexp.new("(a)"*1000).match("a"*1000)),
27                    :>,
28                    ObjectSpace.memsize_of(//.match("")))
29  end
30
31  def test_memsize_of_root_shared_string
32    a = "hello" * 5
33    b = a.dup
34    c = nil
35    ObjectSpace.each_object(String) {|x| break c = x if x == a and x.frozen?}
36    rv_size = GC::INTERNAL_CONSTANTS[:RVALUE_SIZE]
37    assert_equal([rv_size, rv_size, 26 + rv_size], [a, b, c].map {|x| ObjectSpace.memsize_of(x)})
38  end
39
40  def test_argf_memsize
41    size = ObjectSpace.memsize_of(ARGF)
42    assert_kind_of(Integer, size)
43    assert_operator(size, :>, 0)
44    argf = ARGF.dup
45    argf.inplace_mode = nil
46    size = ObjectSpace.memsize_of(argf)
47    argf.inplace_mode = "inplace_mode_suffix"
48    assert_equal(size, ObjectSpace.memsize_of(argf))
49  end
50
51  def test_memsize_of_all
52    assert_kind_of(Integer, a = ObjectSpace.memsize_of_all)
53    assert_kind_of(Integer, b = ObjectSpace.memsize_of_all(String))
54    assert_operator(a, :>, b)
55    assert_operator(a, :>, 0)
56    assert_operator(b, :>, 0)
57    assert_raise(TypeError) {ObjectSpace.memsize_of_all('error')}
58  end
59
60  def test_count_objects_size
61    res = ObjectSpace.count_objects_size
62    assert_not_empty(res)
63    assert_operator(res[:TOTAL], :>, 0)
64  end
65
66  def test_count_objects_size_with_hash
67    arg = {}
68    ObjectSpace.count_objects_size(arg)
69    assert_not_empty(arg)
70    arg = {:TOTAL => 1 }
71    ObjectSpace.count_objects_size(arg)
72    assert_not_empty(arg)
73  end
74
75  def test_count_objects_size_with_wrong_type
76    assert_raise(TypeError) { ObjectSpace.count_objects_size(0) }
77  end
78
79  def test_count_nodes
80    res = ObjectSpace.count_nodes
81    assert_not_empty(res)
82    arg = {}
83    ObjectSpace.count_nodes(arg)
84    assert_not_empty(arg)
85    bug8014 = '[ruby-core:53130] [Bug #8014]'
86    assert_empty(arg.select {|k, v| !(Symbol === k && Integer === v)}, bug8014)
87  end if false
88
89  def test_count_tdata_objects
90    res = ObjectSpace.count_tdata_objects
91    assert_not_empty(res)
92    arg = {}
93    ObjectSpace.count_tdata_objects(arg)
94    assert_not_empty(arg)
95  end
96
97  def test_count_imemo_objects
98    res = ObjectSpace.count_imemo_objects
99    assert_not_empty(res)
100    assert_not_nil(res[:imemo_cref])
101    assert_not_empty res.inspect
102
103    arg = {}
104    res = ObjectSpace.count_imemo_objects(arg)
105    assert_not_empty(res)
106  end
107
108  def test_memsize_of_iseq
109    iseqw = RubyVM::InstructionSequence.compile('def a; a = :b; end')
110    base_obj_size = ObjectSpace.memsize_of(Object.new)
111    assert_operator(ObjectSpace.memsize_of(iseqw), :>, base_obj_size)
112  end
113
114  def test_reachable_objects_from
115    assert_separately %w[--disable-gem -robjspace], "#{<<-"begin;"}\n#{<<-'end;'}"
116    begin;
117      assert_equal(nil, ObjectSpace.reachable_objects_from(nil))
118      assert_equal([Array, 'a', 'b', 'c'], ObjectSpace.reachable_objects_from(['a', 'b', 'c']))
119
120      assert_equal([Array, 'a', 'a', 'a'], ObjectSpace.reachable_objects_from(['a', 'a', 'a']))
121      assert_equal([Array, 'a', 'a'], ObjectSpace.reachable_objects_from(['a', v = 'a', v]))
122      assert_equal([Array, 'a'], ObjectSpace.reachable_objects_from([v = 'a', v, v]))
123
124      long_ary = Array.new(1_000){''}
125      max = 0
126
127      ObjectSpace.each_object{|o|
128        refs = ObjectSpace.reachable_objects_from(o)
129        max = [refs.size, max].max
130
131        unless refs.nil?
132          refs.each_with_index {|ro, i|
133            assert_not_nil(ro, "#{i}: this referenced object is internal object")
134          }
135        end
136      }
137      assert_operator(max, :>=, long_ary.size+1, "1000 elems + Array class")
138    end;
139  end
140
141  def test_reachable_objects_from_root
142    root_objects = ObjectSpace.reachable_objects_from_root
143
144    assert_operator(root_objects.size, :>, 0)
145
146    root_objects.each{|category, objects|
147      assert_kind_of(String, category)
148      assert_kind_of(Array, objects)
149      assert_operator(objects.size, :>, 0)
150    }
151  end
152
153  def test_reachable_objects_size
154    assert_separately %w[--disable-gem -robjspace], "#{<<~"begin;"}\n#{<<~'end;'}"
155    begin;
156      ObjectSpace.each_object{|o|
157        ObjectSpace.reachable_objects_from(o).each{|reached_obj|
158          size = ObjectSpace.memsize_of(reached_obj)
159          assert_kind_of(Integer, size)
160          assert_operator(size, :>=, 0)
161        }
162      }
163    end;
164  end
165
166  def test_trace_object_allocations
167    Class.name
168    o0 = Object.new
169    ObjectSpace.trace_object_allocations{
170      o1 = Object.new; line1 = __LINE__; c1 = GC.count
171      o2 = "xyzzy"   ; line2 = __LINE__; c2 = GC.count
172      o3 = [1, 2]    ; line3 = __LINE__; c3 = GC.count
173
174      assert_equal(nil, ObjectSpace.allocation_sourcefile(o0))
175      assert_equal(nil, ObjectSpace.allocation_sourceline(o0))
176      assert_equal(nil, ObjectSpace.allocation_generation(o0))
177
178      assert_equal(line1,    ObjectSpace.allocation_sourceline(o1))
179      assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o1))
180      assert_equal(c1,       ObjectSpace.allocation_generation(o1))
181      assert_equal(Class.name, ObjectSpace.allocation_class_path(o1))
182      assert_equal(:new,       ObjectSpace.allocation_method_id(o1))
183
184      assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o2))
185      assert_equal(line2,    ObjectSpace.allocation_sourceline(o2))
186      assert_equal(c2,       ObjectSpace.allocation_generation(o2))
187      assert_equal(self.class.name, ObjectSpace.allocation_class_path(o2))
188      assert_equal(__method__,      ObjectSpace.allocation_method_id(o2))
189
190      assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(o3))
191      assert_equal(line3,    ObjectSpace.allocation_sourceline(o3))
192      assert_equal(c3,       ObjectSpace.allocation_generation(o3))
193      assert_equal(self.class.name, ObjectSpace.allocation_class_path(o3))
194      assert_equal(__method__,      ObjectSpace.allocation_method_id(o3))
195    }
196  end
197
198  def test_trace_object_allocations_start_stop_clear
199    ObjectSpace.trace_object_allocations_clear # clear object_table to get rid of erroneous detection for obj3
200    GC.disable # suppress potential object reuse. see [Bug #11271]
201    begin
202      ObjectSpace.trace_object_allocations_start
203      begin
204        ObjectSpace.trace_object_allocations_start
205        begin
206          ObjectSpace.trace_object_allocations_start
207          obj0 = Object.new
208        ensure
209          ObjectSpace.trace_object_allocations_stop
210          obj1 = Object.new
211        end
212      ensure
213        ObjectSpace.trace_object_allocations_stop
214        obj2 = Object.new
215      end
216    ensure
217      ObjectSpace.trace_object_allocations_stop
218      obj3 = Object.new
219    end
220
221    assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj0))
222    assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj1))
223    assert_equal(__FILE__, ObjectSpace.allocation_sourcefile(obj2))
224    assert_equal(nil     , ObjectSpace.allocation_sourcefile(obj3)) # after tracing
225
226    ObjectSpace.trace_object_allocations_clear
227    assert_equal(nil, ObjectSpace.allocation_sourcefile(obj0))
228    assert_equal(nil, ObjectSpace.allocation_sourcefile(obj1))
229    assert_equal(nil, ObjectSpace.allocation_sourcefile(obj2))
230    assert_equal(nil, ObjectSpace.allocation_sourcefile(obj3))
231  ensure
232    GC.enable
233  end
234
235  def test_dump_flags
236    info = ObjectSpace.dump("foo".freeze)
237    assert_match /"wb_protected":true, "old":true/, info
238    assert_match /"fstring":true/, info
239    JSON.parse(info) if defined?(JSON)
240  end
241
242  def test_dump_to_default
243    line = nil
244    info = nil
245    ObjectSpace.trace_object_allocations do
246      line = __LINE__ + 1
247      str = "hello world"
248      info = ObjectSpace.dump(str)
249    end
250    assert_dump_object(info, line)
251  end
252
253  def test_dump_to_io
254    line = nil
255    info = IO.pipe do |r, w|
256      th = Thread.start {r.read}
257      ObjectSpace.trace_object_allocations do
258        line = __LINE__ + 1
259        str = "hello world"
260        ObjectSpace.dump(str, output: w)
261      end
262      w.close
263      th.value
264    end
265    assert_dump_object(info, line)
266  end
267
268  def assert_dump_object(info, line)
269    loc = caller_locations(1, 1)[0]
270    assert_match /"type":"STRING"/, info
271    assert_match /"embedded":true, "bytesize":11, "value":"hello world", "encoding":"UTF-8"/, info
272    assert_match /"file":"#{Regexp.escape __FILE__}", "line":#{line}/, info
273    assert_match /"method":"#{loc.base_label}"/, info
274    JSON.parse(info) if defined?(JSON)
275  end
276
277  def test_dump_special_consts
278    # [ruby-core:69692] [Bug #11291]
279    assert_equal('null', ObjectSpace.dump(nil))
280    assert_equal('true', ObjectSpace.dump(true))
281    assert_equal('false', ObjectSpace.dump(false))
282    assert_equal('0', ObjectSpace.dump(0))
283    assert_equal('{"type":"SYMBOL", "value":"foo"}', ObjectSpace.dump(:foo))
284  end
285
286  def test_dump_dynamic_symbol
287    dump = ObjectSpace.dump(("foobar%x" % rand(0x10000)).to_sym)
288    assert_match /"type":"SYMBOL"/, dump
289    assert_match /"value":"foobar\h+"/, dump
290  end
291
292  def test_dump_includes_imemo_type
293    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
294      begin;
295        def dump_my_heap_please
296          ObjectSpace.dump_all(output: :stdout)
297        end
298
299        dump_my_heap_please
300      end;
301      heap = output.find_all { |l|
302        obj = JSON.parse(l)
303        obj['type'] == "IMEMO" && obj['imemo_type']
304      }
305      assert_operator heap.length, :>, 0
306    end
307  end
308
309  def test_dump_all_full
310    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
311      begin;
312        def dump_my_heap_please
313          ObjectSpace.dump_all(output: :stdout, full: true)
314        end
315
316        dump_my_heap_please
317      end;
318      heap = output.find_all { |l| JSON.parse(l)['type'] == "NONE" }
319      assert_operator heap.length, :>, 0
320    end
321  end
322
323  def test_dump_addresses_match_dump_all_addresses
324    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
325      begin;
326        def dump_my_heap_please
327          obj = Object.new
328          puts ObjectSpace.dump(obj)
329          ObjectSpace.dump_all(output: $stdout)
330        end
331
332        dump_my_heap_please
333      end;
334      needle = JSON.parse(output.first)
335      addr = needle['address']
336      found  = output.drop(1).find { |l| JSON.parse(l)['address'] == addr }
337      assert found, "object #{addr} should be findable in full heap dump"
338    end
339  end
340
341  def test_dump_class_addresses_match_dump_all_addresses
342    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
343      begin;
344        def dump_my_heap_please
345          obj = Object.new
346          puts ObjectSpace.dump(obj)
347          ObjectSpace.dump_all(output: $stdout)
348        end
349
350        dump_my_heap_please
351      end;
352      needle = JSON.parse(output.first)
353      addr = needle['class']
354      found  = output.drop(1).find { |l| JSON.parse(l)['address'] == addr }
355      assert found, "object #{addr} should be findable in full heap dump"
356    end
357  end
358
359  def test_dump_reference_addresses_match_dump_all_addresses
360    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}\n#{<<-'end;'}") do |output, error|
361      begin;
362        def dump_my_heap_please
363          obj = Object.new
364          obj2 = Object.new
365          obj2.instance_variable_set(:@ref, obj)
366          puts ObjectSpace.dump(obj)
367          ObjectSpace.dump_all(output: $stdout)
368        end
369
370        dump_my_heap_please
371      end;
372      needle = JSON.parse(output.first)
373      addr = needle['address']
374      found  = output.drop(1).find { |l| (JSON.parse(l)['references'] || []).include? addr }
375      assert found, "object #{addr} should be findable in full heap dump"
376    end
377  end
378
379  def test_dump_all
380    entry = /"bytesize":11, "value":"TEST STRING", "encoding":"UTF-8", "file":"-", "line":4, "method":"dump_my_heap_please", "generation":/
381
382    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}#{<<-'end;'}") do |output, error|
383      begin;
384        def dump_my_heap_please
385          ObjectSpace.trace_object_allocations_start
386          GC.start
387          str = "TEST STRING".force_encoding("UTF-8")
388          ObjectSpace.dump_all(output: :stdout)
389        end
390
391        dump_my_heap_please
392      end;
393      assert_match(entry, output.grep(/TEST STRING/).join("\n"))
394    end
395
396    assert_in_out_err(%w[-robjspace], "#{<<-"begin;"}#{<<-'end;'}") do |(output), (error)|
397      begin;
398        def dump_my_heap_please
399          ObjectSpace.trace_object_allocations_start
400          GC.start
401          str = "TEST STRING".force_encoding("UTF-8")
402          ObjectSpace.dump_all().path
403        end
404
405        puts dump_my_heap_please
406      end;
407      skip if /is not supported/ =~ error
408      skip error unless output
409      assert_match(entry, File.readlines(output).grep(/TEST STRING/).join("\n"))
410      File.unlink(output)
411    end
412
413    if defined?(JSON)
414      args = [
415        "-rjson", "-",
416        EnvUtil.rubybin,
417        "--disable=gems", "-robjspace", "-eObjectSpace.dump_all(output: :stdout)",
418      ]
419      assert_ruby_status(args, "#{<<~"begin;"}\n#{<<~"end;"}")
420      begin;
421        IO.popen(ARGV) do |f|
422          f.each_line.map { |x| JSON.load(x) }
423        end
424      end;
425    end
426  end
427
428  def test_dump_uninitialized_file
429    assert_in_out_err(%[-robjspace], <<-RUBY) do |(output), (error)|
430      puts ObjectSpace.dump(File.allocate)
431    RUBY
432      assert_nil error
433      assert_match /"type":"FILE"/, output
434      assert_not_match /"fd":/, output
435    end
436  end
437
438  def traverse_classes klass
439    h = {}
440    while klass && !h.has_key?(klass)
441      h[klass] = true
442      klass = ObjectSpace.internal_class_of(klass)
443    end
444  end
445
446  def test_internal_class_of
447    i = 0
448    ObjectSpace.each_object{|o|
449      traverse_classes ObjectSpace.internal_class_of(o)
450      i += 1
451    }
452    assert_operator i, :>, 0
453  end
454
455  def traverse_super_classes klass
456    while klass
457      klass = ObjectSpace.internal_super_of(klass)
458    end
459  end
460
461  def all_super_classes klass
462    klasses = []
463    while klass
464      klasses << klass
465      klass = ObjectSpace.internal_super_of(klass)
466    end
467    klasses
468  end
469
470  def test_internal_super_of
471    klasses = all_super_classes(String)
472    String.ancestors.each{|k|
473      case k
474      when Class
475        assert_equal(true, klasses.include?(k), k.inspect)
476      when Module
477        assert_equal(false, klasses.include?(k), k.inspect) # Internal object (T_ICLASS)
478      end
479    }
480
481    i = 0
482    ObjectSpace.each_object(Module){|o|
483      traverse_super_classes ObjectSpace.internal_super_of(o)
484      i += 1
485    }
486    assert_operator i, :>, 0
487  end
488
489  def test_count_symbols
490    assert_separately(%w[-robjspace], "#{<<~';;;'}")
491    h0 = ObjectSpace.count_symbols
492
493    syms = (1..128).map{|i| ("xyzzy#{i}_#{Process.pid}_#{rand(1_000_000)}_" * 128).to_sym}
494    syms << Class.new{define_method(syms[-1]){}}
495
496    h = ObjectSpace.count_symbols
497    m = proc {h0.inspect + "\n" + h.inspect}
498    assert_equal 127, h[:mortal_dynamic_symbol] - h0[:mortal_dynamic_symbol],   m
499    assert_equal 1, h[:immortal_dynamic_symbol] - h0[:immortal_dynamic_symbol], m
500    assert_operator h[:immortal_static_symbol],  :>=, Object.methods.size, m
501    assert_equal h[:immortal_symbol], h[:immortal_dynamic_symbol] + h[:immortal_static_symbol], m
502    ;;;
503  end
504end
505