Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions lib/concurrent-ruby/concurrent/promises.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1014,17 +1014,23 @@ def exception(*args)
raise Concurrent::Error, 'it is not rejected' unless rejected?
raise ArgumentError unless args.size <= 1
reason = Array(internal_state.reason).flatten.compact
locations_supported = RUBY_VERSION >= '3.4'

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.

Let's make it a constant like:

Suggested change
locations_supported = RUBY_VERSION >= '3.4'
SET_BACKTRACE_LOCATIONS_SUPPORTED = RUBY_VERSION >= '3.4'

I would prefer to feature-detect this but it's probably a bit messy (would cause an exception on older Ruby, not nice with -d) so this is fine enough.

callsites = locations_supported ? caller_locations : caller
if reason.size > 1
ex = Concurrent::MultipleErrors.new reason
ex.set_backtrace(caller)
ex.set_backtrace(callsites)
ex
else
ex = if reason[0].respond_to? :exception
reason[0].exception(*args)
else
RuntimeError.new(reason[0]).exception(*args)
end
ex.set_backtrace Array(ex.backtrace) + caller
if locations_supported && (locations = ex.backtrace_locations)
ex.set_backtrace locations + callsites
else
ex.set_backtrace Array(ex.backtrace) + callsites.map(&:to_s)
end
ex
end
end
Expand Down
38 changes: 38 additions & 0 deletions spec/concurrent/promises_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -566,6 +566,44 @@ def behaves_as_delay(delay, value)
expect(exception).to be_a Concurrent::MultipleErrors
expect(strip_methods[backtrace] - strip_methods[exception.backtrace]).to be_empty
end

it 'sets a consistent backtrace and backtrace_locations for an exception with captured locations' do
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'

raised = (raise TypeError, 'boom' rescue $!)
exception = rejected_future(raised).exception

expect(exception).to be_a TypeError
expect(exception.backtrace).not_to be_nil
expect(exception.backtrace_locations).not_to be_nil
expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace
end

it 'preserves a String-only backtrace when no locations are available' do
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'

string_only = TypeError.new
string_only.set_backtrace %W[/a /b /c]
expect(string_only.backtrace_locations).to be_nil

exception = rejected_future(string_only).exception

expect(exception).to be_a TypeError
expect(exception.backtrace).not_to be_nil
expect(exception.backtrace_locations).to be_nil
expect(exception.backtrace).to start_with %W[/a /b /c]
end

it 'sets a consistent backtrace and backtrace_locations for MultipleErrors' do
skip 'backtrace_locations is only populated on Ruby >= 3.4' if RUBY_VERSION < '3.4'

exception = (rejected_future(TypeError.new) & rejected_future(TypeError.new)).exception

expect(exception).to be_a Concurrent::MultipleErrors
expect(exception.backtrace).not_to be_nil
expect(exception.backtrace_locations).not_to be_nil
expect(exception.backtrace_locations.map(&:to_s)).to eq exception.backtrace
end
end

describe 'ResolvableEvent' do
Expand Down