diff --git a/graph/planner/planner.py b/graph/planner/planner.py index b84187d..bd32137 100644 --- a/graph/planner/planner.py +++ b/graph/planner/planner.py @@ -7,6 +7,7 @@ class Planner: _state: DirectAcyclicGraph _target: DirectAcyclicGraph _modified: list[str] + _unhealthy: list[str] _selected: Union[str, list[str]] = "*" def __init__( @@ -14,11 +15,13 @@ def __init__( state: DirectAcyclicGraph, target: DirectAcyclicGraph, modified: list[str] = None, # TODO naming + unhealthy: list[str] = None, # TODO naming selected: Union[str, list[str]] = "*", # TODO naming ): self._state = state self._target = target self._modified = modified or [] + self._unhealthy = unhealthy or [] self._selected = selected def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: @@ -29,6 +32,7 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: nodes_delete = set() nodes_with_edge_type_tool_delete = set() modified_transitive = set() + unhealthy_transitive = set() node_action_cause = {node: dict() for node in nodes_state.union(nodes_target)} @@ -42,6 +46,12 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: 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: @@ -55,15 +65,15 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: self._state.parents_transitive( node, accept=lambda adjacent: adjacent[1] == EdgeType.TOOL - and adjacent[0] in self._modified, + and adjacent[0] in self._unhealthy, ) ) - modified_transitive = modified_transitive.union( + unhealthy_transitive = unhealthy_transitive.union( parents_edge_type_tool_transitive ) for reachable in parents_edge_type_tool_transitive: if reachable != node: - node_action_cause[reachable] |= {"modified": "True"} + node_action_cause[reachable] |= {"unhealthy": "True"} if "parent" in node_action_cause[reachable]: node_action_cause[reachable]["parent"].add(node) else: @@ -102,18 +112,32 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: else: node = item node_action_cause[node] |= {"selected": True} - if node in self._modified: # and node in node_states <- implicit + + 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}) - modified_transitive = modified_transitive.union( - childs_transitive - ) + 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 + ) + for reachable in childs_transitive: if reachable != node: - node_action_cause[reachable] |= {"modified": True} + node_action_cause[reachable] |= {key: True} if "child" in node_action_cause[reachable]: node_action_cause[reachable]["child"].add(node) else: @@ -134,14 +158,16 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: for node_add in nodes_add: parent_transitive = self._target.parents_transitive( node_add, - accept=lambda adjacent: (adjacent[0] in self._modified), + accept=lambda adjacent: ( + adjacent[0] in self._unhealthy + ), ) - modified_transitive = modified_transitive.union( + unhealthy_transitive = unhealthy_transitive.union( parent_transitive ) for reachable in parent_transitive: if reachable != node: - node_action_cause[reachable] |= {"modified": True} + node_action_cause[reachable] |= {"unhealthy": True} if "parent" in node_action_cause[reachable]: node_action_cause[reachable]["parent"].add(node) else: @@ -160,13 +186,16 @@ def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: for node in modified_transitive: node_action[node] = "modified" + for node in unhealthy_transitive: + node_action[node] = "unhealthy" + operations = [] for node in self._state.topological_order(): - if node_action[node] in ["delete", "modified"]: + if node_action[node] in ["delete", "modified", "unhealthy"]: operations.append(f"-{node}") for node in reversed(self._target.topological_order()): - if node_action[node] in ["add", "modified"]: + if node_action[node] in ["add", "modified", "unhealthy"]: operations.append(f"+{node}") # handle node deletion of nodes w/ edges of type TOOL diff --git a/graph/planner/planner_test.py b/graph/planner/planner_test.py index fc597ba..5e79dc6 100644 --- a/graph/planner/planner_test.py +++ b/graph/planner/planner_test.py @@ -42,7 +42,7 @@ class PlanerTestCase(unittest.TestCase): def test_no_action(self): - planner = Planner(state=DAG1, target=DAG1, modified=[]) + planner = Planner(state=DAG1, target=DAG1) self.assertEqual(([], {}), planner.apply()) @@ -54,6 +54,32 @@ def test_apply_DAG1_modified_multiple(self): planner.apply(), ) + 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"], {}), + planner.apply(), + ) + + def test_apply_DAG1_unhealthy_and_modified(self): + planner = Planner( + state=DAG1, target=DAG1, modified=["b", "d"], unhealthy=["b", "d"] + ) + + self.assertEqual( + (["-a", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], {}), + planner.apply(), + ) + + 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"], {}), + planner.apply(), + ) + def test_apply_DAG1_modified_root(self): planner = Planner(state=DAG1, target=DAG1, modified=["e"]) @@ -62,6 +88,14 @@ def test_apply_DAG1_modified_root(self): planner.apply(), ) + 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"], {}), + planner.apply(), + ) + def test_apply_DAG3_modified_single(self): planner = Planner(state=DAG3, target=DAG3, modified=["d"]) @@ -223,11 +257,30 @@ def test_apply_delete_with_modified_one_TOOL_edge(self): modified=["b"], selected=["-c"], ) + self.assertEqual( + ( + ["-c"], + { + "c": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_delete_with_unhealthy_one_TOOL_edge(self): + planner = Planner( + state=DirectAcyclicGraph(g={"a": [], "b": dependency("a"), "c": tool("b")}), + target=DirectAcyclicGraph( + g={"a": [], "b": dependency("a"), "c": tool("b")} + ), + unhealthy=["b"], + selected=["-c"], + ) self.assertEqual( ( ["-b", "+b", "-c"], { - "b": {"modified": "True", "parent": {"c"}}, + "b": {"unhealthy": "True", "parent": {"c"}}, "c": {"selected": True}, }, ), @@ -245,11 +298,32 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): modified=["b2"], selected=["-c"], ) + self.assertEqual( + ( + ["-c"], + { + "c": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_delete_with_one_unhealthy_two_TOOL_edge_in_chain(self): + planner = Planner( + state=DirectAcyclicGraph( + g={"a": [], "b1": dependency("a"), "b2": tool("b1"), "c": tool("b2")} + ), + target=DirectAcyclicGraph( + g={"a": [], "b1": dependency("a"), "b2": tool("b1"), "c": tool("b2")} + ), + unhealthy=["b2"], + selected=["-c"], + ) self.assertEqual( ( ["-b2", "+b2", "-c"], { - "b2": {"modified": "True", "parent": {"c"}}, + "b2": {"unhealthy": "True", "parent": {"c"}}, "c": {"selected": True}, }, ), @@ -267,12 +341,33 @@ def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): modified=["b1", "b2"], selected=["-c"], ) + self.assertEqual( + ( + ["-c"], + { + "c": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_delete_with_two_unhealthy_two_TOOL_edge_in_chain(self): + planner = Planner( + state=DirectAcyclicGraph( + g={"a": [], "b1": dependency("a"), "b2": tool("b1"), "c": tool("b2")} + ), + target=DirectAcyclicGraph( + g={"a": [], "b1": dependency("a"), "b2": tool("b1"), "c": tool("b2")} + ), + unhealthy=["b1", "b2"], + selected=["-c"], + ) self.assertEqual( ( ["-b2", "-b1", "+b1", "+b2", "-c"], { - "b1": {"modified": "True", "parent": {"c"}}, - "b2": {"modified": "True", "parent": {"c"}}, + "b1": {"unhealthy": "True", "parent": {"c"}}, + "b2": {"unhealthy": "True", "parent": {"c"}}, "c": {"selected": True}, }, ), @@ -335,11 +430,30 @@ def test_apply_DAG3_selected_target_modified(self): selected=["c"], ) + self.assertEqual( + ( + ["+b", "+c"], + { + "b": {"parent": {"c"}}, + "c": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_DAG3_selected_target_unhealthy(self): + planner = Planner( + state=DirectAcyclicGraph(g={"a": dependency()}), + target=DAG3, + unhealthy=["a"], + selected=["c"], + ) + self.assertEqual( ( ["-a", "+a", "+b", "+c"], { - "a": {"modified": True, "parent": {"c"}}, + "a": {"unhealthy": True, "parent": {"c"}}, "b": {"parent": {"c"}}, "c": {"selected": True}, }, @@ -357,11 +471,31 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): selected=["c"], ) + self.assertEqual( + ( + ["+c"], + { + "c": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_DAG3_selected_target_unhealthy_one_TOOL_edge(self): + planner = Planner( + state=DirectAcyclicGraph(g={"a": [], "b": dependency("a")}), + target=DirectAcyclicGraph( + g={"a": [], "b": dependency("a"), "c": tool("b")} + ), + unhealthy=["b"], + selected=["c"], + ) + self.assertEqual( ( ["-b", "+b", "+c"], { - "b": {"modified": True, "parent": {"c"}}, + "b": {"unhealthy": True, "parent": {"c"}}, "c": {"selected": True}, }, ), @@ -403,12 +537,30 @@ def test_apply_selected_TOOL_edge_modified(self): selected=["-a"], ) + self.assertEqual( + ( + ["-a"], + { + "a": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_selected_TOOL_edge_unhealthy(self): + planner = Planner( + state=DirectAcyclicGraph(g={"a": tool("b"), "b": []}), + target=DirectAcyclicGraph(g={"a": tool("b"), "b": []}), + unhealthy=["b"], + selected=["-a"], + ) + self.assertEqual( ( ["-b", "+b", "-a"], { "a": {"selected": True}, - "b": {"modified": "True", "parent": {"a"}}, + "b": {"unhealthy": "True", "parent": {"a"}}, }, ), planner.apply(),