Index: test/unit/issue_test.rb =================================================================== --- test/unit/issue_test.rb (revision 11811) +++ test/unit/issue_test.rb (working copy) @@ -1795,6 +1795,136 @@ assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort end + def test_all_dependent_issues_with_subtask + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_does_not_include_self + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id) + end + + def test_all_dependent_issues_with_parenttask_and_sibling + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue = Issue.generate!(:project => project) + childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id) + + assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id) + end + + def test_all_dependent_issues_with_relation_to_leaf_in_other_tree + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => childIssue2_2, + :relation_type => IssueRelation::TYPE_BLOCKS) + + assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_relation_to_parent_in_other_tree + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => parentIssue2, + :relation_type => IssueRelation::TYPE_BLOCKS) + + assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_transitive_relation + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + independentIssue = Issue.generate!(:project => project) + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => childIssue2_1, + :relation_type => IssueRelation::TYPE_RELATES) + + assert IssueRelation.create(:issue_from => childIssue2_1, + :issue_to => independentIssue, + :relation_type => IssueRelation::TYPE_RELATES) + + assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + end + + def test_all_dependent_issues_with_transitive_relation2 + IssueRelation.delete_all + + project = Project.generate!(:name => "testproject") + + parentIssue1 = Issue.generate!(:project => project) + childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id) + + parentIssue2 = Issue.generate!(:project => project) + childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id) + + independentIssue = Issue.generate!(:project => project) + + assert IssueRelation.create(:issue_from => parentIssue1, + :issue_to => independentIssue, + :relation_type => IssueRelation::TYPE_RELATES) + + assert IssueRelation.create(:issue_from => independentIssue, + :issue_to => childIssue2_1, + :relation_type => IssueRelation::TYPE_RELATES) + + assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort, + parentIssue1.all_dependent_issues.collect(&:id).uniq.sort + + end + def test_all_dependent_issues_with_persistent_circular_dependency IssueRelation.delete_all assert IssueRelation.create!(:issue_from => Issue.find(1), Index: app/models/issue.rb =================================================================== --- app/models/issue.rb (revision 11811) +++ app/models/issue.rb (working copy) @@ -855,17 +855,109 @@ # Returns all the other issues that depend on the issue def all_dependent_issues(except=[]) - except << self + + # The algorithm as a modified bread first search (bfs) + + # The found dependencies dependencies = [] - dependencies += relations_from.map(&:issue_to) - dependencies += children unless leaf? - dependencies.compact! + + # The visited flag for every node (issue) used by the breadth first search + eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before. + + ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of + # the issue when it is processed. + + ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue, + # but its children will not be added to the queue when it is processed. + + eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to + # the queue, but its children have not been added. + + ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but + # the children still need to be processed. + + eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been + # added as dependent issues. It needs no further processing. + + issueStatus = Hash.new(eNOT_DISCOVERED) + + # The queue + queue = [] + + # Initialize the bfs, add start node (self) to the queue + queue << self + issueStatus[self] = ePROCESS_ALL + + while (!queue.empty?) do + + currentIssue = queue.shift + currentIssueStatus = issueStatus[currentIssue] + + dependencies << currentIssue + + # Add parent to queue, if not already in it. + parent = currentIssue.parent + parentStatus = issueStatus[parent] + + if parent && (parentStatus == eNOT_DISCOVERED) && !except.include?(parent) then + + queue << parent + issueStatus[parent] = ePROCESS_RELATIONS_ONLY + + end + + # Add children to queue, but only if they are not already in it and + # the children of the current node need to be processed. + if currentIssue.children && (currentIssueStatus == ePROCESS_CHILDREN_ONLY || currentIssueStatus == ePROCESS_ALL) then + + currentIssue.children.each do |child| + + if (issueStatus[child] == eNOT_DISCOVERED) && !except.include?(child) + queue << child + issueStatus[child] = ePROCESS_ALL + + elsif (issueStatus[child] == eRELATIONS_PROCESSED) && !except.include?(child) + queue << child + issueStatus[child] = ePROCESS_CHILDREN_ONLY + + elsif (issueStatus[child] == ePROCESS_RELATIONS_ONLY) && !except.include?(child) + queue << child + issueStatus[child] = ePROCESS_ALL + end + end + end + + # Add related issues to the queue, if they are not already in it. + currentIssue.relations_from.map(&:issue_to).each do |relatedIssue| + + if (issueStatus[relatedIssue] == eNOT_DISCOVERED) && !except.include?(relatedIssue) then + queue << relatedIssue + issueStatus[relatedIssue] = ePROCESS_ALL + + elsif (issueStatus[relatedIssue] == eRELATIONS_PROCESSED) && !except.include?(relatedIssue) then + queue << relatedIssue + issueStatus[relatedIssue] = ePROCESS_CHILDREN_ONLY + + elsif (issueStatus[relatedIssue] == ePROCESS_RELATIONS_ONLY) && !except.include?(relatedIssue) then + queue << relatedIssue + issueStatus[relatedIssue] = ePROCESS_ALL + end + end + + # Set new status for current issue + if (currentIssueStatus == ePROCESS_ALL) || (currentIssueStatus == ePROCESS_CHILDREN_ONLY) then + issueStatus[currentIssue] = eALL_PROCESSED + + elsif (currentIssueStatus == ePROCESS_RELATIONS_ONLY) then + issueStatus[currentIssue] = eRELATIONS_PROCESSED + end + + end # while + + # Remove the issues from the "except" parameter from the result array dependencies -= except - dependencies += dependencies.map {|issue| issue.all_dependent_issues(except)}.flatten - if parent - dependencies << parent - dependencies += parent.all_dependent_issues(except + parent.descendants) - end + dependencies.delete(self) + dependencies end