From 660358deb295eac1c30842925b6a6b851a8326b0 Mon Sep 17 00:00:00 2001 From: Andreas Gebhardt Date: Sun, 24 Sep 2023 19:41:27 +0200 Subject: [PATCH] [WIP] --- graph/planner/direct_acyclic_graph.py | 6 + graph/planner/planner.py | 269 +++++---- graph/planner/planner_test.py | 823 ++++++++++++++++++++++++-- 3 files changed, 933 insertions(+), 165 deletions(-) diff --git a/graph/planner/direct_acyclic_graph.py b/graph/planner/direct_acyclic_graph.py index 4dde647..234e5b8 100644 --- a/graph/planner/direct_acyclic_graph.py +++ b/graph/planner/direct_acyclic_graph.py @@ -81,6 +81,9 @@ def _topsort(self): result.reverse() self._topological_order = result + def has_node(self, node) -> bool: + return node in self._g + def nodes(self) -> set[str]: return {item[0] for item in self._g.items()} @@ -90,6 +93,9 @@ def topological_order(self) -> list[str]: def inverse(self) -> dict[str, list[AdjacentEdge]]: return self._g_inverse + def dependent_edge_types(self, node) -> set[EdgeType]: + return {adjacent[1] for adjacent in self._g_inverse[node]} + def childs( self, node: str, diff --git a/graph/planner/planner.py b/graph/planner/planner.py index 408c29f..1b62797 100644 --- a/graph/planner/planner.py +++ b/graph/planner/planner.py @@ -1,8 +1,19 @@ -from typing import Union, Tuple, List +from dataclasses import dataclass, field +from typing import Union from direct_acyclic_graph import DirectAcyclicGraph, EdgeType +@dataclass +class NodeState: + user_selected: bool = True + primary_action: None | str = None # TODO set? + transitive_action: None | str = None # TODO set? + state: set[str] = field(default_factory=set) + transitive_state: set[str] = field(default_factory=set) + dependent_edge_types: set[EdgeType] = field(default_factory=set) + + class Planner: _state: DirectAcyclicGraph _target: DirectAcyclicGraph @@ -24,141 +35,151 @@ def __init__( self._unhealthy = unhealthy or [] self._selected = selected - def apply(self) -> list[str]: + def apply(self) -> tuple[dict[str, NodeState], list[str]]: nodes_state = self._state.nodes() nodes_target = self._target.nodes() - nodes_add = set() - nodes_delete = set() - nodes_with_edge_type_tool_delete = set() - modified_transitive = set() - unhealthy_transitive = set() - - if self._selected == "*": - nodes_add = {node for node in nodes_target - nodes_state} - nodes_delete = {node for node in nodes_state - nodes_target} - - for node in self._modified: - modified_transitive = modified_transitive.union( - self._state.childs_transitive( - node, accept=lambda adjacent: adjacent[1] != EdgeType.TOOL - ).union({node}) - ) - for node in self._unhealthy: - unhealthy_transitive = unhealthy_transitive.union( - self._state.childs_transitive( - node, accept=lambda adjacent: adjacent[1] != EdgeType.TOOL - ).union({node}) - ) - else: - # partial apply mode - for item in self._selected: - if item.startswith("-"): - # delete case - node = str(item[1:]) - has_parent_with_edge_type_tool = len(self._state.parents(node)) > 0 - if has_parent_with_edge_type_tool: - parents_edge_type_tool_transitive = ( - self._state.parents_transitive( - node, - accept=lambda adjacent: adjacent[1] == EdgeType.TOOL - and adjacent[0] in self._unhealthy, - ) - ) - unhealthy_transitive = unhealthy_transitive.union( - parents_edge_type_tool_transitive - ) - - childs_transitive = self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, - ).union({node}) - - nodes_with_edge_type_tool_delete = ( - nodes_with_edge_type_tool_delete.union(childs_transitive) - ) - - else: - # use state (graph) instead of target, because target will be mostly ignored in this mode - childs_transitive = self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, - ).union({node}) - - nodes_delete = nodes_delete.union(childs_transitive) - - else: - node = item - - is_node_modified = node in self._modified - is_node_unhealthy = node in self._unhealthy - - if ( - is_node_modified or is_node_unhealthy - ): # and node in node_states <- implicit - childs_transitive = self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, - ).union({node}) - - key = "modified" - if is_node_modified: - modified_transitive = modified_transitive.union( - childs_transitive - ) - if is_node_unhealthy and not is_node_modified: - key = "unhealthy" - unhealthy_transitive = unhealthy_transitive.union( - childs_transitive - ) - - elif node in nodes_target: - node_with_parents_transitive = self._target.parents_transitive( - node - ).union({node}) - nodes_add = node_with_parents_transitive.difference(nodes_state) - - for node_add in nodes_add: - parent_transitive = self._target.parents_transitive( - node_add, - accept=lambda adjacent: ( - adjacent[0] in self._unhealthy - ), - ) - unhealthy_transitive = unhealthy_transitive.union( - parent_transitive - ) - - node_action = {node: None for node in nodes_state.union(nodes_target)} - + nodes_add = (nodes_target - nodes_state).intersection( + (nodes_target - nodes_state) + if self._selected == "*" + else {node for node in self._selected if not node.startswith("-")} + ) + nodes_delete = ( + (nodes_state - nodes_target) + if self._selected == "*" + else {node[1:] for node in self._selected if node.startswith("-")} + ) + nodes_selected = ( + nodes_state.union(nodes_target) + if self._selected == "*" + else { + node if not node.startswith("-") else node[1:] + for node in self._selected + } + ) + + nodes = { + node: NodeState( + user_selected=node in nodes_selected, + primary_action=( + "add" + if node in nodes_add + else "delete" + if node in nodes_delete + else None + ), + state=({"modified"} if node in self._modified else set()).union( + {"unhealthy"} if node in self._unhealthy else set() + ), + dependent_edge_types=( + self._state.dependent_edge_types(node) + if self._state.has_node(node) + else set() + ).union( + self._target.dependent_edge_types(node) + if self._target.has_node(node) + else set() + ), + ) + for node in (nodes_state.union(nodes_target)) + } + + # add required nodes by parent dependency for node in nodes_add: - node_action[node] = "add" + parent_transitive = self._target.parents_transitive(node) + for node_add_transitive in parent_transitive: + if node_add_transitive not in nodes_state: + nodes[node_add_transitive].transitive_action = "add" + # delete child nodes for node in nodes_delete: - node_action[node] = "delete" - - for node in modified_transitive: - node_action[node] = "modified" - - for node in unhealthy_transitive: - node_action[node] = "unhealthy" + childs_transitive = self._state.childs_transitive( + node, + accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, + ) + for node_transitive_delete in childs_transitive: + # if nodes[node_transitive_delete].primary_action != "delete": + nodes[node_transitive_delete].transitive_action = "delete" + + # propagate 'modified'/'unhealthy' state + unhealthy_adjacent_tool_edge_nodes = set() + for key, value in nodes.items(): + node = key + state = value.state + # TODO test if reachable/required from user selected nodes + if "unhealthy" in state: + # propagate 'unhealthy' state to children != EdgeType.TOOL + childs_transitive = self._state.childs_transitive( + node, + accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, + ) + for node_transitive_state in childs_transitive: + nodes[node_transitive_state].transitive_state = nodes[ + node_transitive_state + ].transitive_state.union({"unhealthy"}) + + # collect nodes ('unhealthy')' w/ EdgeType.TOOL in reverse graph ~> incoming edge(s) + if EdgeType.TOOL in value.dependent_edge_types: + unhealthy_adjacent_tool_edge_nodes.add(node) + + if "modified" in state and value.user_selected: + # propagate 'modified' state to children != EdgeType.TOOL + childs_transitive = self._state.childs_transitive( + node, + accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, + ) + for node_transitive_state in childs_transitive: + nodes[node_transitive_state].transitive_state = nodes[ + node_transitive_state + ].transitive_state.union({"modified"}) operations = [] + # recover unhealthy nodes (required by dependent 'active' nodes) + if len(unhealthy_adjacent_tool_edge_nodes) > 0: + for node in reversed(self._state.topological_order()): + if node in unhealthy_adjacent_tool_edge_nodes: + operations.append(f"-{node}") + for node in self._state.topological_order(): + if node in unhealthy_adjacent_tool_edge_nodes: + operations.append(f"+{node}") + + # TODO re-run w/ updated unhealthy list + + nodes_re_add_modified_or_unhealthy = ( + [] + ) # optimization, not evaluate modified/unhealthy again + # 'delete' run + # TODO postpone nodes wich are required as tool for node in self._state.topological_order(): - if node_action[node] in ["delete", "modified", "unhealthy"]: + value = nodes[node] + + modified = ( + "modified" in value.state + and value.user_selected + or "modified" in value.transitive_state + ) + unhealthy = ( + "unhealthy" in value.state and value.user_selected + ) or "unhealthy" in value.transitive_state + delete = ( + "delete" == value.primary_action or "delete" == value.transitive_action + ) + + if not delete and (modified or unhealthy): + operations.append(f"-{node}") + nodes_re_add_modified_or_unhealthy.append(node) + + if delete: operations.append(f"-{node}") + # 'add' run for node in reversed(self._target.topological_order()): - if node_action[node] in ["add", "modified", "unhealthy"]: - operations.append(f"+{node}") + value = nodes[node] - # handle node deletion of nodes w/ edges of type TOOL - node_action = {node: None for node in nodes_state.union(nodes_target)} - for node in nodes_with_edge_type_tool_delete: - node_action[node] = "delete" + add = "add" == value.primary_action or "add" == value.transitive_action + if add or node in nodes_re_add_modified_or_unhealthy: + operations.append(f"+{node}") - for node in self._state.topological_order(): - if node_action[node] in ["delete"]: - operations.append(f"-{node}") + # TODO delete postponed nodes required as tool but selected for 'delete' - return operations + return nodes, operations diff --git a/graph/planner/planner_test.py b/graph/planner/planner_test.py index 19ae392..450f1bd 100644 --- a/graph/planner/planner_test.py +++ b/graph/planner/planner_test.py @@ -1,8 +1,7 @@ import unittest -from direct_acyclic_graph import DirectAcyclicGraph, dependency, tool -from planner import Planner - +from direct_acyclic_graph import DirectAcyclicGraph, dependency, tool, EdgeType +from planner import Planner, NodeState DAG1 = DirectAcyclicGraph( g={ @@ -44,13 +43,46 @@ class PlanerTestCase(unittest.TestCase): def test_no_action(self): planner = Planner(state=DAG1, target=DAG1) - self.assertEqual([], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(), + "b": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + [], + ), + planner.apply(), + ) def test_apply_DAG1_modified_multiple(self): planner = Planner(state=DAG1, target=DAG1, modified=["b", "d"]) self.assertEqual( - ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"modified"}), + "b": NodeState( + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + transitive_state={"modified"}, + ), + "c": NodeState( + state=set(), + dependent_edge_types={EdgeType.DEPENDENCY}, + transitive_state={"modified"}, + ), + "d": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + state=set(), dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) @@ -58,7 +90,25 @@ def test_apply_DAG1_unhealthy_multiple(self): planner = Planner(state=DAG1, target=DAG1, unhealthy=["b", "d"]) self.assertEqual( - ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + state={"unhealthy"}, + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) @@ -68,7 +118,26 @@ def test_apply_DAG1_unhealthy_and_modified(self): ) self.assertEqual( - ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"modified", "unhealthy"}), + "b": NodeState( + state={"modified", "unhealthy"}, + transitive_state={"modified", "unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"modified", "unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"modified", "unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) @@ -76,7 +145,25 @@ def test_apply_DAG1_unhealthy_or_modified(self): planner = Planner(state=DAG1, target=DAG1, modified=["b"], unhealthy=["d"]) self.assertEqual( - ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"modified", "unhealthy"}), + "b": NodeState( + state={"modified"}, + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + }, + ["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) @@ -84,7 +171,27 @@ def test_apply_DAG1_modified_root(self): planner = Planner(state=DAG1, target=DAG1, modified=["e"]) self.assertEqual( - ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"modified"}), + "b": NodeState( + transitive_state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + transitive_state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) @@ -92,19 +199,68 @@ def test_apply_DAG1_unhealthy_root(self): planner = Planner(state=DAG1, target=DAG1, unhealthy=["e"]) self.assertEqual( - ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], + ( + { + "a": NodeState(transitive_state={"unhealthy"}), + "b": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + transitive_state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + state={"unhealthy"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-c", "-d", "-e", "+e", "+d", "+c", "+b", "+a"], + ), planner.apply(), ) def test_apply_DAG3_modified_single(self): planner = Planner(state=DAG3, target=DAG3, modified=["d"]) - self.assertEqual(["-d", "+d"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState( + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL} + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(state={"modified"}), + }, + ["-d", "+d"], + ), + planner.apply(), + ) def test_apply_DAG3_modified_root(self): planner = Planner(state=DAG3, target=DAG3, modified=["a"]) - self.assertEqual(["-d", "-b", "-a", "+a", "+b", "+d"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + state={"modified"}, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + transitive_state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState(transitive_state={"modified"}), + }, + ["-d", "-b", "-a", "+a", "+b", "+d"], + ), + planner.apply(), + ) def test_apply_add_to_empty(self): planner = Planner( @@ -113,7 +269,17 @@ def test_apply_add_to_empty(self): ) self.assertEqual( - ["+b", "+a"], + ( + { + "a": NodeState(primary_action="add"), + "b": NodeState( + transitive_action="add", + primary_action="add", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + }, + ["+b", "+a"], + ), planner.apply(), ) @@ -125,7 +291,19 @@ def test_apply_add_to_existing(self): ), ) - self.assertEqual(["+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(), + "b": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["+c"], + ), + planner.apply(), + ) def test_apply_remove_all(self): planner = Planner( @@ -134,7 +312,16 @@ def test_apply_remove_all(self): ) self.assertEqual( - ["-a", "-b"], + ( + { + "a": NodeState(primary_action="delete", transitive_action="delete"), + "b": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + }, + ["-a", "-b"], + ), planner.apply(), ) @@ -142,7 +329,25 @@ def test_apply_DAG3_remove_all(self): planner = Planner(state=DAG3, target=DirectAcyclicGraph(g={})) self.assertEqual( - ["-d", "-c", "-b", "-a"], + ( + { + "a": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + primary_action="delete", + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState(primary_action="delete", transitive_action="delete"), + }, + ["-d", "-c", "-b", "-a"], + ), planner.apply(), ) @@ -153,7 +358,14 @@ def test_add_and_delete(self): ) self.assertEqual( - ["-b", "+c"], + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState(primary_action="delete"), + "c": NodeState(primary_action="add"), + }, + ["-b", "+c"], + ), planner.apply(), ) @@ -166,13 +378,69 @@ def test_modified_but_delete(self): modified=["c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "b": NodeState(), + "c": NodeState(primary_action="delete", state={"modified"}), + }, + ["-c"], + ), + planner.apply(), + ) def test_apply_delete_DAG2_temporarily(self): planner = Planner(state=DAG2, target=DAG2, selected=["-c1d1"]) self.assertEqual( - ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c2d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + ), planner.apply(), ) @@ -195,19 +463,114 @@ def test_apply_delete_DAG2_temporarily_with_target_changes(self): ) self.assertEqual( - ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c2d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + ), planner.apply(), ) def test_apply_delete_roots_DAG2_temporarily(self): planner = Planner(state=DAG2, target=DAG2, selected=["-c1d1", "-c2d2"]) - actual = planner.apply() self.assertEqual( - ["-a", "-b", "-d", "-d2", "-d1", "-c", "-c2", "-c2d2", "-c1", "-c1d1"], - actual, + ( + { + "a": NodeState(user_selected=False, transitive_action="delete"), + "b": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c1d1": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c2d2": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d1": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d2": NodeState( + user_selected=False, + transitive_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "-d", "-d2", "-d1", "-c", "-c2", "-c2d2", "-c1", "-c1d1"], + ), + planner.apply(), ) - self.assertNotIn("e", actual) def test_apply_delete_with_modified_one_TOOL_edge(self): planner = Planner( @@ -218,7 +581,23 @@ def test_apply_delete_with_modified_one_TOOL_edge(self): modified=["b"], selected=["-c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-c"], + ), + planner.apply(), + ) def test_apply_delete_with_unhealthy_one_TOOL_edge(self): planner = Planner( @@ -229,7 +608,23 @@ def test_apply_delete_with_unhealthy_one_TOOL_edge(self): unhealthy=["b"], selected=["-c"], ) - self.assertEqual(["-b", "+b", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b", "+b", "-c"], + ), + planner.apply(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -242,7 +637,26 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): modified=["b2"], selected=["-c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "b2": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-c"], + ), + planner.apply(), + ) def test_apply_delete_with_one_unhealthy_two_TOOL_edge_in_chain(self): planner = Planner( @@ -255,7 +669,26 @@ def test_apply_delete_with_one_unhealthy_two_TOOL_edge_in_chain(self): unhealthy=["b2"], selected=["-c"], ) - self.assertEqual(["-b2", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "b2": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b2", "+b2", "-c"], + ), + planner.apply(), + ) def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -268,7 +701,28 @@ def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): modified=["b1", "b2"], selected=["-c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-c"], + ), + planner.apply(), + ) def test_apply_delete_with_two_unhealthy_two_TOOL_edge_in_chain(self): planner = Planner( @@ -281,7 +735,28 @@ def test_apply_delete_with_two_unhealthy_two_TOOL_edge_in_chain(self): unhealthy=["b1", "b2"], selected=["-c"], ) - self.assertEqual(["-b2", "-b1", "+b1", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="delete"), + }, + ["-b1", "-b2", "+b2", "+b1", "-c"], + ), + planner.apply(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain_skip(self): planner = Planner( @@ -294,24 +769,104 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain_skip(self): modified=["b1"], selected=["-c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b1": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "b2": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "c": NodeState(primary_action="delete"), + }, + ["-c"], + ), + planner.apply(), + ) def test_no_action_apply_selected(self): planner = Planner(state=DAG1, target=DAG1, modified=[], selected=["c"]) - self.assertEqual([], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(user_selected=False), + "b": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "c": NodeState(dependent_edge_types={EdgeType.DEPENDENCY}), + "d": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + [], + ), + planner.apply(), + ) def test_apply_DAG1_modified_multiple_single_selected(self): planner = Planner(state=DAG1, target=DAG1, modified=["b", "d"], selected=["b"]) - self.assertEqual(["-a", "-b", "+b", "+a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(transitive_state={"modified"}, user_selected=False), + "b": NodeState( + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "c": NodeState( + user_selected=False, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "d": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "e": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["-a", "-b", "+b", "+a"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target(self): planner = Planner( state=DirectAcyclicGraph(g={"a": dependency()}), target=DAG3, selected=["c"] ) - self.assertEqual(["+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + transitive_action="add", + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + ["+b", "+c"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target_modified(self): planner = Planner( @@ -321,7 +876,28 @@ def test_apply_DAG3_selected_target_modified(self): selected=["c"], ) - self.assertEqual(["+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + user_selected=False, + transitive_action="add", + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + ["+b", "+c"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target_unhealthy(self): planner = Planner( @@ -331,7 +907,29 @@ def test_apply_DAG3_selected_target_unhealthy(self): selected=["c"], ) - self.assertEqual(["-a", "+a", "+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + "b": NodeState( + user_selected=False, + transitive_action="add", + dependent_edge_types={EdgeType.DEPENDENCY, EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="add", dependent_edge_types={EdgeType.DEPENDENCY} + ), + "d": NodeState(user_selected=False), + }, + # TODO? ["-a", "+a", "+b", "+c"], + ["+b", "+c"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): planner = Planner( @@ -343,7 +941,23 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): selected=["c"], ) - self.assertEqual(["+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="add"), + }, + ["+c"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target_unhealthy_one_TOOL_edge(self): planner = Planner( @@ -355,7 +969,23 @@ def test_apply_DAG3_selected_target_unhealthy_one_TOOL_edge(self): selected=["c"], ) - self.assertEqual(["-b", "+b", "+c"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState(primary_action="add"), + }, + ["-b", "+b", "+c"], + ), + planner.apply(), + ) def test_apply_DAG3_selected_target_modified_one_TOOL_edge_chain_skip(self): planner = Planner( @@ -373,7 +1003,31 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge_chain_skip(self): selected=["e"], ) - self.assertEqual(["+d", "+e"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState( + user_selected=False, dependent_edge_types={EdgeType.TOOL} + ), + "d": NodeState( + user_selected=False, + dependent_edge_types={EdgeType.DEPENDENCY}, + transitive_action="add", + ), + "e": NodeState(primary_action="add"), + }, + ["+d", "+e"], + ), + planner.apply(), + ) def test_apply_selected_TOOL_edge_modified(self): planner = Planner( @@ -383,7 +1037,20 @@ def test_apply_selected_TOOL_edge_modified(self): selected=["-a"], ) - self.assertEqual(["-a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(primary_action="delete"), + "b": NodeState( + user_selected=False, + state={"modified"}, + dependent_edge_types={EdgeType.TOOL}, + ), + }, + ["-a"], + ), + planner.apply(), + ) def test_apply_selected_TOOL_edge_unhealthy(self): planner = Planner( @@ -393,4 +1060,78 @@ def test_apply_selected_TOOL_edge_unhealthy(self): selected=["-a"], ) - self.assertEqual(["-b", "+b", "-a"], planner.apply()) + self.assertEqual( + ( + { + "a": NodeState(primary_action="delete"), + "b": NodeState( + user_selected=False, + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + }, + ["-b", "+b", "-a"], + ), + planner.apply(), + ) + + def test_apply_unhealthy_tool(self): + planner = Planner( + state=DirectAcyclicGraph( + g={"a": dependency("c") + tool("b"), "b": dependency("c"), "c": []} + ), + target=DirectAcyclicGraph( + g={"a": dependency("c") + tool("b"), "b": dependency("c"), "c": []} + ), + unhealthy=["b"], + selected=["-a", "-b", "-c"], + ) + + self.assertEqual( + ( + { + "a": NodeState( + primary_action="delete", + transitive_action="delete", + ), + "b": NodeState( + primary_action="delete", + transitive_action="delete", + state={"unhealthy"}, + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.DEPENDENCY}, + ), + }, + ["-b", "+b", "-a", "-b", "-c"], + ), + planner.apply(), + ) + + def test_apply_delete_tool(self): + planner = Planner( + state=DirectAcyclicGraph(g={"b": dependency("c"), "c": []}), + target=DirectAcyclicGraph( + g={"a": dependency("c") + tool("b"), "b": dependency("c"), "c": []} + ), + selected=["a", "-b"], + ) + + self.assertEqual( + ( + { + "a": NodeState(primary_action="add"), + "b": NodeState( + primary_action="delete", + dependent_edge_types={EdgeType.TOOL}, + ), + "c": NodeState( + user_selected=False, dependent_edge_types={EdgeType.DEPENDENCY} + ), + }, + ["+a", "-b"], + ), + planner.apply(), + )