Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

Rails アプリの不要なテストデータをガっと消した🚮

こんにちは、id:Pocke です。マネーフォワードではクラウド会計Plusというプロダクトの開発と、RBS という Ruby の静的型のためのライブラリの開発の両方を行っています。今回の記事では、クラウド会計Plusの開発の話を書こうと思います。

TL;DR

  • spec/fixtures/下に想像以上に多くのファイルがあることに気がついた
  • inotify を使って不要なファイルを検出し、削除した

問題の発見

クラウド会計Plusの開発業務として、私は最近不要なコードの削除に取り組んでいます。その一環としてリポジトリの状況を調査していました。その中で以下のコードを用いて「拡張子ごとのファイル数」を計測しました。

$ git ls-files -z | ruby -e 'pp ARGF.read.split("\x0").map{File.extname(_1)}.tally.sort_by{-_2}'

実際の実行結果は共有できませんが、なんと.csvファイルが3番目に多いファイルの拡張子で、600以上の.csvファイルがあることがわかりました。その内訳を簡単に見てみると、spec/fixtures/下のファイルが多数を占めていました。

当初予想していたよりもかなり多くの.csvファイルがあったため、驚きました。 クラウド会計Plusではテスト時に DB に投入するデータや、CSVインポート・エクスポート機能のテストのために CSV ファイルを多用しています。それにしてもこれはファイルが多すぎるように感じます。

そこで「spec/fixtures/下に、すでに使われなくなったファイルが残っているのでは」という仮説を立てました。

対応方法

使われていないファイルは、「存在するファイルの一覧」から「実際に使われたファイルの一覧」を引き算すれば見つけられます。

今回は実際に使われたファイルを検出するために、inotify を使いました。1 inotify はファイルシステムのイベントを監視するための API です。Ruby での使用例としてはguard gem が有名です。2

今回はguard gem の内部でも使われている、rb-inotify gem を使いました。3

検出のためのコード

まず、spec/rails_helper.rbに次のコードを追加しました。4

require 'rb-inotify'
notifier = INotify::Notifier.new
result_path = Rails.root.join('tmp', "accessed-files")
Rails.root.glob('spec/fixtures/**/*').each do |file|
  notifier.watch(file.to_s, :access) do
    result_path.write(file.to_s + "\n", mode: 'a')
  end
end
Thread.new { notifier.run }

このコードではspec/fixtures/下のファイルの:accessイベントを監視しています。:accessイベントは、ファイルがread(2)で読み込まれるなど、何らかの形で参照されたときに発生します。そのためこのイベントを監視することで、テスト中に利用されたファイルを検出できます。

:accessイベントのハンドラでは、アクセスされたファイルのパスをtmp/accessed-filesに追記しています。 テストが完了すると、そのテスト内でアクセスされたspec/fixtures/下のファイルがすべてこのファイルに記録されています。

テストの実行

今回はこのコードを CircleCI 上で実行し、artifact として結果を保存しました。

CircleCI 上ではテストを並列実行しているため、実際には次のような設定を.circleci/config.ymlに追加しました。

# テスト実行 job の最後の step
- run:
  name: Save accessed files
  command: |
    mkdir accessed-files/
    cp tmp/accessed-files accessed-files/${CIRCLE_NODE_INDEX}
- persist_to_workspace:
    root: .
    paths:
      - accessed-files/

# テスト実行 job の後に実行する job
- attach_workspace:
    at: .
- run:
    name: Merge accessed files
    command: |
      cat accessed-files/* > tmp/accessed-files
- store_artifacts:
    path: ./tmp/accessed-files

これによりアクセスされたspec/fixtures/下のファイルのリストを、CircleCI の artifact として保存できます。

不要なファイルの削除

ここまで来たらあとは不要なファイルを計算して、それを消すだけです。

不要なファイルは以下のスクリプトで計算しました。

# CircleCI からダウンロードした artifact を、`tmp/accessed-fixtures`に保存
$ cat > tmp/accessed-fixtures

# 不要なファイルの計算
$ ruby -e 'puts `git ls-files spec/fixtures/`.chomp.split("\n") - File.read("tmp/accessed-fixtures").chomp.split("\n")'

なおtmp/accessed-fixturesには絶対パスでファイルパスが記録されていたため、別途手作業で相対パスに変換しました。

上記のスクリプトで対象ファイルを確認後、次のコマンドで削除しました。

ruby -e '
  paths=`git ls-files spec/fixtures/`.chomp.split("\n") - File.read("tmp/opened-fixtures").chomp.split("\n")
  paths.each{File.unlink _1}
'

結果

テスト内でアクセスされていないファイルを洗い出した結果、案の定、多くのファイルが不要であることがわかりました。 そのため、それらをすべて削除する pull request を作成しました。

削除を行った pull request のヘッダのスクリーンショット

今回の作業により、300個以上、30,000行以上のファイルを削除できました。なかなか良い結果と言えるのではないでしょうか。

この pull request の CI が一発で通ったときはかなり爽快でした。

最後に

inotify を使った不要なテスト用ファイルの削除事例を紹介しました。

このようなファイルは消し忘れやすく、また消し忘れているものを区別しづらいため、不要なファイルが残りがちだと思います。 今回適用した手法は Rails アプリに限らず広く使えると思いますので、ぜひ試していただければ幸いです。


  1. https://man7.org/linux/man-pages/man7/inotify.7.html
  2. https://github.com/guard/guard
  3. https://github.com/guard/rb-inotify
  4. inotify で監視をするのはテストと別プロセスでも構わないのですが、お手軽に実装するためにテストのプロセス内で実行しています。