前回「既存のコードはなるべくいじらない」と書いてますが、それは他人が書いたコードとか、すでにテストを通過したコード、公開されて十分に使われているコードなどについてのことです。
自分で書いたコードで、リリース用のテストが始まる前の段階のものについては積極的に修正を行っていくべきでしょう。もしタイミングを逃してそのままリリースされてしまうと、今度は下手にいじれなくなってしまいますからね。
特にリファクタリングでコードの質を向上させておくことは後々のためになります。とはいえなかなか時間をとれないとは思いますが、例えば変数・関数・メソッドの名前の付け方だけでも見なおしておくことは役立ちます。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年8月7日木曜日
2014年6月13日金曜日
RSpecの"-fs"オプションが無効になったことへの対策
Padrinoのプロジェクトにおいてrake 経由でRSpecのテストを実行しようとして、今まで起きていなかったエラーが突然起きました。
スタックトレースを見てもソースコードの原因箇所が的確に示されていなかったので少々戸惑いましたが、結局原因はspec/spec.rakeファイルにありました。そこではspecコマンドのオプションとして"-fs —color"が指定されていたんですが、実はこの"-fs"が原因でした。"-fs"は"--format specdoc"の短縮形式で、以前はこのオプションは有効だったが無効になったみたいです。
現在はフォーマットとして指定できるのは"documentation"か"progress"になっていますが、詳細な情報を出力したいので"documentation"を指定することにします。この指定にあわせてspec/spec.rakeファイルを修正しようと思います。
Padrinoが自動生成する修正前のspec/spec.rakeファイルはこんな感じです。
これを以下のように変更します。
変更点としては、-fsオプションをやめて--format documentationにしたこと、
t.verbose = falseを追加したこと、"spec"タスクを"spec:フォルダ"の集合ではなく
新たに全specファイルを含むタスクとして定義したことです。
t.verbose = falseを指定するのは、長ったらしい実行コマンドの出力を省いて出力を見やすくするためです。また、"spec"タスクが"spec:フォルダ"の集合だと、何件成功して何件失敗したか、という結果がフォルダの実行ごとに表示されるので見づらくなるので、対象を全てにした1つのテストスイートにまとめなおしています。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
`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月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"の条件は、
click_buttonに使われる"button"の条件は、
click_linkに使われる"link"の条件は、
となっています。これらの条件を満たすように、CSSセレクタに条件を付加してXPathに変換するメソッドを作ってみました。
少々解説します。Nokogiri::CSS.xpath_for は、複数条件CSSに対応するため、返り値はXPath文字列を要素に持つ配列になります。入力欄を選ぶという状況的に、1つのみになると考えられるので最初の要素を選択すればよいから、Nokogiri::CSS.xpath_for(css)[0]としています。
変換したXPathの後ろに例えば [../input or ../textarea] といった要素指定を付加しています。これは、要素自身がinput要素あるいはtextarea要素であることを後から無理やり指定するためです。スマートではない形式ですが、CSSセレクタを変換して条件を付加するという状況なので仕方ありません。
最初はメソッド中のコメントアウトしてある部分のようにかなり動的にXPathを生成していましたが、テスト実行を少しでも早くするために、文字列をベタ書きしておくことにしました。
さて、使い方としてはCSSセレクタを渡してXPathに変換し、その条件でノードを取得して動作を行えばよいです。Cucumberによる例を挙げると、こんな感じでステップ定義に使えます。何やら余計なものが色々付いているように見えますが、その辺の詳細は以前に書いたこの記事を参照。
Gerkinによるfeature定義としては、例えば
のようになります。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
単純な解決策としては、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を変更する方法を調べてみました。
ます、セッション内容を破棄して構わないのであれば、
と単にdestoryメソッドを呼んでセッションを破棄するだけです。自動的に新しいセッションIDでセッションが再生成されます。
セッション内容を引き継ぎたい場合は、事前にセッション内容を取得しておき、
新しいセッションの内容としてまるごと置き換えます。
Padrinoの場合、以下の様な感じでヘルパーメソッドにしておくとよいでしょう。メソッド名はphpのマネです。ヘルパーを定義している部分に適当に追加すればいいと思います。
上記 session_regenerate_id を呼べば、セッションIDが変更されてセッション内容が引き継がれます。
※2014/06/12追記
RackミドルウェアでCSRF対策をしている場合には、セッションIDの変更とともに、セッション内に保持しているCSRFトークンも変更したいと思うでしょう。そういう時は、引き継ぐセッション情報からCSRFトークンの情報を削っておくと、自動的にCSRFトークンを再生成してくれます。ということで修正したコードを載せておきます。
上記の例はCSRFトークンをセッションに格納する際のキーが"csrf"である場合です。このキーは状況に応じて変更してください。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
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 の場合の例です。
などとすれば取得できます。
なお、セッションIDが変化するかどうかをテストしたい場合はセッション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年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 に移動する場合
なお、phantomjsの動作には fontconfig, freetype ライブラリが必要ですが、最近のLinuxデスクトップ環境ならば、ほとんどの場合にはすでに入っているでしょう。
ruby側の対応は巷のサイトに書いてあるとおりでOKです。まずGemfileへPolgergeistの記述を追加します。
僕はCucumber経由でcapybaraを使ってますので、features/support/env.rb に必要ライブラリの require と default_driver, javascript_driverの変更を記述します。(Javascirpt関連テストの時だけ使いたい場合は、default_driver側の記述は不要です。)
なお、Cucumberのテスト自体は書き換えずに動作しました。まだそんなにテストの量がないせいか、速度の違いは感じられません。
PhantomJSは活発に開発されているようなので、今後も色々と改善が期待できますね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
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!の定義は、ソースコードを調べると次のようになってます。
つまり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のロードの主要手順はだいたいこんな感じになっているようです。
なるほど。ちなみに意外だったのは、controllersとhelpersのロード順が予想と逆だったこと。でもどうせメソッド定義なので特にロード順に影響はないはずです。
ロードの順番に影響を受けるようなコードの書き方をすることは少ない(というかなるべく避けるべき)と思いますが、デバッグの役に立つこともありますので一応知っておくとよいと思われます。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
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のロード順が予想と逆だったこと。でもどうせメソッド定義なので特にロード順に影響はないはずです。
ロードの順番に影響を受けるようなコードの書き方をすることは少ない(というかなるべく避けるべき)と思いますが、デバッグの役に立つこともありますので一応知っておくとよいと思われます。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月28日水曜日
Padrinoのhelperで特定インスタンスを使いまわす
Padrinoではコントローラやビューから呼び出すことのできるメソッドをhelperメソッドとして定義することができます。
helperメソッドはリクエスト中に何度も呼ばれたりするので、例えばサーバへの接続を行う場合、何度も接続インスタンスをnewせずに使いまわしたい場合がありますよね。
実はある特定のインスタンスを使いまわす方法は単純で、helperメソッド内でインスタンス変数に入れておけばいいです。例えば、LDAPサーバへの接続を取得するヘルパーメソッドを書くとしたら、
みたいな感じになりますね。見ての通り、@ldap_connが未定義の場合は新規接続を作成してそのインスタンスを@ldap_connに代入するとともに返し、すでに@ldap_connが定義されていれば@ldap_connの中身を返します。
ちなみにこのインスタンス変数@ldap_connはどのインスタンスに属するかというと、MyApplication::Appのインスタンスに属することになります。これは get や post メソッド内で定義したインスタンス変数と同じ扱いです。テンプレートからも参照できることからわかるように、これらのインスタンス変数は同じリクエスト中は直接呼んで使いまわすことが出来ます。
今回はLDAP接続を例にしましたが、DBサーバやキャッシュサーバなどの各種サーバへの接続など生成処理の重いインスタンス全般に使えると思います。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
helperメソッドはリクエスト中に何度も呼ばれたりするので、例えばサーバへの接続を行う場合、何度も接続インスタンスをnewせずに使いまわしたい場合がありますよね。
実はある特定のインスタンスを使いまわす方法は単純で、helperメソッド内でインスタンス変数に入れておけばいいです。例えば、LDAPサーバへの接続を取得するヘルパーメソッドを書くとしたら、
MyApplication::App.helpers do
def ldap_conn
@ldap_conn ||= LDAP::Conn.new('localhost')
end
end
みたいな感じになりますね。見ての通り、@ldap_connが未定義の場合は新規接続を作成してそのインスタンスを@ldap_connに代入するとともに返し、すでに@ldap_connが定義されていれば@ldap_connの中身を返します。
ちなみにこのインスタンス変数@ldap_connはどのインスタンスに属するかというと、MyApplication::Appのインスタンスに属することになります。これは get や post メソッド内で定義したインスタンス変数と同じ扱いです。テンプレートからも参照できることからわかるように、これらのインスタンス変数は同じリクエスト中は直接呼んで使いまわすことが出来ます。
今回はLDAP接続を例にしましたが、DBサーバやキャッシュサーバなどの各種サーバへの接続など生成処理の重いインスタンス全般に使えると思います。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月27日火曜日
Redis中のRackセッションのデータを直接見る
RubyのフレームワークPadrinoで開発しているアプリケーションのセッション管理をRack::Session::Redisでやることにしたわけですけれども、検証作業中にRedisに保存したセッションのデータをRack::Session::Redisを介さずに直接見てみたくなりました。
というわけで、Redisに格納されているセッションのデータを直接見る方法です。
セッションIDが分かっていることが条件です。RackアプリケーションでのセッションID取得方法はこちらを参照。
irbからRedisクラスを直接叩いて見てみることにします(以下、見やすいように改行を加えてあります)。
という感じで直接見ることができます。
Rackアプリケーションのセッションデータは、デフォルト設定では rack:session という名前空間の下に収められています。この名前空間は変更可能ですがあまり変更することはないでしょう。
データはMarshal.#dumpでシリアライズされているようで、読みだす時はMarshal.#loadを使えばよいです。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
というわけで、Redisに格納されているセッションのデータを直接見る方法です。
セッションIDが分かっていることが条件です。RackアプリケーションでのセッションID取得方法はこちらを参照。
irbからRedisクラスを直接叩いて見てみることにします(以下、見やすいように改行を加えてあります)。
[user@host]% bundle exec irb
irb(main):001:0> require 'redis'
=> true
irb(main):002:0> redis = Redis.new(:url => 'redis://127.0.0.1:6379/0')
=> #<Redis client v3.0.7 for redis://127.0.0.1:6379/0>
irb(main):003:0> Marshal.load(redis.get('rack:session:セッションID'))
=> セッションのデータ
という感じで直接見ることができます。
Rackアプリケーションのセッションデータは、デフォルト設定では rack:session という名前空間の下に収められています。この名前空間は変更可能ですがあまり変更することはないでしょう。
データはMarshal.#dumpでシリアライズされているようで、読みだす時はMarshal.#loadを使えばよいです。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月24日土曜日
やっぱりセッション管理はRack::Session::Redisで
昨日Rack::Session::Dalliを使うって言った舌の根も乾かぬうちに別のことを言い始めたよコイツ、とのツッコミを受けそうです。すいません。いや実は職場の人にRack::Session::Redisってのもあるよ、とアドバイスを頂きまして、調べてみたらRedisが結構良さそうだし面白そうなのでRedis使いたくなってしまったのですよ。
ちなみに、Rack::Session::Redisについて調べてみると、このクラスを提供しているライブラリが2つ見つかります。rack-session-redis と redis-rack の2つですね。
redis-rackのほうが新しいです。ソースコードも読んでみましたが、redis-rackのほうがよく整理されている印象を受けました。redisにデータを保存するための抽象化レイヤが redis-store として分離されていて、redis-rackはその上に乗っている構造のようです。
ということで、redis-rackを導入します。以下、Padrinoフレームワークを使っている前提です。
Gemfileに以下の記述を加えます。
app/app.rbに以下の記述を加えます。
セッションに使うライブラリを変更するため disable :sessions とするのですが、:protection と :protect_from_csrf は enable:sessions を前提にしておりエラーになるため事前に falseに設定し、その後手動で該当するミドルウェアである Rack::Protection と Rack::Protection::AuthenticityToken を use します。
なお、Redisの接続先はRack::Session::Redisのデフォルトのオプションで
と定義されてます。特に変更の必要はないので今回はサーバを指定していません。オプションはセッションの有効期限だけを指定しています(上の例だと30日を秒数で指定)。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
ちなみに、Rack::Session::Redisについて調べてみると、このクラスを提供しているライブラリが2つ見つかります。rack-session-redis と redis-rack の2つですね。
redis-rackのほうが新しいです。ソースコードも読んでみましたが、redis-rackのほうがよく整理されている印象を受けました。redisにデータを保存するための抽象化レイヤが redis-store として分離されていて、redis-rackはその上に乗っている構造のようです。
ということで、redis-rackを導入します。以下、Padrinoフレームワークを使っている前提です。
Gemfileに以下の記述を加えます。
gem ‘redis-rack’
app/app.rbに以下の記述を加えます。
set :protection, false set :protect_from_csrf, false disable :sessions use Rack::Session::Redis, :expire_after => 30 * 24 * 60 * 60 use Rack::Protection use Rack::Protection::AuthenticityToken, :authenticity_param => '_csrf_token'
セッションに使うライブラリを変更するため disable :sessions とするのですが、:protection と :protect_from_csrf は enable:sessions を前提にしておりエラーになるため事前に falseに設定し、その後手動で該当するミドルウェアである Rack::Protection と Rack::Protection::AuthenticityToken を use します。
なお、Redisの接続先はRack::Session::Redisのデフォルトのオプションで
:redis_server => 'redis://127.0.0.1:6379/0/rack:session'
と定義されてます。特に変更の必要はないので今回はサーバを指定していません。オプションはセッションの有効期限だけを指定しています(上の例だと30日を秒数で指定)。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月23日金曜日
セッション管理は Rack::Session::Dalli で
昨日書いたように、Rack::Session::Pool使用中にセッションが破棄される という問題が発生してまして、その打開策として最初 Rack::Session::Memcache への移行を検討してました。
Rack::Session::Memcache はバックエンドに memcache-client パッケージを使うのですが、実は memcache-client のメンテを担当している開発者によって memcache-client の後継である dalli というパッケージが出ていました。
ここで、memcache-client と dalli という2つのパッケージが登場したので、他にもあるかと一応調べたら memdached という名前のパッケージもありますね(直球な名前だな)。これはCで書かれたAPIをRubyから呼ぶもので、速さだけで言ったら最速みたいですけれども、Rackに組み込む簡単な方法はパッと見つかりませんでした。やるとしたらRack::Session::Memcache を書き換えるのが一番現実的でしょうけど、あまりやりたくありません。
一方、dalli には Rack::Session::Dalli クラスが付属していて、これを使うと簡単に導入できるみたいでした。これは魅力的。さすがにdalliはPure Ruby実装なので速度面で劣るものの、それでもパフォーマンスは結構良好なようですので大丈夫でしょう。今回はそこまで速度重視の目的でもないですし。
ということで、 Rack::Session::Dalli を導入します。Padrinoフレームワークを前提に説明しますので、適宜読み替えてください。
Gemfileに
と加えます。そして、config/boot.rb に
を追記します(Bundler.require では自動的にロードされないためです)。
そして、app/app.rb に
って感じに設定すればOKです。サーバはデフォルトで 'localhost:11211’ になってますが、IPアドレスを打たないときちんと認識しなかったので明示的に '127.0.0.1:11211’ を設定しています。
参考1→ http://stackoverflow.com/questions/12786976/how-to-specify-memcache-server-to-racksessionmemcache/12787492#12787492
参考2 →http://www.banana-systems.com/2010/09/rails3_memcached/
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
Rack::Session::Memcache はバックエンドに memcache-client パッケージを使うのですが、実は memcache-client のメンテを担当している開発者によって memcache-client の後継である dalli というパッケージが出ていました。
ここで、memcache-client と dalli という2つのパッケージが登場したので、他にもあるかと一応調べたら memdached という名前のパッケージもありますね(直球な名前だな)。これはCで書かれたAPIをRubyから呼ぶもので、速さだけで言ったら最速みたいですけれども、Rackに組み込む簡単な方法はパッと見つかりませんでした。やるとしたらRack::Session::Memcache を書き換えるのが一番現実的でしょうけど、あまりやりたくありません。
一方、dalli には Rack::Session::Dalli クラスが付属していて、これを使うと簡単に導入できるみたいでした。これは魅力的。さすがにdalliはPure Ruby実装なので速度面で劣るものの、それでもパフォーマンスは結構良好なようですので大丈夫でしょう。今回はそこまで速度重視の目的でもないですし。
ということで、 Rack::Session::Dalli を導入します。Padrinoフレームワークを前提に説明しますので、適宜読み替えてください。
Gemfileに
gem ‘dalli’
と加えます。そして、config/boot.rb に
require 'rack/session/dalli’
を追記します(Bundler.require では自動的にロードされないためです)。
そして、app/app.rb に
use Rack::Session::Dalli, :expire_after => nil, :memcache_server => '127.0.0.1:11211’
って感じに設定すればOKです。サーバはデフォルトで 'localhost:11211’ になってますが、IPアドレスを打たないときちんと認識しなかったので明示的に '127.0.0.1:11211’ を設定しています。
参考1→ http://stackoverflow.com/questions/12786976/how-to-specify-memcache-server-to-racksessionmemcache/12787492#12787492
参考2 →http://www.banana-systems.com/2010/09/rails3_memcached/
2014年5月22日木曜日
Rack::Session::Pool使用中にセッションが破棄される
Rack::Session::Poolを使っていると、突然セッションIDが変わる、という問題にぶちあたりました。しかもセッションIDが変わってもセッションに保存した値は引き継がれず消えるので、どうやらセッションが破棄されているようです。
Cucumberでのテストでは引っかからないので今まで気付きませんでした。ブラウザで実際に手動で操作していて初めて気づいた問題です。普通に動作していると思ったらいきなりセッションが消えてログイン状態が無効になったりします。問題発生のタイミングや間隔もランダムで、原因は、正直よくわかりません。
ネット上を探してみると、Rack::Session::Poolを使用中に似たような症状を報告している人がいました。
http://d.hatena.ne.jp/libera-softvaro+dev/20110324/1300982695
上記URLのページではリクエストごとにCookieの値が変わると書いてあります。Rack::Session::Poolを使う場合Cookieの値はセッションIDなので、同じような症状に思えます。unicornだと発生しないと書いてありますが、僕はunicornを使っており、リクエストごとに変わるということはないものの、しかしランダムなタイミングで突然セッションIDが変わります(セッションが破棄されている)。
一応原因特定のため一時的に Rack::Session::Cookieに戻してみると、やはり症状は起きず、この問題は全く発生しなくなります。
う~ん、Rack::Session::Poolはダメなのか・・・。しかし、Rack::Session::Cookieだとセッションのデータも全部Cookieに保存するからセキュリティ的に避けたいところです。となると、やっぱ、Rack::Session::Memcacheしかない気がします。ということで、memcachedの導入を本格的に考えますかね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
Cucumberでのテストでは引っかからないので今まで気付きませんでした。ブラウザで実際に手動で操作していて初めて気づいた問題です。普通に動作していると思ったらいきなりセッションが消えてログイン状態が無効になったりします。問題発生のタイミングや間隔もランダムで、原因は、正直よくわかりません。
ネット上を探してみると、Rack::Session::Poolを使用中に似たような症状を報告している人がいました。
http://d.hatena.ne.jp/libera-softvaro+dev/20110324/1300982695
上記URLのページではリクエストごとにCookieの値が変わると書いてあります。Rack::Session::Poolを使う場合Cookieの値はセッションIDなので、同じような症状に思えます。unicornだと発生しないと書いてありますが、僕はunicornを使っており、リクエストごとに変わるということはないものの、しかしランダムなタイミングで突然セッションIDが変わります(セッションが破棄されている)。
一応原因特定のため一時的に Rack::Session::Cookieに戻してみると、やはり症状は起きず、この問題は全く発生しなくなります。
う~ん、Rack::Session::Poolはダメなのか・・・。しかし、Rack::Session::Cookieだとセッションのデータも全部Cookieに保存するからセキュリティ的に避けたいところです。となると、やっぱ、Rack::Session::Memcacheしかない気がします。ということで、memcachedの導入を本格的に考えますかね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月15日木曜日
ruby-ldapでエントリを再帰的に削除する
※途中、試行錯誤の過程でボツにしたコードも載せてるので、結論だけ見たい方は途中の過程をすっ飛ばして下の方をご覧ください。
ruby-ldapを使ってrubyからLDAPの操作をしている時に子エントリを持つエントリを削除しようとするとエラーが発生し、"Operation not allowed on non-leaf” と言われます。
ruby-ldapの場合は特にエントリを再帰的に削除してくれるような便利なメソッドなんか用意されていません。ですので、先に全ての子エントリを削除するコードを自力で実装しないといけません。
最初に思いついたのは、エラーの発生を捕捉し、子エントリがあって処理に失敗した場合には各子エントリに対して再帰処理をかける方法です。
ということで最初に書いたコードがこちら。
これで一応望み通りに動作しますが、ちょっとスマートではありません。
rubyに限らず一般的に例外処理のためのbegin〜rescue〜end節のような構文は処理が重いので、できれば例外捕捉をしなくて済む方法が望ましいです。
さらに、削除試行して失敗してから子エントリを検索していますが、そもそも子エントリを先に検索すればそ無駄に失敗する削除処理をしなくても済むはずです。
よって、削除処理の前に子エントリを検索しに行き、ヒットした各子エントリに対して再帰処理をかければよさそうです。子エントリがヒットしなければそのままエントリを削除します。
ということで修正したコードがこちら。
この方がスマートですね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
ruby-ldapを使ってrubyからLDAPの操作をしている時に子エントリを持つエントリを削除しようとするとエラーが発生し、"Operation not allowed on non-leaf” と言われます。
ruby-ldapの場合は特にエントリを再帰的に削除してくれるような便利なメソッドなんか用意されていません。ですので、先に全ての子エントリを削除するコードを自力で実装しないといけません。
最初に思いついたのは、エラーの発生を捕捉し、子エントリがあって処理に失敗した場合には各子エントリに対して再帰処理をかける方法です。
ということで最初に書いたコードがこちら。
# 指定DNを子も含めて再帰的に削除する
# ldap_connはLDAP接続を返すメソッドとして別途定義
def ldap_delete_r(dn)
begin
ldap_conn.delete(dn)
rescue LDAP::ResultError
if ldap_conn.err == 66 # "Operation not allowed on non-leaf"
ldap_conn.search(dn, LDAP::LDAP_SCOPE_ONELEVEL, '(objectclass=*)', ['dn']) do |entry|
ldap_delete_r(entry.get_dn)
end
else
raise
end
end
end
これで一応望み通りに動作しますが、ちょっとスマートではありません。
rubyに限らず一般的に例外処理のためのbegin〜rescue〜end節のような構文は処理が重いので、できれば例外捕捉をしなくて済む方法が望ましいです。
さらに、削除試行して失敗してから子エントリを検索していますが、そもそも子エントリを先に検索すればそ無駄に失敗する削除処理をしなくても済むはずです。
よって、削除処理の前に子エントリを検索しに行き、ヒットした各子エントリに対して再帰処理をかければよさそうです。子エントリがヒットしなければそのままエントリを削除します。
ということで修正したコードがこちら。
# 指定DNを子も含めて再帰的に削除する
# ldap_connはLDAP接続を返すメソッドとして別途定義
def ldap_delete_r(dn)
ldap_conn.search(dn, LDAP::LDAP_SCOPE_ONELEVEL, '(objectclass=*)', ['dn']) do |entry|
ldap_delete_r(entry.get_dn)
end
ldap_conn.delete(dn)
end
この方がスマートですね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月14日水曜日
RackアプリケーションでセッションIDを取得する
Webアプリの開発をしていると、現在のセッションIDを取得したくなることがあります。PHPですとsession_idというその名もズバリの関数が言語標準で用意されてたりしますが、Rubyだと言語標準にはないです。そもそもPHPと違ってセッションの仕組み自体言語標準ではなく、よく使われるのはRack::Session系のライブラリです。これは以前紹介したように幾つか種類があります。
しかしRack::Session系のライブラリで、現在のセッションID取得をどうやるかについてはドキュメントに分かりやすく書いておらず、探しにくかったです。(Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納されるようですが、それ以外ではIDは入りません。)結局ソースコード読んで見つけました。結論から先に言いますと、
で取得できます。SinatraやPadrinoを使っている場合はもっと簡潔に
でOKです。(Sinatraにおけるsessionメソッドはrequest.sessionを呼びだします。Rack::Request#sessionは@env['rack.session']を返します。Sinatraにおけるenvメソッドはインスタンス変数@envへのアクセサですので、結局同じことになります。)
なお、今回(メインで)参照したソースコードはこれ↓。
https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb
上記URLにはRack::Session::Abstract::SessionHashとRack::Session::Abstract::IDの定義が書かれています。
ここにセッションIDを取得していそうなメソッドを発見。Rack::Session::Abstract::ID#current_session_id です。こいつを定義している部分で、
とやってますね。ちなみにRack::Session::Abstractモジュールの冒頭の方で
と定義されてます。よって、env[‘rack.session’].idを呼べばセッションIDが取得できるとわかります。
なお、env[‘rack.session’]の中身は何かというと、Rack::Session::Abstract::ID#prepare_session で
としてまして、ここでsession_classが何かといえば、Rack::Session::Abstract::ID#session_class の定義が
となってます。要するに、env[‘rack.session’]に入っているのはRack::Session::Abstract::SessionHashのインスタンスなのでした。ちなみに、アプリケーション中で p env[‘rack.session’] などとするとハッシュに見える結果が返ってきますが、これは inspect メソッドが定義されているからです。
インスタンス変数@data はハッシュですので、 inspect した結果がハッシュになるのですね。(Kernel.#p は内部的にObject#inspect を呼びます。)
ついでに、Rack::Session::Abstract::SessionHash#id を見てみますと
となってまして、セッションIDはRack::Session::Abstract::SessionHashのインスタンス変数@idに保持されていることがわかります。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
※2014/05/22更新:Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納される点について追記。
※2014/06/04更新:SinatraやPadrinoの場合のより簡潔な書き方を追記。
しかしRack::Session系のライブラリで、現在のセッションID取得をどうやるかについてはドキュメントに分かりやすく書いておらず、探しにくかったです。(Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納されるようですが、それ以外ではIDは入りません。)結局ソースコード読んで見つけました。結論から先に言いますと、
env['rack.session'].id
で取得できます。SinatraやPadrinoを使っている場合はもっと簡潔に
session.id
でOKです。(Sinatraにおけるsessionメソッドはrequest.sessionを呼びだします。Rack::Request#sessionは@env['rack.session']を返します。Sinatraにおけるenvメソッドはインスタンス変数@envへのアクセサですので、結局同じことになります。)
なお、今回(メインで)参照したソースコードはこれ↓。
https://github.com/rack/rack/blob/master/lib/rack/session/abstract/id.rb
上記URLにはRack::Session::Abstract::SessionHashとRack::Session::Abstract::IDの定義が書かれています。
ここにセッションIDを取得していそうなメソッドを発見。Rack::Session::Abstract::ID#current_session_id です。こいつを定義している部分で、
def current_session_id(env) env[ENV_SESSION_KEY].id end
とやってますね。ちなみにRack::Session::Abstractモジュールの冒頭の方で
ENV_SESSION_KEY = 'rack.session’.freeze
と定義されてます。よって、env[‘rack.session’].idを呼べばセッションIDが取得できるとわかります。
なお、env[‘rack.session’]の中身は何かというと、Rack::Session::Abstract::ID#prepare_session で
env[ENV_SESSION_KEY] = session_class.new(self, env)
としてまして、ここでsession_classが何かといえば、Rack::Session::Abstract::ID#session_class の定義が
def session_class SessionHash end
となってます。要するに、env[‘rack.session’]に入っているのはRack::Session::Abstract::SessionHashのインスタンスなのでした。ちなみに、アプリケーション中で p env[‘rack.session’] などとするとハッシュに見える結果が返ってきますが、これは inspect メソッドが定義されているからです。
def inspect
if loaded?
@data.inspect
else
"#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
end
end
インスタンス変数@data はハッシュですので、 inspect した結果がハッシュになるのですね。(Kernel.#p は内部的にObject#inspect を呼びます。)
ついでに、Rack::Session::Abstract::SessionHash#id を見てみますと
def id return @id if @loaded or instance_variable_defined?(:@id) @id = @store.send(:extract_session_id, @env) end
となってまして、セッションIDはRack::Session::Abstract::SessionHashのインスタンス変数@idに保持されていることがわかります。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
※2014/05/22更新:Rack::Session::Cookieの場合のみ、セッションのデータ自体にセッションIDが格納される点について追記。
※2014/06/04更新:SinatraやPadrinoの場合のより簡潔な書き方を追記。
2014年5月13日火曜日
PadrinoでSinatra::Flashを使う(Padrino::Flashでなく)
Webアプリを開発してますと、エラーメッセージを画面に出力するときに、次のリクエストの時に出力させたい、なんてことがよくあります。例えば何かの処理をした後に別のURLにリダイレクトさせて、その画面でエラーメッセージを出力させる場合がそうですね。
セッション変数に値を保持すれば、次回以降のリクエストに値を引き継げますが、もしこれがエラーメッセージだとすると、表示したらもう使わないので消さなければいけません。いちいち自分で消すのは面倒です。そこで、次のリクエストまでに限り値を保持するしくみがフレームワークやプラグインで用意されていたりします。このような仕組みは "Flash" と呼ばれています。(綴りは同じですがAdobeのFlashとは別物です。)
Sinatraの場合、公式ページにてRack:Flashを使うように書いてあります。しかしPadrinoでも使おうとしたらエラーになって使えませんでした(Gemfileに追加するだけでエラーになります。詳しい原因は追求してません)。
ということで、Padrinoの設定ファイルに記述があるSinatra:Flashを使うことにします。Sinatra:Flashは、セッション変数 session[:flash] にデータを保持しますが、直接セッション変数をアクセスせずに、flash[:key]を通じてアクセスします。ちなみに動作を見ていくとなかなか面白いです。まずは以下のコードと実行結果をみてください。
入れたはずの値が入っていないように見えますが、実は以下のような仕組みです。flashはSinatra::Flashオブジェクトのインスタンスであり、Sinatra::Flashの[]= メソッドは@nextインスタンス変数に値を代入するようになっていて、[] メソッドは@nowインスタンス変数の値を出力します。そしてFlashオブジェクトのインスタンス変数@next, @nowには同名のメソッドでアクセスできるようになっています。
つまり代入は@nextへ、出力は@nowからってわけですね。よって一見上記は不思議に見えるようですがこれで正しい動作なのです。
でもって、アプリケーションのafterフックで現在の@nowの内容を@nextでまるごと置き換えてしまいます。これで次回リクエスト時には@nextの内容にアクセスされることになり、@nextへ記録した内容は次回リクエストまで限定で有効、ということになります。
ちなみに、Padrino:Flashというライブラリもあります。こちらはセッション変数 session[:_flash] にデータを保持します。使い方はSinatra:Flashと同じで、実際、Gemfileの記述をPadrino:Flashに変えただけでエラーなく動作しました。
ではデータの保存先以外に何が違うかというと、値を保存する際に、その値がシンボルである場合、自動的にI18n.traslateを通してくれること。そして付属するヘルパーメソッドのHTML出力形式が違うこと。
でも、今回は値の保存形式は独自に決めたかったし(エラーの種類ごとに更に内部にハッシュを持たせたかった)、その出力のためのヘルパーメソッドも自分で書くことにしたので、正直どちらでも良かったんですが、結局余計なことをしないシンプルなSinatra:Flashを採用したってわけです。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
セッション変数に値を保持すれば、次回以降のリクエストに値を引き継げますが、もしこれがエラーメッセージだとすると、表示したらもう使わないので消さなければいけません。いちいち自分で消すのは面倒です。そこで、次のリクエストまでに限り値を保持するしくみがフレームワークやプラグインで用意されていたりします。このような仕組みは "Flash" と呼ばれています。(綴りは同じですがAdobeのFlashとは別物です。)
Sinatraの場合、公式ページにてRack:Flashを使うように書いてあります。しかしPadrinoでも使おうとしたらエラーになって使えませんでした(Gemfileに追加するだけでエラーになります。詳しい原因は追求してません)。
ということで、Padrinoの設定ファイルに記述があるSinatra:Flashを使うことにします。Sinatra:Flashは、セッション変数 session[:flash] にデータを保持しますが、直接セッション変数をアクセスせずに、flash[:key]を通じてアクセスします。ちなみに動作を見ていくとなかなか面白いです。まずは以下のコードと実行結果をみてください。
flash[:error] = "hoge" p flash[:error] #=> nil p flash.now[:error] #=> nil p flash.next[:error] #=> "hoge" flash.now[:error] = "fuga" p flash[:error] #=> "fuga" p flash.now[:error] #=> "fuga" p flash.next[:error] #=> "hoge" flash.next[:error] = "foo" p flash[:error] #=> "fuga" p flash.now[:error] #=> "fuga" p flash.next[:error] #=> "foo"
入れたはずの値が入っていないように見えますが、実は以下のような仕組みです。flashはSinatra::Flashオブジェクトのインスタンスであり、Sinatra::Flashの[]= メソッドは@nextインスタンス変数に値を代入するようになっていて、[] メソッドは@nowインスタンス変数の値を出力します。そしてFlashオブジェクトのインスタンス変数@next, @nowには同名のメソッドでアクセスできるようになっています。
つまり代入は@nextへ、出力は@nowからってわけですね。よって一見上記は不思議に見えるようですがこれで正しい動作なのです。
でもって、アプリケーションのafterフックで現在の@nowの内容を@nextでまるごと置き換えてしまいます。これで次回リクエスト時には@nextの内容にアクセスされることになり、@nextへ記録した内容は次回リクエストまで限定で有効、ということになります。
ちなみに、Padrino:Flashというライブラリもあります。こちらはセッション変数 session[:_flash] にデータを保持します。使い方はSinatra:Flashと同じで、実際、Gemfileの記述をPadrino:Flashに変えただけでエラーなく動作しました。
ではデータの保存先以外に何が違うかというと、値を保存する際に、その値がシンボルである場合、自動的にI18n.traslateを通してくれること。そして付属するヘルパーメソッドのHTML出力形式が違うこと。
でも、今回は値の保存形式は独自に決めたかったし(エラーの種類ごとに更に内部にハッシュを持たせたかった)、その出力のためのヘルパーメソッドも自分で書くことにしたので、正直どちらでも良かったんですが、結局余計なことをしないシンプルなSinatra:Flashを採用したってわけです。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月9日金曜日
Capybara で一時的に非同期処理の待ち時間を調整する
Ruby で開発している Web アプリのテストを強力に支援してくれる Capybara。デフォルトで非同期処理のテストにも対応してまして、特定の要素を見つけ出したり、条件に一致する要素の存在確認をしたりするテストにおいて、失敗した場合に一定時間(デフォルトでは2秒間)待ち、再度DOMを読み直してテストを再実行する機能が組み込まれております。
これは便利な機能ですが、しかし非同期処理を想定しないテストの時も、毎回レッドのたびに2秒待たされるのも無駄です。この非同期処理の待ち時間を変更する方法の1つは、using_wait_time メソッドを使うことです。
上記コードは非同期処理の待ち時間を1秒に設定してブロック内のテストを実行します。
あるいは、Capybara 2.1以降なら find メソッドにおいて :wait オプションが使えます。Capybara 2.2以降なら、matcher系メソッド(have_cssなど)でも :wait オプションが使えます。
上記コードは先程載せた using_wait_time の使用例と同じく、非同期処理の待ち時間を1秒に設定してテストを実行できます。
ここで、Cucmuberとの組み合わせの例を示しておこうと思います。僕が実際使っているステップ定義の一部を紹介しましょう。
上記のステップ定義において、末尾に (非同期) という文字列をつけると非同期処理の待ち時間をデフォルト(2秒)にし、付けない場合は0秒すなわち待たない設定としています。例えば、非同期処理を待たない場合はこんなふうになります。
ここで、もし非同期通信を待ちたければ、
と書けばいいです。
ちなみに、このステップの実行直前にデバッグ情報を表示させたければ
と書けばできるようにしています。
正規表現によるステップ定義は高い柔軟性をもたせられるのが利点ですね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
これは便利な機能ですが、しかし非同期処理を想定しないテストの時も、毎回レッドのたびに2秒待たされるのも無駄です。この非同期処理の待ち時間を変更する方法の1つは、using_wait_time メソッドを使うことです。
using_wait_time 1 do expect(page).to have_css '#hoge' end
上記コードは非同期処理の待ち時間を1秒に設定してブロック内のテストを実行します。
あるいは、Capybara 2.1以降なら find メソッドにおいて :wait オプションが使えます。Capybara 2.2以降なら、matcher系メソッド(have_cssなど)でも :wait オプションが使えます。
expect(page).to have_css('#hoge', :wait => 1)
上記コードは先程載せた using_wait_time の使用例と同じく、非同期処理の待ち時間を1秒に設定してテストを実行できます。
ここで、Cucmuberとの組み合わせの例を示しておこうと思います。僕が実際使っているステップ定義の一部を紹介しましょう。
ならば(/^".*?\((.*?)\)" に "(.*?)" と表示(?:され|されていること)(\(非同期\))?( debug)?$/) do |t_css, t_text, t_async, debug| show_debug_info if debug target_node = page.find(t_css, :text => t_text, :visible => true, :wait => t_async ? async_time : 0) expect(target_node.text).to eq(t_text) end def async_time Capybara.default_wait_time end def show_debug_info puts page.response_headers puts page.status_code puts page.current_url puts page.html end
上記のステップ定義において、末尾に (非同期) という文字列をつけると非同期処理の待ち時間をデフォルト(2秒)にし、付けない場合は0秒すなわち待たない設定としています。例えば、非同期処理を待たない場合はこんなふうになります。
ならば "ログイン名(#login-name)" に "admin" と表示されていること
ここで、もし非同期通信を待ちたければ、
ならば "ログイン名(#login-name)" に "admin" と表示されていること(非同期)
と書けばいいです。
ちなみに、このステップの実行直前にデバッグ情報を表示させたければ
ならば "ログイン名(#login-name)" に "admin" と表示されていること debug
と書けばできるようにしています。
正規表現によるステップ定義は高い柔軟性をもたせられるのが利点ですね。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月8日木曜日
Padrino で i18n (他言語化)対応
Padrinoは i18n ライブラリに対応しており、簡単な設定で使いはじめることができます。まずは、config/boot.rbにて以下の記述を追加します。
ちなみに I18n.enforce_available_locales を true にすると、その言語が利用可能ではない(翻訳ファイルが見つからない)時に例外を投げてくれるようになるらしいです。この設定はPadrino::Helpersモジュールによりデフォルトでtrueになっているのでわざわざ設定は不要な気もしますが、念のため明示的にtrueを指定してます(下記ソース参照)。
https://github.com/padrino/padrino-framework/blob/master/padrino-helpers/lib/padrino-helpers.rb
なお、 I18n.default_locale = :ja を I18n.locale = :ja と書いている解説ページもちらほら見かけましたが、cucumberによるテストを実行した際に翻訳が効きませんでした。cucumberによるテストはマルチスレッドでして、どうやら、I18n.locale はスレッドローカルな設定であることが原因で I18n.default_locale しか設定が効かないっぽいです。そこんとこの詳しいメカニズムまでは調べてませんが。まあ、I18n.default_locale のほうを設定しておけばいいと思います。
翻訳ファイルは、app/locale/ja.yml に置けばいいです。形式としてはYAMLで、
みたいな感じです。
翻訳のためのメソッドは I18n.translate ですが、padrino が短縮で呼び出せるペルパーを用意してくれてます(Padrino::Helpers::TranslationHelpers)。
そのおかげで、
と書かなくても
と書くか、あるいはRails風に
と書けば「ログイン」と出力してくれます。
ちなみに、 I18n.localize というのもあって、こちらはDate, DateTime,Timeのいずれかのオブジェクトを受け付け、日付や時刻を言語に対応した形式で表示してくれます。こちらも同様短縮形が用意されているので、
と書くと
などと表示してくれます。その他にも翻訳時のパラメータ対応など機能がありますが、また別の機会に書きたいと思います。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
Padrino.before_load do I18n.enforce_available_locales = true I18n.default_locale = :ja end
ちなみに I18n.enforce_available_locales を true にすると、その言語が利用可能ではない(翻訳ファイルが見つからない)時に例外を投げてくれるようになるらしいです。この設定はPadrino::Helpersモジュールによりデフォルトでtrueになっているのでわざわざ設定は不要な気もしますが、念のため明示的にtrueを指定してます(下記ソース参照)。
https://github.com/padrino/padrino-framework/blob/master/padrino-helpers/lib/padrino-helpers.rb
なお、 I18n.default_locale = :ja を I18n.locale = :ja と書いている解説ページもちらほら見かけましたが、cucumberによるテストを実行した際に翻訳が効きませんでした。cucumberによるテストはマルチスレッドでして、どうやら、I18n.locale はスレッドローカルな設定であることが原因で I18n.default_locale しか設定が効かないっぽいです。そこんとこの詳しいメカニズムまでは調べてませんが。まあ、I18n.default_locale のほうを設定しておけばいいと思います。
翻訳ファイルは、app/locale/ja.yml に置けばいいです。形式としてはYAMLで、
ja: login: ログイン top: トップ
みたいな感じです。
翻訳のためのメソッドは I18n.translate ですが、padrino が短縮で呼び出せるペルパーを用意してくれてます(Padrino::Helpers::TranslationHelpers)。
そのおかげで、
<%= I18n.translate(:login) %>
と書かなくても
<%= t(:login) %>
と書くか、あるいはRails風に
<%=t :login %>
と書けば「ログイン」と出力してくれます。
ちなみに、 I18n.localize というのもあって、こちらはDate, DateTime,Timeのいずれかのオブジェクトを受け付け、日付や時刻を言語に対応した形式で表示してくれます。こちらも同様短縮形が用意されているので、
<%=l Time.now %>
と書くと
2014/05/07 21:01:00
などと表示してくれます。その他にも翻訳時のパラメータ対応など機能がありますが、また別の機会に書きたいと思います。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年5月2日金曜日
PadrinoでRack::Session::Poolを使う方法
先日、Rack::Session::PoolとRack::Session::Cookieの違いについて書きましたけれども、Padrinoで実際にRack::Session::Poolを使う設定をしてみたので、メモしておきます。
Padrinoでは、デフォルトではapp/app.rbに
と記述があり、セッション管理が有効になっています。PadrinoのベースであるSinatraは上記の記述がある場合にRack::Session::Cookieをオプション無しで呼び出します。そのためここではCookieの有効期限などの細かい設定はできません。
細かい設定をしたい場合は、enable :sessions の代わりに Rack::Session::Cookie を直接使うように、Sinatraの公式ドキュメントにも書いてあります。だったら、同様にRack::Session::Cookie ではなくRack::Session::Poolを直接呼び出せば大丈夫のような気がします。
ところがPadrinoだと、プロジェクトジェネレータで生成されたアプリケーションのapp/app.rbにおいて、
などとやろうとすると下記のようなエラーになってしまいます。ちなみにこれはRack::Session::Cookie を使おうとしても同様のエラーになります。
これは、Padrinoのデフォルトで Rack::Protection 関係のオプションが有効になっているためです。config/apps.rbの次のような記述が該当します。
なので、一旦 Rack::Protection 関係のオプションを無効にし、Rack::Session::Poolを直接呼び出した後にRack::Protectionを直接呼び出します(Rack::Protection 関係のオプションを改めて有効にするとエラーになってしまいます)。app/app.rbのenable :sessionsをコメントアウトし、次の記述を追加します。disable :sessionsは不要かもしれませんが念のため入れてます。
これでエラーが消えました。セッションも、Cookieを確認すると、今までBase64でエンコードされた非常に長い文字列(おそらく暗号化したキー・バリューの組み合わせがセッションIDとともに入っている)だったのが、セッションIDのみらしき文字列に変化しています。上記コードでは有効期限を設定していませんが、指定すればCookieに反映されることは確認できました。なお、 Rack::Protection 関係がきちんど動作しているかどうかは未検証ですのでご注意ください。Rack::Protection::AuthenticityTokenも動作してるのを確認しました。
※2014/05/02 Rack::Protection::AuthenticityTokenの動作を確認したことを追記、コードの use Rack::Protection::AuthenticityToken にトークンのパラメータ名を指定する引数を追加
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
Padrinoでは、デフォルトではapp/app.rbに
enable :sessions
と記述があり、セッション管理が有効になっています。PadrinoのベースであるSinatraは上記の記述がある場合にRack::Session::Cookieをオプション無しで呼び出します。そのためここではCookieの有効期限などの細かい設定はできません。
細かい設定をしたい場合は、enable :sessions の代わりに Rack::Session::Cookie を直接使うように、Sinatraの公式ドキュメントにも書いてあります。だったら、同様にRack::Session::Cookie ではなくRack::Session::Poolを直接呼び出せば大丈夫のような気がします。
ところがPadrinoだと、プロジェクトジェネレータで生成されたアプリケーションのapp/app.rbにおいて、
disable :sessions use Rack::Session::Pool
などとやろうとすると下記のようなエラーになってしまいます。ちなみにこれはRack::Session::Cookie を使おうとしても同様のエラーになります。
you need to set up a session middleware *before* Rack::Protection::AuthenticityToken (RuntimeError)
これは、Padrinoのデフォルトで Rack::Protection 関係のオプションが有効になっているためです。config/apps.rbの次のような記述が該当します。
Padrino.configure_apps do # enable :sessions set :session_secret, 'sessionseacretkeystring12345567890abcdef' set :protection, :except => :path_traversal set :protect_from_csrf, true end
なので、一旦 Rack::Protection 関係のオプションを無効にし、Rack::Session::Poolを直接呼び出した後にRack::Protectionを直接呼び出します(Rack::Protection 関係のオプションを改めて有効にするとエラーになってしまいます)。app/app.rbのenable :sessionsをコメントアウトし、次の記述を追加します。disable :sessionsは不要かもしれませんが念のため入れてます。
set :protection, false set :protect_from_csrf, false disable :sessions use Rack::Session::Pool, :key => 'rack.session', :path => '/', :expire_after => nil use Rack::Protection use Rack::Protection::AuthenticityToken, :authenticity_param => '_csrf_token'
これでエラーが消えました。セッションも、Cookieを確認すると、今までBase64でエンコードされた非常に長い文字列(おそらく暗号化したキー・バリューの組み合わせがセッションIDとともに入っている)だったのが、セッションIDのみらしき文字列に変化しています。上記コードでは有効期限を設定していませんが、指定すればCookieに反映されることは確認できました。
※2014/05/02 Rack::Protection::AuthenticityTokenの動作を確認したことを追記、コードの use Rack::Protection::AuthenticityToken にトークンのパラメータ名を指定する引数を追加
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
2014年4月30日水曜日
Rack::Session::PoolとRack::Session::Cookieの違い
RubyでRackアプリケーションを作成している時に、セッションの保存方法を調べていて、幾つかの方法があることがわかったんですがね。とりあえず、Rackには代表的なところで言うと、
と思っていたら、どうやら同じ悩みを抱える方が海外にもいたようです。
http://stackoverflow.com/questions/13573968/using-racksessionpool-over-racksessioncookie
上記サイトと、僕が幾つかのサイトで調べた結果をまとめると、つまりこういうことですかね。
※2014/05/02 Rack::Session::Poolについて、データの保存先の記述を修正しました。(アプリケーションオブジェクトのインスタンス変数 → Rack::Session::Poolのインスタンス変数)
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうぞ。
- Rack::Session::Cookie
- Rack::Session::Memcache
- Rack::Session::Pool
と思っていたら、どうやら同じ悩みを抱える方が海外にもいたようです。
http://stackoverflow.com/questions/13573968/using-racksessionpool-over-racksessioncookie
上記サイトと、僕が幾つかのサイトで調べた結果をまとめると、つまりこういうことですかね。
- Rack::Session::Cookie
→ セッションID、及び全てのキー・バリューのペアをCookieに保存する。 - Rack::Session::Pool
→ セッションIDのみクッキーに保存する。データはRack::Session::Poolのインスタンス変数@poolに、メモリ上で永続化の処理をせず(つまりそのまま)保存される。そのため高速で動作し、かつ保存できるオブジェクトに(永続化しないため)制約がない。ただし、アプリケーションを再起動した場合にはデータは全て失われる。 - Rack::Session::Memcache
→ セッションIDのみクッキーに保存する。データはmemcachedにより保存するため、当然memcachedは導入済みである必要がある。おそらく、永続化の処理は入るため保存できるデータの制約はRack::Session::Cookieと一緒と思われる。高速で動作する。速度的にはRack::Session::Poolとほぼ同等(永続化の操作の分わずかに劣ると思うが)。アプリケーションを再起動してもデータは保持される。
※2014/05/02 Rack::Session::Poolについて、データの保存先の記述を修正しました。(アプリケーションオブジェクトのインスタンス変数 → Rack::Session::Poolのインスタンス変数)
2014年3月18日火曜日
[Ruby] Cucumberで日本語テスト定義がエラーになる時の対処
最近初めてCucumberを導入したんですが、日本語でテストケースやステップ定義をする際に日本語をうまく認識できずエラーが出たので、それに対する対処と、若干のTipsをメモしておきます。
最初はネットにあるCucumberのサンプル通りfeacureファイルと対応するstep定義ファイルを記述して実行しました(この時は日本語なし)。これはうまくいきました。今度は、記述を日本語に変更して実行。ところがエラー。下記のように対処しました。
最初に出たのはこんなエラーでした。
調べてみると、そもそも全く文法を識別できない時のエラーだとか。そんなバカな。しかしどうやら原因は、日本語を使っていると認識できていないようでした。調べた結果、1行目に下記のようにコメントを入れることで日本語を使っていると認識でき、このエラーは出なくなりました。
ところが上記エラーを修正すると、今度はこんなエラーが出ます。
勘弁してくれ。ただ、今度はエラーメッセージから明らかに文字コード認識の問題だと分かります。調べた結果、step定義ファイルも1行目に下記のようにコメントを入れないと、正しく文字コードを判別できないようです。(先ほどとは違うコメントなので注意。)
これらの記述をすることで、日本語で記述したテストが実行できるようになりました。
2014/03/20追記:step定義ファイルの文字コード認識の問題は、Ruby1.9系が上で書いたようなコメントを入れないとエンコーディングを正しく認識できないことが原因みたいなので、Ruby2.0以上をお使いの場合は不要かもしれません。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうそ。
最初はネットにあるCucumberのサンプル通りfeacureファイルと対応するstep定義ファイルを記述して実行しました(この時は日本語なし)。これはうまくいきました。今度は、記述を日本語に変更して実行。ところがエラー。下記のように対処しました。
featureファイル
最初に出たのはこんなエラーでした。
Lexing error on line 1
調べてみると、そもそも全く文法を識別できない時のエラーだとか。そんなバカな。しかしどうやら原因は、日本語を使っていると認識できていないようでした。調べた結果、1行目に下記のようにコメントを入れることで日本語を使っていると認識でき、このエラーは出なくなりました。
#language:ja
step定義ファイル
ところが上記エラーを修正すると、今度はこんなエラーが出ます。
invalid multibyte char (US-ASCII)
勘弁してくれ。ただ、今度はエラーメッセージから明らかに文字コード認識の問題だと分かります。調べた結果、step定義ファイルも1行目に下記のようにコメントを入れないと、正しく文字コードを判別できないようです。(先ほどとは違うコメントなので注意。)
#encoding:utf-8
これらの記述をすることで、日本語で記述したテストが実行できるようになりました。
2014/03/20追記:step定義ファイルの文字コード認識の問題は、Ruby1.9系が上で書いたようなコメントを入れないとエンコーディングを正しく認識できないことが原因みたいなので、Ruby2.0以上をお使いの場合は不要かもしれません。
※この記事について指摘・意見・提案・感想などありましたら下のコメント欄にどうそ。
登録:
投稿 (Atom)