views:

508

answers:

2

I have this Task model:

class Task < ActiveRecord::Base
  acts_as_tree :order => 'sort_order'
end

And I have this test

class TaskTest < Test::Unit::TestCase
  def setup
    @root = create_root
  end

  def test_destroying_a_task_should_destroy_all_of_its_descendants
    d1 = create_task(:parent_id => @root.id, :sort_order => 2)
    d2 = create_task(:parent_id => d1.id, :sort_order => 3)
    d3 = create_task(:parent_id => d2.id, :sort_order => 4)
    d4 = create_task(:parent_id => d1.id, :sort_order => 5)
    assert_equal 5, Task.count

    d1.destroy

    assert_equal @root, Task.find(:first)
    assert_equal 1, Task.count
  end
end

The test is successful: when I destroy d1, it destroys all the descendants of d1. Thus, after the destroy only the root is left.

However, this test is now failing after I have added a before_save callback to the Task. This is the code I added to Task:

before_save :update_descendants_if_necessary

def update_descendants_if_necessary
  handle_parent_id_change if self.parent_id_changed?
  return true
end

def handle_parent_id_change
  self.children.each do |sub_task|
    #the code within the loop is deliberately commented out
  end
end

When I added this code, assert_equal 1, Task.count fails, with Task.count == 4. I think self.children under handled_parent_id_change is the culprit, because when I comment out the self.children.each do |sub_task| block, the test passes again.

Any ideas?

+1  A: 

children is a simple has_many association

This means, when you call .children, it will load them from the database (if not already present). It will then cache them.

I was going to say that your second 'test' will actually be looking at the cached values not the real database, but that shouldn't happen as you are just using Task.count rather than d1.children.count. Hrm

Have you looked at the logs? They will show you the SQL which is being executed. You may see a mysql error in there which will tell you what's going on

Orion Edwards
+3  A: 

I found the bug. The line

d1 = create_task(:parent_id => @root.id, :sort_order => 2)

creates d1. This calls the before_save callback, which in turn calls self.children. As Orion pointed out, this caches the children of d1.

However, at this point, d1 doesn't have any children yet. So d1's cache of children is empty.

Thus, when I try to destroy d1, the program tries to destroy d1's children. It encounters the cache, finds that it is empty, and a result doesn't destroy d2, d3, and d4.

I solved this by changing the task creations like this:

@root.children << (d1 = new_task(:sort_order => 2))
@root.save!

This worked so I'm ok with it :) I think it is also possible to fix this by either reloading d1 (d1.reload) or self.children (self.children(true)) although I didn't try any of these solutions.

gsmendoza