Skip to content

Ensure core exceptions get raised with $! as cause#9409

Open
headius wants to merge 5 commits intojruby:masterfrom
headius:core_exception_causes
Open

Ensure core exceptions get raised with $! as cause#9409
headius wants to merge 5 commits intojruby:masterfrom
headius:core_exception_causes

Conversation

@headius
Copy link
Copy Markdown
Member

@headius headius commented May 1, 2026

Many places where we raise exceptions from JRuby core code, we do not attach $! as a cause. This leads to several issues, including part of the issue reported in #9398. This PR adds specs and fixes known cases.

@headius headius added this to the JRuby 10.1.1.0 milestone May 1, 2026
@headius headius force-pushed the core_exception_causes branch from 1855854 to 9804bb8 Compare May 2, 2026 19:44
@headius
Copy link
Copy Markdown
Member Author

headius commented May 2, 2026

@eregon There are some modifications to mspec's raise_error to support matching causes, modifications to existing specs to use that functionality, and some additional cause-checking specs here, if you want to review.

@headius
Copy link
Copy Markdown
Member Author

headius commented May 2, 2026

Failures appear to be GHA issues or bad timing in specs, unrelated to these changes.

@eregon
Copy link
Copy Markdown
Contributor

eregon commented May 3, 2026

Thank you for adding more cause specs.

My initial thought here is whether making raise_error more complicated for this is worth it:

-> { 1 / 0 }.should raise_error(ZeroDivisionError, cause:)
# vs
-> { 1 / 0 }.should raise_error(ZeroDivisionError) { |e| e.cause.should == cause }

It looks nice and it's expressive but OTOH it's not used in that many files.

The main concern is MSpec tries to be as simple as possible so it can be used by early Ruby implementations.
That means so far we don't use keyword arguments in MSpec (git grep -F '**' lib has none).
That's easily solved though, see https://github.com/ruby/mspec/blob/31e8f11c0bf18cf85fa3d3643ea8fc5a395fc3fe/lib/mspec/matchers/complain.rb#L4-L7 for example.

If you want to modify raise_error, could you make a PR https://github.com/ruby/mspec and add spec for it in https://github.com/ruby/mspec/blob/master/spec/matchers/raise_error_spec.rb? Or you could also do it in this PR and run the mspec specs locally.

Comment thread spec/mspec/lib/mspec/matchers/raise_error.rb Outdated
Comment thread spec/mspec/lib/mspec/matchers/raise_error.rb Outdated
@headius
Copy link
Copy Markdown
Member Author

headius commented May 6, 2026

It looks nice and it's expressive but OTOH it's not used in that many files.

I think it will be used in many more as we continue to specify language-level exceptions that should have causes. I've only added a few cases here.

And even without those additional cases, there were many places doing the block form of raise_error just to check causes. Those are almost all one-liners now.

headius added 5 commits May 5, 2026 22:40
This adds a cause kwarg to raise_error for matching the cause of
a captured exception. The error output for failed cases is also
modified to show both message and cause if they are being matched.
@headius headius force-pushed the core_exception_causes branch from 9804bb8 to 7ae3e91 Compare May 6, 2026 03:41
@headius
Copy link
Copy Markdown
Member Author

headius commented May 6, 2026

@eregon I cleaned up the commit that adds the cause: functionality, added specs for it, and updated others to support the expanded error output. Also fixed all style issues.

Copy link
Copy Markdown
Contributor

@eregon eregon left a comment

Choose a reason for hiding this comment

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

Thank you, looks good, just needs a few more tweaks and it's good to go

Comment on lines +8 to +9
-> { raise RuntimeError, "the consequence" }
.should raise_error(RuntimeError, "the consequence", cause:)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
-> { raise RuntimeError, "the consequence" }
.should raise_error(RuntimeError, "the consequence", cause:)
-> {
raise RuntimeError, "the consequence"
}.should raise_error(RuntimeError, "the consequence", cause:)

This is more consistent with ruby/spec style

"#{exception_class} (#{message})"
else
"#{exception_class}"
string << "#{prefix.()}message: #{message.inspect}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
string << "#{prefix.()}message: #{message.inspect}"
string << "#{prefix.()}#{message.inspect}"

A message: prefix every time seems quite noisy, and it's a clear a String is the message and not the cause, so let's omit that.
The #{message.inspect} sounds fine.

attr_writer :block

def initialize(exception, message, &block)
def initialize(exception, message = nil, options = { }, &block)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
def initialize(exception, message = nil, options = { }, &block)
def initialize(exception, message = nil, options = nil, &block)

This is consistent with what raise_error below passes.
Also it avoids allocating an extra Hash when it's not used in most cases.

Below it becomes

@cause = options ? options.fetch(:cause, UNDEF_CAUSE) : UNDEF_CAUSE

which seems simple enough to avoid the extra Hash allocation.

Comment on lines +214 to +221
matcher = RaiseErrorMatcher.new(RuntimeError, "foo", cause:)
begin
matcher.matches?(proc)
rescue RuntimeError
expect(matcher.failure_message).to eq(
["Expected RuntimeError(message: \"foo\", cause: #<RuntimeError: bar>)", "but got: RuntimeError(message: \"foo\", cause: nil)"]
)
end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

It would be good to test the case with cause but not message too

rescue UnexpectedException => e
expect(matcher.failure_message).to eq(
["Expected ExpectedException (message)", "but got: UnexpectedException (message)"]
["Expected ExpectedException(message: \"message\")", "but got: UnexpectedException(message: \"message\")"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Minor: these would read nicer (and close to what gets printed) if using single-quote string now that we have " inside.

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.

2 participants