diff --git a/networkdiagram/networkdiagram/__pycache__/__init__.cpython-313.pyc b/networkdiagram/networkdiagram/__pycache__/__init__.cpython-313.pyc index 108f390..3028701 100644 Binary files a/networkdiagram/networkdiagram/__pycache__/__init__.cpython-313.pyc and b/networkdiagram/networkdiagram/__pycache__/__init__.cpython-313.pyc differ diff --git a/networkdiagram/networkdiagram/__pycache__/networkdiagram.cpython-313.pyc b/networkdiagram/networkdiagram/__pycache__/networkdiagram.cpython-313.pyc index d37536c..359e382 100644 Binary files a/networkdiagram/networkdiagram/__pycache__/networkdiagram.cpython-313.pyc and b/networkdiagram/networkdiagram/__pycache__/networkdiagram.cpython-313.pyc differ diff --git a/networkdiagram/networkdiagram/networkdiagram.py b/networkdiagram/networkdiagram/networkdiagram.py index a80199c..5bae819 100644 --- a/networkdiagram/networkdiagram/networkdiagram.py +++ b/networkdiagram/networkdiagram/networkdiagram.py @@ -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: @@ -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 @@ -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( @@ -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. " @@ -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}") diff --git a/tests/test_duplicate_and_self_referential.py b/tests/test_duplicate_and_self_referential.py new file mode 100644 index 0000000..6010650 --- /dev/null +++ b/tests/test_duplicate_and_self_referential.py @@ -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()