guard-rubocop 1.5.0 をリリースした

guard-rubocop 1.5.0 をリリースした。RuboCop の GitHub Discussions に投稿されたのがはじまり。

実質的にリリースした機能は以下。

github.com

RuboCop 実行に対するコマンドが rubocop 固定だったものを、cmd オプションを使うことで bin スタブの bin/rubocop に外から切り替えたりできるようにしたいといったもので、bundle exec spring binstub rubocop なんかで Spring を有効にした状態で実行可能にしたいようだった。

そのほかにもコマンドの差し替えを可能にすることで、以下のようなパイプを使った結果の RuboCop 実行など、利用者にとって有用そうな機能だった。

guard :rubocop, cmd: "git ls-files -m | xargs ls -1 2>/dev/null | grep '\.rb$' | xargs bin/rubocop" do
  watch(%r{.+\.rb$})
  watch(%r{(?:.+/)?\.rubocop(?:_todo)?\.yml$}) { |m| File.dirname(m[0]) }
end

余談ですが、guard-rubocop は yujinakayama さんが開発していたものを RuboCop HQ に移管して、コアの方でメンテナンスできるようにしていた流れからの、今回のリリースとなっています。ユーザーの方はアップデートしてみてください。

Rails/OSSパッチ会 2021年8月

Rails/OSS パッチ会だった。オンライン開催になって、それなりに回数も経ってきたところ。

自分は仕事で bin/rails db:schema:load の実行が遅いのが気にかかっていて、結構前に手元で試していた高速化のアイデアについて話したりしていた。

bin/rails db:schema:load は内部的に db/schema.rb を load しているだけなのでリニアに実行される。

そこで、ひとつの高速化アイデアとしてのキーワードは「並列化」。db/schema.rb のうち create_table do ... end, add_foreign_key のコードをそれぞれコレクションにして、1. create_table のコレクションを (Parallel gem なんかで) パラレル実行、2. add_foreign_key のコレクションをパラレル実行、最後に ActiveRecord::Schema.define を実行するとかどうだろうというものだった。

手元にあるアプリケーションへのモンキーパッチによる PoC だと、前段のコレクションに詰める仕込み部分で db/schema.rb を RuboCop を使って AST プログラミングするという異能を使っている (のでこれで upstream に送ることはない) が、そこはさすがに method_missing とか使うとかした方が良いだろうなどコメントをもらいながら話していた。

さらにその先の結論としては、MySQL なんかでの bin/rails db:schema:load はそんなに遅くないらしく、どうも Oracle adapter のスキーマ処理まわりにボトルネックがありそうで、そちらを解決した方が良さそうというオチだった。まあ、そうですよね。

次回の Rails/OSS パッチ会は 2021年9月30日(木) 17:00-19:00 です。

RubyKaigi Takeout 2021 に登壇します

RubyKaigi Takeout 2021 に『RuboCop in 2021: Stable and Beyond』というタイトルで登壇します。

私の登壇は1日目である 2020年9月9日(木) の 11:30-11:55 です。

rubykaigi.org

前回の RubyKaigi Takeout 2020 での講演が RuboCop 1.0 リリース前の道のりだったことに対して、今回は RuboCop 1.0 リリース後の動きと今後に向けたトピックでまとめています (絶賛準備中) 。開発の舞台裏についても少し取りあげる予定なので、人によっては OSS 開発環境の参考になったりするかもしれません。よければテイクアウトしに来てください。

DatabaseCleanerを使ったテストを倍速にした設定の見直し

はじめに

テストのデータベースクリーニングをする gem としては、DatabaseCleaner と DatabaseRewinder がメジャーどころとしてあります。

github.com

github.com

今回は DatabaseCleaner の話です。

クリーニング戦略を切り替えて計測する

DatabaseCleaner では transaction, deletion, truncation といったデータベースのクリーニング戦略があり、E2E での非同期 UI 系のテストでは transaction 以外を選択することになると思います。

早速結論ですが、非同期 UI 系のスローテストについて truncation から deletion に変えたところ 60 分の CI が 30 分になりました。

     if example.metadata[:javascript]
-      DatabaseCleaner.strategy = :truncation, {except: MASTERS}
+      DatabaseCleaner.strategy = :deletion, {except: MASTERS}
     else
       DatabaseCleaner.strategy = :transaction
     end

RDBMS によると思うのですが、truncation は (deletion に比して) 概ね固定された実行効率であることに対して、deletion はデータ量に依存したり変動要素が多いようです。 そしてこれらは、どちらが優れているというものではなくコンテキストによって選択されることになります。

詳しくは DatabaseCleaner から参照されている Stack Overflow の記事を参照してください。

stackoverflow.com

テスト時では閾値とを INSERT するデータ量を小さくするなどの定石があったりするため、テストの作りや RDBMS によって deletion の方が速いことがあるというのはなるほどというものでした。長年開発を続けてスキーマやデータが変遷しているようなアプリケーションは一度計測し直してみて良いかもしれません。

まとめ

実際のところ、どちらが速いというよりはコンテキストによるようなので、テストが遅いなあと思ったら、戦略を切り替えてみるとテストが速くなることがあるかもしれません (し遅くなるかもしれません) 。CI の結果を計測してみましょう。

マジかというものですが、事実のみを示す数字によるとデータベースクリーニングで 30 分くらい余計にかかっていたわけでした。

最後に過去発表した関連スライドのページをのせておきます。

www.slideshare.net

今日はここまでです。ハックを続けましょう。

社内向けに「Rubyコミュニティの歩き方勉強会」を行った

2年くらい前に Rails Developers Meetup 2019 で登壇したときのスライドを使って社内向けに開催した。

コロナ禍でオフラインでのコミュニティ活動というものが休止の中で、オンラインでのあり方を再度模索するきっかけになっていればと思う。

いっぱい話した気がするけれど、「アウトプットは人とのつながりを生む」という話だったと ima1zumi メンバーが日報でワンフレーズにまとめてくれた (さすがのまとめ力!) 。要はそういうことを今回伝えたかったんだなという 2021年春のお気持ちだった。

参加メンバーからは、アウトプットへの機運が高まったという反応をもらえたので、アウトプットによって新たなアウトプットを生むきっかけになったというあたりに、開催の意義があったんじゃないかなと思う。

私は引き続き日々のパッチがアウトプットの中心です。月一で Rails/OSS パッチ会というものを勤務先からオープンに主催しているので、機会があればオンラインミートアップでお会いしましょう。

RuboCopファミリーと周辺の更新 (GW 2021編)

GW に更新のあった RuboCop 周辺のアップデートについてざっくりまとめておく。

Parser 3.0.1.1

github.com

RuboCop が依存する Ruby構文解析の Gem で、Ruby 3.0 で入った endless method definition のバグ修正と Ruby 3.1 (dev) のサポート追加をしている。

Ruby 3.0 で導入された endless method definition は以下のように = で終わる writer メソッドは定義できない。

% ruby -cve 'def foo=(foo) = @foo = foo'
ruby 3.0.1p64 (2021-04-05 revision 0fb782ee38) [x86_64-darwin19]
-e:1: setter method cannot be defined in an endless method definition
def foo=(foo) = @foo = foo

それについて method_name.end_with?('=') でマッチさせていたので、受け入れられるべき比較演算子 (e.g. ==) となるメソッド定義までエラーにしていたという Parser gem のバグだった。

Ruby 3.1 (dev) のサポートについては、パターンマッチの新構文となる pin オペレータをサポートするようになった。大元のサンプルから持ってくると ^ を使った式をこんな感じでパターンマッチ中で取れるようになります。夢がありそうな拡張。

Prime.each_cons(2).lazy.find_all { _1 in [n, ^(n + 2)] }.take(3).to_a
#=> [[3, 5], [5, 7], [11, 13]]

RuboCop 1.4

github.com

Ruby 3.1 (dev) をサポートしました。また、Bundler/GemVersion cop と Layout/SingleLineBlockChain cop がデフォルト無効で追加されています。

Bundler/GemVersion は Gemfile に gem 指定するときにバージョンを必須にする cop だけれど、理由がなければバージョン指定するべきではないという見解でデフォルト無効です。個人的にはデフォルト無効ならまあ (コアに入れて) いいか。。。という気持ちです。 Layout/SingleLineBlockChain cop は、example.select { |item| item.cond? }.join('-') のようなチェーンを許可しない cop で、ふつうに厳しすぎるのでデフォルト無効は妥当だと思って見ていました。余談だけれど blockchain という用語は暗号資産を連想したという bbatsov のコメントがおもしろかった。

あと、RuboCop AST とそれが依存する Parser の依存バージョンを上げているので、bundle update --conservative rubocop でアップグレードしているようであれば、bundle update --conservative rubocop rubocop-ast parser あたりの依存 gem 指定をしないと 1.14 に上がらないかもしれません。

RuboCop AST 1.5

github.com

Ruby 3.1 (dev) をサポートしています。

RuboCop Performance 1.11.3

github.com

Performance/MapCompact cop の不具合修正をして v1.11.3 まで上がりました。あと互換のない compact.mapfilter_map に手動で置換されようとしたケースを rails/rails で見かけたので、それは検出しない旨をドキュメントとテストに追記しています。

RuboCop Rails 2.10

github.com

RuboCop Rails は半年くらいぶりのリリース。古めのイシューなんかもさらって、フィードバックにもとづいた cop の安全性などの見直しを入れており、2.9 系を調整した better バージョンな感じになっていると思います。RuboCop Performance 1.11 系と同様に、Ruby 2.4 のサポートを打ち切ったのと、今後のメジャーアップデートに向けて RuboCop 1.7.0 以上への依存に更新しています。古めのバージョンを使っているようであれば、あわせてアップデートが必要です。

ご活用ください。

RuboCop Performance 1.11 をリリースした

RuboCop Performance 1.11 をリリースした。主な変更点は以下。

  • Ruby 2.4 のサポートを終了した
  • Ruby 2.7 で追加された Enumerable#filter_map への Cop を追加した

github.com

前者はそのままなので、後者について記す。

Enumerable#filter_map に対応づけられるのは、以下の2つの Cop となる。

これは当初 Performance/FilterMap cop として提案していたものだが、対象となる bad ケースのコンテキストや good ケースとの互換性に違いがあるため、別々の Cop として分割した。

Performance/SelectMap cop

github.com

この Cop は select.map のチェーンへのケースを filter_map に置き換えることを提示する。

# bad
ary.select(&:foo).map(&:bar)
ary.filter(&:foo).map(&:bar)

# good
ary.filter_map { |o| o.bar if o.foo }

振る舞いに互換性はあるが、good ケースの可読性について賛否があるのと、複雑なブロック処理については自動修正が困難だと思い自動修正を提供しておらず、手修正も大変そうということからデフォルトで無効にしている。

Performance/MapCompact cop

github.com

この Cop は map.compact のチェーンへのケースを filter_map に置き換える。このケースは good ケースになることでコードもシンプルになると思う。

# bad
ary.map(&:foo).compact
ary.collect(&:foo).compact

# good
ary.filter_map(&:foo)

以下のいずれのケースも nil を除外するが、filter_map は加えて false も除外する。このように compactfilter_map は振る舞いに互換性がないため、SafeAutoCorrect: false (rubocop -A で適用) としている。

[true, false, nil].compact              #=> [true, false]
[true, false, nil].filter_map(&:itself) #=> [true]

なお、Enumerable#filter_map! というメソッドは存在しないので、以下のケースは受け入れている。

ary.map(&:foo).compact!

Performance/SelectMap cop は好みが分かれるところだと思うが、Performance/MapCompact cop は適用可能であれば適用しておくとコードとしてもシンプルになると思う (ただし振る舞いの違いに気をつけて適材適所で) 。

いつもの新規 Cop のようにデフォルトで pending ステータスなので、NewCops: enable にしているわけでなければ、以下のように有効化が必要となる。

# .rubocop.yml
Performance/MapCompact:
  Enabled: true

今日はここまでです。ハックを続けましょう。