From d376dc27c0190a86193c40fb537fee49da892b5c Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Thu, 6 Aug 2026 17:35:02 +0900 Subject: [PATCH] Fix `NoMethodError` when a caller location has no path `Thread::Backtrace::Location#path` returns nil for the Enumerator frames (e.g. "Enumerator::Generator#each") that appear in `caller_locations` during an external iteration of Enumerator (e.g. `Enumerator#next`). When a `TracePoint` callback fires inside such an iteration, `PowerAssert.internal_file?` raises `NoMethodError` and the callback reports: ``` power_assert: [BUG] Failed to trace: NoMethodError: undefined method 'start_with?' for nil ``` This happens in practice when a block-style assertion drives Capybara, whose `Capybara::Result` iterates matched elements with `Enumerator#next`. While https://github.com/ruby/power_assert/issues/31 originally suspected a CRuby issue, the type signature of `Thread::Backtrace::Location#path` in ruby/rbs allows nil (`() -> String?`), and this nil case is observable at least on Ruby 2.6.10, 2.7.8, 3.1.5, 3.3.10, 3.4.10, and 4.0.6, so it seems reasonable for power_assert to tolerate nil either way. Since `internal_file?` answers whether the given file belongs to the power_assert library, treat locations without a path as non-internal ones. Also guard `app_context?`, which walks caller locations the same way. Closes #31. --- lib/power_assert.rb | 4 +++- test/nil_path_location_test.rb | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 test/nil_path_location_test.rb diff --git a/lib/power_assert.rb b/lib/power_assert.rb index 8de209e..5475c71 100644 --- a/lib/power_assert.rb +++ b/lib/power_assert.rb @@ -37,13 +37,15 @@ def app_caller_locations end def app_context? - top_frame = caller_locations.drop_while {|i| i.path.start_with?(POWER_ASSERT_LIB_DIR) }.first + top_frame = caller_locations.drop_while {|i| i.path&.start_with?(POWER_ASSERT_LIB_DIR) }.first top_frame and ! internal_file?(top_frame.path) end private def internal_file?(file) + return false unless file + INTERNAL_LIB_DIRS.find do |_, dir| file.start_with?(dir) end diff --git a/test/nil_path_location_test.rb b/test/nil_path_location_test.rb new file mode 100644 index 0000000..8b1a312 --- /dev/null +++ b/test/nil_path_location_test.rb @@ -0,0 +1,32 @@ +require_relative 'test_helper' + +class TestNilPathLocation < Test::Unit::TestCase + include PowerAssertTestHelper + + t do + obj = [] + def obj.foo; self; end + enum = Enumerator.new {|y| y << obj.foo } + def enum.inspect; '#'; end + assert_equal < +END + enum.next.foo + } + end + + t do + # Yielding on a non-target thread keeps the `TracePoint` for `:call` and `:c_call` events enabled while + # the `Enumerator`'s fiber runs, so that `app_context?` walks caller locations containing a frame without a path. + enum = Enumerator.new {|y| y << 1 } + message = ::PowerAssert.start(-> { enum.next }) do |pa| + Thread.new { pa.yield }.join + pa.message + end + assert_equal '', message + end +end