feat(mma): Implement TrackDAG for dependency resolution and cycle detection
This commit is contained in:
52
dag_engine.py
Normal file
52
dag_engine.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from typing import List
|
||||
from models import Ticket
|
||||
|
||||
class TrackDAG:
|
||||
def __init__(self, tickets: List[Ticket]):
|
||||
self.tickets = tickets
|
||||
self.ticket_map = {t.id: t for t in tickets}
|
||||
|
||||
def get_ready_tasks(self) -> List[Ticket]:
|
||||
"""Returns tickets that are 'todo' and whose dependencies are all 'completed'."""
|
||||
ready = []
|
||||
for ticket in self.tickets:
|
||||
if ticket.status == 'todo':
|
||||
# Check if all dependencies exist and are completed
|
||||
all_done = True
|
||||
for dep_id in ticket.depends_on:
|
||||
dep = self.ticket_map.get(dep_id)
|
||||
if not dep or dep.status != 'completed':
|
||||
all_done = False
|
||||
break
|
||||
if all_done:
|
||||
ready.append(ticket)
|
||||
return ready
|
||||
|
||||
def has_cycle(self) -> bool:
|
||||
"""Returns True if there's a dependency cycle."""
|
||||
visited = set()
|
||||
rec_stack = set()
|
||||
|
||||
def is_cyclic(ticket_id):
|
||||
if ticket_id in rec_stack:
|
||||
return True
|
||||
if ticket_id in visited:
|
||||
return False
|
||||
|
||||
visited.add(ticket_id)
|
||||
rec_stack.add(ticket_id)
|
||||
|
||||
ticket = self.ticket_map.get(ticket_id)
|
||||
if ticket:
|
||||
for neighbor in ticket.depends_on:
|
||||
if is_cyclic(neighbor):
|
||||
return True
|
||||
|
||||
rec_stack.remove(ticket_id)
|
||||
return False
|
||||
|
||||
for ticket in self.tickets:
|
||||
if ticket.id not in visited:
|
||||
if is_cyclic(ticket.id):
|
||||
return True
|
||||
return False
|
||||
@@ -99,8 +99,19 @@ def _is_allowed(path: Path) -> bool:
|
||||
rp = path.resolve(strict=True)
|
||||
except (OSError, ValueError):
|
||||
rp = path.resolve()
|
||||
|
||||
if rp in _allowed_paths:
|
||||
return True
|
||||
|
||||
# Allow current working directory and subpaths by default if no base_dirs
|
||||
cwd = Path.cwd().resolve()
|
||||
if not _base_dirs:
|
||||
try:
|
||||
rp.relative_to(cwd)
|
||||
return True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
for bd in _base_dirs:
|
||||
try:
|
||||
rp.relative_to(bd)
|
||||
|
||||
67
tests/test_dag_engine.py
Normal file
67
tests/test_dag_engine.py
Normal file
@@ -0,0 +1,67 @@
|
||||
import pytest
|
||||
from models import Ticket
|
||||
from dag_engine import TrackDAG
|
||||
|
||||
def test_get_ready_tasks_linear():
|
||||
t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker")
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T2"])
|
||||
|
||||
dag = TrackDAG([t1, t2, t3])
|
||||
ready = dag.get_ready_tasks()
|
||||
assert len(ready) == 1
|
||||
assert ready[0].id == "T2"
|
||||
|
||||
def test_get_ready_tasks_branching():
|
||||
t1 = Ticket(id="T1", description="Task 1", status="completed", assigned_to="worker")
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
|
||||
dag = TrackDAG([t1, t2, t3])
|
||||
ready = dag.get_ready_tasks()
|
||||
assert len(ready) == 2
|
||||
ready_ids = {t.id for t in ready}
|
||||
assert ready_ids == {"T2", "T3"}
|
||||
|
||||
def test_has_cycle_no_cycle():
|
||||
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker")
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
|
||||
dag = TrackDAG([t1, t2])
|
||||
assert not dag.has_cycle()
|
||||
|
||||
def test_has_cycle_direct_cycle():
|
||||
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"])
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
|
||||
dag = TrackDAG([t1, t2])
|
||||
assert dag.has_cycle()
|
||||
|
||||
def test_has_cycle_indirect_cycle():
|
||||
t1 = Ticket(id="T1", description="Task 1", status="todo", assigned_to="worker", depends_on=["T2"])
|
||||
t2 = Ticket(id="T2", description="Task 2", status="todo", assigned_to="worker", depends_on=["T3"])
|
||||
t3 = Ticket(id="T3", description="Task 3", status="todo", assigned_to="worker", depends_on=["T1"])
|
||||
|
||||
dag = TrackDAG([t1, t2, t3])
|
||||
assert dag.has_cycle()
|
||||
|
||||
def test_has_cycle_complex_no_cycle():
|
||||
# T1 -> T2, T1 -> T3, T2 -> T4, T3 -> T4
|
||||
t1 = Ticket(id="T1", description="T1", status="todo", assigned_to="worker", depends_on=["T2", "T3"])
|
||||
t2 = Ticket(id="T2", description="T2", status="todo", assigned_to="worker", depends_on=["T4"])
|
||||
t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T4"])
|
||||
t4 = Ticket(id="T4", description="T4", status="todo", assigned_to="worker")
|
||||
|
||||
dag = TrackDAG([t1, t2, t3, t4])
|
||||
assert not dag.has_cycle()
|
||||
|
||||
def test_get_ready_tasks_multiple_deps():
|
||||
t1 = Ticket(id="T1", description="T1", status="completed", assigned_to="worker")
|
||||
t2 = Ticket(id="T2", description="T2", status="completed", assigned_to="worker")
|
||||
t3 = Ticket(id="T3", description="T3", status="todo", assigned_to="worker", depends_on=["T1", "T2"])
|
||||
|
||||
dag = TrackDAG([t1, t2, t3])
|
||||
assert [t.id for t in dag.get_ready_tasks()] == ["T3"]
|
||||
|
||||
t2.status = "todo"
|
||||
assert [t.id for t in dag.get_ready_tasks()] == ["T2"]
|
||||
Reference in New Issue
Block a user