MySQL/Ruby の test.rb が失敗する理由と対策
前回書いた、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_exit
で Test::Unit::AutoRunner
の run
メソッドが呼び出される。この定義は 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.
… と思ったら駄目でした。
うーん、高速化パッチを適用せずにテストしても同じ結果なので、これはまた別の原因だな。 まあ、浮動小数点関係っぽいので環境依存かもしれん、、てことでスルーしとこう。