diff --git a/app/models/issue.rb b/app/models/issue.rb index 72d31cedb..f8700e9be 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -854,18 +854,99 @@ class Issue < ActiveRecord::Base end # Returns all the other issues that depend on the issue + # The algorithm is a modified breadth first search (bfs) def all_dependent_issues(except=[]) - except << self + # 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. + + issue_status = Hash.new(eNOT_DISCOVERED) + + # The queue + queue = [] + + # Initialize the bfs, add start node (self) to the queue + queue << self + issue_status[self] = ePROCESS_ALL + + while (!queue.empty?) do + current_issue = queue.shift + current_issue_status = issue_status[current_issue] + dependencies << current_issue + + # Add parent to queue, if not already in it. + parent = current_issue.parent + parent_status = issue_status[parent] + + if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent) + queue << parent + issue_status[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 (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL) + current_issue.children.each do |child| + next if except.include?(child) + + if (issue_status[child] == eNOT_DISCOVERED) + queue << child + issue_status[child] = ePROCESS_ALL + elsif (issue_status[child] == eRELATIONS_PROCESSED) + queue << child + issue_status[child] = ePROCESS_CHILDREN_ONLY + elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY) + queue << child + issue_status[child] = ePROCESS_ALL + end + end + end + + # Add related issues to the queue, if they are not already in it. + current_issue.relations_from.map(&:issue_to).each do |related_issue| + next if except.include?(related_issue) + + if (issue_status[related_issue] == eNOT_DISCOVERED) + queue << related_issue + issue_status[related_issue] = ePROCESS_ALL + elsif (issue_status[related_issue] == eRELATIONS_PROCESSED) + queue << related_issue + issue_status[related_issue] = ePROCESS_CHILDREN_ONLY + elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY) + queue << related_issue + issue_status[related_issue] = ePROCESS_ALL + end + end + + # Set new status for current issue + if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY) + issue_status[current_issue] = eALL_PROCESSED + elsif (current_issue_status == ePROCESS_RELATIONS_ONLY) + issue_status[current_issue] = 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 diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb index 1b35c0595..dea7d2797 100644 --- a/test/unit/issue_test.rb +++ b/test/unit/issue_test.rb @@ -1792,6 +1792,136 @@ class IssueTest < ActiveSupport::TestCase 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),