Capistrano 3への手引き
Capistrano、便利ですよね。
最近メジャーバージョンアップがあったのですが、使い方、というかスクリプトの書き方やお作法が変わり、「Capistrano 3にアップデートしたはいいけど全然動かなくてどうなってんだ」という流れはもはやお約束みたいです。
試しに僕も個人で作ってるウェブサイトのCapistranoをアップデートしてみたので、その上でこんなところに気を付けたいな、と思うポイントでも書いておきます。
capifyは使わない
Capistranoを使うときは$ bundle installをし、次に$ bundle exec capify .とするのがお約束の流れですが、これからはcapifyを使ってもcap installを使ってねと言われます。
ですので:
$ bundle exec cap install
としましょう。
マルチステージが最初から有効になっている
ひとつのスクリプトで複数の環境(ステージ)にデプロイするときは今までcapistrano/capistrano-extのcapistrano/ext/multistageを使うのが常套手段でしたが、これはデフォルトで有効になっています*1。
デフォルトで有効であるため、cap installを行うと、Capfileとconfig/deploy.rbという今まで通りのものに加え、config/deploy/staging.rbとconfig/deploy/production.rbが作られます。もしconfig/deploy/*に書き出されるステージ名を変えたい場合は:
$ bundle exec cap install STAGES=development,staging,production
とSTAGES環境変数でcap install渡してあげましょう。
Capistrano拡張の対応
Ruby on Rails然りですが、これだけバージョンが変われば、今まで使ってきた拡張も使えなくなるというものです。
ただいくつかの拡張に関してはcapistrano自身が対応させた拡張を公開していますので、そちらを使いましょう。
# Gemfile source 'https://rubygems.org' group :development do gem 'capistrano' gem 'capistrano-rbenv', github: 'capistrano/rbenv' gem 'capistrano-bundler', github: 'capistrano/bundler' gem 'capistrano-rails' end
capistrano-railsは「Capistranoは別にRuby on Railsだけのものじゃないんだよ」を体現するために別扱いになりました。こちらはrubygems.orgにあるため、githubオプションなどでgitリポジトリを指定する必要はありません。
また、これらの拡張は今まで人によってconfig/deploy.rbなどにrequireを記述していたかもしれませんが、Capfileに書くのが通例のようです。実際、cap installによって生成されたCapfileには上記の拡張がコメントアウトされています。必要に応じてコメントアウトを外しておきましょう。
変数の参照
今まで:
set :application, 'example-app'
として設定した変数は直接applicationとローカル変数のように参照できていましたが、これからは出来ません。fetchメソッドを使いましょう。
set :deploy_to, "/u/apps/#{fetch(:application)}"
なお、current_pathなどは変数とはまた別の扱いらしく、今まで通りローカル変数のように参照できます。
変数名の変更
「なんで変えたんだ!」と言いたくなる気がしますが、repository変数はrepo_url変数と名前が変わりました。拡張ではありますが、capistrano/rbenvの場合、rbenv_pathではなくrbenv_type(:systemで/usr/local/rbenv、:userで~/.rbenv)で、rbenv_ruby_versionはrbenv_rubyになりました。
大きな変更というより、Capistrano開発者の好みの範囲での変更に思えてしまいますが、ドキュメント参照の上、適切に設定する必要がありそうです。
デプロイ先のサーバの設定
今までは下記のように書いていました。
role :app, 'app1.example.com', 'app2.example.com' role :web, 'web1.example.com' role :db, 'db1.example.com', primary: true
Capistrano 3でも上記記述でエラーが出るわけではないのですが、公開鍵での認証が出来なくなってしまいました。以下のように書くのがベターな気がします。
server 'app1.example.com', roles: %w(app), user: 'deploy', ssh_options: { keys: [File.expand_path('~/.ssh/...')] }
正直ここら辺はよく調べていないのですが、サーバごとに認証を設定できたりする方が理に適っていて、個人的にはこちらの方が好きです。
共有ディレクトリの設定
Capistranoはdeploy_toの配下にcurrent, releases, sharedの3つのディレクトリを作成します*2。
その中のsharedにはデプロイの度に上書きを避けたいファイルなどをディレクトリにまとめて置き、デプロイ時にそのディレクトリへシンボリックリンクを張るという処理が行なわれていました。そのシンボリックリンクを張るディレクトリはshared_childrenという変数で設定できていたのですが、それがlinked_dirsに変わりました。ちなみにlinked_filesという変数も追加されています。
ただ名前が変わっただけなら良いのですが、ちょっとディレクトリ構造が変わることがあるので、注意が必要です。具体的には:
set :shared_children, %w(public/system)
とすると、deploy:setup時にshared/systemが作成され、そのディレクトリがreleases/LATEST_RELEASE/public/systemとしてシンボリックリンクを作成するような動作になっていましたが、Capistrano 3では:
set :linked_dirs, %w(public/system)
とすると、shared/systemではなく、shared/public/systemが作成されるようになります。ですので、Capistrano 2から乗り換えた場合、サーバ側でちょっと手を加えてあげないと今まで参照できていたファイルが参照できない、などといったことが発生するため注意が要ります。
各タスクの記述
ここが一番大きな変更点だと思います。今までtaskメソッドを呼び出してタスクを定義し、そのメソッドへのオプションでどのロールで実行するのか、などを定義していました。タスクの中では愚直にrunを並べて、そのタスクが何を行うのかを記述していました。何度もrunを呼びたくないからと、コマンドを組み立て、runメソッド1回で全コマンドを実装するように心掛けていた人もいるんじゃないかと思います。
Capistrano 3では基本taskメソッドはタスク名しか渡さず、その中のonメソッドでいろいろ記述します。
task :example do on roles(:app), in: :sequence, wait: 5 do # ~ end end
そしてonメソッドに渡したブロックの中で実行するコマンドなどを記述していきます。
コマンドの記述
先述のように、onメソッドに渡したブロックの中にコマンドを記述するのですが、ここで様々なメソッドが使えるようになっています。
namespace :log do task :clear do on roles(:app), in: :parallel do # 渡したパスが存在するかを確かめ、存在しなければブロック内のコマンドを実行しない。 # また、コマンド実行時に先頭に"cd 渡したパス && "を付与する within current_path.join('log') do # コマンド実行時に先頭に環境変数の設定を付与する。 # 下記の例では"RAILS_ENV=rails_env変数の値"となる with rails_env: fetch(:rails_env) do # コマンドの実行成否を真偽値で取得する(元々あった?) if test '[ -f development.log ]' execute :rm, 'development.log' end end end end end end
他にもasなどがあるのですが、これらはleehambley/sshkitというSSHによるコマンドの実行を構造的に記述するためgemの仕業です。タスクの記述方法然り、コマンドの記述方法然りなのですが、ここらへんの記述方法が変わったのはSSHKitを導入したからなのかなあ、と思います。
SSHKitを使えばexecuteをもっとスマートに書けたりするので、ぜひ活用していきたいところです。ちなみにonメソッドもSSHKitのものですね。
deploy:setup
なくなりました。いきなりdeployで大丈夫です。他にもいくつかタスクの名前が変わっていたりします。
他にもまだまだたくさんありそうですが、というかcap installで作成されたスケルトンから読み取れることしか書いてないのですが、とりあえずこんなところで。
*1:ていうか2.xのいつからかcapistrano-ext使わなくても有効でしたよね?
*2:Capistrano 3ではrepoも加わりました。単にcloneやpullをするための(サーバから見て)ローカルリポジトリです
EmacsからVimに乗り換えた
ちょっと前にEmacs + markdown-modeでドキュメントを書いていたのですが、Markdownって改行を入れると、ウェブブラウザ上では改行されていなくとも、HTML上で改行(単にCRが入ってる、という意味)されるじゃないですか? そういうときってウェブブラウザ上で見ると、どうしてもCRによるホワイトスペースが入っちゃうんですよね。それがもう嫌で嫌でしょうがないって人、結構いると思います。僕です。
て言うかそもそも改行を入れていたのは、1行あたり80文字程度に収めるためです。つまり、次行への送りを手動でやっていたわけです。そこからしてナンセンスなわけですが、じゃあなぜ行の折り返しを使わなかったのかと言うと、あれ、行の折り返しを使うとCtrl+nとかで移動したとき、折り返された部分を飛び越して、その次の行に飛んじゃいますよね。「あ〜〜〜〜〜〜!」って感じです。
それもEmacs 23からはvisual-line-modeというマイナーモードのおかげで解決できるのですが、恥ずかしながら僕が使っているのはMac OS X LeopardでCarbon Emacsです。バージョンは22…… あまりに酷いので一昨日ぐらいに一番新しいMacBook Air注文しました。これで解決。
ですが、届くまで待てないので、なんか良い機会だし、Vimに乗り換えるか〜っていうのが経緯です。経緯長くてごめんね。
以下何をしたか、です。
続きを読むMeCabのメモリ管理はどうなっているのか
以前mecab-rubyを用いた下記のコードがコケる場合がありました。
node = MeCab::Tagger.new.parse(text)
これはparseメソッドを呼び出している最中にMeCab::TaggerのインスタンスがGCによって解放されてしまい、メモリ違反を起こすためです(昔のことなのであやふやですが)。なので、parseメソッドに与える文字列の長さが短い場合はエラーが発生せず、長ければ長いほど発生し易いといういやらしいものです。
まあ今手元のmecab-ruby(MeCabのバージョンは0.996)で試したら'a' * 100_000な文字列を与えてもそんな問題は発生しませんでしたけどね…… おかしいな……
そもそもなんでこんな話を思い出したかと言うと、MeCabのメモリがどういう風に管理されているかが気になったからです。
MeCabは0.99になってからModelとLatticeという概念が導入され、マルチスレッド時における使用法が変わりました。Modelはこの問題によって存在を知りましたが、実を言うとLatticeは知りませんでした。で、さきほどLatticeが気になりだしたのですが、なぜLatticeはスレッド毎に生成しなければならないのか、あれ、メモリはどうなってるんだろう、と次々と疑問が湧いたのでMeCabのソースコードで確認した次第です。
それのレポートがこちら。
なおMeCabのバージョンは最新の0.996です(ソースコード)。調べた対象のファイルはsrc/tagger.cppです。
続きを読むRubyにpackage_privateを導入しようとしたら
Rubyのメソッドの可視性はpublic, protected, privateの3種類です。そこに同一パッケージ(イコール同一gem?)からのみ呼べるpackage_privateを導入してみましょう。
結論から言うと断念しました。
以下のような場合は簡単です。
class String def nya "#{self}にゃ" end package_private :nya end
privateなどでもお馴染の引数付きでpackage_privateを呼んだ場合です。これは指定されたメソッドを同一パッケージから呼ばれたか判断するメソッドで包んで、同一パッケージからの呼び出し(ここではcallerで判断しました)じゃない場合はNoMethodErrorを投げる、でおっけー。
ではなぜ断念したか。以下のようなケースだと雲行きが怪しくなってきます。
class String package_private def nya "#{self}にゃ" end private def pyon "#{self}ぴょん" end end
この場合、privateが呼ばれたら、package_privateを解除したいわけです。ってことはprivateの定義も上書きしなくちゃいけない?
で、本格的に断念したのが以下のケース。
class String package_private end class String def nya "#{self}にゃ" end end
この場合のnyaメソッドはデフォルトの可視性であるpublicで定義されるべきなのですが、package_privateを解除する術が思い付かず、断念って感じです。ここらへんからはCの世界なので、CRubyのコードも読んでみたのですが、組み込むのはなかなかしんどそう。うむー。
追記(2013-04-16 08:10)
と、こんな記事を書いておいたら、これを読んだ@kyubingから神に等しきコードが飛んできました。
最初パッと見たところ、2度目のclass Nekoでpackage_privateが有効になりっぱなしにならないかな〜と思いましたが、そこらへんがRubyの妙のようで。
Rubyはインスタンスメソッドが追加されると、Module#method_added(method_name)がコールされます。そのときのバックトレースは以下のような感じになるみたいです。
ファイル名:メソッドが定義された行:in `<class:クラス名>' ファイル名:クラスの定義が開始された行:in `<main>' # mainかはクラスを定義した場所に依る
Class.newやclass_evalを使うともう少し違うバックトレースになりますが、それでも基本的に「何行目でクラスの定義が開始されたか」という情報は取れます。
と言うことは、完全に断念したというケースでも判断が付きそうですね! すごい! @kyubing様々です。また改めてコードにしてみようと思います。
Rubyで任意のメソッドの呼び出しを記録するgemを作った
タイトルの通り。
インストールは:
$ gem install peeek
で。処理としては対象のメソッドを呼び出し記録用のメソッドで包んでいるだけです。使い方はREADME.mdなりをご覧ください。が、一部書き漏らしていることがあり、それをここに書いておきます。
peeekコマンド
一応コマンドが付属しており、コマンドラインからでも簡単に試すことが出来ます。
$ peeek -v
peeek-1.0.2
$ peeek -H'Kernel#puts' -e 'puts "Hello World"'
Hello World
Kernel#puts from main with "Hello World" returned nil in -e at 1
ちょっと試したいときに便利。かも。
何に使うんだーって言われると何かに使えるかもね、としか答えられない思い付きgemなんですが、Pull Requestなどがあればどしどしください。
httpstatusコマンドで、HTTPのステータスコード(
一般的なWeb Programmerならば、HTTP Status codeはすべて暗記していると聞きました。
しかし、僕は初心者なので、なかなか覚えきれていないので、HTTPのステータスコードをさがすのに便利なツールを用意しました。httpstatus.erlです。インストール方法は:
$ wget https://gist.github.com/takkkun/5002968/raw/569c3e3ea98d1cd8c17a9529c12ac8a88c97350a/httpstatus.erl $ chmod +x httpstatus.erl
です。escript用に書いてあるのでコンパイルとか必要ありません。
$ ./httpstatus.erl 4 400: Bad Request 401: Unauthorized 402: Payment Required 403: Forbidden 404: Not Found 405: Method Not Allowed 406: Not Acceptable 407: Proxy Authentication Required 408: Request Timeout 409: Conflict 410: Gone 411: Length Required 412: Precondition Failed 413: Request Entity Too Large 414: Request-URI Too Large 415: Unsupported Media Type 416: Request Range Not Satisfiable 417: Expectation Failed 418: I'm a teapot 422: Unprocessable Entity 423: Locked 424: Failed Dependency 425: No code 426: Upgrade Required 428: Precondition Required 429: Too Many Requests 431: Request Header Fields Too Large $ ./httpstatus.erl 40 400: Bad Request 401: Unauthorized 402: Payment Required 403: Forbidden 404: Not Found 405: Method Not Allowed 406: Not Acceptable 407: Proxy Authentication Required 408: Request Timeout 409: Conflict $ ./httpstatus.erl 400 400: Bad Request $ ./httpstatus.erl Not 304: Not Modified 404: Not Found 405: Method Not Allowed 406: Not Acceptable 416: Request Range Not Satisfiable 501: Not Implemented 505: HTTP Version Not Supported 510: Not Extended
以下コード。
#!/usr/bin/env escript main([Query|_]) -> case matcher(Query) of {ok, Matcher} -> print(lists:filter(Matcher, http_statuses())); {error, {Message, Position}} -> io:format("Error: ~s at ~p character~n", [Message, Position]) end; main([]) -> print(http_statuses()). matcher([C|_] = Query) when $0 =< C andalso C =< $9 -> case re:compile("^" ++ Query) of {ok, Regexp} -> {ok, fun({Code, _}) -> re:run(Code, Regexp, [{capture, none}]) =:= match end}; {error, ErrorSpec} -> {error, ErrorSpec} end; matcher(Query) -> {ok, fun({_, Message}) -> string:str(Message, Query) > 0 end}. print(HttpStatuses) -> lists:foreach(fun({Code, Message}) -> io:format("~s: ~s~n", [Code, Message]) end, HttpStatuses). http_statuses() -> [ {"100", "Continue"}, {"101", "Switching Protocols"}, {"102", "Processing"}, {"200", "OK"}, {"201", "Created"}, {"202", "Accepted"}, {"203", "Non-Authoritative Information"}, {"204", "No Content"}, {"205", "Reset Content"}, {"206", "Partial Content"}, {"207", "Multi-Status"}, {"208", "Already Reported"}, {"300", "Multiple Choices"}, {"301", "Moved Permanently"}, {"302", "Found"}, {"303", "See Other"}, {"304", "Not Modified"}, {"305", "Use Proxy"}, {"307", "Temporary Redirect"}, {"400", "Bad Request"}, {"401", "Unauthorized"}, {"402", "Payment Required"}, {"403", "Forbidden"}, {"404", "Not Found"}, {"405", "Method Not Allowed"}, {"406", "Not Acceptable"}, {"407", "Proxy Authentication Required"}, {"408", "Request Timeout"}, {"409", "Conflict"}, {"410", "Gone"}, {"411", "Length Required"}, {"412", "Precondition Failed"}, {"413", "Request Entity Too Large"}, {"414", "Request-URI Too Large"}, {"415", "Unsupported Media Type"}, {"416", "Request Range Not Satisfiable"}, {"417", "Expectation Failed"}, {"418", "I'm a teapot"}, {"422", "Unprocessable Entity"}, {"423", "Locked"}, {"424", "Failed Dependency"}, {"425", "No code"}, {"426", "Upgrade Required"}, {"428", "Precondition Required"}, {"429", "Too Many Requests"}, {"431", "Request Header Fields Too Large"}, {"449", "Retry with"}, {"500", "Internal Server Error"}, {"501", "Not Implemented"}, {"502", "Bad Gateway"}, {"503", "Service Unavailable"}, {"504", "Gateway Timeout"}, {"505", "HTTP Version Not Supported"}, {"506", "Variant Also Negotiates"}, {"507", "Insufficient Storage"}, {"509", "Bandwidth Limit Exceeded"}, {"510", "Not Extended"}, {"511", "Network Authentication Required"} ].
最近のMeCabの使い方
MeCabはC/C++で書かれた形態素解析を行うライブラリなのですが、ちょっと疑問に思うところがあって、以下のようなコードを書きました。
require 'MeCab' mecab_options = '-Owakati' 1000.times do |n| begin tagger = MeCab::Tagger.new(mecab_options) # do something with tagger rescue => e raise "failed at #{n + 1} times, [#{e.class}] #{e.message}" end end
マシンスペックに依りますが、大体例外が発生します。MeCab::Taggerのインスタンスが破棄されているにも関わらず! 僕のMacBook Air(メモリ2GB)で試したところ、"failed at 70 times, [RuntimeError] "と、大体70回MeCab::Tagger.newを呼んだところで落ちます。例外の内容は不明。
プロセスどうなってんのと見てみると、メモリを13GBほど消費してました。ひどい。多分mallocに失敗とかそんなところでしょうか?
どうやらMeCab 0.99からマルチスレッドに対応したようで、本家のドキュメントのコード例も変わってました。要はマルチスレッドでMeCab::Taggerのインスタンスをもりもり作る場合はMeCab::Modelのインスタンスを作り、そこからMeCab::Taggerのインスタンスを生成しろよ、ってことみたいです。もしMeCab::Taggerのインスタンスを直接生成すると、そのたびにMeCab::Modelのインスタンスが生成されるので、メモリを圧迫し、死亡、という流れのようですね。
ということでMeCab 0.99をインストールしている環境では以下のように書けば大丈夫みたいです。
require 'MeCab' mecab_options = '-Owakati' mecab_model = MeCab::Model.create(mecab_options) 1000.times do tagger = mecab_model.createTagger # do something with tagger end
ちなみにMeCab::Taggerをひとつしか生成しないような環境であれば、MeCab::Taggerから直接インスタンスを生成しても構いません(コード例でもそう書いてある)。
ErlangでTwitterのUserStreamを受け取る
以前もErlangでTwitter Streaming APIを使うといったエントリを書いたのですが、いかんせん情報が古すぎます。UserStreamではなく、素のStreaming APIなのはともかく、認証がベーシック認証だったりします。
その割にはどうやら最近参照されているらしい。http://naoyat.hatenablog.jp/entry/2012/01/04/220639やhttp://d.hatena.ne.jp/siritori/20120312/1331503357には以前のエントリのURLが貼られているようで。いや、なんかすみません。
ということで、ちゃんと動くかつOTPで書き直してみました。erlang-oauthに依存しています。
-module(userstream). -author("Takahiro Kondo <heartery@gmail.com>"). -export([start/5, start/6, start_link/5, start_link/6, stop/1]). -behavior(gen_server). -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). -record(state, {id, processor}). start(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret) -> start(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret, []). start(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret, Options) -> Args = [Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret], gen_server:start(?MODULE, Args, Options). start_link(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret) -> start_link(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret, []). start_link(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret, Options) -> Args = [Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret], gen_server:start_link(?MODULE, Args, Options). stop(Server) -> gen_server:cast(Server, stop). %% callback functions init([Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret]) -> Url = "https://userstream.twitter.com/2/user.json", Consumer = {ConsumerKey, ConsumerSecret, hmac_sha1}, Options = [{sync, false}, {stream, self}], case oauth:post(Url, [], Consumer, AccessToken, AccessTokenSecret, Options) of {ok, Id} -> {ok, #state{id = Id, processor = Processor}}; {error, Reason} -> {stop, {http_error, Reason}} end. handle_call(_, _, State) -> {noreply, State}. handle_cast(stop, State) -> {stop, normal, State}. handle_info({http, {Id, stream_start, Headers}}, #state{id = Id, processor = Processor} = State) -> send(Processor, {start, Headers}), {noreply, State}; handle_info({http, {Id, stream, <<"\r\n">>}}, #state{id = Id} = State) -> {noreply, State}; handle_info({http, {Id, stream, Part}}, #state{id = Id, processor = Processor} = State) -> send(Processor, {stream, Part}), {noreply, State}; handle_info({http, {Id, {error, Reason}}}, #state{id = Id, processor = Processor} = State) -> send(Processor, {error, Reason}), {stop, {http_error, Reason}, State}. terminate(_, #state{id = Id, processor = Processor}) -> send(Processor, stop), httpc:cancel_request(Id). code_change(_, State, _) -> {ok, State}. %% private functions send(To, Message) -> To ! Message.
ちゃんと動くかは確認しましたが、process_flagとかは呼んでいないのでそこらへんは適当に。gen_serverですので、ちゃんと設定すればそこまで手こずることなくsupervisor treeに組込めるかと思います。
本当はuserstreamモジュールをさらにビヘイビアにして、handle_status/2, handle_favorite/2とかで各イベントをハンドリングできるようにするといいんですが、それをやるとちょっと複雑になるので、そこまではやりません。
ちなみに使い方はこんな感じで。
-module(example). -author("Takahiro Kondo <heartery@gmail.com>"). -export([start/0, stop/1]). start() -> Processor = spawn(fun() -> process() end), ConsumerKey = "Your consumer key", ConsumerSecret = "Your consumer secret", AccessToken = "Your access token", AccessTokenSecret = "Your access token secret", userstream:start(Processor, ConsumerKey, ConsumerSecret, AccessToken, AccessTokenSecret). stop(Pid) -> userstream:stop(Pid). process() -> receive {start, Headers} -> io:format("Start: ~p~n", [Headers]), process(); {stream, Part} -> io:format("Stream: ~p~n", [Part]), process(); {error, Reason} -> io:format("Error: ~p~n", [Reason]), process(); stop -> io:format("Stop~n") end.
$ erl -s inets -s ssl > {ok, Pid} = example:start(). ここにUserStreamからの応答が表示される(example:process/0で標準出力に吐き出してるため) > example:stop(Pid). Stop >
OTPを使いつつ複雑すぎない書き方をしてみました。必ずしもOTPを使う必要はないですし、メリットばかりでもないんですが、アプリケーションがある程度複雑になってきたら使った方が良いかなと思います。それこそ書き捨てのコードでは不要でしょうが、あのプロセスが動いて、こっちであーでどーで、とかで頭のリソース割かれるならOTPを学ぶ価値はアリかなと。
余談ではありますが、余力があれば自作のtwitterモジュールをGitHubにでもあげておきたいもんですね。それにはTwitterのREST APIはもちろん、先述したUserStream用ビヘイビアも書いてはあるんですよ。ただ随分前からメンテナンスをサボってるので、REST APIが古過ぎるという感じで…… なんか一から書いた方が早そう。
autotest-twitterでブヒる
最近とあるgemを書きながら付随するgemを書いてて優先すべきそれが中々進まない昨今ですこんばんは。
で、恥ずかしながらテストファーストってあんまりやったことなくて、今それを実践しながらの開発をしています。使っているツールはRSpecなんですが、コマンドひとつでテストできるとは言っても、今度はそのコマンドを実行するのがめんどくさい。ひたすら怠惰な生き物ですね。
そういう生き物たちにうってつけなのが当然あって、それのひとつにautotestってのがあります。しばらくはautotest + autotest-growlで開発してたんですが、家で使ってるマシンはMac、職場で使ってるマシンはUbuntuなんですね。Macには当然Growlをインストールしてるんですが、UbuntuとなるとGrowl以前の問題です。なので「Twitterにテストの結果をツイートして、あとは各OS向けのTwitterクライアントから通知すればいいんじゃね」と思い至りました。というわけでautotest-twitterです。まあ後からよく調べたらautotest-growlはLinuxにも対応してましたけどね。ちくしょう。
使い方
README読めと言いたいところですが、GitHubに置いてあるのがいい加減なのでアレです。何がアレってテストを書いていないところですよね。まあとりあえずテストの対象となるアプリケーションなりライブラリが置いてあるディレクトリに.rspecを作り:
--format nested --color
を、.autotestに:
require 'autotest-twitter' Autotest::Twitter.configure do |config| # ツイートするアカウントのアクセストークンを設定 config.consumer_key = 'your consumer key' config.consumer_secret = 'your consumer secret' config.oauth_token = 'your access token' config.oauth_token_secret = 'your access token secret' # ラベル。アプリケーションの名前とか config.label = 'any application' # テストの結果に応じてアイコンを変えられるので、そのアイコンが # 置いてあるディレクトリ # - missing.png: テスト自体がない場合のアイコン # - failed.png: テストに失敗した場合のアイコン # - pending.png: ペンディングが存在する場合のアイコン # - passed.png: テストに成功した場合のアイコン config.image_dir = 'path/to/icons' # テストの結果に応じたツイートの内容。$で始まるのは変数 # - $label: config.labelで設定した内容 # - $all: テストの全件数 # - $failed: 失敗したテストの件数 # - $pending: ペンディングしてるテストの件数 config.missing_messages = ['$labelのテストが存在しないよ'] config.failed_messages = ['$labelのテストに失敗したよ。$all件中$failed件がダメみたい'] config.pending_messages = ['$labelのテストに$pending件のペンディングがあるよ'] config.passed_messages = ['$labelのテストに成功したよ! $all件あったみたいだね'] end
を、Gemfileに:
source 'https://rubygems.org' gem 'autotest' gem 'autotest-fsevent' gem 'autotest-twitter', :git => 'git://github.com/takkkun/autotest-twitter.git'
こう。で:
$ bundle --path vendor/bundle
でもしてautotest諸々をインストール。後は:
$ bundle exec autotest
でテストを開始。後はファイルに変更があるたびにテストが走り、結果に応じてツイートされるはずです。config.image_dirを設定してればアイコンも変わります。ちなみにRSpecでしか試していませんし、とりあえず動いてるっぽいってことしか確認してないのであしからず。
ちなみに僕は @Shinobu_DD で試していました。まるでアイコンセットのような画像(TVアニメ偽物語の一部でしょうが)があったので。でもまあ「$labelで$pending件ペンディングがあるようじゃな。お前様の生き様が垣間見えるの。かか」とか打ってると頭抱えたくなりますし、いざブヒろうにも全然テンション上がらないのであんま向いてなかったようです。ていうか元々そういうのじゃないし!
まあでもブヒろうと思えばブヒれるので、テストがコケたらツンツンされたり、テストが通ったらデレデレされたりして、「今日も開発がんばりましゅううう」とか言ってればいいんじゃないですかね。
あとさっき思い付いたんですけど、ツイートするアカウントを自分のアカウントにし、passed.pngをいつも使ってるアイコン、failed.pngをとてつもなく恥ずかしいアイコンにすると面白いんじゃないかと思います。はやくテスト通さないとエラい思いをするハメになるというマゾい開発が出来ていいんじゃないかナーーー。
Exporterでエクスポートされる関数の挙動を変更する
ひょんなところにこんなモジュールがあRuby。
package Hoge; use strict; use warnings; use base qw/Exporter/; our @EXPORT = qw/hoge/; sub hoge { print "Hoge::hoge called\n"; } 1;
このHogeモジュールのhoge関数を呼び出すためにこんなコードを書Chrome。
use strict; use warnings; use Hoge; hoge; # Hoge::hoge called
いろんな事情が絡んで、Hoge::hoge関数の挙動を変更したEthernet。
use strict; use warnings; use Hoge; { no warnings 'redefine'; local *Hoge::hoge = sub { print "anon called" }; hoge; } hoge; # Hoge::hoge called
一度目のhogeでanon calledと出力したいのだけど、これはHoge::hoge calledと出力されちゃWindows*1。
Exporterを使うと、エクスポート対象となるパッケージ(main)に指定された関数がコピーされているので、元の関数(Hoge::hoge)を書き換えてももはや手遅Rails。
でもどの名前空間にあるhogeが呼ばれるか分からない時もある訳Debian。コンパイル時に名前を解決するためにエクスポートしているけど、実際呼び出す時は必ず書き換えるようにしたりする訳Delphi。
ということでこうすれば良いんじゃないNode.js。
package Hoge; use strict; use warnings; use base qw/Exporter/; our @EXPORT = qw/hoge/; sub hoge { _hoge(@_) } sub _hoge { print "Hoge::hoge called\n"; } 1;
use strict; use warnings; use Hoge; { no warnings 'redefine'; local *Hoge::_hoge = sub { print "anon called" }; hoge; # anon called } hoge; # Hoge::hoge called
間接的に呼べば問題ないようDarwin。関数呼び出しのオーバヘッド増えるけど見ないふLisp。
おっPython。
*1:悔しい