feat(T3.3): implement task dependency graph
- Add internal/graph package for dependency management - Implement DependencyGraph for task ordering - Support task dependencies and prerequisite tracking - Validate graph for cycles (no circular dependencies) - Topological sort for execution order (Kahn's algorithm) - Track task status (pending, completed, failed) - Get ready-to-execute tasks based on dependencies - Get tasks that depend on a given task - Check if task can execute (all deps complete) - Calculate critical path through graph - Task metadata support - 23 graph tests, all passing Features: - AddTask() - add task to graph - AddDependency(dependent, prerequisite) - specify ordering - ValidateGraph() - check for cycles - GetTopologicalOrder() - execution order - GetReadyTasks() - tasks ready to run - MarkCompleted(taskID) - mark as done - MarkFailed(taskID) - mark as failed - GetDependencies(taskID) - what task depends on - GetDependents(taskID) - what depends on task - CanExecuteTask(taskID) - check if ready - GetCriticalPath() - longest path in graph Graph Properties: - Directed acyclic graph (DAG) - Cycle detection (prevents deadlocks) - Multi-dependency support (diamond dependencies) - Status tracking (pending/completed/failed) - Thread-safe (RWMutex) - Kahn's algorithm for topological sort - O(V+E) for validation and sorting Example Usage: - T0.1 Analyze (no deps) - T0.2 Implement (depends on T0.1) - T0.3 Test (depends on T0.2) - T0.4 Review (depends on T0.2, T0.3) Ready Detection: - T0.1 ready (no dependencies) - After T0.1 complete: T0.2 ready - After T0.2 complete: T0.3 ready - After T0.2, T0.3 complete: T0.4 ready Test Coverage: - 23 dependency graph tests - Cycle detection verified - Topological sort tested - Multiple dependency chains - Diamond dependency patterns - Ready task calculation - Status tracking - Critical path calculation - Complex graphs (10+ tasks) - Metadata handling - Performance benchmarks Performance: - Cycle detection: O(V+E) DFS - Topological sort: O(V+E) Kahn's algorithm - Ready tasks: O(V) scan - Add task: O(1) - Add dependency: O(1) amortized Use Cases: - Workflow orchestration (T0.1 -> T0.2 -> T0.3 -> ...) - CI/CD pipelines (build -> test -> deploy) - Milestone hierarchies (T0 milestone with sub-tasks) - Parallel tasks with merge points (diamond deps) Next: T3.4 (Human-in-the-loop gates)
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Task represents a node in the dependency graph
|
||||
type Task struct {
|
||||
ID string
|
||||
Title string
|
||||
Status string // pending, ready, running, completed, failed
|
||||
DependsOn []string
|
||||
Metadata map[string]interface{}
|
||||
}
|
||||
|
||||
// DependencyGraph manages task dependencies
|
||||
type DependencyGraph struct {
|
||||
mu sync.RWMutex
|
||||
tasks map[string]*Task
|
||||
adjacencyList map[string][]string // task -> dependent tasks
|
||||
reverseList map[string][]string // task -> dependencies
|
||||
topologicalOrder []string
|
||||
cycleDetected bool
|
||||
status map[string]string // task -> status
|
||||
}
|
||||
|
||||
// NewDependencyGraph creates a new dependency graph
|
||||
func NewDependencyGraph() *DependencyGraph {
|
||||
return &DependencyGraph{
|
||||
tasks: make(map[string]*Task),
|
||||
adjacencyList: make(map[string][]string),
|
||||
reverseList: make(map[string][]string),
|
||||
topologicalOrder: make([]string, 0),
|
||||
status: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// AddTask adds a task to the graph
|
||||
func (dg *DependencyGraph) AddTask(task *Task) error {
|
||||
if task == nil || task.ID == "" {
|
||||
return fmt.Errorf("task cannot be nil and must have an ID")
|
||||
}
|
||||
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
if _, exists := dg.tasks[task.ID]; exists {
|
||||
return fmt.Errorf("task already exists: %s", task.ID)
|
||||
}
|
||||
|
||||
dg.tasks[task.ID] = task
|
||||
dg.status[task.ID] = "pending"
|
||||
|
||||
// Initialize adjacency lists
|
||||
if _, exists := dg.adjacencyList[task.ID]; !exists {
|
||||
dg.adjacencyList[task.ID] = make([]string, 0)
|
||||
}
|
||||
if _, exists := dg.reverseList[task.ID]; !exists {
|
||||
dg.reverseList[task.ID] = make([]string, 0)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDependency adds a dependency: dependent depends on prerequisite
|
||||
func (dg *DependencyGraph) AddDependency(dependent, prerequisite string) error {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
if _, exists := dg.tasks[dependent]; !exists {
|
||||
return fmt.Errorf("dependent task not found: %s", dependent)
|
||||
}
|
||||
|
||||
if _, exists := dg.tasks[prerequisite]; !exists {
|
||||
return fmt.Errorf("prerequisite task not found: %s", prerequisite)
|
||||
}
|
||||
|
||||
// Check for duplicate
|
||||
for _, dep := range dg.reverseList[dependent] {
|
||||
if dep == prerequisite {
|
||||
return fmt.Errorf("dependency already exists: %s -> %s", dependent, prerequisite)
|
||||
}
|
||||
}
|
||||
|
||||
dg.reverseList[dependent] = append(dg.reverseList[dependent], prerequisite)
|
||||
dg.adjacencyList[prerequisite] = append(dg.adjacencyList[prerequisite], dependent)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateGraph checks for cycles and structural integrity
|
||||
func (dg *DependencyGraph) ValidateGraph() error {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
// Check for cycles using DFS
|
||||
visited := make(map[string]bool)
|
||||
recStack := make(map[string]bool)
|
||||
|
||||
for taskID := range dg.tasks {
|
||||
if !visited[taskID] {
|
||||
if dg.hasCycleLocked(taskID, visited, recStack) {
|
||||
dg.cycleDetected = true
|
||||
return fmt.Errorf("cycle detected in dependency graph")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasCycleLocked detects cycles using DFS (must be called with lock held)
|
||||
func (dg *DependencyGraph) hasCycleLocked(node string, visited, recStack map[string]bool) bool {
|
||||
visited[node] = true
|
||||
recStack[node] = true
|
||||
|
||||
for _, dep := range dg.reverseList[node] {
|
||||
if !visited[dep] {
|
||||
if dg.hasCycleLocked(dep, visited, recStack) {
|
||||
return true
|
||||
}
|
||||
} else if recStack[dep] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
recStack[node] = false
|
||||
return false
|
||||
}
|
||||
|
||||
// GetTopologicalOrder returns tasks in execution order
|
||||
func (dg *DependencyGraph) GetTopologicalOrder() ([]string, error) {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
if dg.cycleDetected {
|
||||
return nil, fmt.Errorf("graph contains cycles")
|
||||
}
|
||||
|
||||
// Kahn's algorithm
|
||||
inDegree := make(map[string]int)
|
||||
for taskID := range dg.tasks {
|
||||
inDegree[taskID] = len(dg.reverseList[taskID])
|
||||
}
|
||||
|
||||
queue := make([]string, 0)
|
||||
for taskID, degree := range inDegree {
|
||||
if degree == 0 {
|
||||
queue = append(queue, taskID)
|
||||
}
|
||||
}
|
||||
|
||||
topOrder := make([]string, 0)
|
||||
for len(queue) > 0 {
|
||||
current := queue[0]
|
||||
queue = queue[1:]
|
||||
topOrder = append(topOrder, current)
|
||||
|
||||
for _, dependent := range dg.adjacencyList[current] {
|
||||
inDegree[dependent]--
|
||||
if inDegree[dependent] == 0 {
|
||||
queue = append(queue, dependent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(topOrder) != len(dg.tasks) {
|
||||
return nil, fmt.Errorf("topological sort failed - graph may have cycles")
|
||||
}
|
||||
|
||||
dg.topologicalOrder = topOrder
|
||||
return topOrder, nil
|
||||
}
|
||||
|
||||
// GetReadyTasks returns tasks that have no remaining dependencies
|
||||
func (dg *DependencyGraph) GetReadyTasks() []string {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
ready := make([]string, 0)
|
||||
|
||||
for taskID, deps := range dg.reverseList {
|
||||
allDepsComplete := true
|
||||
for _, dep := range deps {
|
||||
if dg.status[dep] != "completed" {
|
||||
allDepsComplete = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allDepsComplete && dg.status[taskID] == "pending" {
|
||||
ready = append(ready, taskID)
|
||||
}
|
||||
}
|
||||
|
||||
return ready
|
||||
}
|
||||
|
||||
// MarkCompleted marks a task as completed and updates dependents
|
||||
func (dg *DependencyGraph) MarkCompleted(taskID string) error {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
if _, exists := dg.tasks[taskID]; !exists {
|
||||
return fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
|
||||
dg.status[taskID] = "completed"
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarkFailed marks a task as failed
|
||||
func (dg *DependencyGraph) MarkFailed(taskID string) error {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
if _, exists := dg.tasks[taskID]; !exists {
|
||||
return fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
|
||||
dg.status[taskID] = "failed"
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTaskStatus returns the status of a task
|
||||
func (dg *DependencyGraph) GetTaskStatus(taskID string) (string, error) {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
status, exists := dg.status[taskID]
|
||||
if !exists {
|
||||
return "", fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
|
||||
return status, nil
|
||||
}
|
||||
|
||||
// GetDependencies returns all dependencies of a task
|
||||
func (dg *DependencyGraph) GetDependencies(taskID string) ([]string, error) {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
deps, exists := dg.reverseList[taskID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
|
||||
result := make([]string, len(deps))
|
||||
copy(result, deps)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetDependents returns all tasks that depend on this task
|
||||
func (dg *DependencyGraph) GetDependents(taskID string) ([]string, error) {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
deps, exists := dg.adjacencyList[taskID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("task not found: %s", taskID)
|
||||
}
|
||||
|
||||
result := make([]string, len(deps))
|
||||
copy(result, deps)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// GetTask returns a task by ID
|
||||
func (dg *DependencyGraph) GetTask(taskID string) (*Task, bool) {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
task, exists := dg.tasks[taskID]
|
||||
return task, exists
|
||||
}
|
||||
|
||||
// GetAllTasks returns all tasks
|
||||
func (dg *DependencyGraph) GetAllTasks() map[string]*Task {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
result := make(map[string]*Task)
|
||||
for id, task := range dg.tasks {
|
||||
result[id] = task
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// GetGraphStats returns statistics about the graph
|
||||
func (dg *DependencyGraph) GetGraphStats() map[string]interface{} {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
pending := 0
|
||||
completed := 0
|
||||
failed := 0
|
||||
|
||||
for _, status := range dg.status {
|
||||
switch status {
|
||||
case "pending":
|
||||
pending++
|
||||
case "completed":
|
||||
completed++
|
||||
case "failed":
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_tasks": len(dg.tasks),
|
||||
"pending_tasks": pending,
|
||||
"completed_tasks": completed,
|
||||
"failed_tasks": failed,
|
||||
"cycle_detected": dg.cycleDetected,
|
||||
"total_edges": dg.countEdgesLocked(),
|
||||
}
|
||||
}
|
||||
|
||||
// countEdgesLocked counts total dependencies (must be called with lock held)
|
||||
func (dg *DependencyGraph) countEdgesLocked() int {
|
||||
count := 0
|
||||
for _, deps := range dg.reverseList {
|
||||
count += len(deps)
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
// Clear clears all tasks and dependencies
|
||||
func (dg *DependencyGraph) Clear() {
|
||||
dg.mu.Lock()
|
||||
defer dg.mu.Unlock()
|
||||
|
||||
dg.tasks = make(map[string]*Task)
|
||||
dg.adjacencyList = make(map[string][]string)
|
||||
dg.reverseList = make(map[string][]string)
|
||||
dg.topologicalOrder = make([]string, 0)
|
||||
dg.status = make(map[string]string)
|
||||
dg.cycleDetected = false
|
||||
}
|
||||
|
||||
// CanExecuteTask checks if a task can be executed (all deps complete)
|
||||
func (dg *DependencyGraph) CanExecuteTask(taskID string) bool {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
deps, exists := dg.reverseList[taskID]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, dep := range deps {
|
||||
if dg.status[dep] != "completed" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetCriticalPath returns the longest path through the graph
|
||||
func (dg *DependencyGraph) GetCriticalPath() []string {
|
||||
dg.mu.RLock()
|
||||
defer dg.mu.RUnlock()
|
||||
|
||||
// Use longest path algorithm
|
||||
distances := make(map[string]int)
|
||||
parent := make(map[string]string)
|
||||
|
||||
for taskID := range dg.tasks {
|
||||
distances[taskID] = 0
|
||||
}
|
||||
|
||||
// Process in topological order
|
||||
for _, taskID := range dg.topologicalOrder {
|
||||
for _, dependent := range dg.adjacencyList[taskID] {
|
||||
if distances[dependent] < distances[taskID]+1 {
|
||||
distances[dependent] = distances[taskID] + 1
|
||||
parent[dependent] = taskID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find task with maximum distance
|
||||
maxDist := 0
|
||||
endTask := ""
|
||||
for taskID, dist := range distances {
|
||||
if dist > maxDist {
|
||||
maxDist = dist
|
||||
endTask = taskID
|
||||
}
|
||||
}
|
||||
|
||||
// Reconstruct path
|
||||
path := make([]string, 0)
|
||||
current := endTask
|
||||
for current != "" {
|
||||
path = append([]string{current}, path...)
|
||||
current = parent[current]
|
||||
}
|
||||
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
package graph
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewDependencyGraph(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
assert.NotNil(t, graph)
|
||||
assert.Equal(t, 0, len(graph.GetAllTasks()))
|
||||
}
|
||||
|
||||
func TestAddTask(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
task := &Task{ID: "T1", Title: "Task 1"}
|
||||
err := graph.AddTask(task)
|
||||
|
||||
assert.NoError(t, err)
|
||||
retrieved, exists := graph.GetTask("T1")
|
||||
assert.True(t, exists)
|
||||
assert.Equal(t, "T1", retrieved.ID)
|
||||
}
|
||||
|
||||
func TestAddTaskNil(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
err := graph.AddTask(nil)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAddTaskDuplicate(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
task := &Task{ID: "T1", Title: "Task 1"}
|
||||
graph.AddTask(task)
|
||||
|
||||
err := graph.AddTask(task)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestAddDependency(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
|
||||
err := graph.AddDependency("T2", "T1")
|
||||
assert.NoError(t, err)
|
||||
|
||||
deps, _ := graph.GetDependencies("T2")
|
||||
assert.Equal(t, 1, len(deps))
|
||||
assert.Equal(t, "T1", deps[0])
|
||||
}
|
||||
|
||||
func TestAddDependencyNotFound(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
|
||||
err := graph.AddDependency("T2", "T1")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestValidateGraphNoCycles(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T2")
|
||||
|
||||
err := graph.ValidateGraph()
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestValidateGraphWithCycle(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T2")
|
||||
graph.AddDependency("T1", "T3") // Creates cycle
|
||||
|
||||
err := graph.ValidateGraph()
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetTopologicalOrder(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T2")
|
||||
|
||||
graph.ValidateGraph()
|
||||
order, err := graph.GetTopologicalOrder()
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, 3, len(order))
|
||||
assert.Equal(t, "T1", order[0])
|
||||
assert.Equal(t, "T2", order[1])
|
||||
assert.Equal(t, "T3", order[2])
|
||||
}
|
||||
|
||||
func TestGetReadyTasks(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T1")
|
||||
|
||||
ready := graph.GetReadyTasks()
|
||||
assert.Equal(t, 1, len(ready))
|
||||
assert.Equal(t, "T1", ready[0])
|
||||
|
||||
graph.MarkCompleted("T1")
|
||||
ready = graph.GetReadyTasks()
|
||||
assert.Equal(t, 2, len(ready))
|
||||
}
|
||||
|
||||
func TestMarkCompleted(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
err := graph.MarkCompleted("T1")
|
||||
|
||||
assert.NoError(t, err)
|
||||
status, _ := graph.GetTaskStatus("T1")
|
||||
assert.Equal(t, "completed", status)
|
||||
}
|
||||
|
||||
func TestMarkFailed(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
err := graph.MarkFailed("T1")
|
||||
|
||||
assert.NoError(t, err)
|
||||
status, _ := graph.GetTaskStatus("T1")
|
||||
assert.Equal(t, "failed", status)
|
||||
}
|
||||
|
||||
func TestGetDependencies(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T3", "T1")
|
||||
graph.AddDependency("T3", "T2")
|
||||
|
||||
deps, _ := graph.GetDependencies("T3")
|
||||
assert.Equal(t, 2, len(deps))
|
||||
}
|
||||
|
||||
func TestGetDependents(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
graph.AddTask(&Task{ID: "T3", Title: "Task 3"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T1")
|
||||
|
||||
dependents, _ := graph.GetDependents("T1")
|
||||
assert.Equal(t, 2, len(dependents))
|
||||
}
|
||||
|
||||
func TestCanExecuteTask(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
|
||||
assert.False(t, graph.CanExecuteTask("T2"))
|
||||
|
||||
graph.MarkCompleted("T1")
|
||||
assert.True(t, graph.CanExecuteTask("T2"))
|
||||
}
|
||||
|
||||
func TestGetGraphStats(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
graph.AddTask(&Task{ID: "T2", Title: "Task 2"})
|
||||
|
||||
graph.MarkCompleted("T1")
|
||||
|
||||
stats := graph.GetGraphStats()
|
||||
assert.Equal(t, 2, stats["total_tasks"])
|
||||
assert.Equal(t, 1, stats["completed_tasks"])
|
||||
assert.Equal(t, 1, stats["pending_tasks"])
|
||||
}
|
||||
|
||||
func TestClear(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1", Title: "Task 1"})
|
||||
assert.Equal(t, 1, len(graph.GetAllTasks()))
|
||||
|
||||
graph.Clear()
|
||||
assert.Equal(t, 0, len(graph.GetAllTasks()))
|
||||
}
|
||||
|
||||
func TestMultipleDependencies(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
id := string(rune(48 + i))
|
||||
graph.AddTask(&Task{ID: "T" + id, Title: "Task " + id})
|
||||
}
|
||||
|
||||
// Chain: T1 -> T2 -> T3 -> T4 -> T5
|
||||
for i := 2; i <= 5; i++ {
|
||||
graph.AddDependency("T"+string(rune(48+i)), "T"+string(rune(48+i-1)))
|
||||
}
|
||||
|
||||
ready := graph.GetReadyTasks()
|
||||
assert.Equal(t, 1, len(ready))
|
||||
assert.Equal(t, "T1", ready[0])
|
||||
}
|
||||
|
||||
func TestDiamondDependency(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
// Diamond: T1 -> (T2, T3) -> T4
|
||||
graph.AddTask(&Task{ID: "T1"})
|
||||
graph.AddTask(&Task{ID: "T2"})
|
||||
graph.AddTask(&Task{ID: "T3"})
|
||||
graph.AddTask(&Task{ID: "T4"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T1")
|
||||
graph.AddDependency("T4", "T2")
|
||||
graph.AddDependency("T4", "T3")
|
||||
|
||||
graph.ValidateGraph()
|
||||
order, _ := graph.GetTopologicalOrder()
|
||||
|
||||
assert.Equal(t, 4, len(order))
|
||||
assert.Equal(t, "T1", order[0])
|
||||
assert.Equal(t, "T4", order[3])
|
||||
}
|
||||
|
||||
func TestGetCriticalPath(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
graph.AddTask(&Task{ID: "T1"})
|
||||
graph.AddTask(&Task{ID: "T2"})
|
||||
graph.AddTask(&Task{ID: "T3"})
|
||||
graph.AddTask(&Task{ID: "T4"})
|
||||
|
||||
graph.AddDependency("T2", "T1")
|
||||
graph.AddDependency("T3", "T2")
|
||||
graph.AddDependency("T4", "T3")
|
||||
|
||||
graph.ValidateGraph()
|
||||
graph.GetTopologicalOrder()
|
||||
|
||||
path := graph.GetCriticalPath()
|
||||
assert.Greater(t, len(path), 0)
|
||||
assert.Equal(t, "T1", path[0])
|
||||
}
|
||||
|
||||
func TestComplexGraph(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
// Create 10 tasks with complex dependencies
|
||||
for i := 1; i <= 10; i++ {
|
||||
id := string(rune(48 + i%10))
|
||||
if i >= 10 {
|
||||
id = "T" + id
|
||||
} else {
|
||||
id = "T0" + id
|
||||
}
|
||||
graph.AddTask(&Task{ID: id})
|
||||
}
|
||||
|
||||
// Add various dependencies
|
||||
graph.AddDependency("T02", "T01")
|
||||
graph.AddDependency("T03", "T01")
|
||||
graph.AddDependency("T04", "T02")
|
||||
graph.AddDependency("T04", "T03")
|
||||
|
||||
graph.ValidateGraph()
|
||||
order, _ := graph.GetTopologicalOrder()
|
||||
assert.Equal(t, 10, len(order))
|
||||
}
|
||||
|
||||
func TestTaskWithMetadata(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
task := &Task{
|
||||
ID: "T1",
|
||||
Title: "Task 1",
|
||||
Metadata: map[string]interface{}{
|
||||
"priority": "high",
|
||||
"owner": "team-a",
|
||||
},
|
||||
}
|
||||
|
||||
graph.AddTask(task)
|
||||
retrieved, _ := graph.GetTask("T1")
|
||||
assert.Equal(t, "high", retrieved.Metadata["priority"])
|
||||
}
|
||||
|
||||
func TestGetAllTasks(t *testing.T) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
for i := 1; i <= 5; i++ {
|
||||
id := string(rune(48 + i))
|
||||
graph.AddTask(&Task{ID: "T" + id})
|
||||
}
|
||||
|
||||
all := graph.GetAllTasks()
|
||||
assert.Equal(t, 5, len(all))
|
||||
}
|
||||
|
||||
func BenchmarkAddTask(b *testing.B) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
id := string(rune(48 + i%100))
|
||||
graph.AddTask(&Task{ID: "T" + id})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAddDependency(b *testing.B) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
for i := 0; i < 1000; i++ {
|
||||
graph.AddTask(&Task{ID: "T" + string(rune(48+i%100))})
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
from := "T" + string(rune(48+i%100))
|
||||
to := "T" + string(rune(48+(i+1)%100))
|
||||
graph.AddDependency(from, to)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGetReadyTasks(b *testing.B) {
|
||||
graph := NewDependencyGraph()
|
||||
|
||||
for i := 1; i <= 100; i++ {
|
||||
id := "T" + string(rune(48+i%100))
|
||||
graph.AddTask(&Task{ID: id})
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
graph.GetReadyTasks()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user