Mike H-R Mike H-R - 3 months ago 24
Ruby Question

How do you test whether a Ruby destructor will be called?

I have created a class which I want to hang on to a file descriptor and close it when the instance is GC-ed.

I created a class that looks something like this:

class DataWriter
def initialize(file)
# open file
@file = File.open(file, 'wb')
# create destructor
ObjectSpace.define_finalizer(self, self.class.finalize(@file))
end

# write
def write(line)
@file.puts(line)
@file.flush
end

# close file descriptor, note, important that it is a class method
def self.finalize(file)
proc { file.close; p "file closed"; p file.inspect}
end
end


I then tried to test the destructor method like so:

RSpec.describe DataWriter do
context 'it should call its destructor' do
it 'calls the destructor' do
data_writer = DataWriter.new('/tmp/example.txt')
expect(DataWriter).to receive(:finalize)
data_writer = nil
GC.start
end
end
end


When running this test, even though the "file closed" is printed along with the file.inspect, the test fails with the following output:

1) DataWriter it should call its destructor calls the destructor
Failure/Error: expect(DataWriter).to receive(:finalize)

(DataWriter (class)).finalize(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
# ./spec/utils/data_writer_spec.rb:23:in `block (3 levels) in <top (required)>'

Answer

finalize is called in initialize, returns the proc, and is never called again, so you can't expect it to be called at finalization time. It's the proc that's called when the instance is finalized. To check that, have the proc call a method instead of doing the work itself. This passes:

class DataWriter
  # initialize and write same as above

  def self.finalize(file)
    proc { actually_finalize file }
  end

  def self.actually_finalize(file)
    file.close
  end

end

RSpec.describe DataWriter do
  context 'it should call its destructor' do
    it 'calls the destructor' do
      data_writer = DataWriter.new('/tmp/example.txt')
      expect(DataWriter).to receive(:actually_finalize)
      data_writer = nil
      GC.start
    end
  end
end
Comments