- Ruby / Rails関連
週刊Railsウォッチ: Railsコンソールが最新のIRB APIに移行、assertionless_tests_behaviorほか(20240513前編)
こんにちは、hachi8833です。以下のお知らせに先ほど気づきました🎉。
💎Kaigi on Rails 2024 公式サイト オープン!💎
オンラインとオフラインのハイブリット開催です!
日程:2024.10.25 (Fri.) - 26 (Sat.)
有明セントラルタワーホールhttps://t.co/Jwk5Zro27S#kaigionrails— Kaigi on Rails (@kaigionrails) May 7, 2024
🔗 Rails: 先週の改修(Rails公式ニュースより)
- 公式更新情報: Ruby on Rails — Rails World 2024 tickets on April 30, legacy index name format for Rails 7.0, etc
- 公式更新情報: Ruby on Rails — Rails console improvements, assertionless tests reporting and more!
🔗 Rails 7.0〜7.1の間に作成したマイグレーションのrename_table
で長いインデックス名が意図せず切り詰められることがあった問題を修正
このプルリクは、従来のインデックス名の形式を保持することで、Rails 7.1より前のバージョンとの互換性を確保する。バージョン 7.1以降、Railsで新しいインデックス名形式が導入された結果、
rename_table
を含むマイグレーションでRails 7.0と7.1の間に作成されたインデックス名とは異なるインデックス名が生成される可能性があった。なお、昨年create_table
を含むマイグレーションに対処する同様のプルリク(#47863)もある。
公式更新情報より
つっつきボイス:「issue #50833を見ると、以前7.1にマージされた#47753で自動生成インデックス名が62バイトを超えないようになっていた(ウォッチ20230425)けど、Rails 7.0で生成したマイグレーションでその機能が発動してインデックス名が変わってしまったのか」「以前create_table
でも似たような修正があったんですね」
「この種の問題はbin/rails db:migrate:reset
あたりを実行したときなんかに気づきやすい」「スキーマファイルで差分が出ないはずなのに差分が発生してあれ?と思うヤツですね」
「Railsのマイグレーションファイルで継承するクラスは、ある時期からActiveRecord::Migration[7.0]
のようにマイグレーション生成時のRailsバージョンを含めるようになっていて、そのバージョンに適したマイグレーションを実行することで前方互換性を保つようになっているんですけど、rename_table
でその処理に不備があったので修正したということのようですね」「そうそう、そのおかげでRailsバージョンが異なるマイグレーションファイルが増えていってもマイグレーションの作成時と振る舞いが変わらずに済みますよね」
「マイグレーションで[7.0]
みたいにバージョンを示すようになったのっていつからでしたっけ?」「Rails 5.0あたりからみたいですね」
「いろんなバージョンで作成したマイグレーションがすべて正常に動くかどうかをテストで全部網羅するのはかなり難しそう」「データベースの種類の違いが影響する可能性なんかも考えたら、この種のテストは難しいでしょうね」「こういう前方互換性のレアな対応漏れが起きるのはある程度仕方がない気はします」
参考: Active Record マイグレーション - Railsガイド
🔗 ActiveSupport::XmlMini
でhexBinaryフォーマットをサポート
動機/背景
このプルリクは、XMLの
hexBinary
パーサーを追加する。hexBinary
はXMLのプリミティブデータ型の1つだが、Railsではサポートされていなかった。詳細
このプルリクは、
ActiveSupport::XmlMini
にhex binaryパーサーを追加する。追加情報
つっつきボイス:「XMLにhexBinaryというフォーマットがあるって知りませんでした」「今はW3CがXMLの仕様を担当しているんですね」「XMLの仕様を追いかけたのも随分前だったな〜」
# activesupport/test/xml_mini_test.rb#L340
+ def test_hexBinary
+ parser = @parsing["hexBinary"]
+
+ expected = "Hello, World!"
+ hex_binary = "48656C6C6F2C20576F726C6421"
+
+ assert_equal expected, parser.call(hex_binary)
+
+ parser = @parsing["binary"]
+ assert_equal expected, parser.call(hex_binary, "encoding" => "hexBinary")
+ assert_equal expected, parser.call(hex_binary, "encoding" => "hex")
+ end
参考: HexBinaryValue
クラス (DocumentFormat.OpenXml
) | Microsoft Learn
「ところで、1990年代後半から2000年代初頭にかけてのXMLはデータ交換仕様の究極みたいにもてはやされていましたけど、その後仕様が大きくなりすぎたりいろいろあって実装が追いつかなくなってきた感ありますね」「まさに」「XMLの仕様そのものは割といいと思いますし、謎のバイナリ形式とかを使うよりはずっといいと思うんですが、ある実装で出力したXMLが別の実装だと微妙な部分で通らなかったりするようなことが多くなってくるとつらい」「xsdがあれば検証できる分まだマシですけどね」「XMLは当初の想定以上に実装が追いつかなかった面はありそう」「XMLのようなものはRails自身がサポートするよりライブラリに任せる方がいい気もするけど、動かすためにはライブラリをインストールしないといけなくなるし、悩ましいところ」
参考: XML Schema(XSD)とは - 意味をわかりやすく - IT用語辞典 e-Words
🔗 ActiveSupport::ProxyObject
が非推奨化
ActiveSupport::ProxyObject
を非推奨化する。今後はRuby組み込みのBasicObject
を使うこと。Earlopain
同Changelogより
動機/背景
#51632 (comment)を見てみることにする(cc: @carlosantoniodasilva)。
このクラスは、Rubyに
BasicObject
がまだなかった時代にRuby 1.8で使われていたが、現在ではただのラッパーに過ぎない。
ProxyObject
がRails内部で最後に使われたのは10年前の#16574。以前のRailsは
builder
gemに依存していたが、これは(おそらく特に)BasicObject
をRuby 1.8にバックポートするためだったのではないか(34b5767
)。詳細
ActiveSupport::ProxyObject
を非推奨化する。追加情報
Rails関連のプロジェクトで若干利用例がある。このプルリクがマージされたら
jbuilder
とactiveresource
にもプルリクを投げるつもり。
同PRより
つっつきボイス:「ProxyObject
って見たことも使ったこともなかった」「Ruby 1.8が出てくるくらい昔のクラスなのか」「Ruby 1.9ですらないんですね」
参考: Rails API ActiveSupport::ProxyObject
参考: class BasicObject
(Ruby 3.3 リファレンスマニュアル)
🔗 Railsコンソールが最新のIRB APIに移行
動機/背景
IRBの拡張APIが不足しているため、現在のRailsコンソールは、バックトレースフィルタリング、追加のコマンドやヘルパーなどの機能を提供するためにプライベートコンポーネントへのパッチ適用に依存している。 これにはいくつかの問題がある。
- IRB側で変更をリファクタリングすると、Railsコンソールが壊れる可能性がある
- コマンドやヘルパーが単なるRubyメソッドとして追加されるため、ヘルプメッセージに表示されず、機能がとても見つけにくい
詳細
- コマンドとヘルパーメソッドが、IRBの拡張API経由でIRBに追加されるようになった。
この変更後にテストが適切に動作するように、すべてのコマンド/ヘルパーメソッドのテストを統合テストに変換した。- バックトレースのフィルタリングロジックは、privateの
WorkSpace
クラスにパッチを適用するのではなく、IRBの新しいIRB.conf[:BACKTRACE_FILTER]
コンフィグで適用されるようになった。- これらの新しいAPIやコンフィグを利用するには、
railties
でIRBv1.13.0
が必要。
IRBv1.13.0
は古いバージョンのRailsでもそのまま動作するはず。- IRB固有のロジックが少し長くなった場合に対応するために、
IRBConsole
クラスを新しい専用ファイルに移動した。結果
これで、RailsコンソールのヘルパーやコマンドがIRBのヘルプメッセージに表示されるようになった。
同PRより
Additionally, Rails console has integrated the new API, stepping away from patching IRB internals. This means better discovery for console helpers like `app` and `controller`: https://t.co/q6eOlmemCi
I want to encourage projects to adopt the new API and share feedback with us!
— Stan Lo @st0012@ruby.social (@_st0012) May 5, 2024
つっつきボイス:「従来のRailsコンソールだと、ヘルパーやコマンドをIRBのヘルプに表示するために半ば無理やりなハックを使っていたのを、この改修でRailsコンソールがIRBの新しいAPIを使うようになったことで、特別なことをしなくてもRailsコンソールでIRBのヘルプにヘルパーやコマンドが表示されるようになったそうです」「お〜、IRB APIとRailsコンソールのヘルプ表示の両方を改善するのは地味に大変そう」「IRBを手掛けているst0012さんの以前からの念願↓がついに叶ったということですね🎉」
RailsやHanamiなど、Ruby製Webフレームワークの多くがコンソールのプラットフォームとしてIRBを利用しています。さらに、ライブラリによってはコマンドを利用するカスタム機能でIRBを拡張しているものもあります。
しかし従来のIRBには、こうした新しいコマンドやヘルパーメソッドに対応する標準APIがありませんでした。これによって、以下のような問題が発生しています。
- 拡張機能をIRBのヘルプで表示できない
- 拡張機能は基本的にpublicなので、private APIが直接利用されているとリファクタリングが難しくなる
- IRBを利用するには独自の手法を編み出す必要があるため、ほとんど違いのないソリューションがいくつも林立してしまう
私たちはこうした問題に対処するため、ライブラリやアプリケーションがIRBを拡張するための公式のAPIとドキュメントを提供する計画を立てています。最終的な目標は、IRBを単なる有用なツールから、他のツールからも利用可能な優れたプラットフォームに変えることです。
「Ruby 3.3で大幅に強化されたIRBの解説」より
🔗 bin/rails app:update
をRakeタスクからRailsコマンドに変更して--force
オプションを追加
- PR: Turn app:update into a command to add --force by etiennebarrie · Pull Request #51690 · rails/rails
現在の
bin/rails app:update
はRakeタスクなので、通常のコマンドラインフラグを追加できない(引数はRakeで解析されるため、すべてのRakeフラグがサポートされる)。$ bin/rails app:update -P | head # this is the -P, --prereqs flag from Rake bin/rails app:template environment bin/rails app:templates:copy bin/rails app:update update:configs update:bin update:active_storage update:upgrade_guide_info bin/rails app:update:active_storage bin/rails app:update:bin
このプルリクは、
app:update
タスクをRailsコマンドに変更することで--force
フラグを追加して、すべての変更を受け入れる形でbin/rails app:update
を実行可能にする。 これにより、Railsアプリケーションのアップグレードなども自動化できる。このコマンドを作成しているときに、コマンドのオプションを
AppUpdater
に渡さなければならないことに気づいた(これによりAppGenerator
が作成されてメモ化される)。ジェネレーターを作成するオプションをAppUpdater
に渡して、以後のメソッド呼び出しでそのオプションを渡しても(ジェネレーターがすでにメモ化されていたため)無視されるという問題が発生した。オプションは2つのメソッド呼び出し間で同じだったので問題はなかったが、この設計は好きではない(etiennebarrie@7f364be
)。自分の理解では、
AppUpdater
はframework.rake
ファイル内のコードを増やしすぎないためだけに存在したと考えられる。そこで、これをコマンドにインライン化したところ、コマンドのオプションがジェネレーターのオプションにマージされ、コマンドのオプションが効くようになった。
同PRより
つっつきボイス:「RailsコマンドはRakeタスクを呼び出すショートハンドになっているものも多いけど、その場合オプションを渡してもRakeのオプションとして処理されてしまうからRailsコマンドに変更したのね」「以下の4つのオプションが追加されたんですね」
bin/rails app:update
にオプションを追加
bin/rails app:update
でジェネレータと同様のオプションを渡せるようになった。
--force
: 既存ファイルへの変更についてはすべて容認する--skip
: 既存ファイルへの変更についてはすべてスキップする--pretend
: 一切の変更を行わない--quiet
: 変更結果を一切出力しないÉtienne Barrié
同Changelogより
参考: 1.4 rails/command.rb -- コマンドラインツール - Railsガイド
Railsコマンドを入力すると、
invoke
が指定の名前空間内でコマンドを探索し、見つかった場合はそのコマンドを実行します。
コマンドがRailsによって認識されない場合は、Rakeが引き継いで同じ名前でタスクを実行します。
参考: 1.4 rails/command.rb -- コマンドラインツール - Railsガイドより
🔗 複合主キーでincludes
とreferences
に続けてcount
するとうまく動かない問題を修正
SQLiteと古いMySQLでは複数カラムでの
COUNT DISTINCT
の利用がサポートされていないため、複合主キーでカウントクエリを実行するとエラーが発生した。このプルリクで、サブクエリが使われるように変更する。
公式更新情報より
つっつきボイス:「お、複合主キーのバグがまだあったんですね: サブクエリにすることで修正するのはわかる」「issue #51634を見るのが早そう」
# activerecord/test/cases/calculations_test.rb#L406
+ def test_count_for_a_composite_primary_key_model_with_includes_and_references
+ assert_equal Cpk::Book.count, Cpk::Book.includes(:chapters).references(:chapters).count
+ end
参考: Active Record の複合主キー - Railsガイド
参考: Rails API includes
-- ActiveRecord::QueryMethods
参考: Rails API references
-- ActiveRecord::QueryMethods
🔗 アサーションが動いていないテストを検出するassertionless_tests_behavior
コンフィグが追加
『アサーションが動いていないテストを効果的に発見する方法』にインスパイアされた。
(上の記事にあるように)実は何もテストしていない(あるいは今後そうなる可能性がある)"false positive(偽陽性)"の壊れたテストを書いてしまうのは簡単である。以下はシンプルな例。
def test_active active_users = User.active.to_a active_users.each do |user| assert user.active? end end
上のテストケースにはあるいくつかのアサーションは良さそうに思えるが、スコープが変更されて0件のレコードが返された場合でもテストが失敗せずにパスしてしまい、しかもアサーションは実行されない。このテストをより堅牢にするには、少なくとも
active_users
にレコードが存在することをテストしておかなければならない。このプルリクは、以下のコンフィグでそうしたテストを手軽に検出できるようにする。
# config/environments/test.rb config.active_support.assertionless_tests_behavior = :raise # `:ignore`や`:log`も設定可能
上のように設定しておくことで、アサーションが動いていないテストが失敗するようになり、暗黙でパスしないようになる。
現在、この機能ではデフォルトでそうしたテストを
:ignore
する設定になっているが、最終的に:log
にしておいてその後は:raise
にするといったことも可能。このような問題を抱えたテストの一部は#48065でも既に修正済み。
このプルリクのフォローアップとして、すべてのフレームワークで
:raise
を有効にしたいと思う。現在は、このプルリクが必要以上に大きくなる問題をいくつか修正中。cc @nvasilevski @byroot(興味があれば)
同PRより
つっつきボイス:「この間公開した翻訳記事↓の元記事をヒントに、アサーションが動いていないテストを検出する機能を追加したそうです」
「minitestのテストにassert
が書かれているのに呼び出されなかった場合を検出するコンフィグを追加したのか」「サンプルコード↓で言うと、active_users
に1件もレコードがなければdo
〜end
ブロック自体が実行されなくなるからブロック内のassert
も実行されなくなりますね」「テストコードに書いたアサーションが一度も実行されないのは確実におかしいので、そういう問題を防止できるのはいいですね👍」
def test_active
active_users = User.active.to_a
active_users.each do |user|
assert user.active?
end
end
前編は以上です。
バックナンバー(2024年度第2四半期)
週刊Railsウォッチ: Prismの歴史と現況を振り返る、Steepの"narrowing"実装の内部ドキュメントほか(20240426後編)
- 20240425前編 RailsからOpenStructを削除、Playwrightベストプラクティスほか
- 20240423後編 Kamalはゲームチェンジャーになるか、Solid Queueで使われているfugitほか
- 20240416前編 ジョブのエンキューをトランザクション完了時まで自動先延ばしほか
- 20240410後編 SeleniumでRubyの全クラスとモジュールにRBSが追加ほか
- 20240409前編 Rails公式の"rails-new"ツールでRailsプロジェクトをセットアップほか
- 20240402 solid_queueとmission_control-jobsが正式にRailsのgemに、Rubyの"チルド"文字列ほか
ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。
週刊Railsウォッチについて
TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)