StrictDoc Documentation
strictdoc/core/tree_cycle_detector.py
Source file coverage
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
            return
19
        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
                                continue
34
                            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 merge
58
               them. The added value of this 'single shot' detector is that it
59
               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
                                continue
79
                            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()