126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import unittest
|
|
|
|
from docforge.python_logic import PythonLogicOwner, analyze_python_source
|
|
|
|
|
|
class PythonLogicTests(unittest.TestCase):
|
|
def projection(self, source: str, qualified_name: str, line: int = 1):
|
|
return analyze_python_source(
|
|
source,
|
|
source_id="source.example",
|
|
owners=(PythonLogicOwner("py.symbol.example", qualified_name, line),),
|
|
filename="example.py",
|
|
)[0]
|
|
|
|
def test_boolean_short_circuit_branches_and_terminals(self) -> None:
|
|
projection = self.projection(
|
|
"""\
|
|
def decide(enabled, cached, stale):
|
|
if enabled and (cached is None or stale):
|
|
return "fetch"
|
|
raise RuntimeError("disabled")
|
|
""",
|
|
"decide",
|
|
)
|
|
kinds = [node.kind for node in projection.nodes]
|
|
labels = [node.label for node in projection.nodes]
|
|
edge_labels = [edge.label for edge in projection.edges]
|
|
|
|
self.assertEqual(1, kinds.count("entry"))
|
|
self.assertEqual(1, kinds.count("exit"))
|
|
self.assertEqual(3, kinds.count("condition"))
|
|
self.assertIn("enabled", labels)
|
|
self.assertIn("cached is None", labels)
|
|
self.assertIn("stale", labels)
|
|
self.assertIn("TRUE", edge_labels)
|
|
self.assertIn("FALSE", edge_labels)
|
|
self.assertIn("RETURN", edge_labels)
|
|
self.assertIn("RAISE", edge_labels)
|
|
|
|
def test_loops_match_try_and_control_transfers_are_explicit(self) -> None:
|
|
projection = self.projection(
|
|
"""\
|
|
def process(items, mode):
|
|
for item in items:
|
|
if item.skip:
|
|
continue
|
|
if item.stop:
|
|
break
|
|
consume(item)
|
|
else:
|
|
finish()
|
|
match mode:
|
|
case "safe":
|
|
value = safe()
|
|
case _:
|
|
value = fallback()
|
|
try:
|
|
return value
|
|
except ValueError:
|
|
raise
|
|
finally:
|
|
cleanup()
|
|
""",
|
|
"process",
|
|
)
|
|
kinds = {node.kind for node in projection.nodes}
|
|
relations = {edge.relation for edge in projection.edges}
|
|
labels = {edge.label for edge in projection.edges}
|
|
|
|
self.assertTrue(
|
|
{"loop", "continue", "break", "case", "try", "except", "finally"}.issubset(kinds)
|
|
)
|
|
self.assertTrue(
|
|
{"loop", "continue", "break", "case", "exception", "return", "raise"}.issubset(
|
|
relations
|
|
)
|
|
)
|
|
self.assertIn("EXHAUSTED", labels)
|
|
self.assertIn("NEXT ITEM", labels)
|
|
self.assertIn("NEXT CASE", labels)
|
|
|
|
def test_class_methods_nested_functions_and_async_functions_use_explicit_owners(self) -> None:
|
|
source = """\
|
|
class Worker:
|
|
async def run(self):
|
|
async with self.session():
|
|
await self.step()
|
|
|
|
if self.enabled:
|
|
def nested():
|
|
return True
|
|
|
|
return nested()
|
|
"""
|
|
projections = analyze_python_source(
|
|
source,
|
|
source_id="source.worker",
|
|
owners=(
|
|
PythonLogicOwner("py.symbol.worker.run", "Worker.run", 2),
|
|
PythonLogicOwner("py.symbol.worker.nested", "Worker.run.nested", 7),
|
|
),
|
|
filename="worker.py",
|
|
)
|
|
|
|
self.assertEqual(
|
|
("py.symbol.worker.nested", "py.symbol.worker.run"),
|
|
tuple(projection.owner_node_id for projection in projections),
|
|
)
|
|
run = next(item for item in projections if item.owner_node_id.endswith(".run"))
|
|
self.assertIn("action", {node.kind for node in run.nodes})
|
|
self.assertIn("call", {node.kind for node in run.nodes})
|
|
|
|
def test_projection_is_deterministic(self) -> None:
|
|
source = """\
|
|
def choose(first, second):
|
|
return first if first else second
|
|
"""
|
|
first = self.projection(source, "choose")
|
|
second = self.projection(source, "choose")
|
|
self.assertEqual(first, second)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|