Skip to content

Commit a6e57a6

Browse files
committed
refactor(tests): replace .fail.md xfail mechanism with in-code sets
The file-based .fail.md mechanism had issues: each .scxml generates two tests (sync + async) but the xfail mark was shared, and --upd-fail caused loops when one engine passed but the other failed. Replace with XFAIL_BOTH/XFAIL_SYNC_ONLY/XFAIL_ASYNC_ONLY sets in conftest.py. - Remove 46 .fail.md files (30 mandatory + 16 optional) - Remove --upd-fail CLI option and FailedMark class - Remove unused DebugListener and helper dataclasses - Support per-engine xfail marks via sync/async set split
1 parent e9a9339 commit a6e57a6

49 files changed

Lines changed: 82 additions & 1803 deletions

Some content is hidden

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

tests/conftest.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@
66

77

88
def pytest_addoption(parser):
9-
parser.addoption(
10-
"--upd-fail",
11-
action="store_true",
12-
default=False,
13-
help="Update marks for failing tests",
14-
)
159
parser.addoption(
1610
"--gen-diagram",
1711
action="store_true",

tests/scxml/conftest.py

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,45 +5,91 @@
55
CURRENT_DIR = Path(__file__).parent
66
TESTCASES_DIR = CURRENT_DIR
77

8+
# xfail sets — all tests currently fail identically on both engines
9+
XFAIL_BOTH = {
10+
# mandatory — invoke-related
11+
"test191",
12+
"test192",
13+
"test207",
14+
"test215",
15+
"test216",
16+
"test220",
17+
"test223",
18+
"test224",
19+
"test225",
20+
"test226",
21+
"test228",
22+
"test229",
23+
"test232",
24+
"test233",
25+
"test234",
26+
"test235",
27+
"test236",
28+
"test239",
29+
"test240",
30+
"test241",
31+
"test243",
32+
"test244",
33+
"test245",
34+
"test247",
35+
"test253",
36+
"test276",
37+
"test338",
38+
"test347",
39+
"test422",
40+
"test530",
41+
# optional
42+
"test201",
43+
"test446",
44+
"test509",
45+
"test510",
46+
"test518",
47+
"test519",
48+
"test520",
49+
"test522",
50+
"test531",
51+
"test532",
52+
"test534",
53+
"test557",
54+
"test558",
55+
"test561",
56+
"test567",
57+
"test577",
58+
}
59+
XFAIL_SYNC_ONLY: set[str] = set()
60+
XFAIL_ASYNC_ONLY: set[str] = set()
861

9-
@pytest.fixture(scope="session")
10-
def update_fail_mark(request):
11-
return request.config.getoption("--upd-fail")
62+
XFAIL_SYNC = XFAIL_BOTH | XFAIL_SYNC_ONLY
63+
XFAIL_ASYNC = XFAIL_BOTH | XFAIL_ASYNC_ONLY
1264

1365

1466
@pytest.fixture(scope="session")
1567
def should_generate_debug_diagram(request):
1668
return request.config.getoption("--gen-diagram")
1769

1870

19-
@pytest.fixture()
20-
def processor(testcase_path: Path):
21-
"""
22-
Construct a StateMachine class from the SCXML file
23-
"""
24-
return processor
25-
26-
27-
def compute_testcase_marks(testcase_path: Path) -> list[pytest.MarkDecorator]:
28-
marks = [pytest.mark.scxml]
29-
if testcase_path.with_name(f"{testcase_path.stem}.fail.md").exists():
71+
def compute_testcase_marks(testcase_path: Path, is_async: bool) -> list[pytest.MarkDecorator]:
72+
marks: list[pytest.MarkDecorator] = [pytest.mark.scxml]
73+
test_id = testcase_path.stem
74+
xfail_set = XFAIL_ASYNC if is_async else XFAIL_SYNC
75+
if test_id in xfail_set:
3076
marks.append(pytest.mark.xfail)
31-
if testcase_path.with_name(f"{testcase_path.stem}.skip.md").exists():
32-
marks.append(pytest.mark.skip)
3377
return marks
3478

3579

3680
def pytest_generate_tests(metafunc):
3781
if "testcase_path" not in metafunc.fixturenames:
3882
return
3983

84+
is_async = "async" in metafunc.function.__name__
85+
4086
metafunc.parametrize(
4187
"testcase_path",
4288
[
4389
pytest.param(
4490
testcase_path,
4591
id=str(testcase_path.relative_to(TESTCASES_DIR)),
46-
marks=compute_testcase_marks(testcase_path),
92+
marks=compute_testcase_marks(testcase_path, is_async),
4793
)
4894
for testcase_path in TESTCASES_DIR.glob("**/*.scxml")
4995
if "sub" not in testcase_path.name

tests/scxml/test_scxml_cases.py

Lines changed: 19 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,8 @@
1-
import traceback
2-
from dataclasses import dataclass
3-
from dataclasses import field
41
from pathlib import Path
5-
from typing import Any
62

73
import pytest
8-
from statemachine.event import Event
94
from statemachine.io.scxml.processor import SCXMLProcessor
105

11-
from statemachine import State
126
from statemachine import StateChart
137

148
"""
@@ -22,45 +16,6 @@
2216
""" # noqa: E501
2317

2418

25-
@dataclass(frozen=True, unsafe_hash=True)
26-
class OnTransition:
27-
source: str
28-
event: str
29-
data: str
30-
target: str
31-
32-
33-
@dataclass(frozen=True, unsafe_hash=True)
34-
class OnEnterState:
35-
state: str
36-
event: str
37-
data: str
38-
39-
40-
@dataclass(frozen=True, unsafe_hash=True)
41-
class DebugListener:
42-
events: list[Any] = field(default_factory=list)
43-
44-
def on_transition(self, event: Event, source: State, target: State, event_data):
45-
self.events.append(
46-
OnTransition(
47-
source=f"{source and source.id}",
48-
event=f"{event and event.id}",
49-
data=f"{event_data.trigger_data.kwargs}",
50-
target=f"{target and target.id}",
51-
)
52-
)
53-
54-
def on_enter_state(self, event: Event, state: State, event_data):
55-
self.events.append(
56-
OnEnterState(
57-
state=f"{state.id}",
58-
event=f"{event and event.id}",
59-
data=f"{event_data.trigger_data.kwargs}",
60-
)
61-
)
62-
63-
6419
class AsyncListener:
6520
"""No-op async listener to trigger AsyncEngine selection."""
6621

@@ -69,77 +24,9 @@ async def on_enter_state(
6924
): ... # No-op: presence of async callback triggers AsyncEngine selection
7025

7126

72-
@dataclass
73-
class FailedMark:
74-
reason: str
75-
events: list[OnTransition]
76-
is_assertion_error: bool
77-
exception: Exception
78-
logs: str
79-
configuration: list[str] = field(default_factory=list)
80-
81-
@staticmethod
82-
def _get_header(report: str) -> str:
83-
header_end_index = report.find("---")
84-
return report[:header_end_index]
85-
86-
def write_fail_markdown(self, testcase_path: Path):
87-
fail_file_path = testcase_path.with_suffix(".fail.md")
88-
if not self.is_assertion_error:
89-
exception_traceback = "".join(
90-
traceback.format_exception(
91-
type(self.exception), self.exception, self.exception.__traceback__
92-
)
93-
)
94-
else:
95-
exception_traceback = "Assertion of the testcase failed."
96-
97-
report = """# Testcase: {testcase_path.stem}
98-
99-
{reason}
100-
101-
Final configuration: `{configuration}`
102-
103-
---
104-
105-
## Logs
106-
```py
107-
{logs}
108-
```
109-
110-
## "On transition" events
111-
```py
112-
{events}
113-
```
114-
115-
## Traceback
116-
```py
117-
{exception_traceback}
118-
```
119-
""".format(
120-
testcase_path=testcase_path,
121-
reason=self.reason,
122-
configuration=self.configuration if self.configuration else "No configuration",
123-
logs=self.logs if self.logs else "No logs",
124-
events="\n".join(map(repr, self.events)) if self.events else "No events",
125-
exception_traceback=exception_traceback,
126-
)
127-
128-
if fail_file_path.exists():
129-
last_report = fail_file_path.read_text()
130-
131-
if self._get_header(report) == self._get_header(last_report):
132-
return
133-
134-
with fail_file_path.open("w") as fail_file:
135-
fail_file.write(report)
136-
137-
13827
def _run_scxml_testcase(
13928
testcase_path: Path,
140-
update_fail_mark,
14129
should_generate_debug_diagram,
142-
caplog,
14330
*,
14431
async_mode: bool = False,
14532
) -> StateChart:
@@ -150,65 +37,40 @@ def _run_scxml_testcase(
15037
"""
15138
from statemachine.contrib.diagram import DotGraphMachine
15239

153-
sm: "StateChart | None" = None
154-
try:
155-
debug = DebugListener()
156-
listeners: list = [debug]
157-
if async_mode:
158-
listeners.append(AsyncListener())
159-
processor = SCXMLProcessor()
160-
processor.parse_scxml_file(testcase_path)
161-
162-
sm = processor.start(listeners=listeners)
163-
if should_generate_debug_diagram:
164-
DotGraphMachine(sm).get_graph().write_png(
165-
testcase_path.parent / f"{testcase_path.stem}.png"
166-
)
167-
assert sm is not None
168-
return sm
169-
except Exception as e:
170-
if update_fail_mark:
171-
reason = f"{e.__class__.__name__}: {e.__class__.__doc__}"
172-
is_assertion_error = isinstance(e, AssertionError)
173-
fail_mark = FailedMark(
174-
reason=reason,
175-
is_assertion_error=is_assertion_error,
176-
events=debug.events,
177-
exception=e,
178-
logs=caplog.text,
179-
configuration=[s.id for s in sm.configuration] if sm else [],
180-
)
181-
fail_mark.write_fail_markdown(testcase_path)
182-
raise
183-
184-
185-
def _assert_passed(sm: StateChart, debug: "DebugListener | None" = None):
40+
listeners: list = []
41+
if async_mode:
42+
listeners.append(AsyncListener())
43+
processor = SCXMLProcessor()
44+
processor.parse_scxml_file(testcase_path)
45+
46+
sm = processor.start(listeners=listeners)
47+
if should_generate_debug_diagram:
48+
DotGraphMachine(sm).get_graph().write_png(
49+
testcase_path.parent / f"{testcase_path.stem}.png"
50+
)
51+
assert isinstance(sm, StateChart)
52+
return sm
53+
54+
55+
def _assert_passed(sm: StateChart):
18656
assert isinstance(sm, StateChart)
187-
assert "pass" in {s.id for s in sm.configuration}, debug
57+
assert "pass" in {s.id for s in sm.configuration}
18858

18959

190-
def test_scxml_usecase_sync(
191-
testcase_path: Path, update_fail_mark, should_generate_debug_diagram, caplog
192-
):
60+
def test_scxml_usecase_sync(testcase_path: Path, should_generate_debug_diagram, caplog):
19361
sm = _run_scxml_testcase(
19462
testcase_path,
195-
update_fail_mark,
19663
should_generate_debug_diagram,
197-
caplog,
19864
async_mode=False,
19965
)
20066
_assert_passed(sm)
20167

20268

20369
@pytest.mark.asyncio()
204-
async def test_scxml_usecase_async(
205-
testcase_path: Path, update_fail_mark, should_generate_debug_diagram, caplog
206-
):
70+
async def test_scxml_usecase_async(testcase_path: Path, should_generate_debug_diagram, caplog):
20771
sm = _run_scxml_testcase(
20872
testcase_path,
209-
update_fail_mark,
21073
should_generate_debug_diagram,
211-
caplog,
21274
async_mode=True,
21375
)
21476
# In async context, the engine only queued __initial__ during __init__.

tests/scxml/w3c/mandatory/test191.fail.md

Lines changed: 0 additions & 31 deletions
This file was deleted.

0 commit comments

Comments
 (0)