しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

Dangerプラグインを自作する

コードレビューの補助に、Dangerを導入しているチームも多いと思います。
プラグインも簡単に作れるので、半分覚え書きですが、解説したいと思います。

プロジェクトの生成

ローカルにdangerが入っていない場合は、事前に入れておきます。

sudo gem install danger

最初に、テンプレートからプロジェクトを生成します。 命名は、snake caseのプラグインが多いようです。

わかりやすいように、sample_pluginという名前で進めます。

danger plugins create sample_plugin

この場合、danger-sample_pluginというディレクトリが生成されます。

ファイルの構成

基本的に触る部分は、プラグインのソース(lib以下)とテストコード(spec以下)です。

プラグインのソースはlib/sample_plugin/plugin.rbが中心です。
Dangerfileから触るプロパティなどは、ここに記述します。

テストコード(RSpec)はspec/sample_plugin_spec.rbに記述していきます。

テストコードは書かなくても動作には問題ないですが、Dangerプラグインという性質上、ローカルでのデバッグがしづらいので、テストを書きながら開発するのを推奨します。

プラグイン本体の書き方

最初の状態ではこのようになっています。

module Danger
  class DangerHoge < Plugin
    attr_accessor :my_attribute

    def warn_on_mondays
      warn 'Trying to merge code on a Monday' if Date.today.wday == 1
    end
  end
end

attr_accessormy_attributeというアクセサが公開されています。 Dangerfileから指定するプロパティはここで公開します。

メソッドはwarn_on_mondaysが公開されています。

Dangerfileからは、

sample_plugin.my_attribute = ["hoge"]
sample_plugin.warn_on_mondays

のように利用することが出来ます。

RSpecの書き方

spec/sample_plugin_spec.rbは、最初はこのような内容です。

require File.expand_path("../spec_helper", __FILE__)

module Danger
  describe Danger::DangerHoge do
    # ...

    describe "with Dangerfile" do
      before do
        # それぞれのテストケースの前に呼ばれる共通処理
        @dangerfile = testing_dangerfile
        @my_plugin = @dangerfile.hoge
      end

      it "Warns on a monday" do
        # テストケース
      end
    end
  end
end

itブロック一つひとつがテストケースにあたり、それぞれの実行前にbeforeブロックに書かれた内容が実行されます。

各ケースは、よくある単純比較の場合は、expect(<実際の値>).to eq 期待値のように書きます。

expect(@dangerfile.status_report[:messages]).to eq ["Hello hoge!"]

実際の例

試しに、ダンプされたファイル出力をパースして、その中から警告を抽出するプラグインを作ってみます。

分かりづらいかもしれませんが、ご容赦ください。

テストの記述

先にテストを書いておくTDD的な進め方をすると、検証が楽になります。

テスト対象のファイルを事前に書き出しておいて、specのbeforeでそれを読み込む処理を書きます。
静的ファイルは、spec/fixturesに配置します。

describe "with Dangerfile" do
  # ...

  context "enable linker warnings" do
    before do
      @xcode_warnings.show_build_warnings = true
      @sample_plugin.analyze_file "spec/fixtures/log_with_4_errors"
    end
  end
end

プラグインにshow_build_warningsanalyze_fileはまだ実装していませんが、TDDの考え方では最初に失敗するようなテストを記述するので、問題ありません。

テストケースを実装していきます。
今回は、「警告が4つ出ること」と、「メッセージが表示される」ことを期待する結果とします。

describe "with Dangerfile" do
  context "enable linker warnings" do
    before do
      @xcode_warnings.show_build_warnings = true
      @sample_plugin.analyze_file "spec/fixtures/log_with_4_errors"
    end

    it "warn 4 lines" do
      expect(@dangerfile.status_report[:warnings]).to eq [
        "警告文1",
        "警告文2",
        "警告文3",
        "警告文4"
      ]
    end
    it "put a single message" do
      expect(@dangerfile.status_report[:messages]).to eq [
        "メッセージ"
      ]
    end
  end
end

それぞれ警告とメッセージは、@dangerfile.status_report[:warnings]@dangerfile.status_report[:messages]に入ります。

これらのテストケースが通るように、プラグインの実装をします。

プラグインの実装

show_build_warningsanalyze_fileを実装していきます。
パーサーの実装は省略しますが、テキストをパースして警告を抽出するクラスです。

module Danger
  class SamplePlugin < Plugin
    attr_accessor :show_build_warnings

    # rubocop:disable Lint/DuplicateMethods
    def show_build_warnings
      @show_build_warnings.nil? ? true : @show_build_warnings
    end

    def analyze(log_text)
      parser = LogParser.new
      parser.show_build_warnings = show_build_warnings

      parsed = parser.parse_warnings(log_text)
      parsed.each do |warning|
        warn MessageFormatter.new.format(warning) # 警告文
      end
      message "メッセージ" unless parsed.empty?
    end

    def analyze_file(file_path)
      File.open(file_path) do |f|
        analyze(f.read)
      end
    rescue Errno::ENOENT, Errno::EACCES => e
      puts "Couldn't open the file: #{e}"
    end
  end
end

attr_accessorshow_build_warningsを定義します。

def show_build_warnings
  @show_build_warnings.nil? ? true : @show_build_warnings
end

とすることで、デフォルトでtrueにすることが出来ます。

次にanalyze_file(analyze)の中でテキストをパースして、警告とメッセージを出す処理を書きます。
警告とメッセージの出し方はDangerfileと同じで、warnmessageです。

先程書いたテストに対して実際に、テストが通るかを、

bundle exec rake spec  

で確認しながら実装を進めていきます。(bundlerも入れておいてください)

Travis CIの設定

.travis.ymlが若干古いので、更新が必要です。 一例ですがこんな感じです。

language: ruby
cache:
  directories:
    - bundle

rvm:
  - 2.3.1
  - 2.4.5
  - 2.5.3

branches:
  only:
  - master

script:
    - bundle exec rake spec

gemspecを書けばgemとして登録することも出来ます。