プラグイン化と公開準備 の変更点

更新


SIZE(22){COLOR(RED){このページの内容は非常に古いです(Rails 1.x.x)。最新の Rails では洗練された国際化の機構が標準で入っているため、下記は読むだけ無駄な内容になっています。}}

[[ソフトウェア/rails/言語ネゴシエーション]]

#contents

* プラグイン化 [#j4d21556]

http://akimichi.homeunix.net/hiki/rails/?%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3 ~
http://blog.s21g.com/articles/374 ~
あたりを参考に。

まずは script/generate でプラグインの雛型を作成し、
起動用スクリプト init.rb に require を追加。
 LANG:console
 $ script/generate plugin LanugageNegotiation
       create  vendor/plugins/lanugage_negotiation/lib
       create  vendor/plugins/lanugage_negotiation/tasks
       create  vendor/plugins/lanugage_negotiation/test
       create  vendor/plugins/lanugage_negotiation/README
       create  vendor/plugins/lanugage_negotiation/MIT-LICENSE
       create  vendor/plugins/lanugage_negotiation/Rakefile
       create  vendor/plugins/lanugage_negotiation/init.rb
       create  vendor/plugins/lanugage_negotiation/install.rb
       create  vendor/plugins/lanugage_negotiation/uninstall.rb
       create  vendor/plugins/lanugage_negotiation/lib/lanugage_negotiation.rb
       create  vendor/plugins/lanugage_negotiation/tasks/lanugage_negotiation_tasks.rake
       create  vendor/plugins/lanugage_negotiation/test/lanugage_negotiation_test.rb
       create  vendor/plugins/lanugage_negotiation/test/test_helper.rb
 $ echo 'require "lanugage_negotiation"' >> vendor/plugins/lanugage_negotiation/init.rb
  
そして、config/environment.rb の内容をごそっと 
vendor/plugins/lanugage_negotiation/lib/lanugage_negotiation.rb
へ移動。

動作チェック

 LANG:console
 $ wgetc http://localhost/negotiation/test
  English: Test   OK
  
 $ wgetc http://localhost/negotiation/test.ja
  日本語: テスト ばっちり
  
 $ wgetc http://localhost/negotiation/test
  日本語: テスト ばっちり
 

うーん、簡単すぎる。

恐らく今回の場合、以下のファイルは変更の必要は無い?

 vendor/plugins/lanugage_negotiation/Rakefile
 vendor/plugins/lanugage_negotiation/install.rb
 vendor/plugins/lanugage_negotiation/uninstall.rb
 vendor/plugins/lanugage_negotiation/tasks/lanugage_negotiation_tasks.rake

残るはドキュメントとテストケースだ。

* テストケース [#z90f29b8]

プラグインのテストケースを書かなければ。

テストケースとして書けるのかどうか自信はないのだけれど、
存在しない言語テンプレートがあった場合や、
言語に依存しないテンプレートがある場合にどうなるか、
ちゃんと試してみる必要がありそう。

** テストの動かし方 [#u8d040b6]

まずいきなりテストしてみる。

 LANG:console
 $ cd vendor/plugins/lanugage_negotiation/
 $ rake 
  (in (rails)/negotiation/vendor/plugins/lanugage_negotiation)
  /usr/bin/ruby1.8 -I"(rails)/negotiation/vendor/plugins/lanugage_negotiation/lib" \
                   -I"(rails)/negotiation/vendor/plugins/lanugage_negotiation/lib" \
                   -I"(rails)/negotiation/vendor/plugins/lanugage_negotiation/test" \
                   "/usr/lib/ruby/1.8/rake/rake_test_loader.rb" "test/lanugage_negotiation_test.rb" 
  /usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in `gem_original_require': no such file to load -- active_support (LoadError)
          from /usr/lib/ruby/1.8/rubygems/custom_require.rb:31:in `require'
          from (rails)/negotiation/vendor/plugins/lanugage_negotiation/test/test_helper.rb:2
          from ./test/lanugage_negotiation_test.rb:1:in `require'
          from ./test/lanugage_negotiation_test.rb:1
          from /usr/lib/ruby/1.8/rake/rake_test_loader.rb:5:in `load'
          from /usr/lib/ruby/1.8/rake/rake_test_loader.rb:5
          from /usr/lib/ruby/1.8/rake/rake_test_loader.rb:5:in `each'
          from /usr/lib/ruby/1.8/rake/rake_test_loader.rb:5
  rake aborted!
  Command failed with status (1): [/usr/bin/ruby1.8 -I"(rails)/...]
  
  (See full trace by running task with --trace)
 $ cat test/test_helper.rb 
  require 'rubygems'
  require 'active_support'
  require 'active_support/test_case'

パスが通っていないらしい。

確かに通ってないんだけど、いくら何でも通ってなさすぎ。~
どうしてこんな事に?

ああ、この rake をいきなり起動しちゃだめなのね。

アプリケーションのホームから、

 LANG:console
 $ cd ../../..
 $ pwd
  (rails)/negotiation
 $ rake test:plugins
  (in (rails)/negotiation)
  /usr/bin/ruby1.8 -I"(rails)/negotiation/lib" -I"(rails)/negotiation/test" \
      "/usr/lib/ruby/1.8/rake/rake_test_loader.rb" "vendor/plugins/lanugage_negotiation/test/lanugage_negotiation_test.rb" 
  Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
  Started
  .
  Finished in 0.035457 seconds.
  
  1 tests, 1 assertions, 0 failures, 0 errors

うまく行ってる。

** プラグインのテストの作成 [#n11b0fa7]

あとは vendor/plugins/lanugage_negotiation/test/lanugage_negotiation_test.rb に
テストケースをもりもり書いていけばいい。

テストの書き方は~
 LANG:ruby(linenumber)
 test "what to test" do
   assert true
   assert_equal 1, 1
 end

という感じ。

詳しい書式: http://www.ruby-lang.org/ja/man/html/Test_Unit.html

 LANG:ruby(linenumber)
  test "ActionController_AbstractRequest_acceptable_language?" do
 
    ar = ActionController::AbstractRequest
    ar.class_eval <<-EOS
      def self.acceptable_languages=(v)
        @acceptable_languages = v
      end
    EOS
 
    ar.acceptable_languages= :ja, :en, :fr
 
    assert ar.acceptable_language? :ja
    assert ar.acceptable_language? :en
    assert ar.acceptable_language? :fr
 
    assert ! ar.acceptable_language?(:de)
    assert ! ar.acceptable_language?(:es)
 
    assert ar.acceptable_language? "fr"
    assert ! ar.acceptable_language?("de")
  end

問題なし。

 LANG:ruby(linenumber)
  test "ActionController_AbstractRequest_accepts_languages!" do
 
    ar= ActionController::AbstractRequest
    ar.class_eval <<-EOS
      def self.acceptable_languages=(v)
        @acceptable_languages = v
      end
      attr :cookies, true
      attr :env, true
    EOS
 
    rec = ar.new
    ar.acceptable_languages= :ja, :en, :fr
    rec.cookies = {}
    rec.env = {}
 
    # server default
    assert_equal [ :ja, :en, :fr ], rec.accepts_languages!
 
    # ignore invalid specifications
    rec.cookies = { 'rails_language' => ['xx'] }
    rec.env = { 'HTTP_ACCEPT_LANGUAGE' => 'es, de' }
    assert_equal [ :ja, :en, :fr ], rec.accepts_languages!
    
    # Accept-Language
    rec.env = { 'HTTP_ACCEPT_LANGUAGE' => 'en-us, fr, en, ja' }
    assert_equal [ :en, :fr, :ja ], rec.accepts_languages!
 
    rec.env = { 'HTTP_ACCEPT_LANGUAGE' => 'en-us;q=0.8, fr;q=0.1, en, ja' }
    assert_equal [ :en, :fr, :ja ], rec.accepts_languages!
 
    # cookie
    rec.cookies = { 'rails_language' => ['ja'] }
    assert_equal [ :ja, :en, :fr ], rec.accepts_languages!
 
    rec.cookies = { 'rails_language' => ['fr'] }
    assert_equal [ :fr, :en, :ja ], rec.accepts_languages!
 
    rec.env = {}
    assert_equal [ :fr, :ja, :en ], rec.accepts_languages!
 
    # arg
    assert_equal [ :ja, :fr, :en ], rec.accepts_languages!('ja')
 
    assert_equal [ :en, :fr, :ja ], rec.accepts_languages!('en')
 
    assert_equal [ :fr, :ja, :en ], rec.accepts_languages!('fr')
 
  end

テストに引っかかった。

 LANG:ruby
 class ActionController::AbstractRequest.accepts_languages!(language=nil)
  ...
  
  lsym= l.split(/;|\-/,2)[0].downcase.to_sym

の部分、

 LANG:ruby
  lsym= l.split(/;|\-/,2)[0].strip.downcase.to_sym

とする必要有り。

 LANG:ruby(linenumber)
  class MemoizedTestClass
    extend ActiveSupport::Memoizable
    def count
      @count
    end
    def calc(v)
      @count= ( @count || 0 ) + 1
      v
    end
    memoize :calc
  end
 
  test "memoize" do
    mt= MemoizedTestClass.new
    mt.calc(1)
    mt.calc(2)
    mt.calc(1)
    mt.calc(3)
    mt.calc(1)
    assert_equal 3, mt.count
    mt.calc(1)
    mt.calc(2)
    mt.calc(3)
    assert_equal 3, mt.count
    mt.calc(4)
    assert_equal 4, mt.count
    assert MemoizedTestClass.memoized?(:calc)
    MemoizedTestClass.unmemoize :calc
    assert !MemoizedTestClass.memoized?(:calc)
    assert_equal 4, mt.count
    mt.calc(1)
    mt.calc(2)
    mt.calc(3)
    mt.calc(4)
    assert_equal 8, mt.count
    MemoizedTestClass.memoize :calc
    mt.calc(1)
    mt.calc(2)
    mt.calc(3)
    assert_equal 8, mt.count
  end

unmemoize 後にもう一度 memoize した場合には、
以前の計算結果が残ってしまうことに注意。

 LANG:ruby(linenumber)
 module ActiveSupport::Memoizable
   def unmemoize(*symbols)
       class_eval <<-EOS, __FILE__, __LINE__
         #{memoized_ivar}= nil

などと書いてたけれど、呼び出されるコンテキストが
異なるので変数のクリアはできない。

この行は削除した。

 LANG:console
 $ rake test:plugins
  /usr/bin/ruby1.8 -I"(rails)/negotiation/lib" -I"(rails)/negotiation/test" "/usr/lib/ruby/1.8/rake/rake_test_loader.rb" "vendor/plugins/lanugage_negotiation/test/lanugage_negotiation_test.rb" 
  Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
  Started
  ...
  Finished in 0.038165 seconds.
  
  3 tests, 25 assertions, 0 failures, 0 errors

テスト成功。

他にももう少し plugin test でちまちま書いてく事もできるんだけど、
controller の test と合わせていっきにやってしまった方が楽そう?

** negotiation アプリケーションのテストケース [#p80291d3]

コントローラ込みのファンクショナルテストについては、
プラグインのテストケースに入れるわけに行かないので、
negotiation アプリケーションの
test コントローラのテストケースとして書く事になる。

まずテストに必要なアクションとビューを追加。

 LANG:console
 $ jed app/controllers/test_controller.rb
  class TestController < ApplicationController
    caches_page :pagecached
    caches_action :actioncached
    def index
    end
    def pagecached
    end
    def actioncached
    end
    def fragmentcached
    end
    def render_action
      render :action => :acceptid
    end
    def render_file
      render :file => "test/acceptid.html"
    end
    def acceptid
    end
  end
 $ echo "Test   <%= render :partial=>'ok' %>" > app/views/test/index.html.en.erb
 $ echo "テスト <%= render :partial=>'ok' %>" > app/views/test/index.html.ja.erb
 $ echo "Page cached"      > app/views/test/pagecached.html.en.erb
 $ echo "ページキャッシュ" > app/views/test/pagecached.html.ja.erb
 $ echo "Action cached"        > app/views/test/actioncached.html.en.erb
 $ echo "アクションキャッシュ" > app/views/test/actioncached.html.ja.erb
 $ echo "<% cache :param1=>:test do %>Fragment cached   <%= render :partial=>'ok' %><% end %>" > app/views/test/fragmentcached.html.en.erb
 $ echo "<% cache do %>フラグメントキャッシュ   <%= render :partial=>'ok' %><% end %>" > app/views/test/fragmentcached.html.ja.erb
 $ echo "File"     > app/views/test/renderfile_.html.en.erb
 $ echo "ファイル" > app/views/test/renderfile_.html.ja.erb
 $ echo "html <%= params[:id] %>" > app/views/test/acceptid.html.ja.erb
 $ echo "text <%= params[:id] %>" > app/views/test/acceptid.text.ja.erb
 $ echo "English <%= @content_for_layout %>" > app/views/layouts/application.text.en.erb

テストは test/functional/test_controller_test.rb に書き込む。

書き方は http://d.hatena.ne.jp/elm200/20070724/1185282738 が参考になる。

 LANG:ruby(linenumber)
  def setup
    @ar         = ActionController::AbstractRequest
    @controller = TestController.new
    @request    = ActionController::TestRequest.new
    @response   = ActionController::TestResponse.new
  end

としておくと、

 LANG:ruby(linenumber)
  test "server default1" do
    @ar.acceptable_languages = :ja, :en, :fr
    @request.cookies = {}
    @request.env = {}
    get :index, :format=>"html"
    assert_response :success
    assert_template 'index.html.ja'
    assert_equal "/test", @request.request_uri
  end

のようなコードを書く事ができる。

キャッシュをテストするために、~
config/environments/test.rb
 LANG:ruby(linenumber)
 config.action_controller.perform_caching             = true

さらに、
 LANG:ruby(linenumber)
  def assert_cached_page_exists( page )
    full_path= ActionController::Base.__send__(:page_cache_path, page)
    assert File.exist?(full_path), "Cached file '#{full_path}' not created."
  end
 
  def assert_cached_page( page )
    ActionController::Base.class_eval <<-EOS
      @@page_cache_directory = defined?(Rails.public_path) ? Rails.public_path : ""
      @@page_cache_directory += "/cahce_in_test_environment"
    EOS
    @controller.expire_page page
    yield
    assert_cached_page_exists page 
  end

として、development や production 環境にキャッシュを作成してしまわないようにする。

 LANG:ruby(linenumber)
  test "page cache 1" do
    @ar.acceptable_languages = :fr, :ja, :en
    @request.cookies = {}
    @request.env = { }
    page = "/test/pagecached.html.fr"
    assert_cached_page( page ) do
      get :pagecached, :format=>"html"
      assert_response :success
      assert_template 'pagecached.html.ja'
      assert_equal "/test/pagecached", @request.request_uri
      assert_layout "layouts/application.html.ja"
    end
  end

のようにテストする事ができる。

その他作ったヘルパー関数達

 LANG:ruby(linenumber)
  def assert_content_type(type)
    flunk "Content-Type '#{type}' was expected but "+
          "'#{@response.content_type}' found." unless
              @response.content_type == type
  end
 
  def assert_cached_fragment_exists(key, message="Fragment cache ['#{key}'] not exists.")
    flunk message unless @controller.fragment_exist? key
  end
  
  def assert_cached_fragment(key)
    @controller.expire_fragment key
    yield
    assert_cached_fragment_exists key
  end
  
  def assert_layout(v)
    assert_equal v, @response.template.send(
        :_pick_template, @response.layout).path_without_extension
  end
 
  def log(s)
    if s.is_a? String
      Rails.logger.warn s
    else
      Rails.logger.warn s.inspect
    end
  end

テストはかなりたくさん書いたので、ここに全部は載せない。~
一部を取り出すと、

 LANG:ruby(linenumber)
  test "page cache 3" do
    @ar.acceptable_languages = :fr, :ja, :en
    @request.cookies = {}
    @request.env = { 'HTTP_ACCEPT_LANGUAGE' => 'xx' }
    page = "/test/pagecached.html.fr"
    assert_cached_page( page ) do
      get :pagecached, :format=>"html"
      assert_response :success
      assert_template 'pagecached.html.ja'
      assert_equal "/test/pagecached", @request.request_uri
      assert_layout "layouts/application.html.ja"
      assert_content_type "text/html"
    end
  end
  
  test "page cache 4" do
    @ar.acceptable_languages = :fr, :ja, :en
    @request.cookies = {}
    @request.env = { 'HTTP_ACCEPT_LANGUAGE' => 'en' }
    page = "/test/pagecached.html.en"
    assert_cached_page( page ) do
      get :pagecached, :format=>"html", :extra=>:param
      assert_response :success
      assert_template 'pagecached.html.en'
      assert_equal "/test/pagecached?extra=param", @request.request_uri
      assert_layout "layouts/application.html.en"
      assert_content_type "text/html"
    end
  end
 
  test "action cache 1" do
    @ar.acceptable_languages = :fr, :ja, :en
    @request.cookies = {}
    key = "test.host/test/actioncached?rails_language=fr"
    assert_cached_fragment( key ) do
      get :actioncached, :format=>"html"
      assert_response :success
      assert_template 'actioncached.html.ja'
      assert_equal "/test/actioncached", @request.request_uri
      assert_layout "layouts/application.html.ja"
      assert_content_type "text/html"
    end
  end
  
  test "action cache 2" do
    @ar.acceptable_languages = :fr, :ja, :en
    @request.cookies = {}
    key = "test.host/test/actioncached.ja"
    assert_cached_fragment( key ) do
      get :actioncached, :format=>"html", :rails_language=>"ja"
      assert_response :success
      assert_template 'actioncached.html.ja'
      assert_equal "/test/actioncached.ja", @request.request_uri
      assert_layout "layouts/application.html.ja"
      assert_content_type "text/html"
    end
  end

こんな感じ。

実際にこれを走らせる。

 LANG:console 
 $ rake test:functionals
  rake aborted!
  no such file to load -- (rails)/negotiation/db/schema.rb
  
  (See full trace by running task with --trace)
 $ rake db:schema:dump
 $ rake test:functionals
  /usr/bin/ruby1.8 -I"(rails)/negotiation/lib" -I"(rails)/negotiation/test" "/usr/lib/ruby/1.8/rake/rake_test_loader.rb" "test/functional/test_controller_test.rb" 
  Loaded suite /usr/lib/ruby/1.8/rake/rake_test_loader
  Started
  ..........................
  Finished in 0.358801 seconds.
  
  26 tests, 96 assertions, 0 failures, 0 errors

実行は rake test:functionals で良い。~
schema.rb が無いと言われたので、rake db:schema:dump で作成した。

ミスして入力したテストで気づいた点。

 LANG:ruby(linenumber)
 module ActionView
   class Template
       def split(file)
             elsif ActionController::AbstractRequest.acceptable_language?(m[3])

の部分などで、m[3] が nil になった場合、

 LANG:ruby(linenumber)
 class ActionController::AbstractRequest
   def self.acceptable_language?(l)
     acceptable_languages.include?(l.to_sym)
   end
 end

の l.to_sym でこける。

 LANG:ruby(linenumber)
   def self.acceptable_language?(l)
     l.respond_to?(:to_sym) && acceptable_languages.include?(l.to_sym)
   end

とすべき。

また、routes.rb では
 LANG:ruby(linenumber)
   map.connect ':controller/:action/:id', :format => 'html'

を始めの方に書いておかないと、
 LANG:ruby(linenumber)
  url_for(:controller=>:test, :action=>:acceptid, :index=>1, :format=>"html")

が
 LANG:ruby(linenumber)
   map.connect ':controller/:action/:id.:rails_language', :format => 'html',
                       :requirements => { :rails_language => lang_regexp }

を使って変換されてしまい、  
  /test/acceptid/1.

となるので、順番を入れ替える。

* fcgi 化 [#wd595311]

ちょっとオフトピックだけれど、はまったので。

fcgi 化するには基本的には RewriteRule で dispatch.cgi となっていたところを
dispatch.fcgi にするだけでよい。

 LANG:console
 $ jed public/.htaccess
  RewriteRule ^(.*)$ dispatch.fcgi [QSA,L]
 $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test
  エラー: ActionController::RoutingError (no route found to match to "/negotiation/test")

でも、なんでかうまくいかない。~
.htaccess の SetEnv RAILS_RELATIVE_URL_ROOT /negotiation の効果が得られていないみたい。

 vendor/actionpack/CHANGELOG 
 *2.2 (November 21st, 2008)*
 * AbstractRequest.relative_url_root is no longer automatically configured 
   by a HTTP header. It can now be set in your configuration environment with 
   config.action_controller.relative_url_root [Josh Peek]

どうやらこれらしい。~
なんで fcgi の時だけだめなんだろう?

changelog に書かれているのだからバグという事でもないのだろうし、
コードを追うのはやめにして、素直に environments/production.rb に
設定を追加したところ、動き出した。

さすが、cgi に比べると段違いに速い。

 LANG:console
 $ echo 'config.action_controller.relative_url_root = "/negotiation"' >> environments/production.rb
 $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test
  日本語: テスト ばっちり
 $ rm tmp/wget.cookie
 $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test
  English: Test   OK
 $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test.ja
  日本語: テスト ばっちり
 $ wgetc --header="Accept-Language: en" http://localhost/negotiation/test
  日本語: テスト ばっちり
 $ wgetc http://localhost/negotiation/test/pagecached
  日本語: ページキャッシュ
 $ ls public/test/*
  public/test/pagecached.html.ja
 $ wgetc http://localhost/negotiation/test/pagecached.en
  English: Page cached
 $ ls public/test/*
  public/test/pagecached.html.en  public/test/pagecached.html.ja

Counter: 5090 (from 2010/06/03), today: 1, yesterday: 1