Ruby Hack Challengeに参加した
[Cookpad Ruby Hack Challenge](Cookpad Ruby Hack Challenge)に参加したのでイベントの様子と後日談を。
イベント自体は 8 月に開催されたが、イベントで取り組んだ課題をその後もほそぼそと続けていてある程度使えるようになったので記事にしておく。イベント直後にブログ書くのを忘れていたわけではない。
Ruby Hack Challenge は Ruby インタプリタを Hack する方法を Ruby 開発者の笹田さんがまる二日かけて無料で教えてくださるというなんとも太っ腹なイベント。もともと Ruby の VM に興味があったので応募してみたら運良く当選した。
イベントの教材やアンケートは ko1/rubyhackchallenge で公開されているので、興味がある人は眺めてみると楽しいと思う。
感想
まず感想。参加して非常によかった。良かった理由は次の 3 つ
- Ruby 開発の流れを知ることができた
- RubyVM の話を聞くことができた
- 取り組んだ課題を継続できていて楽しい
前者の Ruby 開発はもちろん Ruby を使った開発ではなく Ruby 本体の開発のこと。どのように C のソースコードを編集して、どのようにテストを書いて、どのようにテストするかという流れを知ることができてよかった。共通課題として Array にメソッドを追加してテストをするといった課題がいくつか用意されていて実際に流れを体験できた。また、Ruby ソースコードの構造、開発で使うコマンドとともに共通課題のその辺りの流れが丁寧に資料にまとまっていて、それも非常に良かった。
当日は Ruby の VM を開発した笹田さんがいらっしゃったので VM の話を直接聞くことができた。YARV については Rubyist Magazine で連載があったり 設計メモ(解説サイト) があったり 論文 が公開されてたり Web でもいろいろな情報が手に入るが、読んでも理解できなかったことや yield より block.call が遅い理由やこれを作るのに時間がかかったという苦労話を直接聞けたのが非常に楽しかった。
課題については後述する。
イベントスケジュール
イベントは次のスケジュールで進行した。
- 1 日目
- 10:00 オープニング
- 10:30 ハックに必要となる事前知識の講義
- 12:00 ランチ
- 13:00 共通課題
- 16:00 発展課題の紹介
- 2 日目
- 10:00 発展課題の開始
- 11:30 まつもとゆきひろ氏 特別講演
- 12:00 Ruby 開発者を交えてのランチ
- 13:00 Ruby 開発者との Q&A セッション
- 14:00 発展課題の再開
- 18:30 打ち上げパーティー
多くの時間は共通課題、発展課題へもくもく取り組むという時間で、質問があれば質問するという形だった。
イベントの様子
2 日とも Cookpad 社の会議室の一室で講義を受けもくもくするという形だった。
参加者は学生が多く社会人の方が少ないぐらいの印象だった。元々は学生が対象のつもりだったらしい。
1 日目はまず事前知識の講義があり、その後共通課題に取り組んだ。
2 日目には Matz の講演があったり、Ruby コミッタの方たちとのランチがあったり、ミニ Ruby Committers vs the World1があったりした。また、取材も入っていてちょいちょいカメラマンが写真を撮っていたりもした。
イベントの裏で Ruby 開発者会議をやっていて、ランチ、Q&A セッション、打ち上げには結構多くのコミッタの方が参加されていた。打ち上げでは発展課題を発表する場もあり、そこで議論が深まったり有益なフィードバックがあったりもした。
参加者、講師、サポート陣はイベントを通して Gitter でコミュニケーションを取っていて課題を報告したり気軽に質問したりしていた。
発展課題
発展課題は自分の好きなテーマで Ruby を Hack してみようというものだった。いくつか用意されている課題もあり自分はその中の 1 つに取り組んだ。
取り組んだ課題は「ビルドしたRubyでのGemテスト」で説明文はこんな感じ(なぜか途中で切れている)。
現在、ビルドした Ruby で Gem など、外部ライブラリを、ビルドした Ruby で、他の影響を与えないで適切にテストする方法がありません。 というのも、Gem の場合、Gem のテストのために他の Gem を利用したりする必要があるためです。 区切られた環境(サンドボックス)を用いることで
はじめは CI でテストできればいいのでは?という考えから Docker をサンドボックスとしてテストする仕組みを作ったり、インストール先を ./configure --prefix で指定してそこをサンドボックスにしたテスト環境を作ったりした。
しかし、話を聞くとそれらでは要件を満たせず不充分だったのでイベント後も継続して取り組んでいる。よくよく考えると説明に書いてあるのだが、前提知識不足や理解不足があって要件を完全に理解できていなかった。推測も含まれているので間違っている部分もあるかもしれないが、前提知識をまとめるとこんな感じになる。
- Ruby 開発者は Ruby を修正してテストプログラムを実行するというサイクルを短期間でまわす
- 修正したコードはできるだけちゃちゃっとテストしたい
- 修正したコードで主要な Gem が動作するかをテストしたいことがある
- 今現在
make installしないで Gem をテストする手段がない - 普段の開発では
make installしない make installするとローカルのシステムに影響を与える可能性があるのでやりたくないmake installしたとしても簡単に Gem をその Ruby 環境でテストする仕組みがあるわけではない
以上の前提を踏まえて「修正した Ruby で make install せずに手軽に Gem をテストする仕組み」というのがこの課題の要件になる。
要件を整理できたところで作ったものを紹介する。
GemTester
ここでは技術的な解説はせず使い方を説明する。
作業は GitHub の ruby/ruby を Fork した nownabe/ruby の gem_tester-2.4.2 ブランチと testgem ブランチでやっている(まだ整理できていない)。
ビルドされた Ruby での Gem のテストを make test-gems で実行できる。これを実行するクラスには GemTester と名づけた。現在 GemTester には次の機能がある。
- ビルドした Ruby を使ったサンドボックスの提供
- GemTester に登録された Gem または指定した Gem のテスト
- Gem のソースコードのダウンロード (レポジトリを clone)
- 依存パッケージのインストール
- テストコマンドの実行
GemTester には予め主要な Gem2が登録されていて conditions.yaml で確認できる。GemTester はテストする Gem が指定されなかった場合は conditions.yaml に登録されているすべての Gem をテストする。また、通常は gemspec のメタ情報などからレポジトリを推測するが、推測できない Gem のレポジトリやテストコマンドも conditions.yaml が持っている。推測できる場合は conditions.yaml に登録がなくてもテストができる。
GemTester を実行するには Ruby のビルドに必要なパッケージの他、各 Gem をビルド・テストするために必要なパッケージを予めインストールしておく必要がある。Ubuntu 16.043では次の操作で GemTester を実行できる。GemTester に登録されている Gem すべてをテストするにはそれなりの時間がかかるので注意して欲しい(clone が走るので初回は特に)。
準備:
# Rubyのビルドに必要なパッケージをインストール
sudo apt-get install -y \
git ruby autoconf bison gcc make zlib1g-dev libffi-dev \
libreadline-dev libgdbm-dev libssl-dev
# 登録されているGemをテストするのに必要なパッケージをインストール
sudo apt-get install -y \
g++ libncurses5-dev ragel libxml2-dev libpq-dev libsqlite3-dev \
libfcgi-dev libmysqlclient-dev libidn11-dev libcurl4-gnutls-dev \
nodejs libtool cmake
# 作業ディレクトリを作成
mkdir gemtester
cd gemtester
# Rubyのソースコードをclone
git clone https://github.com/nownabe/ruby.git -b gem_tester-2.4.2
# Rubyをビルド
cd ruby
autoconf
cd ..
mkdir build
cd build
../ruby/configure --prefix=`pwd`/../install --enable-shared
make -j
GemTester の実行:
$ make test-gems
./miniruby -I../ruby/lib -I. -I.ext/common ../ruby/tool/runruby.rb --extout=.ext ../ruby/tool/test_gems.rb
Installing bundler
Fetching: bundler-1.15.4.gem (100%)
* Gem Test: arel (branch or tag: )
- Installing arel
Fetching: arel-8.0.0.gem (100%)
- Cloning repository from https://github.com/rails/arel.git to /home/nownabe/gemtester/build/gems/repos/github.com/rails/arel
- Updating repository /home/nownabe/gemtester/build/gems/repos/github.com/rails/arel
- Installing dependencies
- Executing command: `rake`
- Succeeded!
...
* Gem Test: unf_ext (branch or tag: )
- Installing unf_ext
- Cloning repository from https://github.com/knu/ruby-unf_ext.git to /home/nownabe/gemtester/build/gems/repos/github.com/knu/ruby-unf_ext
- Updating repository /home/nownabe/gemtester/build/gems/repos/github.com/knu/ruby-unf_ext
- Installing dependencies
- Executing command: `rake compile`
- Executing command: `rake`
- Succeeded!
Total: 70, Success: 70, Failure: 0
これで 70 個の主要な Gem をテストできる。
毎回すべての Gem をテストしていると時間がかかるので特定の Gem だけをテストできるようにもなっている。特定の Gem をテストするには次のようにする。
make test-gems GEMS="activesupport,benchmark_driver"
Gem のブランチやタグを指定したいときは次のようにする。
make test-gems GEMS="activesupport:v4.2.9"
また、Mac で Homebrew を使用している場合は次のようなコマンドになる4。
# configure
../ruby/configure \
--prefix=`pwd`/../install \
--enable-shared \
--with-openssl-dir="$(brew --prefix openssl)" \
--with-readline-dir="$(brew --prefix readline)" \
--disable-libedit
# GemTester
make test-gems OPTIONS="--with-homebrew"
実際に効果があるのか確かめるために String#capitalize が小文字を返すように変更して GemTester を実行してみる。string.c の rb_str_capitalize を次のように変更する。
static VALUE
rb_str_capitalize(int argc, VALUE *argv, VALUE str)
{
return rb_str_dup(str);
}
次に make run で確認する。まずは test.rb を作成する。
puts "hello, world!".capitalize
これを実行する。
$ make run
compiling ../ruby/string.c
linking miniruby
./miniruby -I../ruby/lib -I. -I.ext/common ../ruby/test.rb
hello, world!
小文字になっている。この状態で Gem をテストすればいくつかの Gem のテストが失敗することが期待される。実際に GemTester でテストしてみる。
$ make test-gems
...
Total: 70, Success: 34, Failure: 36
Failed gems:
- arel
- actionmailer
- actionpack
- actionview
- activejob
- activemodel
- activeresource
- activesupport
- aws-sdk
- coffee-rails
- excon
- faraday
- globalid
- highline
- httparty
- jbuilder
- mail
- mini_portile
- mini_portile2
- multi_json
- net-ssh
- rack
- rack-test
- redis
- rdoc
- rest-client
- rspec-core
- rspec-expectations
- rspec-mocks
- sass
- sinatra
- slop
- sprockets-rails
- thor
- thread_safe
- treetop
70 個中 36 個の Gem のテストが失敗している。
今後
なんとなく動くようにはなったものの技術的な部分や進め方で課題がある。
- そもそも方向性はこれでいいのかわかってない
- インストールされた Ruby 2.4.2 環境下だとテストがパスするのに、GemTester 環境下だとテストがパスしない Gem がわずかにある
- 自分の環境でしか実行できていないので環境の差異によって実行できないケースが考えられる
- 今は手動で Checkout してビルドして
make test-gemsコマンドを打つということをやっているので CI 的な仕組みの構築をしたい - CI 的な仕組みを構築してさらに多くの環境でテストをしたい
- オプションで
--depth 1で clone するなどして実行時間の短縮をしたい - (Linux|Mac)でしか動かない Gem など特定の環境でしかテストできない Gem を特定の条件下でのみテストするようにしたい
- 継続的に Gem のテスト手順などの変更に追従する必要がある
- 継続的に Ruby の変更に追従する必要がある
- 継続的な開発が必要になるのでどの段階で Pull Request を出すか
- (そもそもテストがちゃんと動かない Gem のテストの修正)
楽しいので続けていたが、せっかくなら Ruby 本体にマージされると嬉しいので今後はいろいろ調整しつつ開発を続けていきたい。今年は残念ながら RubyKaigi に参加できなかったので開発者の方々と話はできなかったが、Ruby Hack Challenge の Issue で引き続きサポートしていただいているので相談していきたい。
おわりに
笹田さん、サポート陣の皆様、Ruby コミッタの皆様、Cookpad 社、ありがとうございました。勉強になりました。
発展課題はこんなに続けるとは思っていなかったが、思えばこういう仕組みを整備するみたいなことは好きなので自分に向いている課題だったのかもしれない。