こんにちは、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 を作成しました。
今回の作業により、300個以上、30,000行以上のファイルを削除できました。なかなか良い結果と言えるのではないでしょうか。
この pull request の CI が一発で通ったときはかなり爽快でした。
最後に
inotify を使った不要なテスト用ファイルの削除事例を紹介しました。
このようなファイルは消し忘れやすく、また消し忘れているものを区別しづらいため、不要なファイルが残りがちだと思います。 今回適用した手法は Rails アプリに限らず広く使えると思いますので、ぜひ試していただければ幸いです。
- https://man7.org/linux/man-pages/man7/inotify.7.html↩
- https://github.com/guard/guard↩
- https://github.com/guard/rb-inotify↩
- inotify で監視をするのはテストと別プロセスでも構わないのですが、お手軽に実装するためにテストのプロセス内で実行しています。↩