summaryrefslogtreecommitdiff
path: root/spec/ruby/core/objectspace/define_finalizer_spec.rb
blob: 281785b0a4e505b2bae9820f72e15781e5c56f4d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
require_relative '../../spec_helper'
require_relative 'fixtures/classes'

# Why do we not test that finalizers are run by the GC? The documentation
# says that finalizers are never guaranteed to be run, so we can't
# spec that they are. On some implementations of Ruby the finalizers may
# run asynchronously, meaning that we can't predict when they'll run,
# even if they were guaranteed to do so. Even on MRI finalizers can be
# very unpredictable, due to conservative stack scanning and references
# left in unused memory.

describe "ObjectSpace.define_finalizer" do
  it "raises an ArgumentError if the action does not respond to call" do
    -> {
      ObjectSpace.define_finalizer(Object.new, mock("ObjectSpace.define_finalizer no #call"))
    }.should raise_error(ArgumentError)
  end

  it "accepts an object and a proc" do
    handler = -> id { id }
    ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler]
  end

  it "accepts an object and a bound method" do
    handler = mock("callable")
    def handler.finalize(id) end
    finalize = handler.method(:finalize)
    ObjectSpace.define_finalizer(Object.new, finalize).should == [0, finalize]
  end

  it "accepts an object and a callable" do
    handler = mock("callable")
    def handler.call(id) end
    ObjectSpace.define_finalizer(Object.new, handler).should == [0, handler]
  end

  it "accepts an object and a block" do
    handler = -> id { id }
    ObjectSpace.define_finalizer(Object.new, &handler).should == [0, handler]
  end

  it "raises ArgumentError trying to define a finalizer on a non-reference" do
    -> {
      ObjectSpace.define_finalizer(:blah) { 1 }
    }.should raise_error(ArgumentError)
  end

  # see [ruby-core:24095]
  it "calls finalizer on process termination" do
    code = <<-RUBY
      def scoped
        Proc.new { puts "finalizer run" }
      end
      handler = scoped
      obj = "Test"
      ObjectSpace.define_finalizer(obj, handler)
      exit 0
    RUBY

    ruby_exe(code, :args => "2>&1").should include("finalizer run\n")
  end

  ruby_version_is "3.0" do
    it "warns if the finalizer has the object as the receiver" do
      code = <<-RUBY
        class CapturesSelf
          def initialize
            ObjectSpace.define_finalizer(self, proc {
              puts "finalizer run"
            })
          end
        end
        CapturesSelf.new
        exit 0
      RUBY

      ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n")
    end

    it "warns if the finalizer is a method bound to the receiver" do
      code = <<-RUBY
        class CapturesSelf
          def initialize
            ObjectSpace.define_finalizer(self, method(:finalize))
          end
          def finalize(id)
            puts "finalizer run"
          end
        end
        CapturesSelf.new
        exit 0
      RUBY

      ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n")
    end

    it "warns if the finalizer was a block in the receiver" do
      code = <<-RUBY
        class CapturesSelf
          def initialize
            ObjectSpace.define_finalizer(self) do
              puts "finalizer run"
            end
          end
        end
        CapturesSelf.new
        exit 0
      RUBY

      ruby_exe(code, :args => "2>&1").should include("warning: finalizer references object to be finalized\n")
    end
  end

  it "calls a finalizer at exit even if it is self-referencing" do
    code = <<-RUBY
      obj = "Test"
      handler = Proc.new { puts "finalizer run" }
      ObjectSpace.define_finalizer(obj, handler)
      exit 0
    RUBY

    ruby_exe(code).should include("finalizer run\n")
  end

  it "calls a finalizer at exit even if it is indirectly self-referencing" do
    code = <<-RUBY
      class CapturesSelf
        def initialize
          ObjectSpace.define_finalizer(self, finalizer(self))
        end
        def finalizer(zelf)
          proc do
            puts "finalizer run"
          end
        end
      end
      CapturesSelf.new
      exit 0
    RUBY

    ruby_exe(code, :args => "2>&1").should include("finalizer run\n")
  end

  it "calls a finalizer defined in a finalizer running at exit" do
    code = <<-RUBY
      obj = "Test"
      handler = Proc.new do
        obj2 = "Test"
        handler2 = Proc.new { puts "finalizer 2 run" }
        ObjectSpace.define_finalizer(obj2, handler2)
        exit 0
      end
      ObjectSpace.define_finalizer(obj, handler)
      exit 0
    RUBY

    ruby_exe(code, :args => "2>&1").should include("finalizer 2 run\n")
  end

  it "allows multiple finalizers with different 'callables' to be defined" do
    code = <<-RUBY
      obj = Object.new

      ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized1\n" })
      ObjectSpace.define_finalizer(obj, Proc.new { STDOUT.write "finalized2\n" })

      exit 0
    RUBY

    ruby_exe(code).lines.sort.should == ["finalized1\n", "finalized2\n"]
  end
end