Path:
strictdoc/core/tree_cycle_detector.py
Lines:
88
Non-empty lines:
78
Non-empty lines covered with requirements:
78 / 78 (100.0%)
Functions:
5
Functions covered by requirements:
5 / 5 (100.0%)
1
from collections import deque
2
from typing import Callable, Deque, List, Set, Tuple
3
4
from strictdoc.backend.sdoc.errors.document_tree_error import DocumentTreeError
5
6
BEFORE = 1
7
AFTER = 2
8
9
10
class TreeCycleDetector:
11
def __init__(self) -> None:
12
self.checked: Set[str] = set()
13
14
def check_node(
15
self, node: str, links_function: Callable[[str], List[str]]
16
) -> None:
17
if node in self.checked:
18
return19
stack: Deque[Tuple[str, int]] = deque()
20
stack.append((node, BEFORE))
21
visited: Set[str] = set()
22
while stack:
23
current_node, token = stack[-1]
24
if token == BEFORE:
25
visited.add(current_node)
26
stack[-1] = (current_node, AFTER)
27
node_links: List[str] = links_function(current_node)
28
for node_link in reversed(node_links):
29
if node_link in visited:
30
cycled_nodes = []
31
for uid, token in stack:
32
if token == BEFORE:
33
continue34
cycled_nodes.append(uid)
35
raise DocumentTreeError.cycle_error(
36
node_link, cycled_nodes
37
)38
if node_link not in self.checked:
39
stack.append((node_link, BEFORE))
40
elif token == AFTER:
41
visited.remove(current_node)
42
self.checked.add(current_node)
43
stack.pop()
44
self.checked.add(node)
45
46
47
class SingleShotTreeCycleDetector:
48
def check_node(
49
self,
50
new_uid: str,
51
node: str,
52
links_function: Callable[[str], List[str]],
53
) -> None:
54
"""
55
Detect if a new node creates a cycle in the traceability index.56
57
FIXME: Both cycle detector classes are very similar. Find a way to merge58
them. The added value of this 'single shot' detector is that it59
checks for cycles possibly added by a new node.60
"""61
62
checked: Set[str] = set()
63
64
stack: Deque[Tuple[str, int]] = deque()
65
stack.append((node, BEFORE))
66
visited = {new_uid}
67
while stack:
68
current_node, token = stack[-1]
69
if token == BEFORE:
70
visited.add(current_node)
71
stack[-1] = (current_node, AFTER)
72
node_links = links_function(current_node)
73
for node_link in reversed(node_links):
74
if node_link in visited:
75
cycled_nodes = [new_uid]
76
for uid, token in stack:
77
if token == BEFORE:
78
continue79
cycled_nodes.append(uid)
80
raise DocumentTreeError.cycle_error(
81
node_link, cycled_nodes
82
)83
if node_link not in checked:
84
stack.append((node_link, BEFORE))
85
elif token == AFTER:
86
visited.remove(current_node)
87
checked.add(current_node)
88
stack.pop()