From 5d1f9febcc61be349a7b23e64c040331f4023d99 Mon Sep 17 00:00:00 2001 From: Hartley McGuire Date: Fri, 31 Jul 2026 15:11:16 -0400 Subject: [PATCH 1/2] Fix Ractor compatibility regression, add tests The recent change to use `BasicObject.instance_method(:equal?)` broke the ability to share frozen ERB templates across Ractors because `UnboundMethod` isn't shareable. Freezing the `UnboundMethod` _may_ fix the issue (dependong on Ruby version), but replacing the constant with an inline call to the `singleton_class` is simpler (and still avoids calling `equal?` on `@init`). There have previously been many contributions to make ERB Ractor safe, but no tests added to ensure it continues to be Ractor safe, so this commit also adds some regression tests. --- lib/erb.rb | 5 +---- test/erb/test_erb.rb | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/lib/erb.rb b/lib/erb.rb index 0c4653d..040cd18 100644 --- a/lib/erb.rb +++ b/lib/erb.rb @@ -815,9 +815,6 @@ # [template processor]: https://en.wikipedia.org/wiki/Template_processor # class ERB - IDENTITY_METHOD = BasicObject.instance_method(:equal?) # :nodoc: - private_constant :IDENTITY_METHOD - # :markup: markdown # # :call-seq: @@ -1117,7 +1114,7 @@ def new_toplevel(vars = nil) private :new_toplevel def initialized_by_new? # :nodoc: - IDENTITY_METHOD.bind_call(@_init, self.class.singleton_class) + self.class.singleton_class.equal? @_init end private :initialized_by_new? diff --git a/test/erb/test_erb.rb b/test/erb/test_erb.rb index 1de8925..6da2514 100644 --- a/test/erb/test_erb.rb +++ b/test/erb/test_erb.rb @@ -740,3 +740,43 @@ def teardown ERB::Compiler::Scanner.instance_variable_set('@scanner_map', @save_map) end end + +class TestERBRactor < Test::Unit::TestCase + def test_compile_and_result_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + r = Ractor.new do + ERB.new("Hello, <%= 'world' %>!").result(binding) + end + assert_equal("Hello, world!", r.value) + RUBY + end + + def test_trim_mode_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + src = "<% [1, 2].each do |i| %>\\n<%= i %>\\n<% end %>\\n" + r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '-').result(binding) } + assert_equal("\\n1\\n\\n2\\n\\n", r.value) + + r = Ractor.new(src) { |s| ERB.new(s, trim_mode: '<>').result(binding) } + assert_equal("12", r.value) + RUBY + end + + def test_frozen_erb_instance_reused_across_ractors + assert_ractor(<<~RUBY, require: 'erb') + erb = ERB.new("<%= 1 + 1 %>") + erb.freeze + rs = 2.times.map { Ractor.new(erb) { |e| e.result(binding) } } + assert_equal(["2", "2"], rs.map(&:value)) + RUBY + end + + def test_util_html_escape_in_ractor + assert_ractor(<<~RUBY, require: 'erb') + r = Ractor.new do + ERB::Util.html_escape("