ラベル 技術Tips の投稿を表示しています。 すべての投稿を表示
ラベル 技術Tips の投稿を表示しています。 すべての投稿を表示

2014年10月22日水曜日

vimdiffで単語単位の差分表示: diffchar.vimが超便利

今日もプログラミングや文書作成にvimを巧みに操り続ける全国のvimmerの皆様におかれましては、vimを利用した差分表示であるvimdiffは欠かせないツールであることと思います。

当方も今までvimdiffについては色を見やすくしたり差分計算アルゴリズムを賢くしたりと、カスタマイズによって使い勝手を向上させてきましたが、唯一実現できていなかったのが、「単語単位の差分表示」なのであります。

しかしついに、vimdiffで単語単位の差分を表示できるようにするvimプラグイン「diffchar.vim」を先日見つけました。最初の公開が今年5月という新しいプラグインです。
diffchar.vim - Highlight the exact differences, based on characters and words : vim online

ということで、早速インストール。このプラグインはGithubにもミラーされており、すでにNeoBundleをお使いであれば導入はとっても簡単で次の一行を.vimrcに追加して ":NeoBundleInstall" するだけです。それ以外の場合については省略。

NeoBundle 'vim-scripts/diffchar.vim'

実際サンプルで試してみましょう。まず、vimdiffで差分を表示させると、次のようになりますね。


一つの行に複数箇所の変更が含まれる場合も、最初の変更から最後の変更までを含むような大きな1つの変更として認識されてしまっています。

しかしここで<F7>キーを押すと!


うおおおおぉ!すばらしい。単語単位の差分を表示できました。一行に複数の変更があってもちゃんと認識して表示。しかも、追加部分と変更部分で色分けもされています。

ちなみに、再度<F7>キーを押すと元に戻ります。なお、<F7>キーは文書全体について単語単位の差分表示を切り替えますが、<F8>キーを押すと現在の行のみ単語単位の差分表示を切り替えることができます。

これだけでも便利ですが、欲を言えば、vimdiffコマンド実行時やgit diftoolにvimdiffを使っている時に、いちいち<F7>を押すのは面倒。そんな時には、

" vimdiffで起動した際自動的に単語単位の差分(diffchar.vim)を有効にする
if &diff
  augroup enable_diffchar
    autocmd!
    autocmd VimEnter * execute "%SDChar"
  augroup END
endif

と.vimrcに書いておけばOK。vimdiffコマンドで起動した時には自動的に単語単位の差分が有効になります。(この場合、単語単位の差分をOFFにしたければ<F7>キーを押す、という使い方になります。)

これでvimdiffで差分をチェックする作業が一層はかどりますね。

※補足:autocmdのイベントはFilterWritePost、BufReadPost 、BufWinEnterなども試しましたが、FilterWritePostでは効果がなく、BufReadPostやBufWinEnterだとどうやらdiff表示途中に実行されてしまうらしく余計な箇所まで単語単位で差分表示しようとしてしまい表示が崩れます。VimEnterを指定したら期待した動作になりました。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年8月8日金曜日

gitコミットとRedmineチケットは1対1にする

ソフトウェア開発のプロジェクトでgit(あるいはSubversion)を使っていない方が珍しいと思います。と同時に、極めてよく使われるツールがRedmineなどのプロジェクト管理ツールですね。

昔はバグトラッキングシステム(BTS)と呼ばれていましたが、バグの管理はもちろんのこと、プロジェクトのタスク管理につかうシステムとしても非常に優れており、広く使われるようになったことから最近では "プロジェクト管理ソフトウェア" などと呼ばれているようです。BTSに変わる決まった呼び方があるのかどうかはよく知りません。

ともあれ、git と redmine を連携させて使っているプロジェクトも多いことでしょう。Redmine 上のプロジェクトには git のリポジトリを関連付けることができ、gitのコミットメッセージにチケット番号を特定の書式で書くと、自動的にRedmine上のチケットとgitのコミットが関連付けされます。チケットの表示画面にgitコミットの概要とリンクが表示され、またコミットの表示画面ではチケットへのリンクが表示されます。これは後からソースコードの変更を確認したいときに、タスクの記録と一緒に追うことができて大変便利です。

ところで、1つのチケットに複数のコミットを関連付けることもできます。また逆に1つのコミットに複数のチケットを関連付けることもできます。ただし、僕の意見としては、極力チケットとコミットを1対1に対応させると、後から過去の変更を調べる際にすごく楽になります。(当然完全には無理でしょうから、出来る限りということです。)

例えば、1つのコミットに複数のチケットに関する変更を入れてしまうと、それらの変更がそれぞれどのチケットに関するものか判別するのは、変更した本人でない限り結構手間がかかるものです。あるいは、1つのチケットの解決に要した変更が複数のコミットにまたがっていると、これまたどんな変更がそのチケットに関連してなされたのかの結論を確認するのに骨が折れます。

だから、チケットとコミットを1対1に対応させるのがよいのです。もちろん、自分の手元でコミットを1回きりで済ませるのは大変だと思いますので、例えばローカル専用ブランチを作ってそこにコミットし、1つのチケットの分がまとまったら提出用ブランチにmerge --squash でまとめるといった方法をとればよいと思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月25日金曜日

git diffって多用するよね

git diffは非常によく使うコマンドの1つで、僕の仕事のクオリティを支えてくれているツールでもあります。また、difftoolにvimdiffを用いたgit difftoolも同じくらいよく使います。

僕はかなりこまめにgit diffを使います。プログラミングをしていると、ちょっと書いたらgit diffで差分を見て、動作確認し、またちょっと書く、みたいな感じです。差分を確認せずに一気に書くことはほとんどないです。

あとコミット前にはインデックスとHEADの差分をgit diffとgit difftoolの両方で確認します。ここで原則として差分については全て余すところなく確認します。

あと、ローカルブランチのコミットをgit merge --squashでまとめて提出用ブランチにした際も、提出用ブランチの直前との差分を確認しますが、この際はざっと目を通す感じですね。

こんな感じです。実に利用シーンは多いです。ちなみに差分アルゴリズムはhistogramがおすすめです。difftoolにvimdiffを用いる場合に、差分作成アルゴリズムを変更する方法については過去の記事を参照。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月13日日曜日

gitのpush前にmerge --squashとpullとrebaseを

gitでどうブランチを切ってコミット・プッシュするかの運用方法については様々な意見というか流派(?)があるように思います。僕自身も複数人数のチームで開発を実際に行う中で、こうしたらいいんじゃないかって思っている方法を書いてみようと思います。

まず開発のタスク管理にはredmineを使っている、という背景があります。gitの共有リポジトリはredmineと連携させていて、共有リポジトリにプッシュしたコミットのメッセージにチケット番号を記載しておくと、自動的にredmineのチケットからリポジトリビューアにリンクされるようになっています。そのような背景から、チケットごとにコミットを1つにまとめています。

さらに、共有リポジトリへプッシュする際は、自分の名前が付けられたブランチにプッシュすることにしています。(masterへのマージはmasterブランチ管理者以外は行わない。)例えば僕はkasaiという名前のブランチを作成して、そこへ自分が担当するチケットを解決するコミットを行い、それを共有リポジトリへプッシュします。

masterブランチ管理者は、共有リポジトリへプッシュされた他の担当者のブランチをローカルリポジトリへプルしてmasterへのマージを行い、改めて共有リポジトリへプッシュします。(つまり、共有リポジトリのmasterへのプッシュを行えるのはmasterブランチ管理者だけです。)

この辺までは結構よくある運用方法でしょうね。ここまで全体的な流れの話を書きましたが、次に、各担当者が個人的に行う運用方法についてです。

僕の場合、ローカルリポジトリにはローカル用ブランチを作って作業しています。ローカル用ブランチは自分のためだけのブランチで、一度行った変更を後のコミットで元に戻したり、試行錯誤の汚い履歴が混在していて、そのまま共有リポジトリにプッシュするなんて考えられない代物です。ちなみに僕はローカル用ブランチには local/yy.mm.dd という名前を付けています(yy.mm.ddは作成時の日付、1日に複数作成した場合はyy.mm.dd-2などとする)。

とういことで、ローカル用ブランチで試行錯誤して、完成したら提出用ブランチ(前述した例ではkasai)にまとめて取り込むという方法を採っています。その際にはmerge --squashを使います。

なお、提出用ブランチにmerge --squashされた後のローカル用ブランチは消してもいいのですが、バックアップの意味もあるし、コードが動かなくなった際に履歴が残っていると原因究明にも役立ったり、元に戻したりできるので残しておくことにしています。

では、提出用ブランチをこのままpushすればいいかというと、ちょっと考えるべき問題があります。pushする前に、共有リポジトリのmasterに、自分が元にした時点から変更がいくつも加わっている場合、コミット内容がmasterと競合しないにもかかわらず、仕様変更によりコードが動かなくなってしまう可能性があります。例えば、他人の担当範囲で定義されていた関数やメソッドを呼び出してコードを書いたが、自分がローカルで作業している間に、その関数やメソッドの挙動を変えるコミットがmasterに既にマージされていた、といった場合です。(この問題に関してはGitユーザマニュアルの「マージコミットの分割が1本線の履歴の分割よりも困難となる理由」の解説が分かりやすいです。)

だから、push直前に、最新のmasterを改めてpullで取得し、その最新のmasterにこれからpushしようとする変更を統合した状態で動作することを確認しておきたいんですね。そのためにrebaseを行ます。つまりpush直前に、自分のブランチをpullで取得した最新のmaster上にrebaseするのです。

こうすることで、上記のようにmaster側に知らないうちに仕様変更が加わっていて、競合が発生しないにもかかわらずコードが動かなくなる、という事態に陥る危険性を(ゼロではないにしても)かなり減らすことができます。また、もしそのような事態になったとしても、rebaseしておくことで原因の究明はぐっとやりやすくなります(当然ながら、これはpush前のコミットだからrebaseしてもいいのであって、たとえ提出用ブランチでも、一旦pushした後のコミットは決してrebaseしてはいけません)。

まとめると、こういう流れになります。
  1. ローカル用ブランチを作成し開発を行う。コミットは適宜好きなように行う。
  2. 提出用ブランチにてmerge --squash でローカル用ブランチの変更をまとめて取り込む。まとめる単位はredmineのチケット単位とする。
  3. masterブランチにてpullで共有リポジトリの最新の状態を取り込む。
  4. 提出用ブランチにてrebaseで変更を最新のmaster上に移し替える。
  5. rebaseされた提出用ブランチにて共有リポジトリへpushする。
  6. (masterブランチ管理者のみ) 各担当者の提出用ブランチブランチをpullし、ローカルでmasterへのマージを行い、共有リポジトリへpushする。
これが今のところいいんじゃないかと思っている方法です。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月9日水曜日

capybaraのpage.execute_scriptとjQueryの組み合わせの汎用性がすごい

Webアプリ開発のテストに便利な Capybara(+Cucumber) ですが、テストに使うdriver がJavascriptに対応していれば、テスト中にJavascriptの実行を含めてブラウザの動きをシミュレートできるという素晴らしい特長があります。Javascript対応のdriverとしては、Capybara-WebkitやPoltergeistなんかが有名です。

ボタンやリンクのクリックなどのユーザの動作で発火するイベントは、Capybaraでclickなどのメソッドで指定すれば、Javascript側のイベントの動作もそのままシミュレートしてくれるようですが、マウスオーバーなどのイベントはどうやったらテストできるのでしょうか。

実は、Capybaraには execute_script メソッドがありまして、これでJavascriptのコード片を直接記述して実行できます。この中ではページで読み込んだライブラリなどが有効です。なのでjQueryが使えます。よってjQueryを使ってイベントを発火させることにより、かなりの汎用性を持ちえます。

例えば、マウスオーバーのステップ定義はこんなふうに書けてしまいます。

もし(/^".*?\((.*?)\)" にマウスポインタを乗せる$/) do |t_css|
  page.execute_script("$('#{t_css}').trigger('mouseenter')")
end

上記ステップ定義に従い、featureファイルにはこんなふうに書けますね。

もし "メニュー1(#menu1)" にマウスポインタを乗せる

カッコ内のCSSセレクタで指定した要素にmouseenterイベントが発生し、マウスオーバーの動作テストを実行することができます。

という感じで、execute_script メソッド内でjQueryを使うと簡単な記述で様々なことが実現出来そうです。特にテスト実行上重要なイベントの発火が非常に簡潔に書けるのでいいですね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月8日火曜日

vimdiffでより賢いアルゴリズム(patience,histogram)を使う

vimdiff使ってますか?差分を取る際には非常に便利ですよね。git difftoolに設定して使っている人も多いと思います。しかしgit diffは差分計算のアルゴリズムを選択できますが、vimdiffはデフォルトでは差分計算のアルゴリズムを選択できません。

git diffの差分アルゴリズムには標準のもの以外にpatienceやhistogramがあり、より人間に読みやすい差分を表示してくれる「賢い」アルゴリズムになっています。それぞれgit diffコマンドのオプション--patienceや--histogramを付けるか、または~/.gitconfig設定ファイルにアルゴリズムを指定することで利用できます。

具体的なアルゴリズムの詳細は僕も詳しくないですが、patienceアルゴリズムは「ファイル内でユニークかつ比較ファイル同士で一致する行をなるべく"変化していない行"と認識する」よう働きます。また、histogramアルゴリズムは「patienceアルゴリズムの基本的ルールを継承しつつ高速化を図ったアルゴリズム」で、多くのファイルでpatienceアルゴリズムと同一の結果が得られるようです。また、histogramアルゴリズムはEclipseに統合されたGitクライアントEGit(が使うJGit)の標準アルゴリズムです。僕はコマンドラインツールのgitにおいてもhistogramアルゴリズムを使ってます。

さて、vimdiffにおいてもhistogramアルゴリズムを使いたいところです。vimの設定ではvimdiffの差分計算に使用するコマンドをdiffexprという設定値で指定できます。そして、git diffコマンドも--no-indexオプションを指定することでdiffコマンドと同じようにスタンドアロンツールとして利用できます。じゃあ、diffexprにgit diff --no-indexを指定すればいいのか・・・というと実はそれだけでは失敗します。

理由は、vimがdiffexprに設定したコマンドから受け付ける差分形式が、diffコマンドのnormal形式かed形式でなければならないからです(vimのヘルプにはed形式とだけ書いてあるのですが、normal形式も受け付けます)。そして、git diffコマンドでは直接diffコマンドのnormal形式やed形式で出力することができずunified形式のみに対応しています。

そこで、unified形式をnormal形式に変換するrubyスクリプトを書いて使うことにしました。以下がその変換スクリプトです。利用にはrubyが必要です。

#!/usr/bin/env ruby
iwhite = ''
if (ARGV[0] == '-b')
  iwhite = ARGV.shift.dup << ' '
end

diffout = `git diff --no-index --no-color -U0 #{iwhite}#{ARGV[0]} #{ARGV[1]}`
diffout.sub!(/\A.*?@@/m, '@@')
diffout.gsub!(/^\+/, '> ')
diffout.gsub!(/^-/, '< ')
diffout.gsub!(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@.*/) do
  before_start = $1
  before_size = $2
  after_start = $3
  after_size = $4

  action = 'c'
  if (before_size == '0')
    action = 'a'
  elsif (after_size == '0')
    action = 'd'
  end

  before_end = ''
  if (before_size && before_size != '0')
    before_end = ",#{before_start.to_i + before_size.to_i - 1}"
  end

  after_end = ''
  if (after_size && after_size != '0')
    after_end = ",#{after_start.to_i + after_size.to_i - 1}"
  end

  "#{before_start}#{before_end}#{action}#{after_start}#{after_end}"
end
diffout.gsub!(/^(<.*)(\r|\n|\r\n)(>)/, '\1\2---\2\3')
print diffout

上記スクリプトを~/bin/git-diff-normal-formatとして保存し、~/binにPATHを通しておきます。そしたら、.vim設定ファイルに次の記述を追加します。

" diffのコマンド
set diffexpr=MyDiff()
function MyDiff()
  let opt = ""
  if &diffopt =~ "iwhite"
    let opt = opt . "-b "
  endif
  silent execute "!git-diff-normal-format " . opt . v:fname_in . " " . v:fname_new . " > " . v:fname_out
  redraw!
endfunction

最後に、git diffで使うアルゴリズムを指定します。下記記述ではgit difftoolで起動するコマンドにvimdiffを指定する設定も一緒に行っています。

[diff]
  tool = vimdiff
  algorithm = histogram

これでvimdiffやgit difftoolを実行した時にhistogramアルゴリズムが使えるようになります(patienceを使いたい場合は、上記設定のalgorithmの値をpatienceに変えてください)。

では具体的な効果を見てみましょう。サンプルとして用意した2つの比較対象のファイルをvimでウィンドウを分割して並べたものが下の画像です(クリックで拡大できます)。


まずは、今回の設定をする前、デフォルトのアルゴリズムでvimdiffによる差分をとった結果が以下です(クリックで拡大できます)。


そして、今回の設定をした後の、histogramアルゴリズムでvimdiffによる差分をとった結果が以下です(クリックで拡大できます)。


こちらのほうが、より差分内容も分かりやすく、かつ実際に手で行った変更の操作に近いものになっていますね。
※ちなみにこのvimdiffの色設定は過去に書いたこの記事を参照。

※2014/07/10更新:diffupdateを実行した際に画面の描画がおかしくなることがあったので、回避のためMydiff()にredraw!を追加

※参考記事
Making your Git Diff/Merge More Useful | The Interim Developer
Alfedenzo - Patience Diff, a brief summary
Bram Cohen's Journal - Patience Diff Advantages
HistogramDiff (JGit - Core 2.0.0.201206130900-r API)

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年7月4日金曜日

gitでマージの事前に衝突するかどうか確認する

gitを使っていて、特定のブランチとマージしたら衝突が起きないかどうか知りたい場合があります。例えば、プロジェクトチームで共有しているリポジトリにコミットをプッシュする前に、現在作業中のブランチがmasterブランチと衝突を起こさないか確認しておきたい、なんて場合です。

そんな時は、mergeコマンドの —no-commit オプションで master をマージしてコミットせずに止めてみればいいです。あとで簡単に元に戻せます。(本来確かめたい向きとは逆向きのマージですが、衝突の確認をするだけなら結果は同じですから問題ありません。)

[user@host]% git merge master --no-commit
Auto-merging /some/file/path
Automatic merge went well; stopped before committing as requested

同一ファイルに編集があったが、自動的にマージされた場合には上記のような表示になります。衝突を起きて自動的に解決できない場合はその旨がここで表示されるはずです。

確認は終わりましたが、そのままではワーキングツリーとインデックスが変更されたままですので、元に戻します。mergeコマンドの --abort オプションを使えばいいです。

[user@host]% git merge --abort

これで元通り。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月20日金曜日

他人のソースコードを読む際に必要な"読解力"

昨日の投稿で「コメントをしっかり書くべき」だと書きましたけれども、現実問題としてコメントが圧倒的に少ないソースコードを読まなきゃいけない場合もあるわけでしてね。その時には仕方ないんでとにかく意味を頭の中で補完しながら読むしか無いわけです。

ところで、「他人の書いたものを、書いた人間の意図や目的を推測しながら読む」というのは、まさに「読解力」だと思ったりもするわけです。これは普通の文章でもプログラムのソースコードでも共通しています。

もちろん単純に自然言語の読解力とプログラミング言語の読解力を比べることはできませんけれども、繋がっているとは思うんですよね。

エンジニアの世界だと技術的な要素ばかりが注目されがちですし、まあ事実優先順位としては技術的な要素が上で当たりまえではあるんですけど、実は「読解力」も欠かせないんじゃないかと思っています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月19日木曜日

ソースコードのコメントはしっかり書くべき

プログラミングにおいて、"ソースコードに記述するコメント"に対するスタンスとして、

「コメントは極力書かないようにする。なぜなら、ソースコードを見てそのまま意味が分かるのが良いプログラムであるからだ。」

という意見があります。僕もまだプログラミングを覚えたての頃はそうかもしれないと思い、自分がコメントの少ないソースコードを読むのに苦労しているのは、単純に経験不足のせいだと思っていました。

しかし、何年も実際に仕事でプログラミングの経験を積んだ現在、はっきりと断言できます。コメントを書くべき場合というのは存在します。「ソースコードを見てそのまま意味が分かるのが良いプログラムである」という言葉を単純に信じてはいけません。そして、コメントを書かなくて困る場合はあっても、書きすぎて困ることはありません。だから僕はソースコードのコメントはしっかり書くべきだと思っています。

たとえソースコードの意味が完全に理解できたとしても、外部から渡される引数の意味や取りうる値など、ソースコードには書かれないものの動作を理解するために不可欠な情報がありますからね。

また、ソースコードをどのような意図で書いたのか、など、ソースコードを読む開発者にとって知りたい情報もあります。

もちろん書きすぎはよくありませんが、オブジェクト指向プログラミングならば、全てのメソッドについて目的と各引数・返り値の説明くらいは例外なく全部コメントを書くべき、というのが僕の意見です。

そんなのソースコード見りゃ分かるって?嘘ですね。確実に嘘です。

だったら、他人の書いたソースコードを読んでいる時を思い出してみればいいですよ。全くコメントがない状態だと、たった1行からなるメソッドですら、そのメソッドの意図や引数の取りうる値やどんな返り値になるのかを推測するのが大変な場合なんて腐るほどあるでしょ。この業界でエンジニアやってれば経験があるはずですよ。

結局コメントを書くのに反対する意見って単純にコメント書くのが面倒なだけって気がするんですよね。いいから書こうよ、って思います。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月18日水曜日

自動テストのためのid属性とclass属性の使い分け

HTMLのid属性とclass属性の使い分けについては、「id属性はHTML文書内でユニーク(重複しない)であり1つの要素に1つのみ指定、class属性は重複可能で1つの要素に複数使用できる」という特性はあるものの、以前は使い分けは結構曖昧でした。

が、最近になって以下のように使い分けるようになりました。

  • id属性 → 自動化テストの要素特定のために使う。
  • class属性 → CSSによるスタイルの指定に使う。

理由を説明します。

id属性は単一指定だがclass属性は複数指定可能という特性から、自動テストという目的にはid属性のほうが向いています。複数指定だと、意図しない要素が指定されてしまったりする可能性もあり、テスト記述に余計な気を使うことになります。よって、勘違いやミスによるバグの発生はid属性のほうが少なくなると考えられます。

逆にスタイルの指定という観点からすれば、複数指定で重ねがけできるclass属性は都合がよく、CSSに使うのはclass属性でしょう。クラスの名前を工夫すればid属性は使わなくても済みますから、テストコードとデザインを分離することが可能です。

ちなみに動作速度はid属性指定のほうが速いらしいですが、どちらもブラウザの体感速度としては無視できるくらいに速いので、ユーザビリティの観点からは実質的に差はないです。しかし、自動テストでは幾度と無く表示を高速に繰り返すので、id属性指定の効果がある・・・かもしれません。「かもしれません」と書いたのは、実際にまだそこまで大量のテストを書いて試していないからです。まあ、動作速度としてはそのくらい差はわずかでしかないです。

上記の書き方に従えば、一つの要素に同じ値のid属性とclass属性を付ける場合も多いです。でもこれで自動テストとスタイリングの分離ができると考えれば、そんなに大した話ではないと思ってます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月17日火曜日

Ruby/LDAP 経由で LDAPS 接続

Rubyから手軽にLDAP接続するのに使えるRuby/LDAPライブラリですが、LDAPS接続をする方法について、あまり実際の例がないように思います。今回、実際にやってみたので、ポイントを書いておこうと思います。

ポイントは3つ。
  • LDAP::Connの代わりにLDAP::SSLConnを使うこと。
  • 接続先ポート番号を明示的に指定すること。
  • LDAPクライアント用設定(/etc/ldap/ldap.conf)で証明書検証設定を正しく設定すること。
以下、やってみた時の経緯です。
今までSSLなしのLDAP接続はできていました。単にLDAP::Conn を LDAP::SSLConn に置き換えればいいと思っていたんですが、エラーになってしまいます。例をirbで示してみますと、こんな感じです。

irb(main):001:0> require 'ldap'
=> true

irb(main):003:0> conn = LDAP::SSLConn.new('localhost')
=> #<LDAP::SSLConn:0x000000021130d0>

irb(main):004:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x000000021130d0>

irb(main):006:0> conn.bind(‘cn=hoge,dc=foo,dc=localhost’,’pass')
LDAP::ResultError: Can't contact LDAP server
from (irb):6:in `bind'
from (irb):6
from /usr/bin/irb:12:in `<main>'

よく調べてみると、ポート番号があやしいことに気づきました。公式ドキュメントによると、デフォルト値が定数LDAP_PORTになっているんですが、LDAP::LDAP_PORTはSSLではない接続用のポート番号(389)が入った定数なのです。

実はLDAP::LDAPS_PORTの方がSSL接続用のポート番号(636)が入った定数なんですね。なぜデフォルト値がLDAPS_PORTじゃなくてLDAP_PORTなのかはよく分からないのですが、もしかして、LDAP::SSLConnはLDAP::Connのサブクラスだからかもしれません。※参考→http://ruby-ldap.sourceforge.net/rdoc/classes/LDAP/SSLConn.html

ということで、今度はポート番号にLDAP::LDAPS_PORTを指定して再試行してみます。

irb(main):008:0> conn = LDAP::SSLConn.new('localhost', LDAP::LDAPS_PORT)
=> #<LDAP::SSLConn:0x0000000211c180>

irb(main):009:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x0000000211c180>

irb(main):010:0> conn.bind('cn=admin,dc=test,dc=localhost','admin')
LDAP::ResultError: Can't contact LDAP server
from (irb):10:in `bind'
from (irb):10
from /usr/bin/irb:12:in `<main>’

しかし、まだ同じエラーです。slapd側のログを見ると、接続自体は確立されているようです。このようなログが残っています。

conn=1013 fd=13 TLS established tls_ssf=128 ssf=128

このログはポート番号修正前には記録されなかったので一歩前進です。LDAPのSSL接続について更に検索すると、こんな情報が見つかりました。→http://christian.hofstaedtler.name/blog/2009/05/ruby-ldap-ssl.html

要するに/etc/ldap/ldap.confにクライアント設定が原因かもしれないということ。試しに、/etc/ldap/ldap.conf の TLS_CACERT をサーバ側の cn=config で設定している
CA証明書と同じ証明書を指定するようにしたところ、接続に成功しました。

なるほど。サーバ証明書をCA証明書で検証していたわけですね。しかしそもそも、証明書検証が必要なければ省略する方法もあります。※参考→https://wiki.debian.org/LDAP/OpenLDAPSetup#Configuring_LDAPS

/etc/ldap/ldap.conf で TLS_CACERT をコメントアウトし、TLS_REQCERT を never に設定すればいいですね。今回はこちらを採用します。

ということで、irbでもう一度試してみます。

irb(main):001:0> require 'ldap'
=> true

irb(main):002:0> conn = LDAP::SSLConn.new('localhost',LDAP::LDAPS_PORT)
=> #<LDAP::SSLConn:0x0000000166b9e8>

irb(main):003:0> conn.set_option(LDAP::LDAP_OPT_PROTOCOL_VERSION, 3)
=> #<LDAP::SSLConn:0x0000000166b9e8>

irb(main):004:0> conn.bind('cn=admin,dc=test,dc=localhost','admin')
=> #<LDAP::SSLConn:0x0000000166b9e8>

これで接続に成功することを確認できました。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月14日土曜日

slapdをSSL対応で起動させる(Debian)

LDAPサーバをSSL対応(LDAPS)で起動させる際には、サーバ証明書、CA証明書、秘密鍵の3つを設定します。cn=configの属性にこんな感じで設定することになります。

dn: cn=config
olcTLSCACertificateFile: /etc/ssl/certs/ssl-cert-snakeoil.pem
olcTLSCertificateFile: /etc/ssl/certs/ssl-cert-snakeoil.pem
olcTLSCertificateKeyFile: /etc/ssl/private/ssl-cert-snakeoil.key

ちなみにsnakeoilというのはサンプル用証明書によく使われる名前です。

それから、/etc/default/slapd の SLAPD_SERVICES に ldapsプロトコルを含めます。下の例は、SSL対応のLDAPS通信とソケット通信のみ受け付けるよう設定する例です。

SLAPD_SERVICES="ldaps:/// ldapi:///"

これで起動すればOK・・・と言いたいところが、すんなりとはうまくいくと限りません。

main: TLS init def ctx failed: -1

というエラーが発生することがあります。このエラーは、証明書や秘密鍵の読み込みに失敗すると発生します。調べてみると、Debianでは、デフォルトのままだと秘密鍵の読み取り権限がないようです。参考 → https://wiki.debian.org/LDAP/OpenLDAPSetup#Configuring_LDAPS

秘密鍵は所有者のグループを ssl-cert にし、グループに読み取り権限を付与することが推奨されています。slapdはopenldapというユーザ名で実行されますので、openldapユーザをssl-certグループに所属させればOKです。

# usermod -a -G ssl-cert openldap

これで今度こそOKです。

# service slapd start

とすればSSL対応のLDAPS通信を待ち受ける状態で起動するはずです。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月13日金曜日

RSpecの"-fs"オプションが無効になったことへの対策

Padrinoのプロジェクトにおいてrake 経由でRSpecのテストを実行しようとして、今まで起きていなかったエラーが突然起きました。

`find_formatter': Formatter 's' unknown - maybe you meant 'documentation' or 'progress'?. (ArgumentError)

スタックトレースを見てもソースコードの原因箇所が的確に示されていなかったので少々戸惑いましたが、結局原因はspec/spec.rakeファイルにありました。そこではspecコマンドのオプションとして"-fs —color"が指定されていたんですが、実はこの"-fs"が原因でした。"-fs"は"--format specdoc"の短縮形式で、以前はこのオプションは有効だったが無効になったみたいです。

現在はフォーマットとして指定できるのは"documentation"か"progress"になっていますが、詳細な情報を出力したいので"documentation"を指定することにします。この指定にあわせてspec/spec.rakeファイルを修正しようと思います。

Padrinoが自動生成する修正前のspec/spec.rakeファイルはこんな感じです。

begin
  require 'rspec/core/rake_task'

spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
 result << File.basename(d) unless Dir["#{d}*"].empty?
 result
end

  spec_tasks.each do |folder|
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(-fs --color)
    end
  end

  desc "Run complete application spec suite"
  task 'spec' => spec_tasks.map { |f| "spec:#{f}" }
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

これを以下のように変更します。

begin
  require 'rspec/core/rake_task'

  spec_tasks = Dir['spec/*/'].inject([]) do |result, d|
    result << File.basename(d) unless Dir["#{d}*"].empty?
    result
  end

  spec_tasks.each do |folder|
    desc "Run spec suite under #{folder} directory"
    RSpec::Core::RakeTask.new("spec:#{folder}") do |t|
      t.pattern = "./spec/#{folder}/**/*_spec.rb"
      t.rspec_opts = %w(--format documentation --color)
      t.verbose = false
    end
  end

  desc "Run complete application spec suite"
  RSpec::Core::RakeTask.new("spec") do |t|
    t.pattern = "./spec/**/*_spec.rb"
    t.rspec_opts = %w(--format documentation --color)
    t.verbose = false
  end
rescue LoadError
  puts "RSpec is not part of this bundle, skip specs."
end

変更点としては、-fsオプションをやめて--format documentationにしたこと、
t.verbose = falseを追加したこと、"spec"タスクを"spec:フォルダ"の集合ではなく
新たに全specファイルを含むタスクとして定義したことです。

t.verbose = falseを指定するのは、長ったらしい実行コマンドの出力を省いて出力を見やすくするためです。また、"spec"タスクが"spec:フォルダ"の集合だと、何件成功して何件失敗したか、という結果がフォルダの実行ごとに表示されるので見づらくなるので、対象を全てにした1つのテストスイートにまとめなおしています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月7日土曜日

SCSSが便利すぎる

WebアプリのUIを組んでいて、今回初めてSCSSを使ってみているわけですが、便利さにびっくりしてます。以前からその便利さはネット上で色々語られているので知識としては知っていましたが、やっぱり自分で実際に使うと実感できますね。

今回SCSSを書くこと自体初めてでしたが、ものの30分くらいで全く問題なく作業を進められるようになりました。記法としてはSASS記法ではなくSCSS記法です。ネットを見ているとHAMLを採用したSASS記法の方が記述量が少なくすっきりしていて、そちらが好みの人もいるらしいですが、僕はCSSの書き方が流用できるSCSS記法を支持します。

なお、LESSという選択肢もありますが、Rubyを採用したプロジェクトなのでSCSSと相性が良さそうという理由でSCSSを選択しました。Padrinoフレームワークは、.scssファイルを自動的に.cssファイルに変換してくれます。

うーん、一度実際に体験してしまうと、もはや生のCSSなんて書きたくない(笑)

ちなみにSCSSの書き方詳細についてはネット上に良質な解説サイトが沢山あるのでそちらをご参照ください。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月6日金曜日

Capybaraで入力欄・ボタン・リンクをCSSセレクタによって指定する

CapybaraはWebアプリケーションをテストする際に便利なライブラリです。ところで、フォームに入力するメソッドとしてはfill_inがありますが、要素の指定にCSSセレクタが使えません。click_button, click_linkも同様にCSSセレクタが使えません。ではXPathは使えるかというと、XPathも使えません。指定できるのは単なる文字列で、例えばfill_inの場合、その文字列がid属性またはname属性または対応するlabel要素の文字列に合致するものを探すことになっています。他のfindなどの要素指定メソッドではCSSセレクタを使うので、他のメソッドとの統一性がなくて分かりにくいし、細かい指定もできないので、CSSセレクタを使いたいところです。

単純な解決策としては、findメソッドで要素を絞り込んでsetやclickすればいいわけですが、それだけだともとの機能と同一にはになりません。というのは、fill_in, click_button, click_linkの特長として、それぞれのタイプに合致する要素だけを探してくれるのです。例えばfill_inならば入力可能フィールドのみを検索対象としてくれます。

これは、Capybaraのこのソースコードを見るとよくわかります。

例えばfill_inの場合、"fillable_field"という条件で探してsetしています。これと同じことをできればいいわけです。条件の詳細はXPathライブラリのこのソースコードを見るとわかります。

これによるとfill_inに使われる"fillable_field"の条件は、

  • input要素あるいはtextarea要素であり、
  • type属性が次のいずれでも「ない」:'submit', 'image', 'radio', 'checkbox', 'hidden', ‘file'

click_buttonに使われる"button"の条件は、

  • input要素かつ、type属性が次のいずれかである:'submit', 'reset', 'image', ‘button'
  • または、button要素である

click_linkに使われる"link"の条件は、

  • a要素かつ、href属性が存在する

となっています。これらの条件を満たすように、CSSセレクタに条件を付加してXPathに変換するメソッドを作ってみました。

# CSSセレクタをXPathに変換し、さらに入力可能フィールドの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_fillable_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # init_xpath = %(#{base_xpath}[../input or ../textarea])
  # new_xpath = ['submit', 'image', 'radio', 'checkbox', 'hidden', 'file'].inject(init_xpath) do |tmp_xpath, attr|
  #   %(#{tmp_xpath}[not(@type='#{attr}')])
  # end
  # new_xpath
  # 高速化のためベタ書き
  "#{base_xpath}[../input or ../textarea][not(@type='submit')][not(@type='image')][not(@type='radio')][not(@type='checkbox')][not(@type='hidden')][not(@type='file')]"
end

# CSSセレクタをXPathに変換し、さらにボタンの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_button_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  # xpath_input = %(#{base_xpath}[../input][@type='#{['submit', 'reset', 'image', 'button'].join("' or @type='")}'])
  # xpath_button = %(#{base_xpath}[../button])
  # "(#{xpath_input} | #{xpath_button})”
  # 高速化のためベタ書き
  "(#{base_xpath}[../input][@type='submit' or @type='reset' or @type='image' or @type='button'] | #{base_xpath}[../button])"
end

# CSSセレクタをXPathに変換し、さらにリンクの条件を付加する
# @param [String] css CSSセレクタ
# @return [String] 変換後のXPath
def convert_css_to_link_xpath(css)
  base_xpath = %(#{Nokogiri::CSS.xpath_for(css)[0]})
  "#{base_xpath}[../a][@href]"
end

少々解説します。Nokogiri::CSS.xpath_for は、複数条件CSSに対応するため、返り値はXPath文字列を要素に持つ配列になります。入力欄を選ぶという状況的に、1つのみになると考えられるので最初の要素を選択すればよいから、Nokogiri::CSS.xpath_for(css)[0]としています。

変換したXPathの後ろに例えば [../input or ../textarea] といった要素指定を付加しています。これは、要素自身がinput要素あるいはtextarea要素であることを後から無理やり指定するためです。スマートではない形式ですが、CSSセレクタを変換して条件を付加するという状況なので仕方ありません。

最初はメソッド中のコメントアウトしてある部分のようにかなり動的にXPathを生成していましたが、テスト実行を少しでも早くするために、文字列をベタ書きしておくことにしました。

さて、使い方としてはCSSセレクタを渡してXPathに変換し、その条件でノードを取得して動作を行えばよいです。Cucumberによる例を挙げると、こんな感じでステップ定義に使えます。何やら余計なものが色々付いているように見えますが、その辺の詳細は以前に書いたこの記事を参照。

もし(/^".*?\((.*?)\)" 欄に "(.*?)" と入力(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_with, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_fillable_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).set(t_with)
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" ボタンを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_button_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

もし(/^".*?\((.*?)\)" リンクを押下(?:し(、)|した)(\(非同期\))?( debug)?$/) do |t_css, t_eos, t_async, debug|
  show_debug_info if debug
  xpath = convert_css_to_link_xpath(t_css)
  page.find(:xpath, xpath, :visible => true, :wait => t_async ? async_time : 0).click
  puts '' if t_eos.blank?
end

Gerkinによるfeature定義としては、例えば

もし "アカウント名(#account_name)" 欄に “foo" と入力し、
かつ "パスワード(#password)" 欄に “bar" と入力し、
かつ "ログイン(#login-button)" ボタンを押下した
ならば・・・(以下略)

のようになります。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月5日木曜日

Sinatra・PadrinoでセッションIDを変更する

Webアプリケーションでは例えばログインやログアウトしたタイミングなど、セッションハイジャックのリスクを下げるためにセッションIDを変更したほうがよい場面があります。

PHPですと、言語に組み込みでsession_regenerate_idという関数が用意されています。さすがWebに特化した言語ですね。

で、Rubyでは言語そのものにセッションIDを変更する方法はありません。各フレームワーク依存ですね。ということで、軽量フレームワークとしてよく使われるSinatraやその派生のPadrinoにおいてセッションIDを変更する方法を調べてみました。

ます、セッション内容を破棄して構わないのであれば、

session.destroy

と単にdestoryメソッドを呼んでセッションを破棄するだけです。自動的に新しいセッションIDでセッションが再生成されます。

セッション内容を引き継ぎたい場合は、事前にセッション内容を取得しておき、
新しいセッションの内容としてまるごと置き換えます。

old_session_hash = session.to_hash
session.destroy
session.replace(old_session_hash)

Padrinoの場合、以下の様な感じでヘルパーメソッドにしておくとよいでしょう。メソッド名はphpのマネです。ヘルパーを定義している部分に適当に追加すればいいと思います。

# セッションIDを再生成する
# @return [void]
def session_regenerate_id
  old_session_hash = session.to_hash
  session.destroy
  session.replace(old_session_hash)
end

上記 session_regenerate_id を呼べば、セッションIDが変更されてセッション内容が引き継がれます。

※2014/06/12追記
RackミドルウェアでCSRF対策をしている場合には、セッションIDの変更とともに、セッション内に保持しているCSRFトークンも変更したいと思うでしょう。そういう時は、引き継ぐセッション情報からCSRFトークンの情報を削っておくと、自動的にCSRFトークンを再生成してくれます。ということで修正したコードを載せておきます。

# セッションIDを再生成する
# @return [void]
def session_regenerate_id
  old_session_hash = session.to_hash
  old_session_hash.delete('csrf')
  session.destroy
  session.replace(old_session_hash)
end

上記の例はCSRFトークンをセッションに格納する際のキーが"csrf"である場合です。このキーは状況に応じて変更してください。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月4日水曜日

Capybaraでテスト実行中にセッションIDを取得する

テストにCapybaraを使ってるんですが、セッションIDが変化するかどうかをテストしたいと思いまして。ところが、セッションの中身を見る場合は rack_session_access っていうライブラリが使えるんですが、rack_session_accessではセッションID自体を取得する方法は提供してくれません。

Rack::Session::Cookieを使っている場合にはセッションの中身にセッションIDが入っているので、rack_session_access でセッションIDを取得できますが、Rack::Session::Cookie以外によるセッション機能を使っている場合はrack_session_accessではセッションIDが取得できません。

何とかできないものかと思案し、発想を転換しました。Rack::Session::Cookie以外によるセッション機能を使っている場合、ブラウザのCookieにはセッションIDそのものが入っているわけです。つまり、Cookie経由でセッションIDが取得できるはずです。

ということで調べると、Capybaraのドライバを呼び出して、ドライバ内部のCookieを取得するメソッドを呼び出せばいいみたいです。なお、ドライバ内部のCookieを取得するメソッドの呼び出し方法は、ドライバごとに異なります。次の例は、ドライバがPoltergeist の場合の例です。

@session_id =  page.driver.browser.cookies['rack.session'].value


などとすれば取得できます。

なお、セッションIDが変化するかどうかをテストしたい場合はセッションIDを保存しておく必要がありますが、単純にインスタンス変数に入れておけば、後のステップでそのままインスタンス変数として呼び出すことができます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年6月3日火曜日

RSpecで外部コマンド実行結果を判定する

RSpecによる自動テスト中に、外部コマンドを実行して成功したかどうかを判定したいとします。そういう時は、実行ステータスを取得でき、なおかつ外部コマンドの終了を待ち合わせるような外部コマンドの実行方法を採用すればいいでしょう。

Rubyには外部コマンドの実行方法がいくつか存在しますが、Open3.capture2あたりが適しているように思います。コマンドの標準出力への出力結果はテストで使ったりするため取得したいことが多く、なおかつ標準エラー出力はそのままコンソール上に表示することでテスト中に外部コマンドでエラーが起きた際に原因が分かりやすくなります。

コマンドの実行が成功したかどうかを終了ステータスで判定すれば、エラーが起きた際に分かりやすくなるでしょう。コード例を挙げておきます。

stdout, status = Open3.capture2 %(some_command), :stdin_data => stdin_string
expect(status.exitstatus).to eq(0)

上記コードでは終了ステータスが0、つまりコマンドが成功することを期待しています。コマンドが異常終了して終了ステータスに0以外が返ると、テストは失敗となります。

なお、僕の場合実際には上記コードをRSpecから直接実行するのではなく、Cucumberのステップ定義に組み込んで使っています。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月30日金曜日

capybara-webkitからpoltergeist(phantomjs)に乗り換え

RubyによるWebアプリケーションの自動化テストに便利なCapybara。JavascriptによるDOM操作も、capybara-webkitドライバを経由してテストすることができます。当初はそれで良かったんですが、とある事情によりXサーバ関連ファイル群を入れないで動かしたい場合があったので、Xサーバ関連ファイル群を必要とする仮想フレームバッファ(xvfb)への依存を避けたくなりました。

Capybaraにはもう1つ、JavascriptによるDOM操作を含むテストを実行できる手段がありまして、それがPoltergeistドライバを経由してPhantomJSを動かす、というものです。PhantomJSは仮想ブラウザみたいなもので(乱暴な説明)、詳しい説明は他サイトに譲ります。CabybaraからPhantomJSを動かすためのドライバがPoltergeistです。

ということで、Capybara-webkitからPoltergeist(経由のPhantomJS)に乗り換えることにしました。

まず、PhantomJSをインストールします。僕の環境としてはDebian7ですが、公式サイトのビルド済みパッケージを取ってきたらそのまま動公式サイトにLinux用のビルド済みバイナリがあるので、入手してください。2014/05/29現在の最新版はver1.9.7であり、
"phantomjs-1.9.7-linux-x86_64.tar.bz2"というリンクからダウンロードできます。

解凍して出来たディレクトリの bin/phantomjs をPATHの通った適当な場所に移動します。

例)$HOME/bin に移動する場合

[user@host]% tar jxvf phantomjs-1.9.7-linux-x86_64.tar.bz2
[user@host]% cp phantomjs-1.9.7-linux-x86_64/bin/phantomjs ~/bin

なお、phantomjsの動作には fontconfig, freetype ライブラリが必要ですが、最近のLinuxデスクトップ環境ならば、ほとんどの場合にはすでに入っているでしょう。

ruby側の対応は巷のサイトに書いてあるとおりでOKです。まずGemfileへPolgergeistの記述を追加します。

gem 'poltergeist'

僕はCucumber経由でcapybaraを使ってますので、features/support/env.rb に必要ライブラリの require と default_driver, javascript_driverの変更を記述します。(Javascirpt関連テストの時だけ使いたい場合は、default_driver側の記述は不要です。)

require 'capybara/poltergeist'
Capybara.default_driver = :poltergeist
Capybara.javascript_driver = :poltergeist

なお、Cucumberのテスト自体は書き換えずに動作しました。まだそんなにテストの量がないせいか、速度の違いは感じられません。

PhantomJSは活発に開発されているようなので、今後も色々と改善が期待できますね。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。

2014年5月29日木曜日

Padrinoの起動時のロード順・フック実行について

今回のテーマは実は、Padrinoにおいてconfig/boot.rbにはbefore_loadフックとafter_loadフックの登録を行うコード記述があるけれど、config/boot.rbファイルのトップレベルに直接書くのと before_loadやafter_loadの中に書くのではどう違うのか、という疑問が切っ掛けでした。

Padrinoが生成するconfig/boot.rbは末尾で Padrino.load! を呼び出していますが、実はその中でbefore_loadとafter_loadを呼んでいます。Padino.load!の定義は、ソースコードを調べると次のようになってます。

def load!
  return false if loaded?
  began_at = Time.now
  @_called_from = first_caller
  set_encoding
  set_load_paths(*load_paths)
  Logger.setup!
  require_dependencies("#{root}/config/database.rb")
  Reloader.lock!
  before_load.each(&:call)
  require_dependencies(*dependency_paths)
  after_load.each(&:call)
  logger.devel "Loaded Padrino in #{Time.now - began_at} seconds"
  Thread.current[:padrino_loaded] = true
end

つまりbefore_loadとafter_loadの違いは、Padrinoの各種依存ファイルのrequireの前か後か、という違いです。

上記Padino.load!をよく見てみると、Loggerだけ扱いが別になっています。Logger関連の設定はbefore_loadやafter_loadの中に書いてもそれだけでは有効になりません。config/boot.rbのトップレベルかつPadrino.load! の前に記述するか、手動でLogger.setup!を呼ぶ必要があります。

で、ようやく本題ですが、上記のPadrino.load!を見ながら、実際フレームワーク全体がどう読み込まれるのか調べてみたくなりました。Loggerのログレベルをdevelに設定すると、起動時にロードしたファイルをログに出してくれるようになるので、ロードの順番がわかります。まとめるとPadrinoのロードの主要手順はだいたいこんな感じになっているようです。

  • Loggerの設定
  • config/database.rbのロード
  • before_loadフックの実行
  • lib/の中にあるファイルのロード
  • models/の中にあるファイルのロード
  • (shared/lib/の中にあるファイルのロード)※デフォルトでは存在しない
  • (shared/models/の中にあるファイルのロード)※デフォルトでは存在しない
  • config/apps.rbのロード
  • app/app.rbのロード
  • アプリケーション名::Appのインスタンス生成
  • after_loadフックの実行
  • app/controllers/の中にあるファイルのロード
  • app/helpers/の中にあるファイルのロード

なるほど。ちなみに意外だったのは、controllersとhelpersのロード順が予想と逆だったこと。でもどうせメソッド定義なので特にロード順に影響はないはずです。

ロードの順番に影響を受けるようなコードの書き方をすることは少ない(というかなるべく避けるべき)と思いますが、デバッグの役に立つこともありますので一応知っておくとよいと思われます。

※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。