lib/minitest/unit.rb へのパッチ

ruby 1.9 を利用しており Rake を使用してテストしている。
以下、使用している Rakefile

require "rake"
require "rake/testtask"

Rake::TestTask.new do |t|
  t.libs << "lib"
  t.libs << "test"
  t.test_files = FileList["test/**/*_{helper,test}.rb"]
end

task :default => :test

test ディレクトリ以下に require に失敗するスクリプトがある場合、次のような例外が発生した。

ragdoll$ rake19trunk
(in /Users/kouji/work/s7)
/Users/kouji/local/lib/ruby19trunk/1.9.1/minitest/unit.rb:328:in `block in autorun': unexpected return (LocalJumpError)
/Users/kouji/work/s7/test/s7/secret_generator_test.rb:3:in `require': no such file to load -- s7/secret_generator (LoadError)
        from /Users/kouji/work/s7/test/s7/secret_generator_test.rb:3:in `<top (required)>'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `load'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `block in <main>'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `each'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `<main>'
rake aborted!
Command failed with status (1): [/Users/kouji/local/bin/ruby19trunk -Ilib:l...]

(See full trace by running task with --trace)

上記のうち、以下が余計だと思う。原因を調べ、解決する。

/Users/kouji/local/lib/ruby19trunk/1.9.1/minitest/unit.rb:328:in `block in autorun': unexpected return (LocalJumpError)

上記の例外が発生したソースは以下。例外が発生した($! が nil でない)場合、処理を中断したいという意図だと思われる。ここは return ではなく、next がよいと思う。

    def self.autorun
      at_exit {
        return if $! # don't run if there was an exception
        exit_code = MiniTest::Unit.new.run(ARGV)
        exit false if exit_code && exit_code != 0
      } unless @@installed_at_exit
      @@installed_at_exit = true
    end

以下がパッチ。

Index: unit.rb
===================================================================
--- unit.rb	(revision 21138)
+++ unit.rb	(working copy)
@@ -325,7 +325,7 @@
 
     def self.autorun
       at_exit {
-        return if $! # don't run if there was an exception
+        next if $! # don't run if there was an exception
         exit_code = MiniTest::Unit.new.run(ARGV)
         exit false if exit_code && exit_code != 0
       } unless @@installed_at_exit

このパッチを適用した結果。

ragdoll$ rake19trunk        
(in /Users/kouji/work/s7)
/Users/kouji/work/s7/test/s7/secret_generator_test.rb:3:in `require': no such file to load -- s7/secret_generator (LoadError)
        from /Users/kouji/work/s7/test/s7/secret_generator_test.rb:3:in `<top (required)>'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `load'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `block in <main>'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `each'
        from /Users/kouji/local/lib/ruby19trunk/1.9.1/rake/rake_test_loader.rb:5:in `<main>'
rake aborted!
Command failed with status (1): [/Users/kouji/local/bin/ruby19trunk -Ilib:l...]

(See full trace by running task with --trace)

これで、想定通りに動作するようになった。
あとは、ruby-core にメールを送ろう。
英語の作文をするために、述べたいことをまとめておく。

  • test スクリプトの解析時にエラーが発生したときに LocalJump 例外が発生した。
  • 原因は、self.autorun で指定している at_exit のブロックの中で return しているからです。
  • return を next に修正してはどうか。
  • 修正した結果、手元では LocalJump 例外が発生しなくなった。

メールを作成する前に、修正済みかもしれないので、 svn up をしてみよう。最近、Ryan が merge しているようだ。最新のソースでも修正されていないことが分かった。(誰も困っていないのかな?)
以下、作成したメール。(英語でメールを書くことになれていないため、いいたいことが伝わるかどうかが不安です。)

Hi Ryan,

If $! is not nil, returns in the block of the at_exit method in
MiniTest::Unit.autorun. Then raises LocalJump exception.
-----
  def self.autorun
    at_exit {
      return if $! # don't run if there was an exception
      exit_code = MiniTest::Unit.new.run(ARGV)
      exit false if exit_code && exit_code != 0
    } unless @@installed_at_exit
    @@installed_at_exit = true
  end
-----

The attached patch will fix the problem.

Index: lib/minitest/unit.rb
===================================================================
--- lib/minitest/unit.rb	(revision 21508)
+++ lib/minitest/unit.rb	(working copy)
@@ -325,7 +325,7 @@
 
     def self.autorun
       at_exit {
-        return if $! # don't run if there was an exception
+        next if $! # don't run if there was an exception
         exit_code = MiniTest::Unit.new.run(ARGV)
         exit false if exit_code && exit_code != 0
       } unless @@installed_at_exit

Thanks, kouji.