Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
47 changes: 33 additions & 14 deletions networkdiagram/networkdiagram/networkdiagram.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ def _validate_duration(self, duration: float, activity_name: str = "") -> None:

def _validate_duplicate(self, name: str) -> None:
if name in self.nodes:
import sys
sys.tracebacklimit = 0
raise ValueError(f"Activity '{name}' already exists. Duplicate names are not allowed.")

def _detect_cycles(self) -> None:
Expand All @@ -132,6 +134,8 @@ def _detect_cycles(self) -> None:
def dfs(node: str) -> None:
if node in rec_stack:
cycle_nodes = list(rec_stack) + [node]
import sys
sys.tracebacklimit = 0
raise ValueError(f"Circular dependency detected: {' -> '.join(cycle_nodes)}")
if node in visited:
return
Expand Down Expand Up @@ -202,14 +206,22 @@ def add_activities_relations(self, activities: List[str], durations: List[float]
self._validate_activity_name(act)
self._validate_duration(dur, act)
self._validate_duplicate(act)
if act in all_valid_activities:
import sys
sys.tracebacklimit = 0
raise ValueError(f"Activity '{act}' already exists. Duplicate names are not allowed.")
all_valid_activities.add(act)

# Validate predecessors exist (check before adding)
for pred_list in predecessors:
for act, pred_list in zip(activities, predecessors):
if pred_list == '-' or pred_list == '':
continue
preds = [p.strip() for p in pred_list.split(',') if p.strip()]
for pred in preds:
if pred == act:
import sys
sys.tracebacklimit = 0
raise ValueError(f"Self-referential dependency detected: '{act}' cannot be a predecessor of itself.")
# Check if predecessor exists in current nodes or will be added
if pred not in self.nodes and pred not in all_valid_activities:
raise ValueError(
Expand Down Expand Up @@ -255,6 +267,10 @@ def add_relation(self, cur: str, predecessors: str) -> None:
if predecessors and predecessors != '-':
preds = [p.strip() for p in predecessors.split(',') if p.strip()]
for p in preds:
if p == cur:
import sys
sys.tracebacklimit = 0
raise ValueError(f"Self-referential dependency detected: '{cur}' cannot be a predecessor of itself.")
if p != 'O' and p not in self.nodes:
raise ValueError(
f"Predecessor '{p}' does not exist. "
Expand Down Expand Up @@ -777,16 +793,19 @@ def display_crash_schedule(self, crash_schedule):
crash_costs = [1000, 700, 900, 500, 1100]
crash_durations = [4, 2, 3, 2, 2]

cpm.add_activities_relations(
activities, durations, predecessors,
normal_costs=normal_costs,
crash_costs=crash_costs,
crash_durations=crash_durations
)

crash_schedule = cpm.crash_project(target_duration=13)
cpm.display_crash_schedule(crash_schedule)
# Display results
cpm.network_summary()
cpm.display_network()
cpm.generate_gantt_chart()
try:
cpm.add_activities_relations(
activities, durations, predecessors,
normal_costs=normal_costs,
crash_costs=crash_costs,
crash_durations=crash_durations
)

crash_schedule = cpm.crash_project(target_duration=13)
cpm.display_crash_schedule(crash_schedule)
# Display results
cpm.network_summary()
cpm.display_network()
cpm.generate_gantt_chart()
except ValueError as e:
print(f"Error: {e}")
46 changes: 46 additions & 0 deletions tests/test_duplicate_and_self_referential.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest
import sys
import os

# Add parent directory to path so we can import networkdiagram
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'networkdiagram', 'networkdiagram')))
from networkdiagram import CriticalPathMethod, Node

class TestDuplicateAndSelfReferential(unittest.TestCase):
def setUp(self):
self.cpm = CriticalPathMethod()
self.cpm.add_activity('O', 0)

def test_duplicate_activity_in_array(self):
activities = ['A', 'B', 'A']
durations = [1, 2, 3]
predecessors = ['-', 'A', 'B']

with self.assertRaises(ValueError) as context:
self.cpm.add_activities_relations(activities, durations, predecessors)

self.assertIn("Activity 'A' already exists. Duplicate names are not allowed.", str(context.exception))

def test_self_referential_dependency(self):
activities = ['A', 'B', 'C']
durations = [1, 2, 3]
predecessors = ['-', 'B', 'B'] # 'B' is predecessor of 'B'

with self.assertRaises(ValueError) as context:
self.cpm.add_activities_relations(activities, durations, predecessors)

self.assertIn("Self-referential dependency detected: 'B' cannot be a predecessor of itself.", str(context.exception))

def test_duplicate_activity_across_calls(self):
self.cpm.add_activity('A', 5)
activities = ['B', 'A']
durations = [2, 3]
predecessors = ['-', '-']

with self.assertRaises(ValueError) as context:
self.cpm.add_activities_relations(activities, durations, predecessors)

self.assertIn("Activity 'A' already exists. Duplicate names are not allowed.", str(context.exception))

if __name__ == '__main__':
unittest.main()