import pytest from daglib.dag import DAG from daglib.operations import all_nodes @pytest.fixture def build_graph() -> DAG[str]: graph = DAG[str]() graph.add_edge("checkout", "compile") graph.add_edge("toolchain", "compile") graph.add_edge("compile", "package") graph.add_edge("package", "deploy") return graph def test_subgraph_reverse_mutation_preserves_parent_edge_consistency(build_graph: DAG[str]) -> None: assert "compile" in build_graph["toolchain"] assert "toolchain" in build_graph.reverse["compile"] pipeline = build_graph.subgraph("checkout") pipeline.reverse["compile"].discard("toolchain") forward_has_edge = "compile" in build_graph["toolchain"] reverse_has_edge = "toolchain" in build_graph.reverse["compile"] assert forward_has_edge == reverse_has_edge, "Subgraph mutation desynchronized the parent DAG's edge indexes" assert forward_has_edge def test_subgraph_reverse_mutation_preserves_parent_topological_dependencies(build_graph: DAG[str]) -> None: assert "toolchain" in build_graph.topo_sort() pipeline = build_graph.subgraph("checkout") pipeline.reverse["compile"].discard("toolchain") assert "compile" in build_graph["toolchain"] order = build_graph.topo_sort() assert "toolchain" in order, "Topological sorting omitted a node with an outgoing edge in the parent DAG" assert order.index("toolchain") < order.index("compile") def test_build_subgraph_excludes_external_dependencies(build_graph: DAG[str]) -> None: pipeline = build_graph.subgraph("checkout") assert all_nodes(pipeline) == {"checkout", "compile", "package", "deploy"} assert set(pipeline.reverse["compile"]) == {"checkout"} assert len(pipeline) == 3 order = pipeline.topo_sort() assert set(order) == all_nodes(pipeline) assert order.index("checkout") < order.index("compile") < order.index("package") < order.index("deploy") @pytest.mark.parametrize("mutate_parent", [False, True], ids=["child-mutation", "parent-mutation"]) @pytest.mark.parametrize("mutation", ["add", "remove", "reverse-remove", "discard-node"]) def test_build_subgraph_mutations_are_isolated(build_graph: DAG[str], mutate_parent: bool, mutation: str) -> None: pipeline = build_graph.subgraph("checkout") changed, protected = (build_graph, pipeline) if mutate_parent else (pipeline, build_graph) successors = {node: set(protected[node]) for node in protected} predecessors = {node: set(protected.reverse[node]) for node in protected.reverse} match mutation: case "add": changed["package"].add("publish") case "remove": changed["compile"].discard("package") case "reverse-remove": changed.reverse["compile"].discard("checkout") case "discard-node": changed.discard_node("compile") assert {node: set(protected[node]) for node in protected} == successors assert {node: set(protected.reverse[node]) for node in protected.reverse} == predecessors for graph in (changed, protected): forward_edges = {(source, target) for source in graph for target in graph[source]} reverse_edges = {(source, target) for target in graph.reverse for source in graph.reverse[target]} assert forward_edges == reverse_edges