Ruby Coverage実行時のSegmentation Fault修正
Rails 8.1環境でテンプレートアノテーションを無効化した状態でRuby Coverageを実行すると発生していたSegmentation Faultが修正されました。#2541の修正が不完全だったため、CI環境などアノテーションが無効化されている環境で問題が残っていました。
背景
#2541では、Ruby Coverageが有効な環境でのSegmentation Faultを修正するため、class_evalのlineno引数を条件分岐で制御する実装が追加されました。しかし、この修正には見落としがありました。
アノテーションはローカル開発環境では通常有効化されているため、開発者の手元では問題なく動作します。一方、CI環境ではパフォーマンスやログの簡潔性のため、アノテーションを無効化するのが一般的です。#2541の条件分岐は、アノテーション有効時のみに対応したものでした。
従来の条件式では、Coverageが動作していても、アノテーションが無効な場合はlineno = -1が使用されていました:
if coverage_running? && ActionView::Base.annotate_rendered_view_with_filenames
@strip_annotation_line = true
0
else
-1
end
この実装では、coverage_running?とannotate_rendered_view_with_filenamesの両方が真の場合のみSegmentation Faultを回避できます。CI環境のようにアノテーションが無効な場合、Coverage実行時でもlineno = -1となり、依然としてSegmentation Faultが発生していました。
技術的な変更
lib/view_component/template.rbのlineno設定ロジックが修正され、Coverage実行時は常にlineno = 0を使用するよう変更されました。
変更後:
lineno =
if Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0 && details.handler == :erb
if coverage_running?
# Can't use negative lineno with coverage (causes segfault on Linux).
# Strip annotation line if enabled to preserve correct line numbers.
@strip_annotation_line = ActionView::Base.annotate_rendered_view_with_filenames
0
else
-1
end
else
1
end
coverage_running?が真の場合、必ずlineno = 0を返すよう単純化されました。@strip_annotation_lineフラグの設定はアノテーション有効時のみ行われますが、linenoの値自体はアノテーション設定に関係なく0になります。
この変更により、以下の動作が保証されます:
-
Coverage無効時:
lineno = -1でスタックトレースの行番号が正確 -
Coverage有効・アノテーション有効時:
lineno = 0、アノテーション行を削除して行番号を正確に保つ -
Coverage有効・アノテーション無効時:
lineno = 0でSegmentation Faultを回避(今回追加された保護)
アノテーション行の削除処理は依然としてアノテーション有効時のみ実行されるため、不要な処理は発生しません。
テストの追加
test/sandbox/test/inline_template_test.rbに、アノテーション無効時のCoverage実行を検証するテストケースが追加されました。
test "file-based templates compile without segfault when coverage is running and annotations disabled" do
skip unless Rails::VERSION::MAJOR >= 8 && Rails::VERSION::MINOR > 0
without_template_annotations do
with_coverage_running do
# Force recompilation with coverage "enabled" but annotations disabled
ViewComponent::CompileCache.cache.delete(ErbComponent)
# This would segfault in v4.3.0 because it only avoided -1 lineno
# when annotations were enabled
render_inline(ErbComponent.new(message: "Foo bar"))
assert_selector("div", text: "Foo bar")
end
end
end
このテストはwithout_template_annotationsヘルパーを使用してアノテーションを明示的に無効化し、Coverage実行時の動作を検証します。コンポーネントのコンパイルキャッシュをクリアすることで、テスト実行時に確実に再コンパイルが行われます。
Segmentation FaultはLinux環境でのみ再現するため、macOS開発環境では直接検証できません。しかし、このテストケースがあれば、CI環境での検証が可能になり、同様の問題の再発を防げます。
設計判断
Coverage実行時は常に非負のlinenoを使用するという原則が明確化されました。
この制約は、Diff内のコメントで「Can't use negative lineno with coverage (causes segfault on Linux).」と示されている通り、負の行番号とCoverageの組み合わせがLinuxでSegmentation Faultを引き起こす問題に起因します。
当初の#2541では、Coverage実行とアノテーション有効の両方の条件でのみlineno = 0を使用していましたが、本PRでCoverageが動作している場合は無条件でlineno = 0とする方針に統一されました。これにより、条件分岐がシンプルになり、見落としやすいエッジケースを排除できています。
行番号の正確性は、アノテーション行の削除処理で維持されます。アノテーションが無効な場合は削除すべき行も存在しないため、lineno = 0でも実質的な問題は発生しません。
まとめ
本PRは、CI環境などアノテーションが無効化された環境でのSegmentation Faultを修正しました。条件分岐を単純化し、Coverage実行時は常にlineno = 0とすることで、環境設定の組み合わせによる予期しない動作を排除しています。アノテーション無効時の回帰テストの追加により、同様の問題の再発防止も実現されました。