您如何测试 Ruby 析构函数是否会被调用?

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

我创建了一个 class,我想将其挂在文件描述符上并在实例被 GC 编辑时将其关闭。

我创建了一个看起来像这样的 class:

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

然后我尝试像这样测试析构函数方法:

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

当 运行 此测试时,即使 "file closed" 与 file.inspect 一起打印,测试失败并输出以下内容:

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)>'

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

我将您的代码放入一个文件中 运行。鉴于我收到的输出,在 rspec 退出之前,最终代码似乎没有被清理:

Failures:
F

  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
     # /scratch/data_writer.rb:27:in `block (3 levels) in <top (required)>'

Finished in 0.01066 seconds (files took 0.16847 seconds to load)
1 example, 1 failure

Failed examples:

rspec /scratch/data_writer.rb:25 # DataWriter it should call its destructor calls the destructor

"file closed"
"#<File:/tmp/example.txt (closed)>"

至于为什么,我现在还不能说。 你在断言已经发生的事情,所以你的测试永远不会要过去了。您可以通过将测试更改为:

来观察这一点
 it 'calls the destructor' do
   expect(DataWriter).to receive(:finalize).and_call_original
   data_writer = DataWriter.new('/tmp/example.txt')
   data_writer = nil
   GC.start
 end

finalizeinitialize、returns 过程中被调用,并且再也不会被调用,所以你不能期望它在完成时被调用。这是在实例完成时调用的 proc。要检查这一点,让 proc 调用一个方法而不是自己完成工作。这通过了:

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

恕我直言,您不应该在 GC 运行 时完全依赖终结器 运行。他们最终会 运行。但也许只有当流程结束时。据我所知,这也取决于 Ruby 实现和 GC 实现。 1.8 的行为与 1.9+ 不同,Rubinius 和 JRuby 也可能不同。

确保资源的释放可以通过块来实现,这也将确保资源在不再需要时立即释放。

Ruby多个API风格相同:

File.open('thing.txt', 'wb') do |file| # file is passed to block
                                       # do something with file
end                                    # file will be closed when block ends

而不是这样做(正如你在要点中展示的那样)

(1..100_000).each do |i|
  File.open(filename, 'ab') do |file|
    file.puts "line: #{i}"
  end
end

我会这样做:

File.open(filename, 'wb') do |file|
  (1..100_000).each do |i|
    file.puts "line: #{i}"
  end
end

我在下面重写了我的工作解决方案,但我没有运行这段代码。

RSpec.describe DataWriter do
  context 'it should call its destructor' do
    it 'calls the destructor' do

      # creating pipe for IPC to get result from child process
      # after it garbaged
      # http://ruby-doc.org/core-2.0.0/IO.html#method-c-pipe
      rd, wr = IO.pipe

      # forking 
      # https://ruby-doc.org/core-2.1.2/Process.html#method-c-fork
      if fork      
        wr.close
        called = rd.read
        Process.wait
        expect(called).to eq('/tmp/example.txt')
        rd.close
      else
        rd.close
      # overriding DataWriter.actually_finalize(file)
        DataWriter.singleton_class.class_eval do
          define_method(:actually_finalize) do |arg|
            wr.write arg
            wr.close
          end
        end

        data_writer = DataWriter.new('/tmp/example.txt')
        data_writer = nil
        GC.start
      end
    end
  end
end

主要是我发现 GC.start 调用在退出进程时执行实际工作。我已经尝试过块和线程,但在我的情况下 (ruby 2.2.4p230 @ Ubuntu x86_64) 它仅在进程完成时才有效。

我建议,可能存在从子进程获取结果的更好方法,但我使用了进程间通信 (IPC)。

而且我还没有在像 expect(DataWriter).to receive(:actually_finalize).with('/tmp/example.txt') 这样的形式的析构函数调用上构建 rspec 期望得到结果 - 我不知道为什么,但我想 [=26 创建的包装器=] 在调用 class.

的析构函数之前已被垃圾处理或侵犯

希望对您有所帮助!