RSpec の Progress Bar をカスタマイズする

  • 4
    いいね
  • 0
    コメント

はじめに

RSpec の Formatter をデフォルトのままにしている人は以下の様に表示されると思います。

image

. は成功したテスト
* は pending したテスト
F は失敗したテスト

で、これ以降に失敗したテストの詳細が出るわけですが、、、

いくらなんでも寂しすぎる!!:cry:

という事でもっと素敵な Progress Bar を表示させるべく RSpec の Formatter を書き換えます。
変更は以下のステップでやっていきます。

  1. 色を付ける
  2. Fuubar を入れる
  3. 自作する

ではやってみます。

色を付ける

これはスーパー簡単で、--color オプションをつけて実行するだけです。

image

これだけでもだいぶ見やすい。
毎回使うオプションは .rspec に定義してあげるといいです。

.rspec
--color

image

Fuubar を入れる

Fuubar

簡素だった Progress Bar をいい感じにしてくれる gem です。
実際に fuubar を入れて実行してみると

Gemfile
gem 'fuubar'
% bin/rspec spec/ --format Fuubar

現在の進捗をリアルタイム表示してくれて

スクリーンショット 2016-12-19 19.54.40.png

かかっている時間も表示されています。

Pending しているテストがあると黄色になって、

スクリーンショット 2016-12-19 19.54.51.png

失敗すると赤くなります。

スクリーンショット 2016-12-19 19.55.05.png

手元でやってもらうとわかりますが、失敗の詳細が上部に表示されるなど、他にもわかりやすくなっています。
素晴らしい!

カスタマイズもできて、例えば以下の様に RSpec の config に書くと

rails_helper.rb
config.fuubar_progress_bar_options = {format: 'Hogehoge: [%b%i] %a'}

こうなります。

image

いいですね。Fuubar。

自作する

もう Fuubar で十分すぎる気がしますが、ここはぐっとこらえて自作していきます。

実行結果や、失敗詳細などいろいろとカスタマイズすることができますが、今回は Fuubar と同様に実行時に出力される Progress Bar をカスタマイズします。

Progress Bar のインタフェースは以下になります。

rspec/core/formatters/progress_formatter.rb
class ProgressFormatter < BaseTextFormatter
  Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump

  def example_passed(_notification)
    output.print ConsoleCodes.wrap('.', :success)
  end

  def example_pending(_notification)
    output.print ConsoleCodes.wrap('*', :pending)
  end

  def example_failed(_notification)
    output.print ConsoleCodes.wrap('F', :failure)
  end

  def start_dump(_notification)
    output.puts
  end
end
  • example_passed はテストが成功した際
  • example_pending は pending されたテストが呼ばれた際
  • example_passed はテストが失敗した際
  • start_dump は全テストが完了して、テスト結果のメッセージが表示される前

に呼ばれます。
こいつらを override していけば Progress Bar を自作することができます。

例えば、以下のようなファイルを用意します。

custom_bar.rb
require 'rspec/core'
require 'rspec/core/formatters/base_text_formatter'

class CustomBar < RSpec::Core::Formatters::BaseTextFormatter
  RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump

  def example_passed(_notification)
    output.print RSpec::Core::Formatters::ConsoleCodes.wrap('Success!', :success)
  end

  def example_pending(_notification)
    output.print RSpec::Core::Formatters::ConsoleCodes.wrap('Pending!', :pending)
  end

  def example_failed(_notification)
    output.print RSpec::Core::Formatters::ConsoleCodes.wrap('Failure!', :failure)
  end

  def start_dump(_notification)
    output.puts
    output.puts 'テスト終了!!'
  end
end

自作した Formatter は、実行時に --require オプションで指定して読み込んで、--format オプションで指定してあげます。

% bin/rspec spec/ --require /Users/Shohei/Desktop/custom_bar.rb --format CustomBar

image

できた!見づらい!

でもまあ、あとはお絵かきを頑張るだけです。

....

頑張った汚いコードがこちらです。

custom_bar.rb
require 'rspec/core'
require 'rspec/core/formatters/base_text_formatter'

class CustomBar < RSpec::Core::Formatters::BaseTextFormatter
  RSpec::Core::Formatters.register self, :example_passed, :example_pending, :example_failed, :start_dump

  attr_accessor :progress_count, :success_count, :failed_count, :max_length

  def initialize(output)
    super output

    self.progress_count = 0
    self.success_count = 0
    self.failed_count = 0
    self.max_length = IO.console.winsize.last
  end

  def increment
    self.progress_count += 1
  end

  def increment_success_count
    self.success_count += 1
  end

  def increment_failed_count
    self.failed_count += 1
  end

  def example_passed(_notification)
    increment
    increment_success_count

    output.print RSpec::Core::Formatters::ConsoleCodes.wrap(hashiru, :success)

    back_pointer(hashiru)
  end

  def example_pending(_notification)
    increment

    output.print RSpec::Core::Formatters::ConsoleCodes.wrap(tomaru, :pending)

    back_pointer(tomaru)
  end

  def example_failed(_notification)
    increment
    increment_failed_count

    output.print RSpec::Core::Formatters::ConsoleCodes.wrap(korobu, :failure)

    back_pointer(korobu)
  end

  def start_dump(_notification)
    if self.failed_count.zero?
      output.print RSpec::Core::Formatters::ConsoleCodes.wrap(goal, :success)
    else
      output.print RSpec::Core::Formatters::ConsoleCodes.wrap(failed, :failure)
    end
  end

  def now_position
    (self.max_length / @example_count.to_f * self.success_count).to_i
  end

  def back_pointer(str)
    strs = str.split("\n")
    line_num = strs.count

    output.print "\e[#{line_num}A"
  end

  def goal
    <<-"EOF"
#{' ' * (now_position - 8)}   |>
#{' ' * (now_position - 8)} ○ノ|
#{' ' * (now_position - 7)}\/| 
#{'_' * (now_position - 7)}\/ >__
    EOF
  end

  def failed
    <<-"EOF"
#{adjust_str(' ', '', 7, '  ')}
#{adjust_str(' ', '', 7, '|>')}
#{adjust_str('_', ' _| ̄|○_', 8, '|_')}
    EOF
  end

  def hashiru
    effect_line = self.progress_count % 2 == 0 ? '==' : '  '

    <<-"EOF"
#{adjust_str(' ', effect_line + ' ヘ○ノ', 7, '  ')}
#{adjust_str(' ', effect_line + '   ノ ', 7, '|>')}
#{adjust_str('_', effect_line + '/>_', 9, '|_')}
    EOF
  end

  def tomaru
    <<-"EOF"
#{adjust_str(' ', ' ヘ○ヘ', 7, '')}
#{adjust_str(' ', '  |∧ ', 7, '|>')}
#{adjust_str('_', '/__', 7, '|_')}
    EOF
  end

  def korobu
    <<-"EOF"
#{adjust_str(' ', '', 7, '')}
#{adjust_str(' ', '', 7, '|>')}
#{adjust_str('_', ' ヽ_○ノ ', 9, '|_')}
    EOF
  end

  def adjust_str(padstr, printstr, rpad, endstr)
    rpad_length = self.max_length - (printstr.length + now_position + rpad)
    if rpad_length < 0
      "#{padstr * (now_position + rpad_length)}#{printstr}#{endstr}"
    else
      "#{padstr * now_position}#{printstr}#{padstr * rpad_length}#{endstr}"
    end
  end
end

旗が端っこに置いてあるので、
テストが成功すると走って、失敗すると転んで、pending すると止まって、見事全テストが成功すると(pending は OK)旗を GET、失敗すると旗を GET できません。

ちょっと何言ってるかわからないと思うので、実行してみます。

走ります

スクリーンショット 2016-12-19 21.22.23.png

走ります

スクリーンショット 2016-12-19 20.55.05.png

止まります

スクリーンショット 2016-12-19 21.14.43.png

再び走ります

スクリーンショット 2016-12-19 20.55.20.png

転びます

スクリーンショット 2016-12-19 21.15.09.png

走ります

スクリーンショット 2016-12-19 21.19.29.png

スクリーンショット 2016-12-19 21.20.19.png

はい、失敗

スクリーンショット 2016-12-19 21.24.47.png

できたー。
ちなみにテストが成功したバージョン

スクリーンショット 2016-12-19 21.28.09.png

力技でやっているので、あんまり詳しいコードの説明はしないですが、
IO.console.winsize.last で console の幅を取得して、@example_count に合計テスト数を持っているので、success_count を保持しておいて、now_position で走った位置を計算しています。

back_pointer(str) は pointer の位置を元に戻して、同じ場所に出力する様にしています。
あとはゴリゴリやっているだけです。

もっと豪勢な絵になる予定だったのに、、、
あと、終わったら音出すとかもいいなあ。

おわりに

どうでしたか?ぼくは Fuubar に速攻戻しますが、みなさんも是非自作して見て下さい :smiley:(それにしても AA って作るのめちゃくちゃ難しいですね...)

この投稿は Sansan Advent Calendar 201619日目の記事です。