Skip to content

Commit f9bea85

Browse files
committed
chore: New SCXML fail mark with the contents of the errors
1 parent 8c340c4 commit f9bea85

File tree

217 files changed

+4111
-36
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

217 files changed

+4111
-36
lines changed

statemachine/engines/sync.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,13 @@ def processing_loop(self): # noqa: C901
107107
while not self.external_queue.is_empty():
108108
took_events = True
109109
external_event = self.external_queue.pop()
110-
logger.debug("External event: %s", external_event)
111110
current_time = time()
112111
if external_event.execution_time > current_time:
113112
self.put(external_event)
114113
sleep(0.001)
115114
continue
116115

116+
logger.debug("External event: %s", external_event)
117117
# # TODO: Handle cancel event
118118
# if self.is_cancel_event(external_event):
119119
# self.running = False

statemachine/io/scxml/processor.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from typing import Dict
44
from typing import List
55

6+
from ...exceptions import InvalidDefinition
67
from .. import StateDefinition
78
from .. import TransitionDict
89
from .. import TransitionsDict
@@ -114,7 +115,7 @@ def _add(self, location: str, definition: Dict[str, Any]):
114115
self.scs[location] = sc_class
115116
return sc_class
116117
except Exception as e:
117-
raise Exception(
118+
raise InvalidDefinition(
118119
f"Failed to create state machine class: {e} from definition: {definition}"
119120
) from e
120121

tests/conftest.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
import sys
21
from datetime import datetime
3-
from typing import List
42

53
import pytest
64

7-
collect_ignore_glob: List[str] = []
8-
9-
# We support Python 3.8+ positional only syntax
10-
if sys.version_info[:2] < (3, 8): # noqa: UP036
11-
collect_ignore_glob.append("*_positional_only.py")
12-
13-
14-
# TODO: Return django to collect
15-
collect_ignore_glob.append("django")
16-
175

186
@pytest.fixture()
197
def current_time():

tests/scxml/conftest.py

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@
22

33
import pytest
44

5-
from statemachine.io.scxml.processor import SCXMLProcessor
6-
75
CURRENT_DIR = Path(__file__).parent
86
TESTCASES_DIR = CURRENT_DIR
97
SUPPORTED_EXTENSIONS = "scxml"
@@ -14,24 +12,29 @@ def processor(testcase_path: Path):
1412
"""
1513
Construct a StateMachine class from the SCXML file
1614
"""
17-
processor = SCXMLProcessor()
18-
processor.parse_scxml_file(testcase_path)
1915
return processor
2016

2117

18+
def compute_testcase_marks(testcase_path: Path) -> list[pytest.MarkDecorator]:
19+
marks = [pytest.mark.scxml]
20+
if testcase_path.with_name(f"{testcase_path.stem}.fail.md").exists():
21+
marks.append(pytest.mark.xfail)
22+
if testcase_path.with_name(f"{testcase_path.stem}.skip.md").exists():
23+
marks.append(pytest.mark.skip)
24+
return marks
25+
26+
2227
def pytest_generate_tests(metafunc):
2328
if "testcase_path" not in metafunc.fixturenames:
2429
return
2530

26-
fail_marks = [pytest.mark.xfail]
27-
2831
metafunc.parametrize(
2932
"testcase_path",
3033
[
3134
pytest.param(
3235
testcase_path,
3336
id=str(testcase_path.relative_to(TESTCASES_DIR)),
34-
marks=fail_marks if "fail" in testcase_path.name else [],
37+
marks=compute_testcase_marks(testcase_path),
3538
)
3639
for testcase_path in TESTCASES_DIR.glob("**/*.scxml")
3740
if "sub" not in testcase_path.name

tests/scxml/test_scxml_cases.py

Lines changed: 101 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import traceback
12
from dataclasses import dataclass
23
from dataclasses import field
3-
4-
import pytest
4+
from pathlib import Path
55

66
from statemachine import State
77
from statemachine import StateMachine
88
from statemachine.event import Event
9+
from statemachine.io.scxml.processor import SCXMLProcessor
910

1011
"""
1112
Test cases as defined by W3C SCXML Test Suite
@@ -18,29 +19,114 @@
1819
""" # noqa: E501
1920

2021

21-
@dataclass(frozen=True, unsafe_hash=True)
22+
@dataclass(frozen=True, unsafe_hash=True, kw_only=True)
23+
class DebugEvent:
24+
source: str
25+
event: str
26+
data: str
27+
target: str
28+
29+
30+
@dataclass(frozen=True, unsafe_hash=True, kw_only=True)
2231
class DebugListener:
23-
events: list = field(default_factory=list)
32+
events: list[DebugEvent] = field(default_factory=list)
2433

2534
def on_transition(self, event: Event, source: State, target: State, event_data):
2635
self.events.append(
27-
(
28-
f"{source and source.id}",
29-
f"{event and event.id}",
30-
f"{event_data.trigger_data.kwargs}",
31-
f"{target.id}",
36+
DebugEvent(
37+
source=f"{source and source.id}",
38+
event=f"{event and event.id}",
39+
data=f"{event_data.trigger_data.kwargs}",
40+
target=f"{target.id}",
3241
)
3342
)
3443

3544

36-
@pytest.mark.scxml()
37-
def test_scxml_usecase(testcase_path, processor):
45+
@dataclass(kw_only=True)
46+
class FailedMark:
47+
reason: str
48+
events: list[DebugEvent]
49+
is_assertion_error: bool
50+
exception: Exception
51+
logs: str
52+
configuration: list[str] = field(default_factory=list)
53+
54+
@staticmethod
55+
def _get_header(report: str) -> str:
56+
header_end_index = report.find("---")
57+
return report[:header_end_index]
58+
59+
def write_fail_markdown(self, testcase_path: Path):
60+
fail_file_path = testcase_path.with_suffix(".fail.md")
61+
if not self.is_assertion_error:
62+
exception_traceback = "".join(
63+
traceback.format_exception(
64+
type(self.exception), self.exception, self.exception.__traceback__
65+
)
66+
)
67+
else:
68+
exception_traceback = "Assertion of the testcase failed."
69+
70+
report = f"""# Testcase: {testcase_path.stem}
71+
72+
{self.reason}
73+
74+
Final configuration: `{self.configuration if self.configuration else 'No configuration'}`
75+
76+
---
77+
78+
## Logs
79+
```py
80+
{self.logs if self.logs else 'No logs'}
81+
```
82+
83+
## "On transition" events
84+
```py
85+
{'\n'.join(map(repr, self.events)) if self.events else 'No events'}
86+
```
87+
88+
## Traceback
89+
```py
90+
{exception_traceback}
91+
```
92+
"""
93+
94+
if fail_file_path.exists():
95+
last_report = fail_file_path.read_text()
96+
97+
if self._get_header(report) == self._get_header(last_report):
98+
return
99+
100+
with fail_file_path.open("w") as fail_file:
101+
fail_file.write(report)
102+
103+
104+
def test_scxml_usecase(testcase_path: Path, caplog):
38105
# from statemachine.contrib.diagram import DotGraphMachine
39106

40107
# DotGraphMachine(sm_class).get_graph().write_png(
41108
# testcase_path.parent / f"{testcase_path.stem}.png"
42109
# )
43-
debug = DebugListener()
44-
sm = processor.start(listeners=[debug])
45-
assert isinstance(sm, StateMachine)
46-
assert sm.current_state.id == "pass", debug
110+
sm: "StateMachine | None" = None
111+
try:
112+
debug = DebugListener()
113+
processor = SCXMLProcessor()
114+
processor.parse_scxml_file(testcase_path)
115+
116+
sm = processor.start(listeners=[debug])
117+
assert isinstance(sm, StateMachine)
118+
assert "pass" in {s.id for s in sm.configuration}, debug
119+
except Exception as e:
120+
# Import necessary module
121+
reason = f"{e.__class__.__name__}: {e.__class__.__doc__}"
122+
is_assertion_error = isinstance(e, AssertionError)
123+
fail_mark = FailedMark(
124+
reason=reason,
125+
is_assertion_error=is_assertion_error,
126+
events=debug.events,
127+
exception=e,
128+
logs=caplog.text,
129+
configuration=[s.id for s in sm.configuration] if sm else [],
130+
)
131+
fail_mark.write_fail_markdown(testcase_path)
132+
raise
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Testcase: test190
2+
3+
AssertionError: Assertion failed.
4+
5+
Final configuration: `['fail']`
6+
7+
---
8+
9+
## Logs
10+
```py
11+
DEBUG statemachine.engines.base:base.py:374 States to enter: {S0}
12+
DEBUG statemachine.io.scxml.actions:actions.py:443 Error executing actions
13+
Traceback (most recent call last):
14+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 441, in datamodel
15+
act(machine=machine)
16+
~~~^^^^^^^^^^^^^^^^^
17+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 412, in data_initializer
18+
value = _eval(action.expr, **kwargs)
19+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 125, in _eval
20+
return eval(expr, {}, kwargs)
21+
File "<string>", line 1, in <module>
22+
import sys;exec(eval(sys.stdin.readline()))
23+
^^^^^^^^^^
24+
NameError: name '_sessionid' is not defined
25+
DEBUG statemachine.io.scxml.actions:actions.py:467 Error executing actions
26+
Traceback (most recent call last):
27+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 463, in __call__
28+
action(*args, **kwargs)
29+
~~~~~~^^^^^^^^^^^^^^^^^
30+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 231, in __call__
31+
value = _eval(self.action.expr, **kwargs)
32+
File "/home/macedo/projects/python-statemachine/statemachine/io/scxml/actions.py", line 125, in _eval
33+
return eval(expr, {}, kwargs)
34+
File "<string>", line 1, in <module>
35+
import sys;exec(eval(sys.stdin.readline()))
36+
^^^^^^^^^^^
37+
TypeError: can only concatenate str (not "NoneType") to str
38+
DEBUG statemachine.engines.sync:sync.py:64 Processing loop started: s0
39+
DEBUG statemachine.engines.sync:sync.py:89 Eventless/internal queue: {transition * from S0 to Fail}
40+
DEBUG statemachine.engines.base:base.py:283 States to exit: {S0}
41+
DEBUG statemachine.engines.base:base.py:374 States to enter: {Fail}
42+
43+
```
44+
45+
## "On transition" events
46+
```py
47+
DebugEvent(source='s0', event='error.execution', data='{\'event_id\': None, \'error\': NameError("name \'_sessionid\' is not defined")}', target='fail')
48+
```
49+
50+
## Traceback
51+
```py
52+
Assertion of the testcase failed.
53+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Testcase: test191
2+
3+
AssertionError: Assertion failed.
4+
5+
Final configuration: `['fail']`
6+
7+
---
8+
9+
## Logs
10+
```py
11+
DEBUG statemachine.engines.base:base.py:374 States to enter: {S0}
12+
DEBUG statemachine.engines.sync:sync.py:64 Processing loop started: s0
13+
DEBUG statemachine.engines.sync:sync.py:116 External event: TriggerData(machine=<weakproxy at 0x7f3c5f166b10; to 'statemachine.io.test191' at 0x7f3c5f17c1a0>, event=Event('timeout', delay=2000.0, internal=False), send_id='d96644f8d15f49b2bd7440c62b61dd38', _target=None, execution_time=1733943931.664144, model=Model(state=s0), args=(), kwargs={})
14+
DEBUG statemachine.engines.sync:sync.py:131 Enabled transitions: {transition * from S0 to Fail}
15+
DEBUG statemachine.engines.base:base.py:283 States to exit: {S0}
16+
DEBUG statemachine.engines.base:base.py:374 States to enter: {Fail}
17+
18+
```
19+
20+
## "On transition" events
21+
```py
22+
DebugEvent(source='s0', event='timeout', data='{}', target='fail')
23+
```
24+
25+
## Traceback
26+
```py
27+
Assertion of the testcase failed.
28+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Testcase: test192
2+
3+
AssertionError: Assertion failed.
4+
5+
Final configuration: `['fail']`
6+
7+
---
8+
9+
## Logs
10+
```py
11+
DEBUG statemachine.engines.base:base.py:374 States to enter: {S0, S01}
12+
DEBUG statemachine.engines.sync:sync.py:64 Processing loop started: ['s0', 's01']
13+
DEBUG statemachine.engines.sync:sync.py:116 External event: TriggerData(machine=<weakproxy at 0x7fd4c5346ca0; to 'statemachine.io.test192' at 0x7fd4c5429400>, event=Event('timeout', delay=2000.0, internal=False), send_id='a3f00d7e247a45d4bffc85ffc3540742', _target=None, execution_time=1733943932.6457262, model=Model(state=['s0', 's01']), args=(), kwargs={})
14+
DEBUG statemachine.engines.sync:sync.py:131 Enabled transitions: {transition timeout from S0 to Fail}
15+
DEBUG statemachine.engines.base:base.py:283 States to exit: {S0, S01}
16+
DEBUG statemachine.engines.base:base.py:374 States to enter: {Fail}
17+
18+
```
19+
20+
## "On transition" events
21+
```py
22+
DebugEvent(source='s0', event='timeout', data='{}', target='fail')
23+
```
24+
25+
## Traceback
26+
```py
27+
Assertion of the testcase failed.
28+
```
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Testcase: test207
2+
3+
AssertionError: Assertion failed.
4+
5+
Final configuration: `No configuration`
6+
7+
---
8+
9+
## Logs
10+
```py
11+
DEBUG statemachine.engines.base:base.py:374 States to enter: {S0, S01}
12+
DEBUG statemachine.engines.sync:sync.py:64 Processing loop started: ['s0', 's01']
13+
DEBUG statemachine.engines.sync:sync.py:116 External event: TriggerData(machine=<weakproxy at 0x7efc22ac3d30; to 'statemachine.io.test207' at 0x7efc22b08590>, event=Event('timeout', delay=2000.0, internal=False), send_id='c56c759204124df98a1d96e2678a307a', _target=None, execution_time=1733943926.5340483, model=Model(state=['s0', 's01']), args=(), kwargs={})
14+
DEBUG statemachine.engines.sync:sync.py:131 Enabled transitions: {transition timeout from S0 to }
15+
DEBUG statemachine.engines.base:base.py:283 States to exit: {S0, S01}
16+
DEBUG statemachine.engines.base:base.py:374 States to enter: {}
17+
18+
```
19+
20+
## "On transition" events
21+
```py
22+
DebugEvent(source='s0', event='timeout', data='{}', target='')
23+
```
24+
25+
## Traceback
26+
```py
27+
Assertion of the testcase failed.
28+
```

0 commit comments

Comments
 (0)