Fix NoMethodError when a caller location has no path - #69
Conversation
| private | ||
|
|
||
| def internal_file?(file) | ||
| return true unless file |
There was a problem hiding this comment.
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 | |||
There was a problem hiding this comment.
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
Agreed. I left a few review comments. Could you take a look? |
987bf97 to
81a1bcf
Compare
|
|
||
| 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 |
There was a problem hiding this comment.
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
endShould I add this test, or would you prefer to keep the &. as an untested defensive guard?
There was a problem hiding this comment.
Good catch, could you also add it?
There was a problem hiding this comment.
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.
81a1bcf to
d376dc2
Compare
Thread::Backtrace::Location#pathreturns nil for the Enumerator frames (e.g. "Enumerator::Generator#each") that appear incaller_locationsduring an external iteration of Enumerator (e.g.Enumerator#next). When aTracePointcallback fires inside such an iteration,PowerAssert.internal_file?raisesNoMethodErrorand the callback reports:This happens in practice when a block-style assertion drives Capybara, whose
Capybara::Resultiterates matched elements withEnumerator#next.While #31 originally suspected a CRuby issue, the type signature of
Thread::Backtrace::Location#pathin 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.