From 9da3f67995054cf0b3511cce0d0a9b9d94c3240a Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Tue, 10 Nov 2026 21:36:43 +0000 Subject: [PATCH 1/2] Extract singleton method stubbing in colorizer tests Move the remove/redefine/restore steps from #1871 into one helper, so each test only states what the stub does. --- test/rdoc/parser/ruby_colorizer_test.rb | 59 ++++++++++++------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/test/rdoc/parser/ruby_colorizer_test.rb b/test/rdoc/parser/ruby_colorizer_test.rb index 9b283b666f..681a8e64b5 100644 --- a/test/rdoc/parser/ruby_colorizer_test.rb +++ b/test/rdoc/parser/ruby_colorizer_test.rb @@ -32,28 +32,13 @@ def test_deferred_token_stream_materializes_registered_nodes_once context = RDoc::Parser::RubyColorizer::DeferredContext.new(code) loaders = nodes.drop(1).map { |node| context.token_stream_loader(node.node_id) } parse_lex_calls = partial_colorize_calls = 0 - parse_lex = Prism.method(:parse_lex) - Prism.singleton_class.remove_method(:parse_lex) - colorizer = RDoc::Parser::RubyColorizer - partial_colorize = colorizer.method(:partial_colorize) - colorizer.singleton_class.remove_method(:partial_colorize) - - Prism.define_singleton_method(:parse_lex) do |*arguments| - parse_lex_calls += 1 - parse_lex.call(*arguments) - end - colorizer.define_singleton_method(:partial_colorize) do |*arguments| - partial_colorize_calls += 1 - partial_colorize.call(*arguments) - end + count_parse_lex = ->(original, *arguments) { parse_lex_calls += 1; original.call(*arguments) } + count_partial_colorize = ->(original, *arguments) { partial_colorize_calls += 1; original.call(*arguments) } - begin - assert_equal %w[first second], loaders.map { |loader| loader.call.map(&:text).join } - ensure - Prism.singleton_class.remove_method(:parse_lex) - Prism.define_singleton_method(:parse_lex, parse_lex) - colorizer.singleton_class.remove_method(:partial_colorize) - colorizer.define_singleton_method(:partial_colorize, partial_colorize) + stub_singleton_method(Prism, :parse_lex, count_parse_lex) do + stub_singleton_method(RDoc::Parser::RubyColorizer, :partial_colorize, count_partial_colorize) do + assert_equal %w[first second], loaders.map { |loader| loader.call.map(&:text).join } + end end assert_equal 1, parse_lex_calls assert_equal 2, partial_colorize_calls @@ -77,22 +62,16 @@ def test_deferred_token_stream_retries_atomically nodes = Prism.parse(code).value.statements.body context = RDoc::Parser::RubyColorizer::DeferredContext.new(code) loaders = nodes.map { |node| context.token_stream_loader(node.node_id) } - colorizer = RDoc::Parser::RubyColorizer - partial_colorize = colorizer.method(:partial_colorize) - colorizer.singleton_class.remove_method(:partial_colorize) calls = 0 - - colorizer.define_singleton_method(:partial_colorize) do |*arguments| + fail_second_call = lambda do |original, *arguments| calls += 1 raise 'colorization failed' if calls == 2 - partial_colorize.call(*arguments) + original.call(*arguments) end - begin + + stub_singleton_method(RDoc::Parser::RubyColorizer, :partial_colorize, fail_second_call) do assert_raise(RuntimeError) { loaders.first.call } - ensure - colorizer.singleton_class.remove_method(:partial_colorize) - colorizer.define_singleton_method(:partial_colorize, partial_colorize) end assert_equal %w[first second], loaders.map { |loader| loader.call.map(&:text).join } @@ -332,4 +311,22 @@ def test_rational_imaginary assert_include(tokens, token(:value, "2.0r")) assert_include(tokens, token(:value, "2.0ri")) end + + private + + # Replaces +object+.+name+ with +replacement+ while the block runs. + # +replacement+ receives the original method as its first argument. + # The original is removed before each redefinition to avoid method + # redefinition warnings. + def stub_singleton_method(object, name, replacement) + original = object.method(name) + object.singleton_class.remove_method(name) + object.define_singleton_method(name) { |*arguments| replacement.call(original, *arguments) } + begin + yield + ensure + object.singleton_class.remove_method(name) + object.define_singleton_method(name, original) + end + end end From e92cfbc3c8fa641136b68917e74380b425b9b60e Mon Sep 17 00:00:00 2001 From: Stan Lo Date: Wed, 11 Nov 2026 01:02:49 +0000 Subject: [PATCH 2/2] Let colorizer tests define their own stubs without_singleton_method only removes the method and restores it after the block. Each test defines its replacement inside the block, so there is no separate proc argument. --- test/rdoc/parser/ruby_colorizer_test.rb | 49 +++++++++++++++---------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/test/rdoc/parser/ruby_colorizer_test.rb b/test/rdoc/parser/ruby_colorizer_test.rb index 681a8e64b5..ae93ff3bc5 100644 --- a/test/rdoc/parser/ruby_colorizer_test.rb +++ b/test/rdoc/parser/ruby_colorizer_test.rb @@ -32,11 +32,20 @@ def test_deferred_token_stream_materializes_registered_nodes_once context = RDoc::Parser::RubyColorizer::DeferredContext.new(code) loaders = nodes.drop(1).map { |node| context.token_stream_loader(node.node_id) } parse_lex_calls = partial_colorize_calls = 0 - count_parse_lex = ->(original, *arguments) { parse_lex_calls += 1; original.call(*arguments) } - count_partial_colorize = ->(original, *arguments) { partial_colorize_calls += 1; original.call(*arguments) } + colorizer = RDoc::Parser::RubyColorizer + + without_singleton_method(Prism, :parse_lex) do |parse_lex| + Prism.define_singleton_method(:parse_lex) do |*arguments| + parse_lex_calls += 1 + parse_lex.call(*arguments) + end + + without_singleton_method(colorizer, :partial_colorize) do |partial_colorize| + colorizer.define_singleton_method(:partial_colorize) do |*arguments| + partial_colorize_calls += 1 + partial_colorize.call(*arguments) + end - stub_singleton_method(Prism, :parse_lex, count_parse_lex) do - stub_singleton_method(RDoc::Parser::RubyColorizer, :partial_colorize, count_partial_colorize) do assert_equal %w[first second], loaders.map { |loader| loader.call.map(&:text).join } end end @@ -62,15 +71,17 @@ def test_deferred_token_stream_retries_atomically nodes = Prism.parse(code).value.statements.body context = RDoc::Parser::RubyColorizer::DeferredContext.new(code) loaders = nodes.map { |node| context.token_stream_loader(node.node_id) } + colorizer = RDoc::Parser::RubyColorizer calls = 0 - fail_second_call = lambda do |original, *arguments| - calls += 1 - raise 'colorization failed' if calls == 2 - original.call(*arguments) - end + without_singleton_method(colorizer, :partial_colorize) do |partial_colorize| + colorizer.define_singleton_method(:partial_colorize) do |*arguments| + calls += 1 + raise 'colorization failed' if calls == 2 + + partial_colorize.call(*arguments) + end - stub_singleton_method(RDoc::Parser::RubyColorizer, :partial_colorize, fail_second_call) do assert_raise(RuntimeError) { loaders.first.call } end @@ -314,18 +325,16 @@ def test_rational_imaginary private - # Replaces +object+.+name+ with +replacement+ while the block runs. - # +replacement+ receives the original method as its first argument. - # The original is removed before each redefinition to avoid method - # redefinition warnings. - def stub_singleton_method(object, name, replacement) + # Removes the singleton method +name+ from +object+ and yields the + # original method. The block can define a replacement without a method + # redefinition warning. The original method is restored after the block. + def without_singleton_method(object, name) original = object.method(name) object.singleton_class.remove_method(name) - object.define_singleton_method(name) { |*arguments| replacement.call(original, *arguments) } - begin - yield - ensure - object.singleton_class.remove_method(name) + yield original + ensure + if original + object.singleton_class.remove_method(name) if object.singleton_class.method_defined?(name, false) object.define_singleton_method(name, original) end end