RubyのRipperをつかって、go testのExampleを実装した

引き続きgolangのtestingパッケージをRubyに翻訳したRgotを作っていて、 Testing、Benchmarkは実装したので、Example機能を実装してみた。

機能はgolangのtesting packageにあるExampleとおなじを目指した。

Example機能が何かというと、プログラムのサンプルコードを書いたときに、 「ここの時点ではこう出力されるよね」みたいなことをよくコメントで書くと思う。 Example機能は、Exampleを提示しつつ、このコメントが本当にあっているのかついでにテストしてくれる機能だ。

例えばRubyのArray#shiftのサンプルコードを書きたいとすると、

module FooTest
  def example_Array_shift
    a = [1, 2, 3]
    p a.shift
    p a.shift(2)
    p a.shift
    # Output:
    # 1
    # [2,3]
    # nil
  end
end

このように書けばよい。 これでもし、Kernel#pやArray#shiftの仕様が変わって出力が変わったとしても、テストが落ちてすぐ気づけるという仕組み。

例えば先程のコードは、配列の出力が微妙に違う(スペースの有無)のでテストが落ちる。

$ rgot foo_test.rb
=== RUN example_Array_shift
got:
1
[2, 3]
nil
want:
1
[2,3]
nil
FAIL

これをRuby実装するためには、どのメソッド内でどのコメントが書かれているか解析しなければならない。

そこで今回はRipperを使ってみた。

RipperはRubyに標準添付されているRubyのコードを解析するライブラリ。

RipperはCRubyの構文解析器のparse.yに直接埋め込まれているのでこれ以上正確なRubyパーサーはないだろう。

require 'ripper'
class Parser < Ripper
  attr_accessor :output
  def initialize(code)
    super
    @output = ""
  end

  def on_comment(comment)
    @output << comment
  end

  def on_kw(key)
    @output << key
  end
end
parser = Parser.new(code)
parser.parse
parser.output

このようにRipper classを継承してon_xxxとイベントドリブンな書き方でRubyのコードを解析できる。

今回はメソッドを定義したときにメソッド名が取得できるon_defと、コメントを見つけた時取得できるon_commentと、予約語を見つけたとき取得できるon_kwを使った。

on_kwdefキーワードを見つけたらコメント取得開始、on_commentでコメントをためて、on_defメソッドの定義が完了したらメソッド名と関連付ける。

さらに、Module#instance_methodで取得しておいたUnboundMethodオブジェクトからメソッドが定義されているファイル名を取り出して、 ファイルを丸ごと読み込んで、先ほどのRipperにかけて、コメントを取得しておいて、UnboundMethodからメソッドを実行させて出力をStringIOでキャプチャーして比較すれば出来上がり。

詳しくはコードでどうぞ。

github.com

もしくは、

$ gem install rgot

で、すぐに試せるようになっています。

Example機能はv0.0.3から。

本当はgodocみたいに、rdocにExampleを表示させてとかできればいいんだけど、 それは今後の課題ということで。