You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@ariatosca.apache.org by mx...@apache.org on 2016/10/31 09:19:42 UTC

incubator-ariatosca git commit: CFY-4 reworked task_graph API and added some test [Forced Update!]

Repository: incubator-ariatosca
Updated Branches:
  refs/heads/ARIA-3-api-for-creating-workflows 94bbcf6f8 -> d31aa9c6e (forced update)


CFY-4 reworked task_graph API and added some test


Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/d31aa9c6
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/d31aa9c6
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/d31aa9c6

Branch: refs/heads/ARIA-3-api-for-creating-workflows
Commit: d31aa9c6ec287ed620f814832e884b13148d0c56
Parents: 936e2f9
Author: mxmrlv <mx...@gmail.com>
Authored: Sun Oct 30 17:50:37 2016 +0200
Committer: mxmrlv <mx...@gmail.com>
Committed: Mon Oct 31 11:19:27 2016 +0200

----------------------------------------------------------------------
 aria/workflows/api/task_graph.py            | 184 ++++++++-------
 aria/workflows/builtin/execute_operation.py |   2 +-
 aria/workflows/builtin/heal.py              |   8 +-
 aria/workflows/builtin/install.py           |   7 +-
 aria/workflows/builtin/uninstall.py         |   2 +-
 aria/workflows/builtin/workflows.py         |   4 +-
 tests/workflows/api/test_task_graph.py      | 279 ++++++++++++++---------
 7 files changed, 278 insertions(+), 208 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/api/task_graph.py
----------------------------------------------------------------------
diff --git a/aria/workflows/api/task_graph.py b/aria/workflows/api/task_graph.py
index 2d8ee54..2cb92cb 100644
--- a/aria/workflows/api/task_graph.py
+++ b/aria/workflows/api/task_graph.py
@@ -18,6 +18,7 @@ Task graph. Used by users to build workflows
 """
 
 from uuid import uuid4
+from collections import Iterable
 
 from networkx import DiGraph, topological_sort
 
@@ -29,6 +30,16 @@ class TaskNotInGraphError(Exception):
     pass
 
 
+def _filter_out_empty_tasks(func=None):
+    if func is None:
+        return lambda f: _filter_out_empty_tasks(func=f)
+
+    def _wrapper(task, *tasks, **kwargs):
+        tasks = filter(lambda t: bool(t), [task] + list(tasks))
+        return func(*tasks, **kwargs)
+    return _wrapper
+
+
 class TaskGraph(object):
     """
     A tasks graph builder.
@@ -79,7 +90,7 @@ class TaskGraph(object):
         :yields: Iterator over all tasks which dependency_task depends on
         :raise: TaskNotInGraphError if dependent_task is not in the graph
         """
-        if not self.has_task(dependent_task):
+        if not self.has_tasks(dependent_task):
             raise TaskNotInGraphError('Task id: {0}'.format(dependent_task.id))
         for _, dependency_id in self._graph.out_edges_iter(dependent_task.id):
             yield self.get_task(dependency_id)
@@ -91,7 +102,7 @@ class TaskGraph(object):
         :yields: Iterator over all tasks which depend on dependency_task
         :raise: TaskNotInGraphError if dependency_task is not in the graph
         """
-        if not self.has_task(dependency_task):
+        if not self.has_tasks(dependency_task):
             raise TaskNotInGraphError('Task id: {0}'.format(dependency_task.id))
         for dependent_id, _ in self._graph.in_edges_iter(dependency_task.id):
             yield self.get_task(dependent_id)
@@ -111,38 +122,61 @@ class TaskGraph(object):
         data = self._graph.node[task_id]
         return data['task']
 
-    def add_task(self, task):
+    @_filter_out_empty_tasks
+    def add_tasks(self, *tasks):
         """
         Add a task to the graph
         :param BaseTask task: The task
-        :return: False if task was already in the graph, otherwise True
-        :rtype: bool
+        :return: A list of added tasks
+        :rtype: list
         """
-        if self.has_task(task):
-            return False
-        self._graph.add_node(task.id, task=task)
-        return True
+        return_tasks = []
+
+        for task in tasks:
+            if isinstance(task, Iterable):
+                return_tasks += self.add_tasks(*task)
+            elif not self.has_tasks(task):
+                self._graph.add_node(task.id, task=task)
+                return_tasks.append(task)
+
+        return return_tasks
 
-    def remove_task(self, task):
+    @_filter_out_empty_tasks
+    def remove_tasks(self, *tasks):
         """
         Remove the provided task from the graph
         :param BaseTask task: The task
-        :return: False if task wasn't in the graph, otherwise True
-        :rtype: bool
+        :return: A list of removed tasks
+        :rtype: list
         """
-        if not self.has_task(task):
-            return False
-        self._graph.remove_node(task.id)
-        return True
+        return_tasks = []
+
+        if len(tasks) > 1:
+            for sub_task in tasks:
+                return_tasks += self.remove_tasks(*sub_task)
+        elif self.has_tasks(tasks[0]):
+            self._graph.remove_node(tasks[0].id)
+            return_tasks.append(tasks[0])
+
+        return return_tasks
 
-    def has_task(self, task):
+    @_filter_out_empty_tasks
+    def has_tasks(self, *tasks):
         """
         Check whether a task is in the graph or not
         :param BaseTask task: The task
-        :return: True if task is in the graph, otherwise True
-        :rtype: bool
+        :return: True if all tasks are in the graph, otherwise True
+        :rtype: list
         """
-        return self._graph.has_node(task.id)
+        return_value = True
+
+        for task in tasks:
+            if isinstance(task, Iterable):
+                return_value &= self.has_tasks(*task)
+            else:
+                return_value &= self._graph.has_node(task.id)
+
+        return return_value
 
     def add_dependency(self, dependent, dependency):
         """
@@ -157,14 +191,22 @@ class TaskGraph(object):
         :raise TaskNotInGraphError if either the dependent or dependency are tasks which
          are not in the graph
         """
+        if not (self.has_tasks(dependent) and self.has_tasks(dependency)):
+            raise TaskNotInGraphError()
+
         if self.has_dependency(dependent, dependency):
             return False
 
-        dependent_tasks, dependency_tasks = \
-            self._extract_dependent_and_dependency_tasks(dependent, dependency)
-        for dependent_task in dependent_tasks:
-            for dependency_task in dependency_tasks:
-                self._graph.add_edge(dependent_task.id, dependency_task.id)
+        if isinstance(dependent, Iterable):
+            for dependent_task in dependent:
+                self.add_dependency(dependent_task, dependency)
+        else:
+            if isinstance(dependency, Iterable):
+                for dependency_task in dependency:
+                    self.add_dependency(dependent, dependency_task)
+            else:
+                self._graph.add_edge(dependent.id, dependency.id)
+
         return True
 
     def has_dependency(self, dependent, dependency):
@@ -182,13 +224,22 @@ class TaskGraph(object):
         :raise TaskNotInGraphError if either the dependent or dependency are tasks
          which are not in the graph
         """
-        dependent_tasks, dependency_tasks = \
-            self._extract_dependent_and_dependency_tasks(dependent, dependency)
-        for dependent_task in dependent_tasks:
-            for dependency_task in dependency_tasks:
-                if not self._graph.has_edge(dependent_task.id, dependency_task.id):
-                    return False
-        return True
+        if not (self.has_tasks(dependent) and self.has_tasks(dependency)):
+            raise TaskNotInGraphError()
+
+        return_value = True
+
+        if isinstance(dependent, Iterable):
+            for dependent_task in dependent:
+                return_value &= self.has_dependency(dependent_task, dependency)
+        else:
+            if isinstance(dependency, Iterable):
+                for dependency_task in dependency:
+                    return_value &= self.has_dependency(dependent, dependency_task)
+            else:
+                return_value &= self._graph.has_edge(dependent.id, dependency.id)
+
+        return return_value
 
     def remove_dependency(self, dependent, dependency):
         """
@@ -205,61 +256,30 @@ class TaskGraph(object):
         :raise TaskNotInGraphError if either the dependent or dependency are tasks
          which are not in the graph
         """
+        if not (self.has_tasks(dependent) and self.has_tasks(dependency)):
+            raise TaskNotInGraphError()
+
         if not self.has_dependency(dependent, dependency):
             return False
 
-        dependent_tasks, dependency_tasks = \
-            self._extract_dependent_and_dependency_tasks(dependent, dependency)
-        for dependent_task in dependent_tasks:
-            for dependency_task in dependency_tasks:
-                self._graph.remove_edge(dependent_task.id, dependency_task.id)
-        return True
-
-    def sequence(self, *items):
-        group = self.parallel(*items)
-
-        for i in xrange(1, len(items)):
-            self.add_dependency(items[i], items[i-1])
-
-        return group
-
-    def parallel(self, *items):
-        for item in items:
-            if not isinstance(item, _TasksGroup):
-                self.add_task(item)
-
-        return _TasksGroup(items)
-
-    def _extract_dependent_and_dependency_tasks(self, dependent, dependency):
-        if isinstance(dependent, _TasksGroup):
-            dependent_tasks = dependent.tasks
+        if isinstance(dependent, Iterable):
+            for dependent_task in dependent:
+                self.remove_dependency(dependent_task, dependency)
         else:
-            if not self._graph.has_node(dependent.id):
-                raise TaskNotInGraphError(
-                    'dependent task {0!r} is not in graph (task id: {0.id})'.format(dependent))
-            dependent_tasks = [dependent]
-
-        if isinstance(dependency, _TasksGroup):
-            dependency_tasks = dependency.tasks
-        else:
-            if not self._graph.has_node(dependency.id):
-                raise TaskNotInGraphError(
-                    'dependency task {0!r} is not in graph (task id: {0.id})'.format(dependency))
-            dependency_tasks = [dependency]
-
-        return dependent_tasks, dependency_tasks
+            if isinstance(dependency, Iterable):
+                for dependency_task in dependency:
+                    self.remove_dependency(dependent, dependency_task)
+            else:
+                self._graph.remove_edge(dependent.id, dependency.id)
 
+        return True
 
-class _TasksGroup(object):
+    @_filter_out_empty_tasks
+    def sequence(self, *tasks):
+        if tasks:
+            self.add_tasks(*tasks)
 
-    def __init__(self, tasks):
-        self._tasks = map(lambda t: t.tasks if isinstance(t, _TasksGroup) else t, tasks)
+            for i in xrange(1, len(tasks)):
+                self.add_dependency(tasks[i], tasks[i-1])
 
-    @property
-    def tasks(self):
-        """
-        The tasks in this group
-        :rtype list
-        :return: tasks list
-        """
-        return self._tasks
+        return tasks

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/builtin/execute_operation.py
----------------------------------------------------------------------
diff --git a/aria/workflows/builtin/execute_operation.py b/aria/workflows/builtin/execute_operation.py
index a664904..2843375 100644
--- a/aria/workflows/builtin/execute_operation.py
+++ b/aria/workflows/builtin/execute_operation.py
@@ -76,7 +76,7 @@ def execute_operation(
             allow_kwargs_override=allow_kwargs_override)
 
     for _, node_instance_sub_workflow in subgraphs.items():
-        graph.add_task(node_instance_sub_workflow)
+        graph.add_tasks(node_instance_sub_workflow)
 
     # adding tasks dependencies if required
     if run_by_dependency_order:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/builtin/heal.py
----------------------------------------------------------------------
diff --git a/aria/workflows/builtin/heal.py b/aria/workflows/builtin/heal.py
index 78a8312..efdd7b1 100644
--- a/aria/workflows/builtin/heal.py
+++ b/aria/workflows/builtin/heal.py
@@ -52,8 +52,8 @@ def heal(context, graph, node_instance_id):
         failing_node_instances=failed_node_instance_subgraph,
         targeted_node_instances=targeted_node_instances)
 
-    graph.add_task(uninstall_subgraph)
-    graph.add_task(install_subgraph)
+    graph.add_tasks(uninstall_subgraph)
+    graph.add_tasks(install_subgraph)
     graph.add_dependency(install_subgraph, uninstall_subgraph)
 
 
@@ -75,7 +75,7 @@ def heal_uninstall(context, graph, failing_node_instances, targeted_node_instanc
         node_instance_sub_workflow = context.task_graph(
             name='uninstall_stub_{0}'.format(node_instance.id))
         node_instance_sub_workflows[node_instance.id] = node_instance_sub_workflow
-        graph.add_task(node_instance_sub_workflow)
+        graph.add_tasks(node_instance_sub_workflow)
 
     # Create install sub workflow for each failing node
     uninstall(
@@ -122,7 +122,7 @@ def heal_install(context, graph, failing_node_instances, targeted_node_instances
         node_instance_sub_workflow = context.task_graph(
             name='install_stub_{0}'.format(node_instance.id))
         node_instance_sub_workflows[node_instance.id] = node_instance_sub_workflow
-        graph.add_task(node_instance_sub_workflow)
+        graph.add_tasks(node_instance_sub_workflow)
 
     # create install sub workflow for every node instance
     install(

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/builtin/install.py
----------------------------------------------------------------------
diff --git a/aria/workflows/builtin/install.py b/aria/workflows/builtin/install.py
index 552af90..c6b392f 100644
--- a/aria/workflows/builtin/install.py
+++ b/aria/workflows/builtin/install.py
@@ -42,13 +42,12 @@ def install(context, graph, node_instances=(), node_instance_sub_workflows=None)
             context=context,
             node_instance=node_instance)
         node_instance_sub_workflows[node_instance.id] = node_instance_sub_workflow
-        graph.add_task(node_instance_sub_workflow)
+        graph.add_tasks(node_instance_sub_workflow)
 
     # create dependencies between the node instance sub workflow
     for node_instance in node_instances:
         node_instance_sub_workflow = node_instance_sub_workflows[node_instance.id]
         if node_instance.relationship_instances:
-            dependencies = \
-                graph.parallel(*(node_instance_sub_workflows[relationship.target_id]
-                                 for relationship in node_instance.relationship_instances))
+            dependencies = [node_instance_sub_workflows[relationship_instance.target_id]
+                            for relationship_instance in node_instance.relationship_instances]
             graph.add_dependency(node_instance_sub_workflow, dependencies)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/builtin/uninstall.py
----------------------------------------------------------------------
diff --git a/aria/workflows/builtin/uninstall.py b/aria/workflows/builtin/uninstall.py
index 9659cb9..9557dd8 100644
--- a/aria/workflows/builtin/uninstall.py
+++ b/aria/workflows/builtin/uninstall.py
@@ -42,7 +42,7 @@ def uninstall(context, graph, node_instances=(), node_instance_sub_workflows=Non
             context=context,
             node_instance=node_instance)
         node_instance_sub_workflows[node_instance.id] = node_instance_sub_workflow
-        graph.add_task(node_instance_sub_workflow)
+        graph.add_tasks(node_instance_sub_workflow)
 
     # create dependencies between the node instance sub workflow
     for node_instance in node_instances:

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/aria/workflows/builtin/workflows.py
----------------------------------------------------------------------
diff --git a/aria/workflows/builtin/workflows.py b/aria/workflows/builtin/workflows.py
index 3763f5c..0119e26 100644
--- a/aria/workflows/builtin/workflows.py
+++ b/aria/workflows/builtin/workflows.py
@@ -185,7 +185,7 @@ def execute_operation_on_instance(
         node_instance=node_instance,
         operation_name=operation)
 
-    graph.add_task(
+    graph.add_tasks(
         context.operation(
             name=task_name,
             operation_details=node_instance.node.operations[operation],
@@ -256,4 +256,4 @@ def relationship_tasks(graph,
         node_instance=context.model.node_instance.get(relationship_instance.target_id),
         operation_details=relationship_instance.relationship.target_operations[
             operation_name])
-    return graph.parallel(source_operation, target_operation)
+    return graph.add_tasks(source_operation, target_operation)

http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/d31aa9c6/tests/workflows/api/test_task_graph.py
----------------------------------------------------------------------
diff --git a/tests/workflows/api/test_task_graph.py b/tests/workflows/api/test_task_graph.py
index efa1e9a..a3752ad 100644
--- a/tests/workflows/api/test_task_graph.py
+++ b/tests/workflows/api/test_task_graph.py
@@ -34,18 +34,18 @@ class TestTaskGraphTasks(object):
 
     def test_add_task(self, graph):
         task = MockTask()
-        add_result = graph.add_task(task)
-        assert add_result is True
+        add_result = graph.add_tasks(task)
+        assert add_result == [task]
         tasks = [t for t in graph.tasks]
         assert len(tasks) == 1
         assert tasks[0] == task
 
     def test_add_existing_task(self, graph):
         task = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         # adding a task already in graph - should have no effect, and return False
-        add_result = graph.add_task(task)
-        assert add_result is False
+        add_result = graph.add_tasks(task)
+        assert add_result == []
         tasks = [t for t in graph.tasks]
         assert len(tasks) == 1
         assert tasks[0] == task
@@ -53,21 +53,21 @@ class TestTaskGraphTasks(object):
     def test_remove_task(self, graph):
         task = MockTask()
         other_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(other_task)
-        graph.remove_task(other_task)
+        graph.add_tasks(task)
+        graph.add_tasks(other_task)
+        graph.remove_tasks(other_task)
         tasks = [t for t in graph.tasks]
         assert len(tasks) == 1
         assert tasks[0] == task
 
-    def test_remove_task_with_dependency(self, graph):
+    def test_remove_tasks_with_dependency(self, graph):
         task = MockTask()
         dependent_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependent_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependent_task)
         graph.add_dependency(dependent_task, task)
-        remove_result = graph.remove_task(dependent_task)
-        assert remove_result is True
+        remove_result = graph.remove_tasks(dependent_task)
+        assert remove_result == [dependent_task]
         tasks = [t for t in graph.tasks]
         assert len(tasks) == 1
         assert tasks[0] == task
@@ -77,34 +77,34 @@ class TestTaskGraphTasks(object):
     def test_remove_nonexistent_task(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         # removing a task not in graph - should have no effect, and return False
-        remove_result = graph.remove_task(task_not_in_graph)
-        assert remove_result is False
+        remove_result = graph.remove_tasks(task_not_in_graph)
+        assert remove_result == []
         tasks = [t for t in graph.tasks]
         assert len(tasks) == 1
         assert tasks[0] == task
 
     def test_has_task(self, graph):
         task = MockTask()
-        graph.add_task(task)
-        assert graph.has_task(task) is True
+        graph.add_tasks(task)
+        assert graph.has_tasks(task) is True
 
     def test_has_nonexistent_task(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
-        assert graph.has_task(task_not_in_graph) is False
+        graph.add_tasks(task)
+        assert graph.has_tasks(task_not_in_graph) is False
 
     def test_get_task(self, graph):
         task = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         assert graph.get_task(task.id) == task
 
     def test_get_nonexistent_task(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.get_task(task_not_in_graph.id)
 
@@ -114,8 +114,8 @@ class TestTaskGraphGraphTraversal(object):
     def test_tasks_iteration(self, graph):
         task = MockTask()
         other_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(other_task)
+        graph.add_tasks(task)
+        graph.add_tasks(other_task)
         tasks = [t for t in graph.tasks]
         assert set(tasks) == set([task, other_task])
 
@@ -125,10 +125,10 @@ class TestTaskGraphGraphTraversal(object):
         dependent_task_2 = MockTask()
         transitively_dependent_task = MockTask()
 
-        graph.add_task(task)
-        graph.add_task(dependent_task_1)
-        graph.add_task(dependent_task_2)
-        graph.add_task(transitively_dependent_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependent_task_1)
+        graph.add_tasks(dependent_task_2)
+        graph.add_tasks(transitively_dependent_task)
 
         graph.add_dependency(dependent_task_1, task)
         graph.add_dependency(dependent_task_2, task)
@@ -141,15 +141,15 @@ class TestTaskGraphGraphTraversal(object):
     def test_get_task_empty_dependents(self, graph):
         task = MockTask()
         other_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(other_task)
+        graph.add_tasks(task)
+        graph.add_tasks(other_task)
         dependent_tasks = list(graph.get_task_dependents(task))
         assert len(dependent_tasks) == 0
 
     def test_get_nonexistent_task_dependents(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             list(graph.get_task_dependents(task_not_in_graph))
 
@@ -159,10 +159,10 @@ class TestTaskGraphGraphTraversal(object):
         dependency_task_2 = MockTask()
         transitively_dependency_task = MockTask()
 
-        graph.add_task(task)
-        graph.add_task(dependency_task_1)
-        graph.add_task(dependency_task_2)
-        graph.add_task(transitively_dependency_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task_1)
+        graph.add_tasks(dependency_task_2)
+        graph.add_tasks(transitively_dependency_task)
 
         graph.add_dependency(task, dependency_task_1)
         graph.add_dependency(task, dependency_task_2)
@@ -175,15 +175,15 @@ class TestTaskGraphGraphTraversal(object):
     def test_get_task_empty_dependencies(self, graph):
         task = MockTask()
         other_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(other_task)
+        graph.add_tasks(task)
+        graph.add_tasks(other_task)
         dependency_tasks = list(graph.get_task_dependencies(task))
         assert len(dependency_tasks) == 0
 
     def test_get_nonexistent_task_dependencies(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             list(graph.get_task_dependencies(task_not_in_graph))
 
@@ -194,9 +194,9 @@ class TestTaskGraphDependencies(object):
         task = MockTask()
         dependency_task = MockTask()
         unrelated_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependency_task)
-        graph.add_task(unrelated_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task)
+        graph.add_tasks(unrelated_task)
         add_result = graph.add_dependency(task, dependency_task)
         assert add_result is True
         dependency_tasks = list(graph.get_task_dependencies(task))
@@ -206,8 +206,8 @@ class TestTaskGraphDependencies(object):
     def test_add_existing_dependency(self, graph):
         task = MockTask()
         dependency_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependency_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task)
         graph.add_dependency(task, dependency_task)
         # adding a dependency already in graph - should have no effect, and return False
         add_result = graph.add_dependency(task, dependency_task)
@@ -219,43 +219,43 @@ class TestTaskGraphDependencies(object):
     def test_add_dependency_nonexistent_dependent(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.add_dependency(task_not_in_graph, task)
 
     def test_add_dependency_nonexistent_dependency(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.add_dependency(task, task_not_in_graph)
 
     def test_has_dependency(self, graph):
         task = MockTask()
         dependency_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependency_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task)
         graph.add_dependency(task, dependency_task)
         assert graph.has_dependency(task, dependency_task) is True
 
     def test_has_nonexistent_dependency(self, graph):
         task = MockTask()
         other_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(other_task)
+        graph.add_tasks(task)
+        graph.add_tasks(other_task)
         assert graph.has_dependency(task, other_task) is False
 
     def test_has_dependency_nonexistent_dependent(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.has_dependency(task_not_in_graph, task)
 
     def test_has_dependency_nonexistent_dependency(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.has_dependency(task, task_not_in_graph)
 
@@ -263,9 +263,9 @@ class TestTaskGraphDependencies(object):
         task = MockTask()
         dependency_task = MockTask()
         another_dependent_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependency_task)
-        graph.add_task(another_dependent_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task)
+        graph.add_tasks(another_dependent_task)
         graph.add_dependency(task, dependency_task)
         graph.add_dependency(another_dependent_task, dependency_task)
 
@@ -277,8 +277,8 @@ class TestTaskGraphDependencies(object):
     def test_remove_nonexistent_dependency(self, graph):
         task = MockTask()
         dependency_task = MockTask()
-        graph.add_task(task)
-        graph.add_task(dependency_task)
+        graph.add_tasks(task)
+        graph.add_tasks(dependency_task)
         # removing a dependency not in graph - should have no effect, and return False
         remove_result = graph.remove_dependency(task, dependency_task)
         assert remove_result is False
@@ -288,7 +288,7 @@ class TestTaskGraphDependencies(object):
     def test_remove_dependency_nonexistent_dependent(self, graph):
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.remove_dependency(task_not_in_graph, task)
 
@@ -296,7 +296,7 @@ class TestTaskGraphDependencies(object):
         # in this test the dependency *task* is not in the graph, not just the dependency itself
         task = MockTask()
         task_not_in_graph = MockTask()
-        graph.add_task(task)
+        graph.add_tasks(task)
         with pytest.raises(TaskNotInGraphError):
             graph.remove_dependency(task, task_not_in_graph)
 
@@ -315,8 +315,8 @@ class TestTaskGraphArrangements(object):
     def test_sequence_with_some_tasks_and_dependencies_already_in_graph(self, graph):
         tasks = [MockTask(), MockTask(), MockTask()]
         # insert some tasks and dependencies to the graph
-        graph.add_task(tasks[1])
-        graph.add_task(tasks[2])
+        graph.add_tasks(tasks[1])
+        graph.add_tasks(tasks[2])
         graph.add_dependency(tasks[2], tasks[1])
 
         graph.sequence(*tasks)
@@ -338,10 +338,9 @@ class TestTaskGraphArrangements(object):
         assert len(list(graph.get_task_dependencies(tasks[3]))) == 2
         assert len(list(graph.get_task_dependencies(tasks[4]))) == 3
 
-
     def test_sequence_with_nested_parallel(self, graph):
         tasks = [MockTask() for _ in xrange(5)]
-        graph.sequence(tasks[0], graph.parallel(tasks[1], tasks[2], tasks[3]), tasks[4])
+        graph.sequence(tasks[0], (tasks[1], tasks[2], tasks[3]), tasks[4])
         graph_tasks = [t for t in graph.tasks]
         assert set(graph_tasks) == set(tasks)
         # first task should have no dependencies
@@ -354,7 +353,7 @@ class TestTaskGraphArrangements(object):
 
     def test_parallel(self, graph):
         tasks = [MockTask(), MockTask(), MockTask()]
-        graph.parallel(*tasks)
+        graph.add_tasks(tasks)
         graph_tasks = [t for t in graph.tasks]
         assert set(graph_tasks) == set(tasks)
         for i in xrange(len(tasks)):
@@ -364,40 +363,47 @@ class TestTaskGraphArrangements(object):
         tasks = [MockTask(), MockTask(), MockTask()]
 
         # insert some tasks and dependencies to the graph
-        graph.add_task(tasks[1])
-        graph.add_task(tasks[2])
+        graph.add_tasks(tasks[1])
+        graph.add_tasks(tasks[2])
+        graph.add_tasks(*tasks)
 
-        graph.parallel(*tasks)
-        graph_tasks = [t for t in graph.tasks]
-        assert set(graph_tasks) == set(tasks)
+        assert set(graph.tasks) == set(tasks)
         for i in xrange(len(tasks)):
             assert len(list(graph.get_task_dependencies(tasks[i]))) == 0
 
     def test_parallel_with_nested_sequence(self, graph):
-        tasks = [MockTask() for _ in xrange(5)]
-        graph.parallel(tasks[0], graph.sequence(tasks[1], tasks[2], tasks[3]), tasks[4])
-        graph_tasks = [t for t in graph.tasks]
-        assert set(graph_tasks) == set(tasks)
+        all_tasks = [MockTask() for _ in xrange(5)]
+        graph.add_tasks(all_tasks[0],
+                        graph.sequence(all_tasks[1], all_tasks[2], all_tasks[3]),
+                        all_tasks[4])
+        assert set(graph.tasks) == set(all_tasks)
 
         # tasks[2] and tasks[3] should each have a single dependency; the rest should have none
-        assert len(list(graph.get_task_dependencies(tasks[0]))) == 0
-        assert len(list(graph.get_task_dependencies(tasks[1]))) == 0
-        assert set(graph.get_task_dependencies(tasks[2])) == set([tasks[1]])
-        assert set(graph.get_task_dependencies(tasks[3])) == set([tasks[2]])
-        assert len(list(graph.get_task_dependencies(tasks[4]))) == 0
+        assert len(list(graph.get_task_dependencies(all_tasks[0]))) == 0
+        assert len(list(graph.get_task_dependencies(all_tasks[1]))) == 0
+        assert set(graph.get_task_dependencies(all_tasks[2])) == set([all_tasks[1]])
+        assert set(graph.get_task_dependencies(all_tasks[3])) == set([all_tasks[2]])
+        assert len(list(graph.get_task_dependencies(all_tasks[4]))) == 0
 
     def test_parallel_with_nested_parallel(self, graph):
         tasks = [MockTask() for _ in xrange(5)]
-        graph.parallel(tasks[0], graph.parallel(tasks[1], tasks[2], tasks[3]), tasks[4])
+        graph.add_tasks(tasks[0], (tasks[1], tasks[2], tasks[3]), tasks[4])
         graph_tasks = [t for t in graph.tasks]
         assert set(graph_tasks) == set(tasks)
         # none of the tasks should have any dependencies
         for i in xrange(len(tasks)):
             assert len(list(graph.get_task_dependencies(tasks[i]))) == 0
 
-    def test_sequence_and_parallel_deep_nesting(self, graph):
-        # including connecting seq to parallel
-        pass
+    def test_group_with_twice_nested_group(self, graph):
+        twice_nested_tasks = [MockTask(), MockTask(), MockTask()]
+        once_nested_tasks = [MockTask(), MockTask(), MockTask(), twice_nested_tasks]
+        tasks = [MockTask(), MockTask(), MockTask(), once_nested_tasks]
+        graph.add_tasks(*tasks)
+
+        assert set(graph.tasks) == set(tasks[:3] + once_nested_tasks[:3] + twice_nested_tasks)
+        for tasks_list in [tasks, once_nested_tasks, twice_nested_tasks]:
+            for i in xrange(len(tasks_list[:3])):
+                assert len(list(graph.get_task_dependencies(tasks_list[i]))) == 0
 
 
 class TestTaskGraphDependenciesWithArrangements(object):
@@ -405,7 +411,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_add_dependency_dependent_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         graph.add_dependency(sequence, task)
         assert graph.has_dependency(sequence_tasks[0], task) is True
@@ -415,7 +421,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_add_dependency_dependency_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         graph.add_dependency(task, sequence)
         assert graph.has_dependency(task, sequence_tasks[0]) is True
@@ -425,8 +431,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_add_dependency_dependent_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(parallel, task)
         assert graph.has_dependency(parallel_tasks[0], task) is True
         assert graph.has_dependency(parallel_tasks[1], task) is True
@@ -435,8 +441,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_add_dependency_dependency_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(task, parallel)
         assert graph.has_dependency(task, parallel_tasks[0]) is True
         assert graph.has_dependency(task, parallel_tasks[1]) is True
@@ -446,7 +452,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(sequence, parallel)
         for parallel_task in parallel_tasks:
             assert graph.has_dependency(sequence_tasks[0], parallel_task) is True
@@ -457,7 +463,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(parallel, sequence)
         for parallel_task in parallel_tasks:
             assert graph.has_dependency(parallel_task, sequence_tasks[0]) is True
@@ -467,8 +473,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_add_dependency_dependency_parallel_with_some_existing_dependencies(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         # adding a dependency on a specific task manually,
         # before adding a dependency on the whole parallel
         graph.add_dependency(task, parallel_tasks[1])
@@ -481,7 +487,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         add_result = graph.add_dependency(sequence, parallel)
         assert add_result is True
         # adding a dependency already in graph - should have no effect, and return False
@@ -495,7 +501,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_has_dependency_dependent_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         assert graph.has_dependency(sequence, task) is False
         graph.add_dependency(sequence, task)
@@ -504,7 +510,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_has_dependency_dependency_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         assert graph.has_dependency(task, sequence) is False
         graph.add_dependency(task, sequence)
@@ -513,8 +519,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_has_dependency_dependent_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         assert graph.has_dependency(parallel, task) is False
         graph.add_dependency(parallel, task)
         assert graph.has_dependency(parallel, task) is True
@@ -522,8 +528,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_has_dependency_dependency_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         assert graph.has_dependency(task, parallel) is False
         graph.add_dependency(task, parallel)
         assert graph.has_dependency(task, parallel) is True
@@ -532,7 +538,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         assert graph.has_dependency(sequence, parallel) is False
         graph.add_dependency(sequence, parallel)
         assert graph.has_dependency(sequence, parallel) is True
@@ -541,7 +547,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         assert graph.has_dependency(parallel, sequence) is False
         graph.add_dependency(parallel, sequence)
         assert graph.has_dependency(parallel, sequence) is True
@@ -549,8 +555,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_has_dependency_dependency_parallel_with_some_existing_dependencies(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(task, parallel_tasks[1])
         # only a partial dependency exists - has_dependency is expected to return False
         assert graph.has_dependency(task, parallel) is False
@@ -559,13 +565,13 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         assert graph.has_dependency(sequence, parallel) is False
 
     def test_remove_dependency_dependent_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         graph.add_dependency(sequence, task)
         remove_result = graph.remove_dependency(sequence, task)
@@ -577,7 +583,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_remove_dependency_dependency_sequence(self, graph):
         task = MockTask()
         sequence_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
+        graph.add_tasks(task)
         sequence = graph.sequence(*sequence_tasks)
         graph.add_dependency(task, sequence)
         remove_result = graph.remove_dependency(task, sequence)
@@ -589,8 +595,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_remove_dependency_dependent_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(parallel, task)
         remove_result = graph.remove_dependency(parallel, task)
         assert remove_result is True
@@ -601,8 +607,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_remove_dependency_dependency_parallel(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(task, parallel)
         remove_result = graph.remove_dependency(task, parallel)
         assert remove_result is True
@@ -614,7 +620,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(sequence, parallel)
         remove_result = graph.remove_dependency(sequence, parallel)
         assert remove_result is True
@@ -627,7 +633,7 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(parallel, sequence)
         remove_result = graph.remove_dependency(parallel, sequence)
         assert remove_result is True
@@ -639,8 +645,8 @@ class TestTaskGraphDependenciesWithArrangements(object):
     def test_remove_dependency_dependency_parallel_with_some_existing_dependencies(self, graph):
         task = MockTask()
         parallel_tasks = [MockTask() for _ in xrange(3)]
-        graph.add_task(task)
-        parallel = graph.parallel(*parallel_tasks)
+        graph.add_tasks(task)
+        parallel = graph.add_tasks(*parallel_tasks)
         graph.add_dependency(task, parallel_tasks[1])
         remove_result = graph.remove_dependency(task, parallel)
         # only a partial dependency exists - remove_dependency is expected to return False
@@ -654,7 +660,52 @@ class TestTaskGraphDependenciesWithArrangements(object):
         sequence_tasks = [MockTask() for _ in xrange(3)]
         parallel_tasks = [MockTask() for _ in xrange(3)]
         sequence = graph.sequence(*sequence_tasks)
-        parallel = graph.parallel(*parallel_tasks)
+        parallel = graph.add_tasks(*parallel_tasks)
         # removing a dependency not in graph - should have no effect, and return False
         remove_result = graph.remove_dependency(sequence, parallel)
         assert remove_result is False
+
+    def test_sequence_with_twice_nested_sequence_and_interdependencies(self, graph):
+        twice_nested_tasks = list(graph.sequence(MockTask(), MockTask(), MockTask()))
+        once_nested_tasks = list(graph.sequence(MockTask(), MockTask(), MockTask(), twice_nested_tasks))
+        tasks = [MockTask(), MockTask(), MockTask(), once_nested_tasks]
+        graph.sequence(*tasks)
+
+        assert set(graph.tasks) == set(tasks[:3] + once_nested_tasks[:3] + twice_nested_tasks)
+        assert set(graph.get_task_dependencies(tasks[0])) == set()
+        for i in xrange(1, len(tasks[:-1])):
+            assert set(graph.get_task_dependencies(tasks[i])) == set([tasks[i-1]])
+
+        assert set(graph.get_task_dependencies(once_nested_tasks[0])) == set([tasks[2]])
+        for i in xrange(1, len(once_nested_tasks[:-1])):
+            assert set(graph.get_task_dependencies(once_nested_tasks[i])) == \
+                   set([tasks[2], once_nested_tasks[i-1]])
+
+        assert set(graph.get_task_dependencies(twice_nested_tasks[0])) == \
+               set([tasks[2], once_nested_tasks[2]])
+        for i in xrange(1, len(twice_nested_tasks[:-1])):
+            assert set(graph.get_task_dependencies(twice_nested_tasks[i])) == \
+                   set([tasks[2], once_nested_tasks[2], twice_nested_tasks[i-1]])
+
+    def test_group_with_twice_nested_group_and_interdependencies(self, graph):
+        twice_nested_tasks = [MockTask(), MockTask(), MockTask()]
+        once_nested_tasks = [MockTask(), MockTask(), MockTask(), twice_nested_tasks]
+        tasks = [MockTask(), MockTask(), MockTask(), once_nested_tasks]
+        graph.add_tasks(*tasks)
+
+        graph.add_dependency(tasks[2], once_nested_tasks[2])
+        graph.add_dependency(once_nested_tasks[1], twice_nested_tasks[0])
+        graph.add_dependency(twice_nested_tasks[1], tasks[0])
+
+        assert set(graph.tasks) == set(tasks[:3] + once_nested_tasks[:3] + twice_nested_tasks)
+        assert set(graph.get_task_dependencies(tasks[0])) == set()
+        assert set(graph.get_task_dependencies(tasks[1])) == set()
+        assert set(graph.get_task_dependencies(tasks[2])) == set([once_nested_tasks[2]])
+
+        assert set((graph.get_task_dependencies(once_nested_tasks[0]))) == set()
+        assert set(graph.get_task_dependencies(once_nested_tasks[1])) == set([twice_nested_tasks[0]])
+        assert set(graph.get_task_dependencies(once_nested_tasks[2])) == set()
+
+        assert set((graph.get_task_dependencies(twice_nested_tasks[0]))) == set()
+        assert set(graph.get_task_dependencies(twice_nested_tasks[1])) == set([tasks[0]])
+        assert set(graph.get_task_dependencies(twice_nested_tasks[2])) == set()