DAG test org
This commit is contained in:
@@ -18,194 +18,190 @@ class TestDAGInit:
|
|||||||
assert g.on_remove is None
|
assert g.on_remove is None
|
||||||
|
|
||||||
|
|
||||||
class TestDAGAddEdge:
|
class TestDAGOps:
|
||||||
"""Test adding edges."""
|
class TestDAGAddEdge:
|
||||||
|
"""Test adding edges."""
|
||||||
|
|
||||||
def test_add_single_edge(self) -> None:
|
def test_add_single_edge(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
assert "b" in g["a"]
|
assert "b" in g["a"]
|
||||||
assert "a" in g.reverse["b"]
|
assert "a" in g.reverse["b"]
|
||||||
|
|
||||||
def test_add_multiple_edges(self) -> None:
|
def test_add_multiple_edges(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
g.add_edge("a", "c")
|
g.add_edge("a", "c")
|
||||||
g.add_edge("b", "c")
|
g.add_edge("b", "c")
|
||||||
assert "b" in g["a"]
|
assert "b" in g["a"]
|
||||||
assert "c" in g["a"]
|
assert "c" in g["a"]
|
||||||
assert "c" in g["b"]
|
assert "c" in g["b"]
|
||||||
|
|
||||||
def test_add_edge_creates_nodes(self) -> None:
|
def test_add_edge_creates_nodes(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
assert "a" in g._succ
|
assert "a" in g._succ
|
||||||
assert "b" in g._pred
|
assert "b" in g._pred
|
||||||
|
|
||||||
def test_self_loop_raises_error(self) -> None:
|
def test_self_loop_raises_error(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
with pytest.raises(ValueError, match="Self-loops are not allowed"):
|
with pytest.raises(ValueError, match="Self-loops are not allowed"):
|
||||||
g.add_edge("a", "a")
|
g.add_edge("a", "a")
|
||||||
|
|
||||||
def test_add_duplicate_edge_idempotent(self) -> None:
|
def test_add_duplicate_edge_idempotent(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
assert len(g["a"]) == 1
|
assert len(g["a"]) == 1
|
||||||
|
|
||||||
|
class TestDAGRemoveEdge:
|
||||||
|
"""Test removing edges."""
|
||||||
|
|
||||||
|
def test_remove_existing_edge(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.remove_edge("a", "b")
|
||||||
|
assert "b" not in g["a"]
|
||||||
|
assert "a" not in g.reverse["b"]
|
||||||
|
|
||||||
|
def test_remove_nonexistent_edge_missing_ok(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.remove_edge("a", "b", missing_ok=True) # should not raise
|
||||||
|
|
||||||
|
def test_remove_nonexistent_edge_error(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
g.remove_edge("a", "b", missing_ok=False)
|
||||||
|
|
||||||
|
class TestDAGDiscardNode:
|
||||||
|
"""Test node removal."""
|
||||||
|
|
||||||
|
def test_discard_node_removes_outgoing_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.discard_node("a")
|
||||||
|
assert "a" not in g._succ
|
||||||
|
assert "a" not in g.reverse["b"]
|
||||||
|
assert "a" not in g.reverse["c"]
|
||||||
|
|
||||||
|
def test_discard_node_removes_incoming_edges(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
g.discard_node("c")
|
||||||
|
assert "c" not in g._pred
|
||||||
|
assert "c" not in g["a"]
|
||||||
|
assert "c" not in g["b"]
|
||||||
|
|
||||||
|
def test_discard_nonexistent_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.discard_node("z") # should not raise
|
||||||
|
|
||||||
|
|
||||||
class TestDAGRemoveEdge:
|
class TestDAGBasicOps:
|
||||||
"""Test removing edges."""
|
class TestSetItem:
|
||||||
|
"""Test dictionary-style assignment."""
|
||||||
|
|
||||||
def test_remove_existing_edge(self) -> None:
|
def test_with_set(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g["a"] = {"b", "c"}
|
||||||
g.remove_edge("a", "b")
|
assert "b" in g["a"]
|
||||||
assert "b" not in g["a"]
|
assert "c" in g["a"]
|
||||||
assert "a" not in g.reverse["b"]
|
|
||||||
|
|
||||||
def test_remove_nonexistent_edge_missing_ok(self) -> None:
|
def test_with_list(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.remove_edge("a", "b", missing_ok=True) # should not raise
|
g["a"] = ["b", "c"]
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "c" in g["a"]
|
||||||
|
|
||||||
def test_remove_nonexistent_edge_error(self) -> None:
|
def test_with_string(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
with pytest.raises(KeyError):
|
g["a"] = "b"
|
||||||
g.remove_edge("a", "b", missing_ok=False)
|
assert "b" in g["a"]
|
||||||
|
|
||||||
|
def test_with_single_item(self) -> None:
|
||||||
|
g = DAG[int]()
|
||||||
|
g[1] = 2
|
||||||
|
assert 2 in g[1]
|
||||||
|
|
||||||
|
def test_updates_reverse(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"] = {"b", "c"}
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
assert "a" in g.reverse["c"]
|
||||||
|
|
||||||
|
class TestGetItem:
|
||||||
|
"""Test dictionary-style access."""
|
||||||
|
|
||||||
|
def test_getitem_returns_dagset(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
dagset = g["a"]
|
||||||
|
assert "b" in dagset
|
||||||
|
|
||||||
|
def test_getitem_empty_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
dagset = g["nonexistent"]
|
||||||
|
assert len(dagset) == 0
|
||||||
|
|
||||||
|
def test_getitem_mutation_updates_graph(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g["a"].add("b")
|
||||||
|
assert "b" in g["a"]
|
||||||
|
assert "a" in g.reverse["b"]
|
||||||
|
|
||||||
|
def test_getitem_mutation_triggers_callbacks(self) -> None:
|
||||||
|
added: list[tuple[str, str]] = []
|
||||||
|
g = DAG[str]()
|
||||||
|
g.on_add = lambda u, v: added.append((u, v))
|
||||||
|
g["a"].add("b")
|
||||||
|
assert ("a", "b") in added
|
||||||
|
|
||||||
|
class TestDAGDelItem:
|
||||||
|
"""Test del operation."""
|
||||||
|
|
||||||
|
def test_removes_node(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
del g["a"]
|
||||||
|
assert "a" not in g._succ
|
||||||
|
|
||||||
|
|
||||||
class TestDAGDiscardNode:
|
class TestDAGIterOps:
|
||||||
"""Test node removal."""
|
class TestIter:
|
||||||
|
"""Test iteration."""
|
||||||
|
|
||||||
def test_discard_node_removes_outgoing_edges(self) -> None:
|
def test_empty(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
assert list(g) == []
|
||||||
g.add_edge("a", "c")
|
|
||||||
g.discard_node("a")
|
|
||||||
assert "a" not in g._succ
|
|
||||||
assert "a" not in g.reverse["b"]
|
|
||||||
assert "a" not in g.reverse["c"]
|
|
||||||
|
|
||||||
def test_discard_node_removes_incoming_edges(self) -> None:
|
def test_returns_nodes(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "c")
|
g.add_edge("a", "b")
|
||||||
g.add_edge("b", "c")
|
g.add_edge("b", "c")
|
||||||
g.discard_node("c")
|
assert {"a", "b"} == set(g)
|
||||||
assert "c" not in g._pred
|
|
||||||
assert "c" not in g["a"]
|
|
||||||
assert "c" not in g["b"]
|
|
||||||
|
|
||||||
def test_discard_nonexistent_node(self) -> None:
|
class TestLen:
|
||||||
g = DAG[str]()
|
"""Test length (edge count)."""
|
||||||
g.discard_node("z") # should not raise
|
|
||||||
|
|
||||||
|
def test_len_empty(self) -> None:
|
||||||
|
g = DAG[str]()
|
||||||
|
assert len(g) == 0
|
||||||
|
|
||||||
class TestDAGGetItem:
|
def test_len_counts_edges(self) -> None:
|
||||||
"""Test dictionary-style access."""
|
g = DAG[str]()
|
||||||
|
g.add_edge("a", "b")
|
||||||
|
g.add_edge("a", "c")
|
||||||
|
g.add_edge("b", "c")
|
||||||
|
assert len(g) == 3
|
||||||
|
|
||||||
def test_getitem_returns_dagset(self) -> None:
|
def test_len_after_removal(self) -> None:
|
||||||
g = DAG[str]()
|
g = DAG[str]()
|
||||||
g.add_edge("a", "b")
|
g.add_edge("a", "b")
|
||||||
dagset = g["a"]
|
g.add_edge("a", "c")
|
||||||
assert "b" in dagset
|
g.remove_edge("a", "b")
|
||||||
|
assert len(g) == 1
|
||||||
def test_getitem_empty_node(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
dagset = g["nonexistent"]
|
|
||||||
assert len(dagset) == 0
|
|
||||||
|
|
||||||
def test_getitem_mutation_updates_graph(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g["a"].add("b")
|
|
||||||
assert "b" in g["a"]
|
|
||||||
assert "a" in g.reverse["b"]
|
|
||||||
|
|
||||||
def test_getitem_mutation_triggers_callbacks(self) -> None:
|
|
||||||
added: list[tuple[str, str]] = []
|
|
||||||
g = DAG[str]()
|
|
||||||
g.on_add = lambda u, v: added.append((u, v))
|
|
||||||
g["a"].add("b")
|
|
||||||
assert ("a", "b") in added
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGSetItem:
|
|
||||||
"""Test dictionary-style assignment."""
|
|
||||||
|
|
||||||
def test_setitem_with_set(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g["a"] = {"b", "c"}
|
|
||||||
assert "b" in g["a"]
|
|
||||||
assert "c" in g["a"]
|
|
||||||
|
|
||||||
def test_setitem_with_list(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g["a"] = ["b", "c"]
|
|
||||||
assert "b" in g["a"]
|
|
||||||
assert "c" in g["a"]
|
|
||||||
|
|
||||||
def test_setitem_with_string(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g["a"] = "b"
|
|
||||||
assert "b" in g["a"]
|
|
||||||
|
|
||||||
def test_setitem_with_single_item(self) -> None:
|
|
||||||
g = DAG[int]()
|
|
||||||
g[1] = 2
|
|
||||||
assert 2 in g[1]
|
|
||||||
|
|
||||||
def test_setitem_updates_reverse(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g["a"] = {"b", "c"}
|
|
||||||
assert "a" in g.reverse["b"]
|
|
||||||
assert "a" in g.reverse["c"]
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGDelItem:
|
|
||||||
"""Test del operation."""
|
|
||||||
|
|
||||||
def test_delitem_removes_node(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g.add_edge("a", "b")
|
|
||||||
del g["a"]
|
|
||||||
assert "a" not in g._succ
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGIter:
|
|
||||||
"""Test iteration."""
|
|
||||||
|
|
||||||
def test_iter_empty(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
assert list(g) == []
|
|
||||||
|
|
||||||
def test_iter_returns_nodes(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g.add_edge("a", "b")
|
|
||||||
g.add_edge("b", "c")
|
|
||||||
nodes = set(g)
|
|
||||||
assert "a" in nodes
|
|
||||||
assert "b" in nodes
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGLen:
|
|
||||||
"""Test length (edge count)."""
|
|
||||||
|
|
||||||
def test_len_empty(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
assert len(g) == 0
|
|
||||||
|
|
||||||
def test_len_counts_edges(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g.add_edge("a", "b")
|
|
||||||
g.add_edge("a", "c")
|
|
||||||
g.add_edge("b", "c")
|
|
||||||
assert len(g) == 3
|
|
||||||
|
|
||||||
def test_len_after_removal(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g.add_edge("a", "b")
|
|
||||||
g.add_edge("a", "c")
|
|
||||||
g.remove_edge("a", "b")
|
|
||||||
assert len(g) == 1
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGReverse:
|
class TestDAGReverse:
|
||||||
@@ -252,24 +248,6 @@ class TestDAGCallbacks:
|
|||||||
assert ("a", "b") in removed
|
assert ("a", "b") in removed
|
||||||
|
|
||||||
|
|
||||||
class TestDAGRepr:
|
|
||||||
"""Test string representation."""
|
|
||||||
|
|
||||||
def test_repr_empty(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
r = repr(g)
|
|
||||||
assert "DAG" in r
|
|
||||||
assert "{}" in r
|
|
||||||
|
|
||||||
def test_repr_with_edges(self) -> None:
|
|
||||||
g = DAG[str]()
|
|
||||||
g.add_edge("a", "b")
|
|
||||||
r = repr(g)
|
|
||||||
assert "DAG" in r
|
|
||||||
assert "a" in r
|
|
||||||
assert "b" in r
|
|
||||||
|
|
||||||
|
|
||||||
class TestDAGComplexScenarios:
|
class TestDAGComplexScenarios:
|
||||||
"""Test complex usage patterns."""
|
"""Test complex usage patterns."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user