From 49f0bd0266950c3de46ea0d4e40b053f24790ba7 Mon Sep 17 00:00:00 2001 From: Andreas Gebhardt Date: Sun, 10 Sep 2023 20:14:25 +0200 Subject: [PATCH] add cause of action --- graph/planner/planner.py | 105 +++++++++++++++----- graph/planner/planner_test.py | 179 +++++++++++++++++++++++++++++----- 2 files changed, 233 insertions(+), 51 deletions(-) diff --git a/graph/planner/planner.py b/graph/planner/planner.py index 0d8bf1a..b84187d 100644 --- a/graph/planner/planner.py +++ b/graph/planner/planner.py @@ -1,4 +1,4 @@ -from typing import Union +from typing import Union, Tuple, List from direct_acyclic_graph import DirectAcyclicGraph, EdgeType @@ -21,7 +21,7 @@ def __init__( self._modified = modified or [] self._selected = selected - def apply(self) -> list[str]: + def apply(self) -> tuple[list[str], dict[str, bool | set[str]]]: nodes_state = self._state.nodes() nodes_target = self._target.nodes() @@ -30,6 +30,8 @@ def apply(self) -> list[str]: nodes_with_edge_type_tool_delete = set() modified_transitive = set() + node_action_cause = {node: dict() for node in nodes_state.union(nodes_target)} + if self._selected == "*": nodes_add = {node for node in nodes_target - nodes_state} nodes_delete = {node for node in nodes_state - nodes_target} @@ -46,6 +48,7 @@ def apply(self) -> list[str]: if item.startswith("-"): # delete case node = str(item[1:]) + node_action_cause[node] |= {"selected": True} has_parent_with_edge_type_tool = len(self._state.parents(node)) > 0 if has_parent_with_edge_type_tool: parents_edge_type_tool_transitive = ( @@ -58,47 +61,93 @@ def apply(self) -> list[str]: modified_transitive = modified_transitive.union( parents_edge_type_tool_transitive ) + for reachable in parents_edge_type_tool_transitive: + if reachable != node: + node_action_cause[reachable] |= {"modified": "True"} + if "parent" in node_action_cause[reachable]: + node_action_cause[reachable]["parent"].add(node) + else: + node_action_cause[reachable] |= {"parent": {node}} + + 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( - self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] - != EdgeType.TOOL, - ).union({node}) - ) + nodes_with_edge_type_tool_delete.union(childs_transitive) ) + + for reachable in childs_transitive: + if reachable != node: + if "child" in node_action_cause[reachable]: + node_action_cause[reachable]["child"].add(node) + else: + node_action_cause[reachable] |= {"child": {node}} else: - nodes_delete = nodes_delete.union( - # use state (graph) instead of target, because target will be mostly ignored in this mode - self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, - ).union({node}) - ) + # 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) + for reachable in childs_transitive: + if reachable != node: + if "child" in node_action_cause[reachable]: + node_action_cause[reachable]["child"].add(node) + else: + node_action_cause[reachable] |= {"child": {node}} else: node = item + node_action_cause[node] |= {"selected": True} if node in self._modified: # 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( - self._state.childs_transitive( - node, - accept=lambda adjacent: adjacent[1] != EdgeType.TOOL, - ).union({node}) + childs_transitive ) + for reachable in childs_transitive: + if reachable != node: + node_action_cause[reachable] |= {"modified": True} + if "child" in node_action_cause[reachable]: + node_action_cause[reachable]["child"].add(node) + else: + node_action_cause[reachable] |= {"child": {node}} + 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 reachable in nodes_add: + if reachable != node: + if "parent" in node_action_cause[reachable]: + node_action_cause[reachable]["parent"].add(node) + else: + node_action_cause[reachable] |= {"parent": {node}} + for node_add in nodes_add: + parent_transitive = self._target.parents_transitive( + node_add, + accept=lambda adjacent: (adjacent[0] in self._modified), + ) modified_transitive = modified_transitive.union( - self._target.parents_transitive( - node_add, - accept=lambda adjacent: ( - adjacent[0] in self._modified - ), - ) + parent_transitive ) + for reachable in parent_transitive: + if reachable != node: + node_action_cause[reachable] |= {"modified": True} + if "parent" in node_action_cause[reachable]: + node_action_cause[reachable]["parent"].add(node) + else: + node_action_cause[reachable] |= { + "parent": {node} + } node_action = {node: None for node in nodes_state.union(nodes_target)} @@ -129,4 +178,6 @@ def apply(self) -> list[str]: if node_action[node] in ["delete"]: operations.append(f"-{node}") - return operations + return operations, { + key: value for key, value in node_action_cause.items() if len(value) > 0 + } diff --git a/graph/planner/planner_test.py b/graph/planner/planner_test.py index 26ab31e..fc597ba 100644 --- a/graph/planner/planner_test.py +++ b/graph/planner/planner_test.py @@ -44,13 +44,13 @@ class PlanerTestCase(unittest.TestCase): def test_no_action(self): planner = Planner(state=DAG1, target=DAG1, modified=[]) - self.assertEqual([], planner.apply()) + self.assertEqual(([], {}), 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", "-b", "-c", "-d", "+d", "+c", "+b", "+a"], {}), planner.apply(), ) @@ -58,19 +58,22 @@ 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", "-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((["-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( + (["-d", "-b", "-a", "+a", "+b", "+d"], {}), + planner.apply(), + ) def test_apply_add_to_empty(self): planner = Planner( @@ -78,7 +81,10 @@ def test_apply_add_to_empty(self): target=DirectAcyclicGraph(g={"a": dependency("b"), "b": []}), ) - self.assertEqual(["+b", "+a"], planner.apply()) + self.assertEqual( + (["+b", "+a"], {}), + planner.apply(), + ) def test_apply_add_to_existing(self): planner = Planner( @@ -88,7 +94,7 @@ def test_apply_add_to_existing(self): ), ) - self.assertEqual(["+c"], planner.apply()) + self.assertEqual((["+c"], {}), planner.apply()) def test_apply_remove_all(self): planner = Planner( @@ -96,12 +102,18 @@ def test_apply_remove_all(self): target=DirectAcyclicGraph(g={}), ) - self.assertEqual(["-a", "-b"], planner.apply()) + self.assertEqual( + (["-a", "-b"], {}), + planner.apply(), + ) def test_apply_DAG3_remove_all(self): planner = Planner(state=DAG3, target=DirectAcyclicGraph(g={})) - self.assertEqual(["-d", "-c", "-b", "-a"], planner.apply()) + self.assertEqual( + (["-d", "-c", "-b", "-a"], {}), + planner.apply(), + ) def test_add_and_delete(self): planner = Planner( @@ -109,7 +121,10 @@ def test_add_and_delete(self): target=DirectAcyclicGraph(g={"c": dependency("a"), "a": []}), ) - self.assertEqual(["-b", "+c"], planner.apply()) + self.assertEqual( + (["-b", "+c"], {}), + planner.apply(), + ) def test_modified_but_delete(self): planner = Planner( @@ -120,13 +135,25 @@ def test_modified_but_delete(self): modified=["c"], ) - self.assertEqual(["-c"], planner.apply()) + self.assertEqual((["-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"], planner.apply() + ( + ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + { + "a": {"child": {"c1d1"}}, + "b": {"child": {"c1d1"}}, + "c": {"child": {"c1d1"}}, + "c1": {"child": {"c1d1"}}, + "c1d1": {"selected": True}, + "d": {"child": {"c1d1"}}, + "d1": {"child": {"c1d1"}}, + }, + ), + planner.apply(), ) def test_apply_delete_DAG2_temporarily_with_target_changes(self): @@ -148,7 +175,19 @@ def test_apply_delete_DAG2_temporarily_with_target_changes(self): ) self.assertEqual( - ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], planner.apply() + ( + ["-a", "-b", "-d", "-d1", "-c", "-c1", "-c1d1"], + { + "a": {"child": {"c1d1"}}, + "b": {"child": {"c1d1"}}, + "c": {"child": {"c1d1"}}, + "c1": {"child": {"c1d1"}}, + "c1d1": {"selected": True}, + "d": {"child": {"c1d1"}}, + "d1": {"child": {"c1d1"}}, + }, + ), + planner.apply(), ) def test_apply_delete_roots_DAG2_temporarily(self): @@ -156,7 +195,21 @@ def test_apply_delete_roots_DAG2_temporarily(self): actual = planner.apply() self.assertEqual( - ["-a", "-b", "-d", "-d2", "-d1", "-c", "-c2", "-c2d2", "-c1", "-c1d1"], + ( + ["-a", "-b", "-d", "-d2", "-d1", "-c", "-c2", "-c2d2", "-c1", "-c1d1"], + { + "a": {"child": {"c1d1", "c2d2"}}, + "b": {"child": {"c1d1", "c2d2"}}, + "c": {"child": {"c1d1", "c2d2"}}, + "c1": {"child": {"c1d1"}}, + "c1d1": {"selected": True}, + "c2": {"child": {"c2d2"}}, + "c2d2": {"selected": True}, + "d": {"child": {"c1d1", "c2d2"}}, + "d1": {"child": {"c1d1"}}, + "d2": {"child": {"c2d2"}}, + }, + ), actual, ) self.assertNotIn("e", actual) @@ -170,7 +223,16 @@ def test_apply_delete_with_modified_one_TOOL_edge(self): modified=["b"], selected=["-c"], ) - self.assertEqual(["-b", "+b", "-c"], planner.apply()) + self.assertEqual( + ( + ["-b", "+b", "-c"], + { + "b": {"modified": "True", "parent": {"c"}}, + "c": {"selected": True}, + }, + ), + planner.apply(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -183,7 +245,16 @@ def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain(self): modified=["b2"], selected=["-c"], ) - self.assertEqual(["-b2", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + ["-b2", "+b2", "-c"], + { + "b2": {"modified": "True", "parent": {"c"}}, + "c": {"selected": True}, + }, + ), + planner.apply(), + ) def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): planner = Planner( @@ -196,7 +267,17 @@ def test_apply_delete_with_two_modified_two_TOOL_edge_in_chain(self): modified=["b1", "b2"], selected=["-c"], ) - self.assertEqual(["-b2", "-b1", "+b1", "+b2", "-c"], planner.apply()) + self.assertEqual( + ( + ["-b2", "-b1", "+b1", "+b2", "-c"], + { + "b1": {"modified": "True", "parent": {"c"}}, + "b2": {"modified": "True", "parent": {"c"}}, + "c": {"selected": True}, + }, + ), + planner.apply(), + ) def test_apply_delete_with_one_modified_two_TOOL_edge_in_chain_skip(self): planner = Planner( @@ -209,18 +290,24 @@ 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((["-c"], {"c": {"selected": True}}), planner.apply()) def test_no_action_apply_selected(self): planner = Planner(state=DAG1, target=DAG1, modified=[], selected=["c"]) - self.assertEqual([], planner.apply()) + self.assertEqual(([], {"c": {"selected": True}}), 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"], + ( + ["-a", "-b", "+b", "+a"], + { + "a": {"child": {"b"}, "modified": True}, + "b": {"selected": True}, + }, + ), planner.apply(), ) @@ -230,7 +317,13 @@ def test_apply_DAG3_selected_target(self): ) self.assertEqual( - ["+b", "+c"], + ( + ["+b", "+c"], + { + "b": {"parent": {"c"}}, + "c": {"selected": True}, + }, + ), planner.apply(), ) @@ -243,7 +336,14 @@ def test_apply_DAG3_selected_target_modified(self): ) self.assertEqual( - ["-a", "+a", "+b", "+c"], + ( + ["-a", "+a", "+b", "+c"], + { + "a": {"modified": True, "parent": {"c"}}, + "b": {"parent": {"c"}}, + "c": {"selected": True}, + }, + ), planner.apply(), ) @@ -258,7 +358,13 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge(self): ) self.assertEqual( - ["-b", "+b", "+c"], + ( + ["-b", "+b", "+c"], + { + "b": {"modified": True, "parent": {"c"}}, + "c": {"selected": True}, + }, + ), planner.apply(), ) @@ -279,6 +385,31 @@ def test_apply_DAG3_selected_target_modified_one_TOOL_edge_chain_skip(self): ) self.assertEqual( - ["+d", "+e"], + ( + ["+d", "+e"], + { + "d": {"parent": {"e"}}, + "e": {"selected": True}, + }, + ), + planner.apply(), + ) + + def test_apply_selected_TOOL_edge_modified(self): + planner = Planner( + state=DirectAcyclicGraph(g={"a": tool("b"), "b": []}), + target=DirectAcyclicGraph(g={"a": tool("b"), "b": []}), + modified=["b"], + selected=["-a"], + ) + + self.assertEqual( + ( + ["-b", "+b", "-a"], + { + "a": {"selected": True}, + "b": {"modified": "True", "parent": {"a"}}, + }, + ), planner.apply(), )