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

# はじめに

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

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/8cc4ba45-1204-4f03-02b0-5896b3e663ad.png)

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

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

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

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

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

ではやってみます。

# 色を付ける

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

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/d6c60189-e84a-29eb-e208-1c09e09d07c9.png)

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

```:.rspec
--color
```

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/49e3030f-49e8-a9da-9816-513f5998de11.png)

# Fuubar を入れる

[Fuubar](https://github.com/thekompanee/fuubar)

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

```:Gemfile
gem 'fuubar'
```

```
% bin/rspec spec/ --format Fuubar
```

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

<img width="1270" alt="スクリーンショット 2016-12-19 19.54.40.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/489b035e-7d40-4ff3-03a6-449dc4529e4e.png">

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

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

<img width="1271" alt="スクリーンショット 2016-12-19 19.54.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/bba78ffb-7e98-1d20-11fd-c1fe460ef18e.png">

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

<img width="1279" alt="スクリーンショット 2016-12-19 19.55.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/94d9a99e-fe8f-fd5b-28de-8d96aa87d0ff.png">

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

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

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

こうなります。

![image](https://qiita-image-store.s3.amazonaws.com/0/41416/062e956f-da6c-1c9b-d94f-9d4663875bb5.png)

いいですね。Fuubar。

# 自作する

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

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

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

```rb: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 を自作することができます。

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

```rb: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](https://qiita-image-store.s3.amazonaws.com/0/41416/9ecfd4c3-0af6-361a-b694-5aaf3ae55a50.png)

できた！見づらい！

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

....

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

```rb: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 できません。

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

走ります

<img width="1241" alt="スクリーンショット 2016-12-19 21.22.23.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/00694d1b-ffed-aca7-6afa-3f23106867ef.png">

走ります

<img width="1246" alt="スクリーンショット 2016-12-19 20.55.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/889740d9-28d7-380a-ef96-d70ec9a4832d.png">

止まります

<img width="1235" alt="スクリーンショット 2016-12-19 21.14.43.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/77ee09bf-9b8a-cb7f-1c97-eb58228aae5f.png">

再び走ります

<img width="1244" alt="スクリーンショット 2016-12-19 20.55.20.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/4257f92e-59fc-8045-4660-ec0ae839da10.png">

転びます

<img width="1247" alt="スクリーンショット 2016-12-19 21.15.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/c30a9f1a-f8f3-bdf2-d6f5-bd48722b070c.png">

走ります

<img width="1246" alt="スクリーンショット 2016-12-19 21.19.29.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/570c2767-f7c1-b676-967c-d9770ace2474.png">

<img width="1236" alt="スクリーンショット 2016-12-19 21.20.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/28dda818-17da-5443-3c39-e0c25396324b.png">

はい、失敗

<img width="1240" alt="スクリーンショット 2016-12-19 21.24.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/441083e3-e43f-0918-6e58-4450836e5f1e.png">

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

<img width="1268" alt="スクリーンショット 2016-12-19 21.28.09.png" src="https://qiita-image-store.s3.amazonaws.com/0/41416/0b49e559-583a-e265-c6be-7b19c07831a9.png">

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

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

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

# おわりに

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