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