Fix self-loop detection to use the Definition 6 self-relation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 10:00:45 +08:00
co-authored by Claude Fable 5
parent 11045c57d3
commit 79bb561693
2 changed files with 140 additions and 40 deletions
+123 -6
View File
@@ -455,15 +455,16 @@ class TestForkAfterEnd(unittest.TestCase):
class TestSelfLoop(unittest.TestCase):
"""Tests self-loop detection from lifecycle traces.
An activity that completes more than once in a trace
is a self-loop.
An activity a is a self-loop iff a ->r a holds per
Definition 6, that is, an end event of a is followed by
a start event of a with no other end event in between.
"""
def setUp(self) -> None:
"""Set up trace with self-loop: A repeats.
"""Set up trace with self-loop: A repeats at once.
Trace: As Ae Bs Be As Ae Cs Ce
Activity A completes twice.
Trace: As Ae As Ae Bs Be Cs Ce
A ends and immediately starts again, so A ->r A.
:return: None.
"""
@@ -477,8 +478,8 @@ class TestSelfLoop(unittest.TestCase):
tuple[tuple[Task, str], ...], int
] = {
((a, S), (a, E),
(b, S), (b, E),
(a, S), (a, E),
(b, S), (b, E),
(c, S), (c, E)): 1,
}
self.__dfg: RefinedDirectlyFollowsGraph = (
@@ -505,6 +506,27 @@ class TestSelfLoop(unittest.TestCase):
self.__dfg.self_loops,
)
def test_no_self_edge(self) -> None:
"""The self-loop edge A->A is not in the edges.
:return: None.
"""
a: Task = self.__tasks["A"]
self.assertNotIn((a, a), self.__dfg.edges)
def test_other_edges(self) -> None:
"""The other edges are A->B and B->C.
:return: None.
"""
a: Task = self.__tasks["A"]
b: Task = self.__tasks["B"]
c: Task = self.__tasks["C"]
self.assertEqual(
self.__dfg.edges,
{(a, b), (b, c)},
)
def test_no_self_loop_in_paper(self) -> None:
"""Paper example has no self-loops.
@@ -514,5 +536,100 @@ class TestSelfLoop(unittest.TestCase):
self.assertEqual(dfg.self_loops, set())
class TestSelfLoopWithoutRepeatedEnd(unittest.TestCase):
"""Tests a self-loop with a single end event.
Definition 6 only requires an end event of a followed by
a start event of a, not a second end event of a.
"""
def setUp(self) -> None:
"""Set up trace: As Ae As Bs Be.
A ends, then A starts again with no end event in
between, so A ->r A.
:return: None.
"""
self.__tasks: dict[str, Task] = (
_make_tasks("A", "B")
)
a: Task = self.__tasks["A"]
b: Task = self.__tasks["B"]
traces: dict[
tuple[tuple[Task, str], ...], int
] = {
((a, S), (a, E), (a, S),
(b, S), (b, E)): 1,
}
self.__dfg: RefinedDirectlyFollowsGraph = (
RefinedDirectlyFollowsGraph(traces)
)
def test_a_is_self_loop(self) -> None:
"""A is a self-loop even though it ends once.
:return: None.
"""
self.assertIn(
self.__tasks["A"],
self.__dfg.self_loops,
)
class TestTwoLoopIsNotSelfLoop(unittest.TestCase):
"""Tests that a 2-loop is not mistaken for a self-loop.
An activity repeating after another activity completes
forms a 2-loop, not a self-loop.
"""
def setUp(self) -> None:
"""Set up trace: As Ae Bs Be As Ae.
B ends between the end of A and the next start of
A, so A ->r A does not hold.
:return: None.
"""
self.__tasks: dict[str, Task] = (
_make_tasks("A", "B")
)
a: Task = self.__tasks["A"]
b: Task = self.__tasks["B"]
traces: dict[
tuple[tuple[Task, str], ...], int
] = {
((a, S), (a, E),
(b, S), (b, E),
(a, S), (a, E)): 1,
}
self.__dfg: RefinedDirectlyFollowsGraph = (
RefinedDirectlyFollowsGraph(traces)
)
def test_a_not_self_loop(self) -> None:
"""A is not a self-loop.
:return: None.
"""
self.assertNotIn(
self.__tasks["A"],
self.__dfg.self_loops,
)
def test_two_loop_edges(self) -> None:
"""The edges form the 2-loop A->B and B->A.
:return: None.
"""
a: Task = self.__tasks["A"]
b: Task = self.__tasks["B"]
self.assertEqual(
self.__dfg.edges,
{(a, b), (b, a)},
)
if __name__ == "__main__":
unittest.main()