外側から読むRSpec 1.0.x (1)
RSpec 1.0 リリース記念ということで、ちょっと本腰を入れてソースをよんでみました。「外側から読む」というタイトルどおり、specコマンドから起動されるシーケンスに沿って読んでいこうと思います。
読む spec はこんなの。
# written in array_spec.rb describe Array, ",initialized as [:one, :two, :three]" do before do @array = [:one, :two, :three] end it "should not be empty" do @array.should_not be_empty end end
これを
spec -fs -c array_spec.rb
で実行します。
今回は起動されてから"describe"宣言が評価され、Behaviourオブジェクトが生成されるまでです。
specコマンド〜起動
specコマンドの中身はこんな感じ。
bin/spec
::Spec::Runner::CommandLine.run(ARGV, STDERR, STDOUT, true, true)
ということで、Runnner::CommandLine.run()を読みます。
主要なところはこんな感じ。
lib/spec/runner/command_line.rb
module Spec module Runner class CommandLine def self.run(argv, err, out, exit=true, warn_if_no_files=true) ... # 14行目 $behaviour_runner = OptionParser.new.create_behaviour_runner(argv, err, out, warn_if_no_files) ... # 17行目 $behaviour_runner.run(argv, exit) ... end end end end
14行目はいろいろオプションをパースして、結局 BehaviourRunnerのインスタンスを生成してるみたいです。TextMateなんかでカスタムランナーを作る場合はこのへんを詳しく見れば良いのかと。関係ないですが、レオさんに見せてもらったTextMateのランナーはかっこよすぎてやばいです。
で、17行目でBehaviourRunner#run()を呼び出しています。しかし仮引数名exitって。。
BehaviourRunner#run()
lib/spec/runner/behaviour_runnner.rb
module Spec module Runner class BehaviourRunner ... # 18行目 def run(paths, exit_when_done) unless paths.nil? # It's nil when running single specs with ruby ... # 22行目 load_specs(sorted_paths) end ... begin # 27行目 run_behaviours(behaviours) rescue Interrupt ensure @options.reporter.end end ... end ...
盛り上がってまいりました。22行目あたりがいろいろと面白そうです。
と思ったらload_pathは普通にrequireしてるだけの模様。ふむ。ということはdescribeなんかのテスティングDSLによって@behavioursに追加するのはObject(またはKernel)ヘのメソッドとして定義されるんだろう、と予想がつきますね。
いつものようにgrepします。
$ grep describe lib/**/*.rb | grep -w "def" lib/spec/dsl/behaviour.rb: def described_type lib/spec/expectations/handler.rb: def describe(matcher) lib/spec/runner/extensions/kernel.rb: def describe(*args, &block)
たぶんビンゴ。一番下のファイル名が怪しすぎます。
Kernel#describe()
改めて、盛り上がってまいりました。
まずは非本質的なところから
lib/spec/runner/extensions/kernel.rb
module Kernel # 26行目 alias :context :describe end
0.8.x厨大喜びwww
module Kernel def describe(*args, &block) ... # 24行目 register_behaviour(Spec::DSL::BehaviourFactory.create(*args, &block)) end ... end
で、Spec::DSL::BehaviourFactory.create()ですが、
lib/spec/dsl/behaviour_factory.rb
module Spec module DSL class BehaviourFactory class << self BEHAVIOUR_CLASSES = {:default => Spec::DSL::Behaviour} ... # 25行目 def create(*args, &block) opts = Hash === args.last ? args.last : {} ... # 36行目 return BEHAVIOUR_CLASSES[behaviour_type].new(*args, &block) end end end end end
最終的には36行目で、Spec::DSL::Behaviour.new()をしている流れになります。基本的にここまではdescribe()の引数がそのまま渡ってきていますね。
Spec::DSL::Behaviour#initialize()
続くSpec::DSL::Behaviour#initialize()が、まず第一のDSL、"describe"のキモになります。
module Spec module DSL class EvalModule < Module; end class Behaviour extend BehaviourCallbacks ... # 26行目 def initialize(*args, &behaviour_block) init_description(*args) init_eval_module before_eval eval_behaviour(&behaviour_block) end def init_description(*args) @description = Description.new(*args) end def init_eval_module @eval_module = EvalModule.new @eval_module.extend BehaviourEval::ModuleMethods @eval_module.include BehaviourEval::InstanceMethods @eval_module.behaviour = self @eval_module.description = @description end def eval_behaviour(&behaviour_block) @eval_module.class_eval(&behaviour_block) end protected def before_eval end ...
Behaviourが初期化されるときには、大きく3つの処理が走っていることがわかります。まずは、BehaviourのDescriptionの生成(init_description())。これは、最初の例でいうところの"Array, initialized as [:one, :two, :three]"を作ってるんでしょうね。あとで読む。
で、二つ目がinit_eval_module()でEvalModuleオブジェクトを生成しています。
init_eval_module()の中では生成されたインスタンスに対してBehaviourEval::ModuleMethodsをextendしています。いまは流し読みにとどめたいと思いますが、"it"についてはこのモジュールで定義されているようです。(see lib/spec/dsl/behaviour_eval.rb)
さらに、(現在は空の)コールバックであるbefore_eval()を呼び出したあと、実際に各々のExample(いわゆるテスト本体)を読込みに入ります。それがeval_behaviour(before)ですね。これは与えられたブロックdescribe do ... end をEvalModuleのクラスのコンテキストで評価します。そのなかで、先ほど触れたBehaviourEval::ModuleMethodsで定義されている"it"が呼ばれるのでしょうね。
とりあえず今回はこのへんで。次回はDescriptionの生成と、BehaviourEval::ModuleMethods、つまり"it"がどうなっているかを深追いしたいと思います。
まとめ
- 起動すると起動方法(rakeとかTextMateとかspecコマンドとか)ごとにレポーターを生成し、メイン処理に進む。
- describeメソッドはKernelモジュールに追加される。
- describeメソッドは、BehaviourFactoryを通じて(大抵は)Spec::DSL::Behaviourインスタンスを生成する。
- Behaviourインスタンス生成時にEvalModuleインスタンスが生成され、そのインスタンスがdescribe do ... end ブロックを評価する。
- EvalModuleは"it"メソッドを持っており、これが各々のExampleをつくって、Behaviourインスタンスに追加しているのではないかと予想。
-- 次回の中心はこのへん。