nownab.log

nownabe's daily posts

Ruby Hack Challengeに参加した

Posted on Sep 22, 2017

[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.crb_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社、ありがとうございました。勉強になりました。

発展課題はこんなに続けるとは思っていなかったが、思えばこういう仕組みを整備するみたいなことは好きなので自分に向いている課題だったのかもしれない。


  1. RubyKaigi恒例のRubyコミッタの方々へのQ&Aセッション ↩︎

  2. BestGemsのTop100のうちインストール済みのRuby 2.4.2でテストが通るもの ↩︎

  3. GCEでテスト ↩︎

  4. まだ全部テストできてないので多分どこかでこける ↩︎