Tech Racho エンジニアの「?」を「!」に。
  • Ruby / Rails関連

週刊Railsウォッチ: Railsコンソールが最新のIRB APIに移行、assertionless_tests_behaviorほか(20240513前編)

こんにちは、hachi8833です。以下のお知らせに先ほど気づきました🎉。

週刊Railsウォッチについて

  • 各記事冒頭には🔗でパーマリンクを置いてあります: 社内やTwitterでの議論などにどうぞ
  • 「つっつきボイス」はRailsウォッチ公開前ドラフトを(鍋のように)社内有志でつっついたときの会話の再構成です👄
  • お気づきの点がありましたら@hachi8833までメンションをいただければ確認・対応いたします🙏

TechRachoではRubyやRailsなどの最新情報記事を平日に公開しています。TechRacho記事をいち早くお読みになりたい方はTwitterにて@techrachoのフォローをお願いします。また、タグやカテゴリごとにRSSフィードを購読することもできます(例:週刊Railsウォッチタグ)

🔗 Rails: 先週の改修(Rails公式ニュースより)

🔗 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パーサーを追加する。hexBinaryXMLのプリミティブデータ型の1つだが、Railsではサポートされていなかった。

詳細

このプルリクは、ActiveSupport::XmlMiniにhex binaryパーサーを追加する。

追加情報

https://www.w3.org/TR/xmlschema-2/#hexBinary
同PRより


つっつきボイス:「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関連のプロジェクトで若干利用例がある。このプルリクがマージされたらjbuilderactiveresourceにもプルリクを投げるつもり。
同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メソッドとして追加されるため、ヘルプメッセージに表示されず、機能がとても見つけにくい

詳細

  1. コマンドとヘルパーメソッドが、IRBの拡張API経由でIRBに追加されるようになった。
    この変更後にテストが適切に動作するように、すべてのコマンド/ヘルパーメソッドのテストを統合テストに変換した。
  2. バックトレースのフィルタリングロジックは、privateのWorkSpaceクラスにパッチを適用するのではなく、IRBの新しい IRB.conf[:BACKTRACE_FILTER]コンフィグで適用されるようになった。
  3. これらの新しいAPIやコンフィグを利用するには、railtiesでIRBv1.13.0が必要。
    IRB v1.13.0は古いバージョンのRailsでもそのまま動作するはず。
  4. IRB固有のロジックが少し長くなった場合に対応するために、IRBConsoleクラスを新しい専用ファイルに移動した。

結果

これで、RailsコンソールのヘルパーやコマンドがIRBのヘルプメッセージに表示されるようになった。

同PRより


つっつきボイス:「従来の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の解説」より

Ruby 3.3で大幅に強化されたIRBの解説(翻訳)

🔗 bin/rails app:updateをRakeタスクからRailsコマンドに変更して--forceオプションを追加

現在の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)。

自分の理解では、AppUpdaterframework.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ガイドより

🔗 複合主キーでincludesreferencesに続けて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より


つっつきボイス:「この間公開した翻訳記事↓の元記事をヒントに、アサーションが動いていないテストを検出する機能を追加したそうです」

Rails: アサーションが動いていないテストを効果的に発見する方法(翻訳)

「minitestのテストにassertが書かれているのに呼び出されなかった場合を検出するコンフィグを追加したのか」「サンプルコード↓で言うと、active_usersに1件もレコードがなければdoendブロック自体が実行されなくなるからブロック内の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後編)

ソースの表記されていない項目は独自ルート(TwitterやはてブやRSSやruby-jp SlackやRedditなど)です。

Rails公式ニュース


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。