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?
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.