【问题标题】:How do you test whether a Ruby destructor will be called?如何测试是否会调用 Ruby 析构函数?
【发布时间】:2016-08-30 01:57:22
【问题描述】:

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

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

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

【问题讨论】:

  • 支持,因为你让我了解了 ruby​​ 中的垃圾收集和终结:D
  • @GavinMiller 我也刚了解它,在涉足 C++ 领域后刚回到 ruby​​,所以我自然而然地开始寻找析构函数,并了解到它们在 ruby​​ 中有点不寻常。

标签: ruby rspec mocking destructor


【解决方案1】:

finalizeinitialize 中被调用,返回proc,并且再也不会被调用,所以你不能指望它在完成时被调用。它是实例完成时调用的 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.start 只是 GC 可能现在运行的 提示,不保证 GC 会 实际运行。即使它运行,也不能保证它会收集 哪些 个对象。例如,如果它是实时 GC,它可能对允许运行的时间有硬性限制,因此它可能根本无法收集所有孤立对象。因此,即使终结器完美运行,该测试也可能因各种原因而失败。
  • @JörgWMittag 我怀疑是这样。您是否有任何文档可以阅读有关 ruby​​ GC 的更多信息?
【解决方案2】:

即使“文件已关闭”与 file.inspect 一起打印,测试也会失败并显示以下输出

我将您的代码放入一个文件中并运行它。鉴于我收到的输出,在 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)>"

至于为什么,我现在不知道。 Dave is right 你在断言已经发生的事情,所以你的测试永远不会通过。您可以通过将测试更改为:

 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

【讨论】:

    【解决方案3】:

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

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

    多个 API 在 Ruby 中具有相同的样式:

    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
    

    【讨论】:

    • 我应该如何使用我现在拥有的代码风格来做到这一点?我觉得每次我想写的时候都保存文件名并打开它比只保存文件描述符一次然后使用它直到我放弃我的课程效率低得多。 (我将可耻地承认我还没有真正分析过这个,但这只是我的默认写作风格。)此外,这只是一个测试问题,对于我的代码,我只关心事情是否得到清理。对于我的测试,我关心什么时候。
    • @MikeH-R 我添加了一个小样本。依赖终结器会带来麻烦,因为它并没有真正定义它何时运行。
    • 我认为你错过了我的意思,我创建了this gist 来表明我的意思,第一种方法慢了 9 倍。
    • 我明白你的意思,但不明白你为什么这样做。检查我的编辑。除非你能告诉我一个消息来源说终结器在实现中以一致的方式立即运行,否则我不明白你为什么认为它是依赖它们的好解决方案。
    • 感谢您的回答 pascal,这是将大量顺序数据写入文件的正确且明显的方法(以及我 90% 的时间会使用的方法),但这不适合我目前正在尝试做的事情的语义。我并不特别需要立即运行终结器(仅在最终运行,以确保 fd 不会被泄露),我唯一关心它们何时运行的时间是在单元测试时。
    【解决方案4】:

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

    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 期望结果 - 我不知道为什么,但我认为 Rspec 创建的包装器在调用一个班级。

    希望这会有所帮助!

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2013-06-25
      • 1970-01-01
      • 2017-02-27
      • 2014-10-25
      • 1970-01-01
      • 1970-01-01
      • 2016-08-07
      • 2019-07-04
      相关资源
      最近更新 更多