Skip to content

Fix NoMethodError when a caller location has no path - #69

Open
koic wants to merge 1 commit into
ruby:masterfrom
koic:fix-nil-path-crash-in-internal_file
Open

Fix NoMethodError when a caller location has no path#69
koic wants to merge 1 commit into
ruby:masterfrom
koic:fix-nil-path-crash-in-internal_file

Conversation

@koic

@koic koic commented Aug 6, 2026

Copy link
Copy Markdown

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 #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.

Treat locations without a path as internal ones since they can never be the assertion's source location. Also guard app_context?, which walks caller locations the same way.

Closes #31.

Comment thread lib/power_assert.rb Outdated
private

def internal_file?(file)
return true unless file

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal_file? returns true when the given file belongs to the power_assert library. I think it should return false when file is nil.

@@ -0,0 +1,18 @@
require_relative 'test_helper'

class TestNilPathLocation < Test::Unit::TestCase

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for parser-related code, this project generally does not test individual methods directly. Instead, tests check that the expected power assert message is generated.

For this issue, I think it would be better to add the following reproduction test:

    t do
      obj = []
      def obj.foo; self; end
      enum = Enumerator.new {|y| y << obj.foo }
      def enum.inspect; '#<Enumerator>'; end
      assert_equal <<END.chomp, assertion_message {
        enum.next.foo
        |    |    |
        |    |    []
        |    []
        #<Enumerator>
END
        enum.next.foo
      }
    end

@k-tsj

k-tsj commented Aug 9, 2026

Copy link
Copy Markdown
Member

it seems reasonable for power_assert to tolerate nil either way.

Agreed. I left a few review comments. Could you take a look?

@koic
koic force-pushed the fix-nil-path-crash-in-internal_file branch from 987bf97 to 81a1bcf Compare August 9, 2026 10:17
Comment thread lib/power_assert.rb

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I applied both suggestions.

One thing I noticed: the &. in app_context? is not covered by the test suite (removing it still passes all tests). The following test covers it by yielding on a non-target thread, which keeps the :call
TracePoint enabled while the Enumerator fiber runs. It fails with NoMethodError if the &. is removed:

t do
  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

Should I add this test, or would you prefer to keep the &. as an untested defensive guard?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, could you also add it?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your review. I've added the test.

`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 ruby#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 ruby#31.
@koic
koic force-pushed the fix-nil-path-crash-in-internal_file branch from 81a1bcf to d376dc2 Compare August 11, 2026 05:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Failed to trace: NoMethodError: undefined method start_with?

2 participants