Skip to content

webp関連テストを安定して通るようにした#9022

Merged
komagata merged 8 commits intomainfrom
chore/fix-flaky-webp-avatar-test
Aug 11, 2025
Merged

webp関連テストを安定して通るようにした#9022
komagata merged 8 commits intomainfrom
chore/fix-flaky-webp-avatar-test

Conversation

@Ryooo-k
Copy link
Copy Markdown
Contributor

@Ryooo-k Ryooo-k commented Aug 8, 2025

Issue

概要

ユーザーアイコンがwebpになっていることを期待するテストがFlakyとなっていましたので、改善をいたしました。

改善内容

ユーザー画像リセットによる安定化

webp関連のテストメソッド内で、ユーザー画像をリセットする処理を導入し、テストを安定させました。
(複数テストで画像変換処理が行われることにより、元データが削除されテストがFlakyとなっていたため)

key重複による一意制約エラーを回避

create_and_upload!メソッドでは、並列処理時にkey重複が起こる場合があり、一意制約エラーが発生してました。
create_or_find_by!メソッドに変更し、keyが重複していればcreateしないようにし、一意制約エラーを回避しました。

変更確認方法

下記テストが問題ないことを確認する。

  • rails test test/system/admin/users_test.rb ci=1
    • 「update user」テストが通ることを確認する
  • rails test test/system/searchables_test.rb ci=1
    • 「show icon and go profile page when click icon」テストが通ることを確認する。
  • rails test test/system/attachments_test.rb ci=1
    • 「attachment user avatar」テストが通ることを確認する。
  • rails test test/models/user_test.rb ci=1
    • 「#avatar_url」テストが通ることを確認する。
  • rails test test/system/notifications_test.rb ci=1
    • RuntimeError: Neutered Exception ActionView::Template::Errorでテストが失敗しないことを確認する。
  • rails test test/system/current_user_test.rb ci=1
    • ActionView::Template::Errorでテストが失敗しないことを確認する。

Summary by CodeRabbit

  • テスト
    • テスト環境でユーザーのアバターをリセットするヘルパーを追加し、複数のシステム/モデルテストで利用するようにしました。
    • 一部の並列テスト用セットアップ/後処理を削除しました。
  • 改善
    • アバター登録処理を重複作成を避ける仕組みに変更し、既存データの再アップロードを抑制しました。
  • 設定変更
    • テスト用ストレージのルートを環境変数に依存しない固定パスに変更しました。

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Aug 8, 2025

Walkthrough

テスト用のアバタリセットヘルパーAvatarHelperを追加し、複数のシステム/モデルテストでreset_avatarを呼ぶようにした。ApplicationSystemTestCaseからActiveStorageの並列セットアップ/ティアダウンを削除。User#attach_custom_avatarはカスタムキーで既存Blobを再利用し、新規作成時のみアップロード/添付するよう変更。config/storage.ymlのtest rootの環境変数分岐を削除した。

Changes

Cohort / File(s) Change Summary
Avatarヘルパー追加
test/supports/avatar_helper.rb
AvatarHelperモジュールを追加し、reset_avatar(user)でテスト用アバターファイルをActive Storageに添付する処理を実装。
SystemTest基盤の修正
test/application_system_test_case.rb
AvatarHelperrequire/includeし、Active Storageのparallelize_setup/parallelize_teardownブロックを削除。
システムテストでのアバターリセット適用
test/system/admin/users_test.rb, test/system/attachments_test.rb, test/system/searchables_test.rb
各テストの開始前にreset_avatar(...)を呼び出すよう追加(参照や一部変数化の小修正を含む)。
モデル内アバター添付ロジックの変更
app/models/user.rb
カスタムキー(avatars/#{login_name}.#{AVATAR_FORMAT})でActiveStorage::Blob.create_or_find_by!を使い既存Blobを再利用。新規Blob作成時のみuploadおよびattachを行い、エラーはlog_avatar_errorで集約。
モデルテストにヘルパー組み込み
test/models/user_test.rb
AvatarHelperrequire/includeし、avatar_urlテストでreset_avatarを呼ぶよう追加。
testストレージ設定の簡素化
config/storage.yml
testサービスのroot設定から環境変数フォールバックを削除し、常にRails.root.join("tmp/storage")を使用するよう変更。

Sequence Diagram(s)

sequenceDiagram
  participant Test as System Test
  participant Helper as AvatarHelper
  participant AS as ActiveStorage
  participant User as User model

  Test->>Helper: reset_avatar(user)
  Helper->>AS: open fixture file and build blob params
  Helper->>AS: create_or_find blob by custom key
  alt blob exists
    AS-->>Helper: existing blob returned
    Helper-->>User: ensure user avatar attached (no upload)
  else blob created
    Helper->>AS: upload blob data
    Helper->>User: attach new blob to user
  end
  Test->>Test: 実際のテスト操作
Loading
sequenceDiagram
  participant UserModel as User#attach_custom_avatar
  participant AS as ActiveStorage

  UserModel->>AS: create_or_find blob by custom key
  alt blob exists
    UserModel-->>UserModel: 既存Blobを利用(アップロード省略)
  else blob created
    UserModel->>AS: upload data to new blob
    UserModel->>AS: attach blob to user
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 分

Possibly related issues

Possibly related PRs

Suggested reviewers

  • komagata
  • tyrrell-IH

Poem

🐇
テストの朝 ぼくはぴょんと跳ねて、resetの鐘、
小さな画像をそっと貼り直すよ、ふわり。
Blobを探し 無ければ作る、重複はいやさ、
するとテストはきらり、Flakyは逃げる。
レビューの草原で一息ついて、また跳ねる。

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 RuboCop (1.76.1)
app/models/user.rb

rubocop-minitest extension supports plugin, specify plugins: rubocop-minitest instead of require: rubocop-minitest in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
rubocop-capybara extension supports plugin, specify plugins: rubocop-capybara instead of require: rubocop-capybara in /.rubocop.yml.
For more information, see https://docs.rubocop.org/rubocop/plugin_migration_guide.html.
Unable to find gem rubocop-fjord; is the gem installed? Gem::MissingSpecError
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:309:in rescue in gem_config_path' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:293:in gem_config_path'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:84:in block (2 levels) in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in reverse_each'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:82:in block in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in each_pair'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader_resolver.rb:76:in resolve_inheritance_from_gems' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_loader.rb:58:in load_file'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/config_store.rb:29:in options_config=' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:162:in act_on_options'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:47:in block in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:82:in profile_if_needed'
/var/lib/gems/3.1.0/gems/rubocop-1.79.2/lib/rubocop/cli.rb:43:in run' /var/lib/gems/3.1.0/gems/rubocop-1.79.2/exe/rubocop:19:in <top (required)>'
/usr/local/bin/rubocop:25:in load' /usr/local/bin/rubocop:25:in

'

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch chore/fix-flaky-webp-avatar-test

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot requested a review from komagata August 8, 2025 05:52
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
test/supports/avatar_helper.rb (1)

3-13: 実装は適切ですが、エラーハンドリングの追加を検討してください。

AvatarHelper の実装は正しく、ActiveStorage の attach メソッドを適切に使用しています。ただし、テストフィクスチャファイルが存在しない場合のエラーハンドリングを追加することで、より堅牢になります。

以下の diff でエラーハンドリングを追加できます:

 def reset_avatar(user)
   filename = "#{user.login_name}.jpg"
   path = Rails.root.join("test/fixtures/files/users/avatars", filename)
+  
+  unless File.exist?(path)
+    raise "Avatar fixture file not found: #{path}"
+  end
+  
   user.avatar.attach(
     io: File.open(path, "rb"),
     filename:,
     content_type: 'image/jpeg'
   )
 end
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e7d5722 and c971949.

📒 Files selected for processing (5)
  • test/application_system_test_case.rb (2 hunks)
  • test/supports/avatar_helper.rb (1 hunks)
  • test/system/admin/users_test.rb (1 hunks)
  • test/system/attachments_test.rb (1 hunks)
  • test/system/searchables_test.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rb

⚙️ CodeRabbit Configuration File

**/*.rb: # refactoring

  • まずFat Controllerを避け、次にFat Modelを避ける。
  • Serviceクラスの乱用を避ける。
  • controller concernを作ろうとしたらPORO(Plain Old Ruby Object)やActiveRecordモデルでの実装で代替できないか検討する。

Rails Patterns

  • ViewHelperにメソッドを実装する時にはまずDecoratorパターンを使うことを検討する。(active_decorator gemを導入しているのでそれを使う)
  • 複雑なActiveRecordクエリがあり、再利用できそうな場合はQueryObjectパターンを検討する。(rails-patterns gemを導入しているのでそれのQuery機能を使う)
  • Viewにpartialを作る場合はViewComponentを使うことを検討する。
  • 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。(interactor gemを導入しているのでそれを使う)
  • 複数のInteractorを実行するような処理がある場合Organizerオブジェクトパターンを検討する。(interactor gemを導入しており、その中にOrganizerの機能があるので使う)

Files:

  • test/system/admin/users_test.rb
  • test/system/searchables_test.rb
  • test/application_system_test_case.rb
  • test/system/attachments_test.rb
  • test/supports/avatar_helper.rb
test/**/*

⚙️ CodeRabbit Configuration File

test/**/*: # Test

  • TestCase名は英語で書く。
  • どうしても避けられない時以外にsystem testでsleepは使わない。

Unit Test

model, helper, decorator, view_componentについてはメソッドを追加した場合は必ず対応したUnit TestのTestCaseを1つは書く。

Files:

  • test/system/admin/users_test.rb
  • test/system/searchables_test.rb
  • test/application_system_test_case.rb
  • test/system/attachments_test.rb
  • test/supports/avatar_helper.rb
🧠 Learnings (4)
📓 Common learnings
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: PRのスコープを維持することが重要であり、バグ修正PRにおいて、修正対象以外の機能(例:ルーティングテスト)の追加提案は適切ではない。そのような改善が必要な場合は別PRで対応すべきである。
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: Railsの標準的なヘルパーメソッド(例:`question_url`、`user_path`など)が通常の方法で使用されている場合、それらの動作テストは不要である。これらのメソッドはRailsフレームワーク自体でテストされており、アプリケーション固有のロジックではないため。
📚 Learning: 2025-07-10T12:59:27.807Z
Learnt from: kitarou888
PR: fjordllc/bootcamp#8277
File: test/models/searcher_test.rb:326-331
Timestamp: 2025-07-10T12:59:27.807Z
Learning: モデルテストにおいて、1つのクラスメソッドの複数の挙動を検証する場合、機能の異なる側面を同じテストメソッドでテストすることは、包括的なテストとして適切である。特に`only_me`のような機能では、異なる検索条件(空文字と具体的な検索語)を使い分けることで、より広範囲な動作保証が可能となる。

Applied to files:

  • test/system/searchables_test.rb
  • test/system/attachments_test.rb
📚 Learning: 2025-07-30T07:26:36.599Z
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: Railsの標準的なヘルパーメソッド(例:`question_url`、`user_path`など)が通常の方法で使用されている場合、それらの動作テストは不要である。これらのメソッドはRailsフレームワーク自体でテストされており、アプリケーション固有のロジックではないため。

Applied to files:

  • test/application_system_test_case.rb
  • test/system/attachments_test.rb
📚 Learning: 2025-06-21T22:30:20.116Z
Learnt from: tyrrell-IH
PR: fjordllc/bootcamp#8807
File: app/views/welcome/job_support.html.slim:391-392
Timestamp: 2025-06-21T22:30:20.116Z
Learning: このプロジェクトにはRailsの組み込みmarkdownヘルパが利用できないため、markdownのHTML変換には自前でヘルパを実装する必要がある。

Applied to files:

  • test/application_system_test_case.rb
🧬 Code Graph Analysis (4)
test/system/admin/users_test.rb (1)
test/supports/avatar_helper.rb (1)
  • reset_avatar (4-12)
test/system/searchables_test.rb (1)
test/supports/avatar_helper.rb (1)
  • reset_avatar (4-12)
test/application_system_test_case.rb (1)
test/test_helper.rb (2)
  • include (22-55)
  • include (57-60)
test/system/attachments_test.rb (1)
test/supports/avatar_helper.rb (1)
  • reset_avatar (4-12)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test
🔇 Additional comments (4)
test/system/admin/users_test.rb (1)

100-102: 適切な修正とコメントです。

webpテストでのアバターリセットの追加は適切で、なぜこの処理が必要かを明確に説明するコメントも含まれています。テスト環境でのアバター共有による画像変換の問題を解決する良いアプローチです。

test/system/searchables_test.rb (1)

189-191: 一貫した実装で適切です。

他のシステムテストと同様のパターンでアバターリセットを実装しており、コメントも統一されています。webp形式の確認テストに必要な前準備として適切です。

test/system/attachments_test.rb (1)

8-14: 適切な修正とコードの改善です。

アバターリセットの追加に加えて、ローカル変数 user の導入によりコードの可読性が向上しています。他のシステムテストと一貫したパターンで実装されており、適切です。

test/application_system_test_case.rb (1)

15-15: AvatarHelper の統合が適切に実装されています。

新しい AvatarHelper モジュールの require と include が正しく追加されており、すべてのシステムテストで reset_avatar メソッドが利用できるようになっています。

Also applies to: 29-29

@Ryooo-k Ryooo-k force-pushed the chore/fix-flaky-webp-avatar-test branch from c971949 to e29d2a0 Compare August 8, 2025 20:35
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🔭 Outside diff range comments (1)
app/models/user.rb (1)

707-713: avatar_urlの判定が不十分—既存Blobがあっても未attachだとwebp化をスキップ

ActiveStorage::Blob.find_by(key: ...) の存在で呼び出し可否を決めると、「Blobはあるがユーザーのavatarが未付け替え」の場合に attach_custom_avatar が呼ばれず、非webpのURLを返してしまいます。現在のユーザーが参照しているBlobのkeyを見て判断してください。

-      attach_custom_avatar if !ActiveStorage::Blob.find_by(key: "avatars/#{login_name}.#{AVATAR_FORMAT}")
+      target_key = "avatars/#{login_name}.#{AVATAR_FORMAT}"
+      attach_custom_avatar if avatar.blob.key != target_key

任意でさらに厳格にするなら content_type も併せて判定可:

attach_custom_avatar if avatar.blob.key != target_key || avatar.blob.content_type != "image/#{AVATAR_FORMAT}"
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c971949 and e29d2a0.

📒 Files selected for processing (7)
  • app/models/user.rb (1 hunks)
  • test/application_system_test_case.rb (2 hunks)
  • test/models/user_test.rb (2 hunks)
  • test/supports/avatar_helper.rb (1 hunks)
  • test/system/admin/users_test.rb (1 hunks)
  • test/system/attachments_test.rb (1 hunks)
  • test/system/searchables_test.rb (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • test/application_system_test_case.rb
🚧 Files skipped from review as they are similar to previous changes (4)
  • test/system/admin/users_test.rb
  • test/supports/avatar_helper.rb
  • test/system/searchables_test.rb
  • test/system/attachments_test.rb
🧰 Additional context used
📓 Path-based instructions (2)
**/*.rb

⚙️ CodeRabbit Configuration File

**/*.rb: # refactoring

  • まずFat Controllerを避け、次にFat Modelを避ける。
  • Serviceクラスの乱用を避ける。
  • controller concernを作ろうとしたらPORO(Plain Old Ruby Object)やActiveRecordモデルでの実装で代替できないか検討する。

Rails Patterns

  • ViewHelperにメソッドを実装する時にはまずDecoratorパターンを使うことを検討する。(active_decorator gemを導入しているのでそれを使う)
  • 複雑なActiveRecordクエリがあり、再利用できそうな場合はQueryObjectパターンを検討する。(rails-patterns gemを導入しているのでそれのQuery機能を使う)
  • Viewにpartialを作る場合はViewComponentを使うことを検討する。
  • 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。(interactor gemを導入しているのでそれを使う)
  • 複数のInteractorを実行するような処理がある場合Organizerオブジェクトパターンを検討する。(interactor gemを導入しており、その中にOrganizerの機能があるので使う)

Files:

  • test/models/user_test.rb
  • app/models/user.rb
test/**/*

⚙️ CodeRabbit Configuration File

test/**/*: # Test

  • TestCase名は英語で書く。
  • どうしても避けられない時以外にsystem testでsleepは使わない。

Unit Test

model, helper, decorator, view_componentについてはメソッドを追加した場合は必ず対応したUnit TestのTestCaseを1つは書く。

Files:

  • test/models/user_test.rb
🧠 Learnings (2)
📓 Common learnings
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: PRのスコープを維持することが重要であり、バグ修正PRにおいて、修正対象以外の機能(例:ルーティングテスト)の追加提案は適切ではない。そのような改善が必要な場合は別PRで対応すべきである。
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: Railsの標準的なヘルパーメソッド(例:`question_url`、`user_path`など)が通常の方法で使用されている場合、それらの動作テストは不要である。これらのメソッドはRailsフレームワーク自体でテストされており、アプリケーション固有のロジックではないため。
📚 Learning: 2025-07-10T12:59:27.807Z
Learnt from: kitarou888
PR: fjordllc/bootcamp#8277
File: test/models/searcher_test.rb:326-331
Timestamp: 2025-07-10T12:59:27.807Z
Learning: モデルテストにおいて、1つのクラスメソッドの複数の挙動を検証する場合、機能の異なる側面を同じテストメソッドでテストすることは、包括的なテストとして適切である。特に`only_me`のような機能では、異なる検索条件(空文字と具体的な検索語)を使い分けることで、より広範囲な動作保証が可能となる。

Applied to files:

  • test/models/user_test.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test

@Ryooo-k Ryooo-k force-pushed the chore/fix-flaky-webp-avatar-test branch from 1efdb5c to ae2efd5 Compare August 9, 2025 00:05
@Ryooo-k Ryooo-k changed the title webpテスト内でavatarをリセットする うぇ Aug 9, 2025
@Ryooo-k Ryooo-k changed the title うぇ webp関連テストを安定して通るようにした Aug 9, 2025
@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 9, 2025

@komagata
webp関連のテストを安定化しました。
こちらはメンバーレビューを通さず、直接komagataさんにレビューお願いしてもよいでしょうか?
ご確認お願いいたします🙏

)
avatar.attach(custom_blob)

with_lock do
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

すみません、これってこうすることでどういう問題をどうやって防ぐ感じですかね?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

意図

同一ユーザーでavatar_urlが並列に呼ばれた際、key重複による一意制約違反を防ぐため、with_lockで悲観的ロックを取り、ロック内で再チェックしています。

起きていた事象

  1. A・Bが並列でavatar_urlを実行
  2. avatar_urlのkeyチェックでは、どちらも「custom_keyのBlobはない」と判定
  3. A・Bともattach_custom_avatarを実行
  4. Aがcustom_avatarを作成 → 直後にBも同じkeyで作成しようとする
  5. Bでは既にAで作成されたcustom_avatarが存在するので、一意制約違反が発生

この事象はtest/system/notifications_test.rbで発生しました。

対策

  • with_lockで同一ユーザーへのDB更新を直列化
  • ロック後にkeyを再チェック
  • 無ければ作成、あればreturn

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ryooo-k なるほどです。説明ありがとうございます。

with_lockは悲観的ロックなのでパフォーマンスがすごい下がる可能性がありそうでできれば使いたくないなと思います。

ActiveRecordのtransactionを使うやり方とかcreate_or_find_byなどで対応できないでしょうか?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create_or_find_byを使用する形で修正いたしました!
96d82d3 ご確認お願いします。

assert_equal '/images/users/avatars/default.png', user_with_default_avatar.avatar_url

user_with_custom_avatar = users(:komagata)
# ユーザーアイコンがwebpに変換されていることを確認するテストは、対象となるavatarをresetする。
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この説明はreset_avatarの方にメソッドの説明コメントとして書いた方がいいかなと思いました。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a0b68f2 にてメソッド定義位置にコメントを移動しました!

@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 9, 2025

@komagata
修正、返信いたしました!
ご確認お願いいたします🙏

一部、修正がございましたので、改めてご連絡いたします。申し訳ありません🙇

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0b68f2 and 16bea76.

📒 Files selected for processing (1)
  • config/storage.yml (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: PRのスコープを維持することが重要であり、バグ修正PRにおいて、修正対象以外の機能(例:ルーティングテスト)の追加提案は適切ではない。そのような改善が必要な場合は別PRで対応すべきである。
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: Railsの標準的なヘルパーメソッド(例:`question_url`、`user_path`など)が通常の方法で使用されている場合、それらの動作テストは不要である。これらのメソッドはRailsフレームワーク自体でテストされており、アプリケーション固有のロジックではないため。
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test

test:
service: Disk
root: <%= ENV['ACTIVE_STORAGE_TEST_ROOT'] || Rails.root.join("tmp/storage") %>
root: <%= Rails.root.join("tmp/storage") %>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

並列テスト時の衝突リスク:test用ストレージrootの固定化によりワーカー間で干渉する可能性があります

tmp/storage に固定すると、並列実行(プロセス/スレッド)時に同一ディレクトリ配下で書き込み/削除が競合し、まれにフレークの再発や意図せぬパージが起き得ます。以前のような環境変数によるオーバーライドを残しておくと、必要に応じてワーカー毎に分離したrootを指定でき、安定性確保の逃げ道になります(デフォルトは現状の固定パスのままにできます)。

後方互換を保ちつつの修正案(推奨):

 test:
   service: Disk
-  root: <%= Rails.root.join("tmp/storage") %>
+  root: <%= ENV["ACTIVE_STORAGE_TEST_ROOT"].presence || Rails.root.join("tmp/storage") %>
   public: true

補足:

  • 本PRでテストの安定化(avatarリセット、attach_custom_avatarのwith_lock)は進んでいますが、ストレージrootの固定は将来的な並列度変更やCI環境差分で再度フレーク要因になり得ます。envオーバーライドを残すだけならスコープを広げずにリスク低減できます。

並列実行状況と既存参照の有無を確認するため、以下をリポジトリルートで実行してください(sandboxでクローン済み前提):


🏁 Script executed:

#!/bin/bash
set -eu

echo "== ActiveStorage test root の既存利用確認 =="
rg -n 'ACTIVE_STORAGE_TEST_ROOT' -S || true

echo "== Railsの並列化設定の確認(minitest/rails) =="
rg -n 'parallelize\(' -A 3 test || true
rg -n 'Parallelization' -S test || true

echo "== parallel_tests系の導入痕跡(TEST_ENV_NUMBERなど) =="
rg -n 'TEST_ENV_NUMBER' -S || true
rg -n 'parallel_tests' -S || true

Length of output: 699


テスト用ストレージrootに環境変数オーバーライドを復活し、並列実行の衝突を回避

test/test_helper.rbでparallelize(workers: :number_of_processors)が有効化されているため、全ワーカーで同一のtmp/storageを共有すると書き込み/削除が競合し、テストがフレークする恐れがあります。以下のように環境変数ACTIVE_STORAGE_TEST_ROOTを残しつつ、デフォルトで各ワーカーごとに別フォルダを参照する設定にすると安全です。

修正箇所:

  • config/storage.yml の test.root

提案差分:

 test:
   service: Disk
-  root: <%= Rails.root.join("tmp/storage") %>
+  root: <%= ENV["ACTIVE_STORAGE_TEST_ROOT"].presence ||
+            Rails.root.join("tmp/storage#{ENV['TEST_ENV_NUMBER']}") %>
   public: true

必要に応じてCIやローカルの並列テスト設定でACTIVE_STORAGE_TEST_ROOTをワーカーごとに設定してください。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
root: <%= Rails.root.join("tmp/storage") %>
test:
service: Disk
root: <%= ENV["ACTIVE_STORAGE_TEST_ROOT"].presence ||
Rails.root.join("tmp/storage#{ENV['TEST_ENV_NUMBER']}") %>
public: true
🤖 Prompt for AI Agents
In config/storage.yml at line 3, the test storage root is currently hardcoded to
tmp/storage, which causes conflicts during parallel test execution. Modify the
root setting to use the environment variable ACTIVE_STORAGE_TEST_ROOT if set,
falling back to tmp/storage with a unique subdirectory per worker to avoid
collisions. This ensures each parallel test worker uses a separate storage path,
preventing write/delete conflicts.

Copy link
Copy Markdown
Contributor Author

@Ryooo-k Ryooo-k Aug 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
こちらは、test_helper.rbにて以下のように設定しているため、root設定は現状のままでOKです。

parallelize_setup do |i|
  ActiveStorage::Blob.service.instance_variable_set(:@root, "#{ActiveStorage::Blob.service.root}/storage-#{i}")
  ActiveStorage::Blob.services.fetch(:test_fixtures).instance_variable_set(:@root, "#{ActiveStorage::Blob.services.fetch(:test_fixtures).root}/fixtures-#{i}")
end

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify the parallelize_setup configuration in test_helper.rb
echo "=== Checking test_helper.rb for parallelize_setup configuration ==="
cat test/test_helper.rb | grep -A 5 -B 2 "parallelize_setup" || echo "parallelize_setup not found in test/test_helper.rb"

# Also check if there are any other parallelization-related configurations
echo -e "\n=== Checking for other parallelization configurations ==="
rg "parallelize" test/test_helper.rb -A 3 -B 1

Length of output: 2039


@Ryooo-k なるほど、確認いたしました。test_helper.rbparallelize_setupを使用して、各ワーカーごとにstorage-#{i}のサフィックスを付けてディレクトリを分離しているため、並列実行時の衝突は適切に回避されていますね。

parallelize_setup do |i|
  ActiveStorage::Blob.service.instance_variable_set(:@root, "#{ActiveStorage::Blob.service.root}/storage-#{i}")
  ActiveStorage::Blob.services.fetch(:test_fixtures).instance_variable_set(:@root, "#{ActiveStorage::Blob.services.fetch(:test_fixtures).root}/fixtures-#{i}")
end

この実装により、storage.ymlでの環境変数による設定は不要となり、シンプルな固定パスで問題ないことを理解しました。ご説明ありがとうございます。


🧠 Learnings used
Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: PRのスコープを維持することが重要であり、バグ修正PRにおいて、修正対象以外の機能(例:ルーティングテスト)の追加提案は適切ではない。そのような改善が必要な場合は別PRで対応すべきである。

Learnt from: ryufuta
PR: fjordllc/bootcamp#8992
File: app/models/correct_answer_notifier.rb:23-23
Timestamp: 2025-07-30T07:26:36.599Z
Learning: Railsの標準的なヘルパーメソッド(例:`question_url`、`user_path`など)が通常の方法で使用されている場合、それらの動作テストは不要である。これらのメソッドはRailsフレームワーク自体でテストされており、アプリケーション固有のロジックではないため。

@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 9, 2025

@komagata
16bea76 の不備がありましたので、コミットを追加しました。
改めてご確認お願いいたします。

@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 11, 2025

@komagata
修正いたしましたー!ご確認お願いいたします🙏

content_type: "image/#{AVATAR_FORMAT}",
identify: false
)
created = false
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createdという変数を用意しているのが不要な気がしました。

custom_blob.id_previously_was.present?メソッドでcreateされたのかどうか確かめられた気がします。

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

31112c6 にて修正しました🙇
便利なメソッドがたくさんあるんですね、、、勉強になります!

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96d82d3 and 31112c6.

📒 Files selected for processing (1)
  • app/models/user.rb (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
**/*.rb

⚙️ CodeRabbit Configuration File

**/*.rb: # refactoring

  • まずFat Controllerを避け、次にFat Modelを避ける。
  • Serviceクラスの乱用を避ける。
  • controller concernを作ろうとしたらPORO(Plain Old Ruby Object)やActiveRecordモデルでの実装で代替できないか検討する。

Rails Patterns

  • ViewHelperにメソッドを実装する時にはまずDecoratorパターンを使うことを検討する。(active_decorator gemを導入しているのでそれを使う)
  • 複雑なActiveRecordクエリがあり、再利用できそうな場合はQueryObjectパターンを検討する。(rails-patterns gemを導入しているのでそれのQuery機能を使う)
  • Viewにpartialを作る場合はViewComponentを使うことを検討する。
  • 複数のActiveRecordモデルを操作する1つの責務がある時や外部APIとやりとりする処理がある場合にはInteractorオブジェクトパターンを検討する。(interactor gemを導入しているのでそれを使う)
  • 複数のInteractorを実行するような処理がある場合Organizerオブジェクトパターンを検討する。(interactor gemを導入しており、その中にOrganizerの機能があるので使う)

Files:

  • app/models/user.rb
🧠 Learnings (1)
📚 Learning: 2025-07-23T20:31:13.856Z
Learnt from: jun-kondo
PR: fjordllc/bootcamp#8977
File: app/controllers/reports_controller.rb:63-63
Timestamp: 2025-07-23T20:31:13.856Z
Learning: fjordllc/bootcampプロジェクトの`app/controllers/reports_controller.rb`において、`create`と`update`アクションは両方とも`report.save_uniquely`を使用し、同じ`:report_save`イベントと`'report.save'`イベントを発行する。これは両方とも本質的に「レポートの保存」操作であり、作成と更新を区別する必要がないためである。

Applied to files:

  • app/models/user.rb
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build_and_test

Comment on lines +956 to 969
custom_key = "avatars/#{login_name}.#{AVATAR_FORMAT}"
variant_avatar = avatar.variant(resize_to_fill: AVATAR_SIZE, autorot: true, saver: { strip: true, quality: 60 }, format: AVATAR_FORMAT).processed
io = StringIO.new(variant_avatar.download)
custom_blob = ActiveStorage::Blob.create_and_upload!(
io:,
key: "avatars/#{login_name}.#{AVATAR_FORMAT}",
filename: "#{login_name}.#{AVATAR_FORMAT}",
content_type: "image/#{AVATAR_FORMAT}",
identify: false
)
custom_blob = ActiveStorage::Blob.create_or_find_by!(key: custom_key) do |blob|
blob.filename = "#{login_name}.#{AVATAR_FORMAT}"
blob.content_type = "image/#{AVATAR_FORMAT}"
blob.byte_size = io.size
blob.checksum = Digest::MD5.base64digest(io.read)
io.rewind
end
return if custom_blob.id_previously_was.present?

custom_blob.upload(io, identify: false)
avatar.attach(custom_blob)
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Aug 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

既存Blob未attach・無駄なvariant生成・並列時の不安定さを解消しましょう

現状の問題点:

  • 既存Blobが存在してもユーザーのavatarへ付け替えずにreturnするため、staleなattachmentを参照し続けるケースが残ります(webp化されない・フレークの原因)。
  • create_or_find_by! 実行前に毎回 variant 生成・download しており、既存Blobがある場合でも不要な画像処理が走ります。
  • 並列実行で先行が作成し、後続が find パスに入った場合の挙動が早期returnに寄りすぎで、attach漏れや不要uploadのリスクがあります。

以下の修正で改善してください:

  • 先に key で find し、存在すれば即 attach(variantは生成しない)。
  • 無ければ variant 生成→ create_or_find_by!。既存返却なら upload はせず attach のみ。新規作成時のみ upload→attach。
  • avatar_url 側の呼び出し条件も「Blobが存在しない」ではなく「現在のattachmentのkeyが custom_key と異なる」場合に変更。

適用差分(該当範囲のみ):

-    custom_key = "avatars/#{login_name}.#{AVATAR_FORMAT}"
-    variant_avatar = avatar.variant(resize_to_fill: AVATAR_SIZE, autorot: true, saver: { strip: true, quality: 60 }, format: AVATAR_FORMAT).processed
-    io = StringIO.new(variant_avatar.download)
-    custom_blob = ActiveStorage::Blob.create_or_find_by!(key: custom_key) do |blob|
-      blob.filename = "#{login_name}.#{AVATAR_FORMAT}"
-      blob.content_type = "image/#{AVATAR_FORMAT}"
-      blob.byte_size = io.size
-      blob.checksum = Digest::MD5.base64digest(io.read)
-      io.rewind
-    end
-    return if custom_blob.id_previously_was.present?
-
-    custom_blob.upload(io, identify: false)
-    avatar.attach(custom_blob)
+    custom_key = "avatars/#{login_name}.#{AVATAR_FORMAT}"
+    # 既存Blobがあれば付け替えて終了(variant生成は不要)
+    if (existing = ActiveStorage::Blob.find_by(key: custom_key))
+      avatar.attach(existing) unless avatar.blob_id == existing.id
+      return
+    end
+
+    # ここに到達したら初回生成(またはレースで直前に他プロセスが作成した可能性)
+    variant_avatar = avatar.variant(
+      resize_to_fill: AVATAR_SIZE,
+      autorot: true,
+      saver: { strip: true, quality: 60 },
+      format: AVATAR_FORMAT
+    ).processed
+    io = StringIO.new(variant_avatar.download)
+
+    custom_blob = ActiveStorage::Blob.create_or_find_by!(key: custom_key) do |blob|
+      blob.filename = "#{login_name}.#{AVATAR_FORMAT}"
+      blob.content_type = "image/#{AVATAR_FORMAT}"
+      blob.byte_size = io.size
+      blob.checksum = Digest::MD5.base64digest(io.string)
+    end
+
+    # レースで既存が返ってきた場合はuploadせずに付け替えのみ
+    if custom_blob.id_previously_was.present?
+      avatar.attach(custom_blob) unless avatar.blob_id == custom_blob.id
+      return
+    end
+
+    custom_blob.upload(io, identify: false)
+    avatar.attach(custom_blob)

avatar_url 側も合わせて以下のように変更することで、既存Blobがあっても「現在のattachmentがcustom_keyでない場合」は確実に付け替えられます(参考コード・別箇所のため差分外):

def avatar_url
  if avatar.attached? && avatar.blob.present?
    custom_key = "avatars/#{login_name}.#{AVATAR_FORMAT}"
    attach_custom_avatar unless avatar.blob.key == custom_key
    "#{avatar.url}?v=#{avatar.created_at.to_i}"
  else
    image_url DEFAULT_IMAGE_PATH
  end
rescue ActiveStorage::FileNotFoundError, ActiveStorage::InvariableError => e
  log_avatar_error('avatar_url', e)
  image_url DEFAULT_IMAGE_PATH
end

この変更で:

  • 既存Blobがあるときは O(1) attach、無駄な変換ダウンロードを回避
  • レース時も「attach漏れ」や「不要upload」を避けつつ一貫して custom_key に収束
  • webp期待のテストのフレーク要因を除去

必要であれば該当テストの修正提案や追加の回帰テストも用意します。

🤖 Prompt for AI Agents
In app/models/user.rb around lines 956 to 969, avoid generating the image
variant and downloading it before checking for an existing blob; instead first
try to find an ActiveStorage::Blob by the key
("avatars/#{login_name}.#{AVATAR_FORMAT}") and if found immediately attach it to
the avatar (no variant work), otherwise generate the variant, build the blob via
create_or_find_by! and then: if create_or_find_by! returns an existing blob
simply attach it (do not upload), but if it returns a newly created blob upload
the IO and then attach; also remove the early return that skips attaching when
id_previously_was is present so attach always occurs when the current attachment
key differs; finally update avatar_url caller logic (separate method) to trigger
attach_custom_avatar when avatar.blob.key != custom_key instead of only when
blob is missing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
問題点に関して、回答します。

既存Blobが存在してもユーザーのavatarへ付け替えずにreturnするため、staleなattachmentを参照し続けるケースが残ります(webp化されない・フレークの原因)。

「既存Blobがcustom_keyであればwebpへの変換は行われている」ということになるため、その場合は付け替え不要でり、returnで返すことは問題ないと思います。

create_or_find_by! 実行前に毎回 variant 生成・download しており、既存Blobがある場合でも不要な画像処理が走ります。

avatar_urlメソッドでcustom_keyのチェックは行っていますので、create_or_find_by! 実行前に毎回 variant 生成・download を行っても問題ないと考えています。

並列実行で先行が作成し、後続が find パスに入った場合の挙動が早期returnに寄りすぎで、attach漏れや不要uploadのリスクがあります。

findされた場合は、custom_keyが存在していることになり、custom_keyであればwebpに変換されているはずなので、その場合は、attachやuploadは不要です。

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Ryooo-k ご説明ありがとうございます。実装の意図を理解しました。

確かに以下の点でロジックは一貫していますね:

  • custom_keyのBlobが存在 = 既にwebp変換済み
  • avatar_urlでcustom_keyの存在確認を行い、存在しない場合のみ変換処理を実行
  • 並列実行時もcreate_or_find_by!で適切にハンドリング

パフォーマンス面では既存Blob存在時のvariant生成を避ける余地はありますが、機能的には問題なく動作することを理解しました。テストが安定して通るようになったという結果も、この実装が適切に機能していることを示していますね。

私の最初の提案は過度に最適化を意識したものでした。現在の実装で問題ないと判断されたことを承知しました。

(^・ω・^)

@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 11, 2025

@komagata
修正いたしましたー!ご確認お願いいたします🙏

Copy link
Copy Markdown
Member

@komagata komagata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

確認させて頂きました。OKです〜🙆‍♂️

@komagata komagata merged commit e2ed771 into main Aug 11, 2025
16 checks passed
@komagata komagata deleted the chore/fix-flaky-webp-avatar-test branch August 11, 2025 23:44
@github-actions github-actions bot mentioned this pull request Aug 11, 2025
87 tasks
@Ryooo-k
Copy link
Copy Markdown
Contributor Author

Ryooo-k commented Aug 11, 2025

ご確認ありがとうございます!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants