minitest / minitest

minitest provides a complete suite of testing facilities supporting TDD, BDD, mocking, and benchmarking.

Home Page:https://docs.seattlerb.org/minitest/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Neuter exception error for ActiveRecord::RecordInvalid

moiristo opened this issue · comments

For some reason, an AR object I have is unmarshallable (rails 7.0.3.1). When this record is passed in ActiveRecord::RecordInvalid and raised in a test, the following error occurs since 5.16.0:

Minitest::UnexpectedError:         NoMethodError: undefined method `errors' for "Validation failed: Receipt is invalid":String
            activerecord (7.0.3.1) lib/active_record/validations.rb:21:in `initialize'
            minitest (5.16.2) lib/minitest/test.rb:223:in `new'
            minitest (5.16.2) lib/minitest/test.rb:223:in `new_exception'
            minitest (5.16.2) lib/minitest/test.rb:215:in `neuter_exception'
            minitest (5.16.2) lib/minitest/test.rb:208:in `rescue in sanitize_exception'
            minitest (5.16.2) lib/minitest/test.rb:204:in `sanitize_exception'
            minitest (5.16.2) lib/minitest/test.rb:201:in `rescue in capture_exceptions'
            minitest (5.16.2) lib/minitest/test.rb:194:in `capture_exceptions'
            minitest (5.16.2) lib/minitest/test.rb:95:in `block (2 levels) in run'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:38:in `block (2 levels) in time_it'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:31:in `around'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:37:in `block in time_it'
            minitest (5.16.2) lib/minitest.rb:296:in `time_it'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:36:in `time_it'
            minitest (5.16.2) lib/minitest/test.rb:94:in `block in run'
            minitest (5.16.2) lib/minitest.rb:391:in `on_signal'
            minitest (5.16.2) lib/minitest/test.rb:243:in `with_info_handler'
            minitest (5.16.2) lib/minitest/test.rb:93:in `run'
            minitest-reporters (1.5.0) lib/minitest/reporters.rb:48:in `run_with_hooks'
            minitest (5.16.2) lib/minitest.rb:1059:in `run_one_method'
            minitest (5.16.2) lib/minitest.rb:365:in `run_one_method'
            minitest (5.16.2) lib/minitest.rb:352:in `block (2 levels) in run'
            minitest (5.16.2) lib/minitest.rb:351:in `each'
            minitest (5.16.2) lib/minitest.rb:351:in `block in run'
            minitest (5.16.2) lib/minitest.rb:391:in `on_signal'
            minitest (5.16.2) lib/minitest.rb:378:in `with_info_handler'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:81:in `block in with_info_handler'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:26:in `around_all'
            minitest-hooks (1.5.0) lib/minitest/hooks/test.rb:70:in `with_info_handler'
            minitest (5.16.2) lib/minitest.rb:350:in `run'
            railties (7.0.3.1) lib/rails/test_unit/line_filtering.rb:10:in `run'
            minitest (5.16.2) lib/minitest.rb:182:in `block in __run'
            minitest (5.16.2) lib/minitest.rb:182:in `map'
            minitest (5.16.2) lib/minitest.rb:182:in `__run'
            minitest (5.16.2) lib/minitest.rb:159:in `run'
            minitest (5.16.2) lib/minitest.rb:83:in `block in autorun'
            activesupport (7.0.3.1) lib/active_support/fork_tracker.rb:18:in `fork'
            activesupport (7.0.3.1) lib/active_support/fork_tracker.rb:18:in `fork'
            /Users/be/.rbenv/versions/2.7.5/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:83:in `require'
            /Users/be/.rbenv/versions/2.7.5/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:83:in `require'
            -e:1:in `<main>'

This is because the same error class is instantiated with a String message, while the constructor expects an AR object: https://github.com/rails/rails/blob/main/activerecord/lib/active_record/validations.rb#L18. I hope I can find out why the object cannot be marshalled (maybe caused by pry?), but I think the neutering of the exception should be improved anyway to properly handle this case.

C'mon people... Ya gotta give me more information than that. This is unworkable so far.

Oh, I thought my explanation was pretty clear. In short, when an exception is unmarshallable, the fallback approach is to instantiate the same exception class with a message string (https://github.com/minitest/minitest/blob/master/lib/minitest/test.rb#L215). However, custom exceptions like ActiveRecord::RecordInvalid expect the first argument to be an AR object (https://github.com/rails/rails/blob/main/activerecord/lib/active_record/validations.rb#L18).

I don't think you can really deduce from a custom exception what the first argument in its constructor is supposed to be. So maybe the final approach in https://github.com/minitest/minitest/blob/master/lib/minitest/test.rb#L219 should always be done instead?

I can write a repro if you like. It doesn't need to be an ActiveRecord::RecordInvalid exception of course; it can be any exception with a custom constructor..

Please see moiristo@9cebd67. This fails with

  1) Error:
TestMinitestTest#test_spec_marshal_with_custom_constructor_exception:
NoMethodError: undefined method `text' for "this is bad!":String

Proposed fix is to never reuse the original exception class: moiristo@47d2a01

I wonder if the https://ruby-doc.org/core-3.1.2/Exception.html#method-i-exception could be used on ActiveRecord object and help here https://github.com/moiristo/minitest/blob/47d2a013e03d308bb92bd90a82aea215d7c1d1fe/lib/minitest/test.rb#L222

Though the #exception method is sometimes "custom" even in ruby stdlib too. Just few days ago discovered it to be defined in Timeout https://ruby-doc.org/stdlib-3.1.2/libdoc/timeout/rdoc/Timeout/Error.html#method-i-exception and have no clue what it is doing but it was ruining my passed args.

In what way? It doesn't make the exception marshallable I think. Today, we found out that this bug also breaks our CI build and custom reporting when an error occurs, so for now we've monkeypatched sanitize_exception, as we are not really interested in marshallability anyway:

module Minitest
  class Test
    def sanitize_exception e
      e
    end
  end
end

Would removing the TypeError restriction be sufficient?