# frozen_string_literal: false
require 'test/unit'

class TestGc < Test::Unit::TestCase
  class S
    def initialize(a)
      @a = a
    end
  end

  def test_gc
    prev_stress = GC.stress
    GC.stress = false

    assert_nothing_raised do
      tmp = nil
      1.upto(10000) {
        tmp = [0,1,2,3,4,5,6,7,8,9]
      }
      tmp
    end
    l=nil
    100000.times {
      l = S.new(l)
    }
    GC.start
    assert true   # reach here or dumps core
    l = []
    100000.times {
      l.push([l])
    }
    GC.start
    assert true   # reach here or dumps core

    GC.stress = prev_stress
  end

  def use_rgengc?
    GC::OPTS.include? 'USE_RGENGC'.freeze
  end

  def test_enable_disable
    EnvUtil.without_gc do
      GC.enable
      assert_equal(false, GC.enable)
      assert_equal(false, GC.disable)
      assert_equal(true, GC.disable)
      assert_equal(true, GC.disable)
      assert_nil(GC.start)
      assert_equal(true, GC.enable)
      assert_equal(false, GC.enable)
    end
  end

  def test_gc_config_full_mark_by_default
    config = GC.config
    assert_not_empty(config)
    assert_true(config[:rgengc_allow_full_mark])
  end

  def test_gc_config_invalid_args
    assert_raise(ArgumentError) { GC.config(0) }
  end

  def test_gc_config_setting_returns_updated_config_hash
    old_value = GC.config[:rgengc_allow_full_mark]
    assert_true(old_value)

    new_value = GC.config(rgengc_allow_full_mark: false)[:rgengc_allow_full_mark]
    assert_false(new_value)
    new_value = GC.config(rgengc_allow_full_mark: nil)[:rgengc_allow_full_mark]
    assert_false(new_value)
  ensure
    GC.config(rgengc_allow_full_mark: old_value)
    GC.start
  end

  def test_gc_config_setting_returns_nil_for_missing_keys
    missing_value = GC.config(no_such_key: true)[:no_such_key]
    assert_nil(missing_value)
  ensure
    GC.config(full_mark: true)
    GC.start
  end

  def test_gc_config_disable_major
    GC.enable
    GC.start

    GC.config(rgengc_allow_full_mark: false)
    major_count = GC.stat[:major_gc_count]
    minor_count = GC.stat[:minor_gc_count]

    arr = []
    (GC.stat_heap[0][:heap_eden_slots] * 2).times do
      arr << Object.new
      Object.new
    end

    assert_equal(major_count, GC.stat[:major_gc_count])
    assert_operator(minor_count, :<=, GC.stat[:minor_gc_count])
    assert_nil(GC.start)
  ensure
    GC.config(rgengc_allow_full_mark: true)
    GC.start
  end

  def test_gc_config_disable_major_gc_start_always_works
    GC.config(full_mark: false)

    major_count = GC.stat[:major_gc_count]
    GC.start

    assert_operator(major_count, :<, GC.stat[:major_gc_count])
  ensure
    GC.config(full_mark: true)
    GC.start
  end

  def test_gc_config_implementation
    omit unless /darwin|linux/.match(RUBY_PLATFORM)

    gc_name = (ENV['RUBY_GC_LIBRARY'] || "default")
    assert_equal gc_name, GC.config[:implementation]
  end

  def test_gc_config_implementation_is_readonly
    omit unless /darwin|linux/.match(RUBY_PLATFORM)

    assert_raise(ArgumentError) { GC.config(implementation: "somethingelse") }
  end

  def test_start_full_mark
    return unless use_rgengc?
    omit 'stress' if GC.stress

    3.times { GC.start } # full mark and next time it should be minor mark
    GC.start(full_mark: false)
    assert_nil GC.latest_gc_info(:major_by)

    GC.start(full_mark: true)
    assert_not_nil GC.latest_gc_info(:major_by)
  end

  def test_start_immediate_sweep
    omit 'stress' if GC.stress

    GC.start(immediate_sweep: false)
    assert_equal false, GC.latest_gc_info(:immediate_sweep)

    GC.start(immediate_sweep: true)
    assert_equal true, GC.latest_gc_info(:immediate_sweep)
  end

  def test_count
    c = GC.count
    GC.start
    assert_operator(c, :<, GC.count)
  end

  def test_stat
    res = GC.stat
    assert_equal(false, res.empty?)
    assert_kind_of(Integer, res[:count])

    arg = Hash.new
    res = GC.stat(arg)
    assert_equal(arg, res)
    assert_equal(false, res.empty?)
    assert_kind_of(Integer, res[:count])

    stat, count = {}, {}
    2.times{ # to ignore const cache imemo creation
      GC.start
      GC.stat(stat)
      ObjectSpace.count_objects(count)
      # repeat same methods invocation for cache object creation.
      GC.stat(stat)
      ObjectSpace.count_objects(count)
    }
    assert_equal(count[:TOTAL]-count[:FREE], stat[:heap_live_slots])
    assert_equal(count[:FREE], stat[:heap_free_slots])

    # measure again without GC.start
    2.times{ # to ignore const cache imemo creation
      1000.times{ "a" + "b" }
      GC.stat(stat)
      ObjectSpace.count_objects(count)
    }
    assert_equal(count[:FREE], stat[:heap_free_slots])
  end

  def test_stat_argument
    assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) {GC.stat(:"\u{30eb 30d3 30fc}")}
  end

  def test_stat_single
    omit 'stress' if GC.stress

    stat = GC.stat
    assert_equal stat[:count], GC.stat(:count)
    assert_raise(ArgumentError){ GC.stat(:invalid) }
  end

  def test_stat_constraints
    omit 'stress' if GC.stress

    stat = GC.stat
    # marking_time + sweeping_time could differ from time by 1 because they're stored in nanoseconds
    assert_in_delta stat[:time], stat[:marking_time] + stat[:sweeping_time], 1
    assert_equal stat[:total_allocated_pages], stat[:heap_allocated_pages] + stat[:total_freed_pages]
    assert_equal stat[:heap_available_slots], stat[:heap_live_slots] + stat[:heap_free_slots] + stat[:heap_final_slots]
    assert_equal stat[:heap_live_slots], stat[:total_allocated_objects] - stat[:total_freed_objects] - stat[:heap_final_slots]
    assert_equal stat[:heap_allocated_pages], stat[:heap_eden_pages]

    if use_rgengc?
      assert_equal stat[:count], stat[:major_gc_count] + stat[:minor_gc_count]
    end
  end

  def test_stat_heap
    omit 'stress' if GC.stress

    stat_heap = {}
    stat = {}
    # Initialize to prevent GC in future calls
    GC.stat_heap(0, stat_heap)
    GC.stat(stat)

    GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
      EnvUtil.without_gc do
        GC.stat_heap(i, stat_heap)
        GC.stat(stat)
      end

      assert_equal (GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE] + GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD]) * (2**i), stat_heap[:slot_size]
      assert_operator stat_heap[:heap_eden_pages], :<=, stat[:heap_eden_pages]
      assert_operator stat_heap[:heap_eden_slots], :>=, 0
      assert_operator stat_heap[:total_allocated_pages], :>=, 0
      assert_operator stat_heap[:force_major_gc_count], :>=, 0
      assert_operator stat_heap[:force_incremental_marking_finish_count], :>=, 0
      assert_operator stat_heap[:total_allocated_objects], :>=, 0
      assert_operator stat_heap[:total_freed_objects], :>=, 0
      assert_operator stat_heap[:total_freed_objects], :<=, stat_heap[:total_allocated_objects]
    end

    GC.stat_heap(0, stat_heap)
    assert_equal stat_heap[:slot_size], GC.stat_heap(0, :slot_size)
    assert_equal stat_heap[:slot_size], GC.stat_heap(0)[:slot_size]

    assert_raise(ArgumentError) { GC.stat_heap(-1) }
    assert_raise(ArgumentError) { GC.stat_heap(GC::INTERNAL_CONSTANTS[:HEAP_COUNT]) }
  end

  def test_stat_heap_all
    omit "flaky with RJIT, which allocates objects itself" if defined?(RubyVM::RJIT) && RubyVM::RJIT.enabled?
    stat_heap_all = {}
    stat_heap = {}
    # Initialize to prevent GC in future calls
    GC.stat_heap(0, stat_heap)
    GC.stat_heap(nil, stat_heap_all)

    GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
      GC.stat_heap(nil, stat_heap_all)
      GC.stat_heap(i, stat_heap)

      # Remove keys that can vary between invocations
      %i(total_allocated_objects).each do |sym|
        stat_heap[sym] = stat_heap_all[i][sym] = 0
      end

      assert_equal stat_heap, stat_heap_all[i]
    end

    assert_raise(TypeError) { GC.stat_heap(nil, :slot_size) }
  end

  def test_stat_heap_constraints
    omit 'stress' if GC.stress

    stat = GC.stat
    stat_heap = GC.stat_heap
    2.times do
      GC.stat(stat)
      GC.stat_heap(nil, stat_heap)
    end

    stat_heap_sum = Hash.new(0)
    stat_heap.values.each do |hash|
      hash.each { |k, v| stat_heap_sum[k] += v }
    end

    assert_equal stat[:heap_eden_pages], stat_heap_sum[:heap_eden_pages]
    assert_equal stat[:heap_available_slots], stat_heap_sum[:heap_eden_slots]
    assert_equal stat[:total_allocated_objects], stat_heap_sum[:total_allocated_objects]
    assert_equal stat[:total_freed_objects], stat_heap_sum[:total_freed_objects]
  end

  def test_measure_total_time
    assert_separately([], __FILE__, __LINE__, <<~RUBY)
      GC.measure_total_time = false

      time_before = GC.stat(:time)

      # Generate some garbage
      Random.new.bytes(100 * 1024 * 1024)
      GC.start

      time_after = GC.stat(:time)

      # If time measurement is disabled, the time stat should not change
      assert_equal time_before, time_after
    RUBY
  end

  def test_latest_gc_info
    omit 'stress' if GC.stress

    assert_separately([], __FILE__, __LINE__, <<-'RUBY')
      GC.start
      count = GC.stat(:heap_free_slots) + GC.stat(:heap_allocatable_slots)
      count.times{ "a" + "b" }
      assert_equal :newobj, GC.latest_gc_info[:gc_by]
    RUBY

    GC.latest_gc_info(h = {}) # allocate hash and rehearsal
    GC.start
    GC.start
    GC.start
    GC.latest_gc_info(h)

    assert_equal :force,  h[:major_by] if use_rgengc?
    assert_equal :method, h[:gc_by]
    assert_equal true,    h[:immediate_sweep]
    assert_equal true,    h.key?(:need_major_by)

    GC.stress = true
    assert_equal :force, GC.latest_gc_info[:major_by]
  ensure
    GC.stress = false
  end

  def test_latest_gc_info_argument
    info = {}
    GC.latest_gc_info(info)

    assert_not_empty info
    assert_equal info[:gc_by], GC.latest_gc_info(:gc_by)
    assert_raise(ArgumentError){ GC.latest_gc_info(:invalid) }
    assert_raise_with_message(ArgumentError, /\u{30eb 30d3 30fc}/) {GC.latest_gc_info(:"\u{30eb 30d3 30fc}")}
  end

  def test_latest_gc_info_need_major_by
    return unless use_rgengc?
    omit 'stress' if GC.stress

    3.times { GC.start }
    assert_nil GC.latest_gc_info(:need_major_by)

    # allocate objects until need_major_by is set or major GC happens
    objects = []
    while GC.latest_gc_info(:need_major_by).nil?
      objects.append(100.times.map { '*' })
    end

    EnvUtil.without_gc do
      # We need to ensure that no GC gets ran before the call to GC.start since
      # it would trigger a major GC. Assertions could allocate objects and
      # trigger a GC so we don't run assertions until we perform the major GC.
      need_major_by = GC.latest_gc_info(:need_major_by)
      GC.start(full_mark: false) # should be upgraded to major
      major_by = GC.latest_gc_info(:major_by)

      assert_not_nil(need_major_by)
      assert_not_nil(major_by)
    end
  end

  def test_latest_gc_info_weak_references_count
    assert_separately([], __FILE__, __LINE__, <<~RUBY)
      GC.disable
      count = 10_000
      # Some weak references may be created, so allow some margin of error
      error_tolerance = 100

      # Run full GC to clear out weak references
      GC.start
      # Run full GC again to collect stats about weak references
      GC.start

      before_weak_references_count = GC.latest_gc_info(:weak_references_count)
      before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count)

      # Create some objects and place it in a WeakMap
      wmap = ObjectSpace::WeakMap.new
      ary = Array.new(count)
      enum = count.times
      enum.each.with_index do |i|
        obj = Object.new
        ary[i] = obj
        wmap[obj] = nil
      end

      # Run full GC to collect stats about weak references
      GC.start

      assert_operator(GC.latest_gc_info(:weak_references_count), :>=, before_weak_references_count + count - error_tolerance)
      assert_operator(GC.latest_gc_info(:retained_weak_references_count), :>=, before_retained_weak_references_count + count - error_tolerance)
      assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count))

      before_weak_references_count = GC.latest_gc_info(:weak_references_count)
      before_retained_weak_references_count = GC.latest_gc_info(:retained_weak_references_count)

      ary = nil

      # Free ary, which should empty out the wmap
      GC.start
      # Run full GC again to collect stats about weak references
      GC.start

      # Sometimes the WeakMap has one element, which might be held on by registers.
      assert_operator(wmap.size, :<=, 1)

      assert_operator(GC.latest_gc_info(:weak_references_count), :<=, before_weak_references_count - count + error_tolerance)
      assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, before_retained_weak_references_count - count + error_tolerance)
      assert_operator(GC.latest_gc_info(:retained_weak_references_count), :<=, GC.latest_gc_info(:weak_references_count))
    RUBY
  end

  def test_stress_compile_send
    assert_in_out_err([], <<-EOS, [], [], "")
      GC.stress = true
      begin
        eval("A::B.c(1, 1, d: 234)")
      rescue
      end
    EOS
  end

  def test_singleton_method
    assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:42832]")
      GC.stress = true
      10.times do
        obj = Object.new
        def obj.foo() end
        def obj.bar() raise "obj.foo is called, but this is obj.bar" end
        obj.foo
      end
    EOS
  end

  def test_singleton_method_added
    assert_in_out_err([], <<-EOS, [], [], "[ruby-dev:44436]")
      class BasicObject
        undef singleton_method_added
        def singleton_method_added(mid)
          raise
        end
      end
      b = proc {}
      class << b; end
      b.clone rescue nil
      GC.start
    EOS
  end

  def test_gc_parameter
    env = {
      "RUBY_GC_HEAP_INIT_SLOTS" => "100"
    }
    assert_in_out_err([env, "-W0", "-e", "exit"], "", [], [])
    assert_in_out_err([env, "-W:deprecated", "-e", "exit"], "", [],
                       /The environment variable RUBY_GC_HEAP_INIT_SLOTS is deprecated; use environment variables RUBY_GC_HEAP_%d_INIT_SLOTS instead/)

    env = {}
    GC.stat_heap.keys.each do |heap|
      env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "200000"
    end
    assert_normal_exit("exit", "", :child_env => env)

    env = {}
    GC.stat_heap.keys.each do |heap|
      env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = "0"
    end
    assert_normal_exit("exit", "", :child_env => env)

    env = {
      "RUBY_GC_HEAP_GROWTH_FACTOR" => "2.0",
      "RUBY_GC_HEAP_GROWTH_MAX_SLOTS" => "10000"
    }
    assert_normal_exit("exit", "", :child_env => env)
    assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_FACTOR=2.0/, "")
    assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_HEAP_GROWTH_MAX_SLOTS=10000/, "[ruby-core:57928]")

    if use_rgengc?
      env = {
        "RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0.4",
      }
      # always full GC when RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR < 1.0
      assert_in_out_err([env, "-e", "GC.start; 1000_000.times{Object.new}; p(GC.stat[:minor_gc_count] < GC.stat[:major_gc_count])"], "", ['true'], //, "")
    end

    env = {
      "RUBY_GC_MALLOC_LIMIT"               => "60000000",
      "RUBY_GC_MALLOC_LIMIT_MAX"           => "160000000",
      "RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR" => "2.0"
    }
    assert_normal_exit("exit", "", :child_env => env)
    assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT=6000000/, "")
    assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT_MAX=16000000/, "")
    assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_MALLOC_LIMIT_GROWTH_FACTOR=2.0/, "")

    if use_rgengc?
      env = {
        "RUBY_GC_OLDMALLOC_LIMIT"               => "60000000",
        "RUBY_GC_OLDMALLOC_LIMIT_MAX"           => "160000000",
        "RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR" => "2.0"
      }
      assert_normal_exit("exit", "", :child_env => env)
      assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT=6000000/, "")
      assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_MAX=16000000/, "")
      assert_in_out_err([env, "-w", "-e", "exit"], "", [], /RUBY_GC_OLDMALLOC_LIMIT_GROWTH_FACTOR=2.0/, "")
    end

    ["0.01", "0.1", "1.0"].each do |i|
      env = {"RUBY_GC_HEAP_OLDOBJECT_LIMIT_FACTOR" => "0", "RUBY_GC_HEAP_REMEMBERED_WB_UNPROTECTED_OBJECTS_LIMIT_RATIO" => i}
      assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY)
        GC.disable
        GC.start
        assert_equal((GC.stat[:old_objects] * #{i}).to_i, GC.stat[:remembered_wb_unprotected_objects_limit])
      RUBY
    end
  end

  def test_gc_parameter_init_slots
    assert_separately([], __FILE__, __LINE__, <<~RUBY, timeout: 60)
      # Constant from gc.c.
      GC_HEAP_INIT_SLOTS = 10_000

      gc_count = GC.stat(:count)
      # Fill up all of the size pools to the init slots
      GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
        capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"]
        while GC.stat_heap(i, :heap_eden_slots) < GC_HEAP_INIT_SLOTS
          Array.new(capa)
        end
      end

      assert_equal gc_count, GC.stat(:count)
    RUBY

    env = {}
    sizes = GC.stat_heap.keys.reverse.map { 20_000 }
    GC.stat_heap.keys.each do |heap|
      env["RUBY_GC_HEAP_#{heap}_INIT_SLOTS"] = sizes[heap].to_s
    end
    assert_separately([env, "-W0"], __FILE__, __LINE__, <<~RUBY, timeout: 60)
      SIZES = #{sizes}

      gc_count = GC.stat(:count)
      # Fill up all of the size pools to the init slots
      GC::INTERNAL_CONSTANTS[:HEAP_COUNT].times do |i|
        capa = (GC.stat_heap(i, :slot_size) - GC::INTERNAL_CONSTANTS[:RVALUE_OVERHEAD] - (2 * RbConfig::SIZEOF["void*"])) / RbConfig::SIZEOF["void*"]
        while GC.stat_heap(i, :heap_eden_slots) < SIZES[i]
          Array.new(capa)
        end
      end

      assert_equal gc_count, GC.stat(:count)
    RUBY
  end

  def test_profiler_enabled
    GC::Profiler.enable
    assert_equal(true, GC::Profiler.enabled?)
    GC::Profiler.disable
    assert_equal(false, GC::Profiler.enabled?)
  ensure
    GC::Profiler.disable
  end

  def test_profiler_clear
    omit "for now"
    assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 30)
      GC::Profiler.enable

      GC.start
      assert_equal(1, GC::Profiler.raw_data.size)
      GC::Profiler.clear
      assert_equal(0, GC::Profiler.raw_data.size)

      200.times{ GC.start }
      assert_equal(200, GC::Profiler.raw_data.size)
      GC::Profiler.clear
      assert_equal(0, GC::Profiler.raw_data.size)
    RUBY
  end

  def test_profiler_raw_data
    GC::Profiler.enable
    GC.start
    assert GC::Profiler.raw_data
  ensure
    GC::Profiler.disable
  end

  def test_profiler_total_time
    GC::Profiler.enable
    GC::Profiler.clear

    GC.start
    assert_operator(GC::Profiler.total_time, :>=, 0)
  ensure
    GC::Profiler.disable
  end

  def test_finalizing_main_thread
    assert_in_out_err([], <<-EOS, ["\"finalize\""], [], "[ruby-dev:46647]")
      ObjectSpace.define_finalizer(Thread.main) { p 'finalize' }
    EOS
  end

  def test_expand_heap
    assert_separately([], __FILE__, __LINE__, <<~'RUBY')
      GC.start
      base_length = GC.stat[:heap_eden_pages]
      (base_length * 500).times{ 'a' }
      GC.start
      base_length = GC.stat[:heap_eden_pages]
      (base_length * 500).times{ 'a' }
      GC.start
      assert_in_epsilon base_length, (v = GC.stat[:heap_eden_pages]), 1/8r,
            "invalid heap expanding (base_length: #{base_length}, GC.stat[:heap_eden_pages]: #{v})"

      a = []
      (base_length * 500).times{ a << 'a'; nil }
      GC.start
      assert_operator base_length, :<, GC.stat[:heap_eden_pages] + 1
    RUBY
  end

  def test_thrashing_for_young_objects
    # This test prevents bugs like [Bug #18929]

    assert_separately([], __FILE__, __LINE__, <<-'RUBY', timeout: 60)
      # Grow the heap
      @ary = 100_000.times.map { Object.new }

      # Warmup to make sure heap stabilizes
      1_000_000.times { Object.new }

      # We need to pre-allocate all the hashes for GC.stat calls, because
      # otherwise the call to GC.stat/GC.stat_heap itself could cause a new
      # page to be allocated and the before/after assertions will fail
      before_stats = {}
      after_stats = {}
      # stat_heap needs a hash of hashes for each heap; easiest way to get the
      # right shape for that is just to call stat_heap with no argument
      before_stat_heap = GC.stat_heap
      after_stat_heap = GC.stat_heap

      # Now collect the actual stats
      GC.stat before_stats
      GC.stat_heap nil, before_stat_heap

      1_000_000.times { Object.new }

      # Previous loop may have caused GC to be in an intermediate state,
      # running a minor GC here will guarantee that GC will be complete
      GC.start(full_mark: false)

      GC.stat after_stats
      GC.stat_heap nil, after_stat_heap

      # Debugging output to for failures in trunk-repeat50@phosphorus-docker
      debug_msg = "before_stats: #{before_stats}\nbefore_stat_heap: #{before_stat_heap}\nafter_stats: #{after_stats}\nafter_stat_heap: #{after_stat_heap}"

      # Should not be thrashing in page creation
      assert_equal before_stats[:heap_allocated_pages], after_stats[:heap_allocated_pages], debug_msg
      assert_equal 0, after_stats[:heap_empty_pages], debug_msg
      assert_equal 0, after_stats[:total_freed_pages], debug_msg
      # Only young objects, so should not trigger major GC
      assert_equal before_stats[:major_gc_count], after_stats[:major_gc_count], debug_msg
    RUBY
  end

  def test_gc_internals
    assert_not_nil GC::INTERNAL_CONSTANTS[:HEAP_PAGE_OBJ_LIMIT]
    assert_not_nil GC::INTERNAL_CONSTANTS[:BASE_SLOT_SIZE]
  end

  def test_sweep_in_finalizer
    bug9205 = '[ruby-core:58833] [Bug #9205]'
    2.times do
      assert_ruby_status([], <<-'end;', bug9205, timeout: 120)
        raise_proc = proc do |id|
          GC.start
        end
        1000.times do
          ObjectSpace.define_finalizer(Object.new, raise_proc)
        end
      end;
    end
  end

  def test_exception_in_finalizer
    bug9168 = '[ruby-core:58652] [Bug #9168]'
    assert_normal_exit(<<-'end;', bug9168, encoding: Encoding::ASCII_8BIT)
      raise_proc = proc {raise}
      10000.times do
        ObjectSpace.define_finalizer(Object.new, raise_proc)
        Thread.handle_interrupt(RuntimeError => :immediate) {break}
        Thread.handle_interrupt(RuntimeError => :on_blocking) {break}
        Thread.handle_interrupt(RuntimeError => :never) {break}
      end
    end;
  end

  def test_interrupt_in_finalizer
    bug10595 = '[ruby-core:66825] [Bug #10595]'
    src = <<-'end;'
      Signal.trap(:INT, 'DEFAULT')
      pid = $$
      Thread.start do
        10.times {
          sleep 0.1
          Process.kill("INT", pid) rescue break
        }
      end
      f = proc {1000.times {}}
      loop do
        ObjectSpace.define_finalizer(Object.new, f)
      end
    end;
    out, err, status = assert_in_out_err(["-e", src], "", [], [], bug10595, signal: :SEGV) do |*result|
      break result
    end
    unless /mswin|mingw/ =~ RUBY_PLATFORM
      assert_equal("INT", Signal.signame(status.termsig), bug10595)
    end
    assert_match(/Interrupt/, err.first, proc {err.join("\n")})
    assert_empty(out)
  end

  def test_finalizer_passed_object_id
    assert_in_out_err([], <<~RUBY, ["true"], [])
      o = Object.new
      obj_id = o.object_id
      ObjectSpace.define_finalizer(o, ->(id){ p id == obj_id })
    RUBY
  end

  def test_verify_internal_consistency
    assert_nil(GC.verify_internal_consistency)
  end

  def test_gc_stress_on_realloc
    assert_normal_exit(<<-'end;', '[Bug #9859]')
      class C
        def initialize
          @a = nil
          @b = nil
          @c = nil
          @d = nil
          @e = nil
          @f = nil
        end
      end

      GC.stress = true
      C.new
    end;
  end

  def test_gc_stress_at_startup
    assert_in_out_err([{"RUBY_DEBUG"=>"gc_stress"}], '', [], [], '[Bug #15784]', success: true, timeout: 60)
  end

  def test_gc_disabled_start
    EnvUtil.without_gc do
      c = GC.count
      GC.start
      assert_equal 1, GC.count - c
    end

    EnvUtil.without_gc do
      c = GC.count
      GC.start(immediate_mark: false, immediate_sweep: false)
      10_000.times { Object.new }
      assert_equal 1, GC.count - c
    end
  end

  def test_vm_object
    assert_normal_exit <<-'end', '[Bug #12583]'
      ObjectSpace.each_object{|o| o.singleton_class rescue 0}
      ObjectSpace.each_object{|o| case o when Module then o.instance_methods end}
    end
  end

  def test_exception_in_finalizer_procs
    assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2])
    c1 = proc do
      puts "c1"
      raise
    end
    c2 = proc do
      puts "c2"
      raise
    end
    begin;
      tap do
        obj = Object.new
        ObjectSpace.define_finalizer(obj, c1)
        ObjectSpace.define_finalizer(obj, c2)
        obj = nil
      end
    end;
  end

  def test_exception_in_finalizer_method
    assert_in_out_err(["-W0"], "#{<<~"begin;"}\n#{<<~'end;'}", %w[c1 c2])
    def self.c1(x)
      puts "c1"
      raise
    end
    def self.c2(x)
      puts "c2"
      raise
    end
    begin;
      tap do
        obj = Object.new
        ObjectSpace.define_finalizer(obj, method(:c1))
        ObjectSpace.define_finalizer(obj, method(:c2))
        obj = nil
      end
    end;

    assert_normal_exit "#{<<~"begin;"}\n#{<<~'end;'}", '[Bug #20042]'
    begin;
      def (f = Object.new).call = nil # missing ID
      o = Object.new
      ObjectSpace.define_finalizer(o, f)
      o = nil
      GC.start
    end;
  end

  def test_object_ids_never_repeat
    GC.start
    a = 1000.times.map { Object.new.object_id }
    GC.start
    b = 1000.times.map { Object.new.object_id }
    assert_empty(a & b)
  end

  def test_ast_node_buffer
    # https://github.com/ruby/ruby/pull/4416
    Module.new.class_eval( (["# shareable_constant_value: literal"] +
                            (0..100000).map {|i| "M#{ i } = {}" }).join("\n"))
  end

  def test_old_to_young_reference
    EnvUtil.without_gc do
      require "objspace"

      old_obj = Object.new
      4.times { GC.start }

      assert_include ObjectSpace.dump(old_obj), '"old":true'

      young_obj = Object.new
      old_obj.instance_variable_set(:@test, young_obj)

      # Not immediately promoted to old generation
      3.times do
        assert_not_include ObjectSpace.dump(young_obj), '"old":true'
        GC.start
      end

      # Takes 4 GC to promote to old generation
      GC.start
      assert_include ObjectSpace.dump(young_obj), '"old":true'
    end
  end
end
