外側から読む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インスタンスに追加しているのではないかと予想。

-- 次回の中心はこのへん。