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)
|
rp = path.resolve(strict=True)
|
||||||
except (OSError, ValueError):
|
except (OSError, ValueError):
|
||||||
rp = path.resolve()
|
rp = path.resolve()
|
||||||
|
|
||||||
if rp in _allowed_paths:
|
if rp in _allowed_paths:
|
||||||
return True
|
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:
|
for bd in _base_dirs:
|
||||||
try:
|
try:
|
||||||
rp.relative_to(bd)
|
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