Rails 4.1 の spring で paralell_tests を使う方法
みなさん、こんにちは! 2週間ぶりのご無沙汰、MUGENUP の osada です。
ruby 2.1.0
、Rails 4.1
で開発した、みなゲー編集部が正式リリースとなりました!
よろしくお願いします。
さて、そんなRails 4.1
の新機能として、プレローダーspring
が標準装備となりました。
本日はspring
とparalell_tests
を併用する方法についてのお話です。
要旨は下記となります。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
では、よろしくお願いします。
PARALLEL_TESTS_EXECUTABLE
を設定して、paralell_tests
にspring
を使わせない
Rails 4.1
のテストで、rspec
では成功するのに、paralell_tests
では多くが失敗する、という現象に遭いました。
spring
が原因と考えられます。
通常、rake
やrspec
コマンドを使うとき、コード(code
)を読み込んで、メモリ上にRails
のapp
インスタンスを生成し、使用します。処理が終わると、app
は解放されます。
この処理はかなり重い処理なので、毎回作るのではなく、一度作ったapp
を使いまわそう、というのがspring
の動きです。spring
で作られたapp
は処理が終わっても維持され、再度使用されます。
このspring
はbin_stub
として提供され、bin/
というディレクトリに
rails
, rspec
, rake
のコマンドが用意されます。例えば、
be bin/rake db:create
のように、通常のコマンドの代わりに、bin/
下のコマンドを呼び出すことで、spring
が使用されます。
一方、paralell_tests
というのは、rake
などの処理を複数のプロセスに分けて同時に処理する方法です。
例えば、parallel:spec
を実行すると、下記の4つのコマンドが実行されます。
※
be
はbundle exec
のalias
です
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=3;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=4;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec TEST_ENV_NUMBER=2;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;rspec
しかし、これにspring
を使用すると、4つのプロセスがあるのに、実際に稼働するapp
は1つになってしまいます。
よって、テストが失敗してしまうのです。
原因はparalell_tests
がbin/rspec
を使用するという点にあります。これをどうにかしましょう。
$ be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bin/rspec ……
結論からいうと、PARALLEL_TESTS_EXECUTABLE
を設定することで、bin/rspec
の使用を回避できます。
PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" be rake parallel:spec TEST_ENV_NUMBER=;export TEST_ENV_NUMBER;PARALLEL_TEST_GROUPS=4;export PARALLEL_TEST_GROUPS;bundle exec rspec ……
この理由は、parallel_tests
は、実行するファイルを下記のように設定しているからです。
def executable ENV['PARALLEL_TESTS_EXECUTABLE'] || determine_executable end
またrspec
のdetermine_executable
は下記であり、[bin/rspec
, script/spec
, bundle exec rspec
, spec
, rspec
] のどれかを起動する事がわかります。
def determine_executable cmd = case when File.exists?("bin/rspec") "bin/rspec" when File.file?("script/spec") "script/spec" when ParallelTests.bundler_enabled? cmd = (run("bundle show rspec-core") =~ %r{Could not find gem.*} ? "spec" : "rspec") "bundle exec #{cmd}" else %w[spec rspec].detect{|cmd| system "#{cmd} --version > /dev/null 2>&1" } end cmd or raise("Can't find executables rspec or spec") end
bin/rspec
を削除しても良いのですが、それではspring
が使えなくなるので、
PARALLEL_TESTS_EXECUTABLE
を設定する方が適切でしょう。
以上、本項の結論は、
parallel_tests
に、spring
を使わせないためには、PARALLEL_TESTS_EXECUTABLE
を設定する となります。
bin/rspec
を書き換えて、始めのプロセスだけspring
を使う
しかしせっかくのspring
です。
4つの内1つだけでspring
を使い、
残り3つを別のプロセスで動かせば良いのではないでしょうか?
相手はbin_stub
ですから、修正はとても簡単です。
bin/rspec
のこれを、
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") end
こうしましょう!
if !Process.respond_to?(:fork) || Gem::Specification.find_all_by_name("spring").empty? exec "bundle", "exec", "rspec", *ARGV else if ENV["TEST_ENV_NUMBER"].nil? || ENV["TEST_ENV_NUMBER"].empty? ARGV.unshift "rspec" load Gem.bin_path("spring", "spring") else exec "bundle", "exec", "rspec", *ARGV end end
ENV["TEST_ENV_NUMBER"].nil?
は通常のbin/rspec
起動時です。
ENV["TEST_ENV_NUMBER"].empty?
は、parallel_tests
の一番始めのプロセスの時です。
つまり、
bin/rspec
として起動したとき、または、parallel_tests
の一番始めのプロセスの時に、spring
を使用する。
というbin_stub
になりました。
さて、肝心のスピードアップですが、feature
テストなどが多いため、私のプロジェクトでは違いが現れませんでした。
参考までとはなりますが、環境の影響が少ないspec/model
に適用した結果を載せておきます。
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" bundle exec rake parallel:spec[model] …… 444 examples, 0 failures Took 216.680576 seconds real 3m47.661s user 3m20.585s sys 0m25.249s
[feature/parallel stash]~/projects/mugenup/workstation: time RAILS_ENV=test bundle exec bin/rake parallel:spec[model] …… 444 examples, 0 failures Took 212.844729 seconds real 3m37.395s user 0m1.378s sys 0m0.285s
real
を見ると10 sec
ほど速くなっているように見えますが、誤差の範囲かもしれません。
もし「速くなったよ!」という方がいらっしゃいましたら、ご一報ください!
FailuresLogger
を使って、失敗したテストを再実行する
feature
テストは実際のブラウザの挙動を再現するため、
タイムアウトなどの理由により、
タイミングによって失敗することもあります。
parallel_tests
を使うと、さらに失敗しやすくなります。
たまたま失敗したのか、本当に失敗しているのかを確認するため、
失敗したテストのみを再度実行するのですが、
その時に役立つのが、FailuresLogger
です。
.rspec_parallel
に paralell_tests
のみのオプションを設定できます。
# .rspec_parallel --format progress --format ParallelTests::RSpec::FailuresLogger --out tmp/failing_specs.log
これには失敗したテストがログに残ります。標準出力と同じものです。
ここから、再度テストを実行しましょう。
$ grep rspec tmp/failing_specs.log | awk '{print $2}' | xargs bundle exec rspec /Users/osada/projects/mugenup/workstation/vendor/bundle/gems/rspec-core-2.14.7/lib/rspec/core/configuration.rb:896:in `load': cannot load such file -- /Users/osada/projects/mugenup/workstation/spec/views/projects/edit.html.erb_spec.rb:34 (LoadError)
あら、失敗してしまいました。なぜでしょうか?
実際にファイルを開いてみたところ、ascii color code
が含まれていました。
たしかに、これでは、rspec
が通りませんね。
といっても--color
を消してしまうのも、嬉しくありません。
そこで、正規表現を使って、抜き出すことにしました。
- rspec から始まり、「ドット、スラッシュ、コロン、英数字とアンダースコア」で構成される文字列を抽出する
$ ruby -ne 'puts $1 if /(?<=rspec )([\.\/:\w]+)/' tmp/failing_specs.log | xargs bundle exec rspec
これで、失敗したテストのみ、再実行することができます。
rake task
にするべきか?とも思ったのですが、さらに実行を遅らせてしまいそうなので、
alias
の登録だけにしておきました。
こうゆうとき、どういう技を使えばいいか、わからないの
ruby
でワンライナーを書くのは、少し大変ですね。
RuntimeLogger
を使って、テストのグループを平均化する
さて、paralell_tests
はデフォルトでは、ファイルサイズ順に均等化します。
ファイルサイズの大きい順に並べて、4つのグループに順番に追加していくわけです。
RuntimeLogger
を使うことで、テストにかかった時間で、均等化することができます。
--format progress --format ParallelTests::RSpec::RuntimeLogger --out tmp/parallel_runtime_rspec.log
tmp/parallel_runtime_rspec.log
には、テストファイル名と、その実行時間が記録されています。
[feature/parallel stash]~/projects/mugenup/workstation: tail tmp/parallel_runtime_rspec.log …… ./spec/views/sub_projects/show.html.erb_spec.rb:2.261982 ./spec/views/projects/show.html.erb_spec.rb:2.806851
テストのグループ分けのとき、with_runtime_info
メソッドが呼ばれます。
runtime_log
つまりtmp/parallel_runtime_rspec
があるとき、かつ、
今回のtests
ファイル群が、このログの中に含まれていると考えられるときに、
このログを使ってグループ分けします。
puts "Using recorded test runtime"
が出力されれば、成功です。
def with_runtime_info(tests) lines = File.read(runtime_log).split("\n") rescue [] # use recorded test runtime if we got enough data if lines.size * 1.5 > tests.size puts "Using recorded test runtime" times = Hash.new(1) lines.each do |line| test, time = line.split(":") next unless test and time times[File.expand_path(test)] = time.to_f end tests.sort.map{|test| [test, times[File.expand_path(test)]] } else # use file sizes tests.sort.map{|test| [test, File.stat(test).size] } end end
実際に、使用したときと、使用していないときを比較してみます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Finished in 2 minutes 19.6 seconds 78 examples, 0 failures Finished in 3 minutes 19.1 seconds 124 examples, 0 failures Finished in 3 minutes 10.7 seconds 116 examples, 0 failures Finished in 3 minutes 13.1 seconds 126 examples, 0 failures
使用していないときの処理時間は2:19.6 〜 3:19.1
であり、60
秒以上のズレがあります。
一方、runtime_log
を使用したときは下記です。
Using recorded test runtime
が表示されていることが確認できます。
[feature/parallel stash]~/projects/mugenup/workstation: RAILS_ENV=test bundle exec bin/rake parallel:spec[model] Using recorded test runtime Finished in 3 minutes 7.8 seconds 102 examples, 0 failures Finished in 2 minutes 58.3 seconds 108 examples, 0 failures Finished in 3 minutes 10.2 seconds 123 examples, 0 failures Finished in 3 minutes 11.8 seconds 111 examples, 0 failures
2:58.3 〜 3:11.8
であり、13
秒ほどのズレでした。
残念ながら、こちらも大きなスピードアップは感じられませんでしたが、 速くなったよ、という方はご一報いただけると嬉しいです。
まとめ
parallel_tests
とspring
の使い方、いかがだったでしょうか?
- PARALLEL_TESTS_EXECUTABLE="bundle exec rspec" を設定して、
paralell_tests
にspring
を使わせないbin/rspec
を書き換えて、始めのプロセスだけspring
を使うFailuresLogger
を使って、失敗したテストを再実行するRuntimeLogger
を使って、テストのグループを平均化する
弊社でもテストが遅いということが、問題になっており、日々改善に勤しんでおります。
こんなやり方あるよ!という情報をお持ちの方、いらっしゃいましたら、 ご教授いただけますと幸いです。
よろしくお願いします!
宣伝
MUGENUP では、rails を使いたいエンジニアを募集中です。 無限流開発、ご一緒しませんか?
大きな裁量で自社サービス開発!Rubyエンジニアをウォンテッド! - 株式会社MUGENUPの求人 - Wantedly