1

I'm running into a very odd issue with a validator that is meant to be used only when this module gets included. When running certain tests, I would get a validation failure as expected. However, the error is not a RecordInvalid; instead, it's an ArgumentError: wrong number of arguments (0 for 1), which makes absolutely no sense in this case.

Here is the mixin containing the validation code.

module AccountStateIntegrityMixin
  def self.included(base)
    base.class_eval do
      base.validate :account_state_integrity
    end
  end

  def account_state_integrity
    history = self.account_state_history_entries.order("start ASC").all
    return if history.blank?

    vald_methods = [
      :end_with_nil,
      :no_self_transitions,
      :no_overlaps_or_gaps
    ]

    vald_methods.each do |m|
      p "history = #{history.inspect}"
      if !self.send(m, history)
        return
      end
    end
  end

  def no_self_transitions(*args)
    p "no_self_transitions"
    p args
    history = args[0]
    # I want everything except the last element. I want to compare each
    # element with its successor
    history[0..-2].each_with_index do |entry, i|
      next_elem = history[i+1]
      if entry.account_state_id == next_elem.account_state_id
        self.errors.add(:account_state_history, "has a self-transition entries with " +
                        "IDs #{entry.id} and #{next_elem.id}")
        self.errors.add(:no_self_transitions, "violated")
        return false
      end
    end

    true
  end

  def end_with_nil(*args)  # ArgumentError was being thrown here
    p "end_with_nil"
    p args
    history = args[0]
    last = history.last  # with debugging statement, NoMethodError here

    if last.end != nil
      self.errors.add(:account_state_history, "is missing a nil ending. Offending " +
                      "entry has ID = #{last.id}")
      self.errors.add(:end_with_nil, "violated")
      return false
    end

    true
  end

  def no_overlaps_or_gaps(*args)
    p "no_overlaps_or_gaps"
    p args
    history = args[0]
    # Again, everything except the last element
    history[0..-2].each_with_index do |entry, i|
      next_elem = history[i+1]
      if next_elem.start != entry.end
        self.errors.add(:account_state_history, "has an overlap or gap between " +
                        "entries with IDs #{entry.id} and #{next_elem.id}")
        self.errors.add(:no_overlaps_or_gaps, "violated")
        return false
      end
    end

    true
  end
end

As you may have noticed, I added some print statements to help me debug the state of certain variables during testing. I also changed the method parameters to take a variable number of parameters so I could see what's actually being sent to the methods. (Note 1: as a result of making these changes to make things easier to debug, the error has been changed to a MethodError: You have a nil object when you didn't expect it!. Note 2: the method signature changes mainly consist of def method(history) -> def method(*args) and setting history = args[0]) Here is some output as a result of these statements:

"history = [#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >]"
"end_with_nil"
[[#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >]]
"history = [#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >]"
"no_self_transitions"
[[#<AccountStateHistoryEntry id: 1007, account_id: 684, ... ]]
"history = [#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >]"
"no_overlaps_or_gaps"
[[#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >]]
"history = [#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >, <AccountStateHistoryEntry id: 1008, account_id: 684, ... >]"
"end_with_nil"
[[#<AccountStateHistoryEntry id: 1007, account_id: 684, ... >, #<AccountStateHistoryEntry id: 1008, account_id: 684, ... >]]
"end_with_nil"
[]

Given the contents of the account_state_integrity method, I can't see any reason why end_with_nil is suddenly missing an argument that is clearly being passed to it in account_state_integrity.

I'll make this clear in case this bug is caused by an already-fixed legacy bug: we're using old versions of Ruby (1.8.7) and Rails (2.3.14). We're going to be migrating to much more recent versions of Ruby and Rails in the very near future.

Edit: Full stack trace output from the unit test

A free-trial account An account with no state with an account state history entry set to end in the future should still be able to end the account state:
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occurred while evaluating nil.last
app/models/account_state_integrity_mixin.rb:51:in `end_with_nil'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:42:in `send'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:42:in `value'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:125:in `default_options'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:36:in `full_message'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:287:in `full_messages'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:287:in `map'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:287:in `full_messages'
/Users/Daniel/.rvm/gems/.../activesupport/lib/active_support/whiny_nil.rb:52:in `inject'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:286:in `each'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:286:in `inject'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:286:in `full_messages'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:13:in `initialize'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:1101:in `new'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/validations.rb:1101:in `save_without_dirty!'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/dirty.rb:87:in `save_without_transactions!'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/transactions.rb:200:in `save!'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb:136:in `transaction'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/transactions.rb:182:in `transaction_without_always_new'
config/initializers/always_nest_transactions.rb:11:in `transaction'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/transactions.rb:200:in `save!'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/transactions.rb:208:in `rollback_active_record_state!'
/Users/Daniel/.rvm/gems/.../activerecord/lib/active_record/transactions.rb:200:in `save!'
app/models/account_state_lib.rb:260:in `clear_capability_overrides!'
app/models/account_state_lib.rb:321:in `clear_capabilities_cache'
app/models/account_state_lib.rb:256:in `clear_capability_overrides!'
app/models/account_state_lib.rb:121:in `end_state!'
test/unit/account_state_lib_test.rb:173:in `__bind_1340223707_845108'
/Users/Daniel/.rvm/gems/.../thoughtbot-shoulda-2.11.1/lib/shoulda/context.rb:382:in `call'
/Users/Daniel/.rvm/gems/.../thoughtbot-shoulda-2.11.1/lib/shoulda/context.rb:382:in `test: A free-trial account An account with no state with an account state history entry set to end
in the future should still be able to end the account state. '
/Users/Daniel/.rvm/gems/.../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `__send__'
/Users/Daniel/.rvm/gems/.../activesupport/lib/active_support/testing/setup_and_teardown.rb:62:in `run'
 (2.221 s) (0.000 s)
kibibyte
  • 3,007
  • 5
  • 30
  • 40
  • You're posting a wall of code but forgot to include the line number where the error is. – Casper Jun 20 '12 at 19:58
  • Also why is `end_with_nil` called 3 times for record id 1007? You're calling it from somewhere else than just your `each` loop. Look at the call stack of your error message. The last entry in your printout is `end_with_nil` but without `history = ...`. It's getting called from some other place in your code. – Casper Jun 20 '12 at 20:06
  • The unit test says it's failing at line 50. Line 50 corresponds to the method signature for `end_with_nil`. I'll edit my question to make that clearer. – kibibyte Jun 20 '12 at 20:17
  • What are you testing with? RSpec, MiniTest? You need to turn on full backtrace to see where it goes wrong. See here: http://stackoverflow.com/questions/7649037/how-to-get-rspec-2-to-give-the-full-trace-associated-with-a-test-failure or here: http://stackoverflow.com/questions/9219461/unit-tests-w-full-stack-traces – Casper Jun 20 '12 at 20:24
  • Without getting too far into the implementation details of the tests, the reason `end_with_nil` is being called 3 times is likely because `account_state_integrity` is being called 3 times. This particular technique is, admittedly, probably not the best way to do this validation, and the validation traverses the history every time the record gets saved. – kibibyte Jun 20 '12 at 20:24
  • I'm not sure what test module we're using (god I feel dumb for saying that). That said, I do have a full trace, which I edited into the post. – kibibyte Jun 20 '12 at 20:36
  • The call stack shows the problem is like I said: it's being called from somewhere else in your code. If it would be called from `account_state_integrity` then that method should appear in the call stack. It's not there, hence somewhere else in your code you are calling `end_with_nil`. You should do a search of all your ruby files for the string `end_with_nil`, and you will find the location. – Casper Jun 20 '12 at 20:43
  • Indeed, `end_with_nil` was actually being called multiple times. But a grep of all files failed to turn up any instances of this being called independently. I decided to see what would happen if if I made all three of those individual methods in `vald_methods` `private`, and suddenly, the weird issue simply disappeared and became the expected RecordInvalid. Any ideas why this seemed to have fixed the issue? Is Rails doing something under the covers that I'm not realizing? None of the `vald_methods` are ever individually called. – kibibyte Jun 20 '12 at 20:56
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/12824/discussion-between-casper-and-kibibyte) – Casper Jun 20 '12 at 21:00

1 Answers1

2

The problem is ActiveRecord::Errors.add is meant to be used when adding errors to attributes.

end_with_nil is not an attribute. You need to use ActiveRecord::Errors.add_to_base instead.

You should correct the other errors.add lines too.

What happens is Shoulda (your test runner) notices that there is an error on an attribute named end_with_nil, and it will try and generate a message for a failed test that goes something like this:

"You have an error for end_with_nil: value was #{record.end_with_nil}, 
 when it should have been xyz".

..and hence end_with_nil gets called without arguments and everything fails.

Casper
  • 33,403
  • 4
  • 84
  • 79
  • +1 upvote because you not only went through the trouble of helping me individually (via chat discussion), but also did a non-trivial amount of research on the source of the problem to come up with a well-done, yet concise, answer. – kibibyte Jun 20 '12 at 22:01