MySQL/Ruby の test.rb が失敗する理由と対策

2006/12/28 10:07am

前回書いた、MySQL/Ruby の test.rb が失敗する原因を調査する。

まずは、実行結果の出力を調べてみよう。 すべてのテストが失敗しているため長いので、最初のテスト結果のみ抜粋する。

% ruby ./test.rb localhost root newpass
...
1) Failure:
test_connect(TC_Mysql) [./test.rb:39]:
Exception raised:
Class: <Mysql::Error>
**Message: <"Access denied for user 'ishikawa'@'localhost' (using password: NO)">**
---Backtrace---
./test.rb:39:in `connect'
./test.rb:39:in `test_connect'
./test.rb:39:in `test_connect'
...

MySQL のエラーで "Access denied for user ‘ishikawa’@‘localhost’ (using password: NO)“ と出力されているので、これは単なるアクセス制御の問題だ。

そして、test.rb のコマンドライン引数で root ユーザを指定しているにも関わらず、実際には ishikawa ユーザでアクセスしようとしている。

うまくいくわけがない。 コマンドライン引数による指定が無視されているわけだ。

何故、コマンドライン引数が無視されるのか

コマンドライン引数が無視される原因を調べてみると、どうやら test.rb が使っている Test::Unit が悪さをしているらしい(Test::Unit は Ruby の標準添付ライブラリで、いわゆる xUnit ツールの Ruby 版である)。

簡単な検証スクリプトを書いてみる。

require "test/unit"

puts "before test: ARGV = #{ARGV.inspect}"
class SimpleTestCase < Test::Unit::TestCase
  def test_argv()
    puts "in test: ARGV = #{ARGV.inspect}"
  end
end

ユニットテストが実行される前と、実行されるときに ARGV(コマンドライン引数の配列)をダンプするだけのスクリプトだ。これを適当な引数つきで実行して、その結果を確認する。

% ruby ./simple_test.rb 1 2 3
**before test: ARGV = ["1", "2", "3"]**
Loaded suite ./simple_test
Started
**in test: ARGV = **
.
Finished in 0.000316 seconds.

1 tests, 0 assertions, 0 failures, 0 errors

ユニットテストを実行する前では ARGV に引数の 1, 2, 3 が格納されているが、テストの内部では ARGV が空になっている。となると、Test::Unit が渡ってきた引数を食べてしまっているのに違いない。

Test::Unit のソースを調べる

実際のとこ、どうなっているのか。ソースコードに聞いてみよう。場所は /usr/local/lib/ruby/1.8/test/。まずは動作を確認するために、unit.rb の 276 行目だ。

at_exit do
  unless $! || Test::Unit.run?
    exit **Test::Unit::AutoRunner.run**
  end
end

このコードは require されたときに実行され、at_exitTest::Unit::AutoRunnerrun メソッドが呼び出される。この定義は autorunner.rb だ。

module Test
  module Unit
    class AutoRunner
      def self.run(force_standalone=false, default_dir=nil, **argv=ARGV**, &block)
        r = new(force_standalone || standalone?, &block)
        if((**!r.process_args(argv)**) && default_dir)
          r.to_run << default_dir
        end
        r.run
      end

怪しげなコードを発見できた。 インスタンスの process_args メソッドに ARGV をそのまま渡している。

def process_args(args = ARGV)
  begin
    **options.order!(args)** {|arg| @to_run << arg}
  rescue OptionParser::ParseError => e
    puts e
    puts options
    $! = nil
    abort
  else
    @filters << proc{false} unless(@filters.empty?)
  end
  not @to_run.empty?
end

肝になるのは options.order!(args) の部分だ。options では、OptionParser インスタンスが返り、そして、OptionParser#order! はマニュアルによると、

与えられた argv を順番にパースします。 オプションではないコマンドの引数(下の例で言うと somefile)に出会うと、パースを中断します。 ブロックが与えられている場合は、パースを中断せずに 引数をブロックに渡してブロックを評価し、パースを継続します。argv を返します。

order! は与えられた argv を破壊的にパースします。 argv からオプションがすべて取り除かれます。

とのことなので、ここで ARGV の中身が破壊されているのは明白だ。

test.rb を変更して対策

では、どうするか?

結局、Test::Unit::AutoRunner.run で直接 ARGV を渡してしまうのが不味いわけで、自前で Test::Unit::AutoRunner.run を呼び出し、そのさいに ARGV のコピーを渡すようにしてみた。

--- test.rb.orig        2006-12-28 01:20:19.000000000 +0900
+++ test.rb     2006-12-28 01:21:04.000000000 +0900
@@ -1429,3 +1429,4 @@
   end

 end if Mysql.client_version >= 40100
+Test::Unit::AutoRunner.run(false, nil, ARGV.dup)

これでテストも実行できるはず …

% ruby ./test.rb localhost root
Loaded suite ./test
Started
....................................................................FF...........................................
Finished in 0.32177 seconds.

通った!…

1) Failure:
test_fetch_double(TC_MysqlStmt2) [./test.rb:920]:
<-1.79769313486232e+308> and
<-1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.

  2) Failure:
test_fetch_double_unsigned(TC_MysqlStmt2) [./test.rb:937]:
<1.79769313486232e+308> and
<1.79769313486232e+308> expected to be within
<2.22044604925031e-16> of each other.

… と思ったら駄目でした。

うーん、高速化パッチを適用せずにテストしても同じ結果なので、これはまた別の原因だな。 まあ、浮動小数点関係っぽいので環境依存かもしれん、、てことでスルーしとこう。