Skip to content

Commit 5267b95

Browse files
committed
chore: clean up fail marks and enforce variant-specific SCXML test marks
Remove all generic .fail.md files. SCXML test marks are now exclusively variant-specific (testXXX.sync.fail.md / testXXX.async.fail.md). Regenerate with --upd-fail when needed.
1 parent 33edf3b commit 5267b95

46 files changed

Lines changed: 75 additions & 1564 deletions

Some content is hidden

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

INVOKE_PLAN.md

Lines changed: 68 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -11,127 +11,99 @@ sessions when entering a state, with automatic cancellation on exit.
1111

1212
---
1313

14+
## Rules
15+
16+
### SCXML Test Fail Marks
17+
- Only variant-specific marks exist: `testXXX.sync.fail.md` / `testXXX.async.fail.md`
18+
- No generic `.fail.md` — each variant is tracked independently
19+
- Marks are ephemeral: delete all and regenerate with `--upd-fail`
20+
- During development, let tests fail — don't manage marks manually
21+
22+
### Investigation Methodology
23+
- SCXML W3C tests are black-box integration tests
24+
- To investigate failures, write **unit tests** that verify specific hypotheses
25+
- Only modify code after hypotheses are confirmed by failing unit tests
26+
27+
---
28+
1429
## Steps
1530

1631
### 1. Create worktree and branch
17-
- [x] Create git worktree and branch `feat/invoke`
32+
- [x] Done
1833

1934
### 2. Schema + Parser SCXML
20-
- [x] Add `InvokeDefinition` dataclass to `schema.py`
21-
- [x] Add `invocations` field to `schema.State`
22-
- [x] Add `parse_invoke()` to `parser.py`
23-
- [x] Call from `parse_state()` for `<invoke>` elements
24-
- [x] **Commit:** `feat: parse SCXML <invoke> elements` (fe19eca → rebased to 6d8067d)
35+
- [x] `InvokeDefinition` dataclass, `parse_invoke()`, `invocations` field
36+
- [x] **Commit:** 6d8067d
2537

2638
### 3. InvokeConfig and InvokeManager
27-
- [x] Create `statemachine/invoke.py` with:
28-
- `InvokeConfig` — static configuration
29-
- `Invocation` — runtime state
30-
- `ParentBridge` — listener on child for `#_parent` sends
31-
- `InvokeManager` — spawn/cancel/finalize lifecycle
32-
- [x] **Commit:** `feat: add InvokeManager for child session lifecycle` (76b9627 → rebased to 2b94c34)
39+
- [x] `InvokeConfig`, `Invocation`, `ParentBridge`, `InvokeManager`
40+
- [x] **Commit:** 2b94c34
3341

3442
### 4. Engine integration — spawn and cancel
35-
- [x] Add `invoke_manager` and `states_to_invoke` to `BaseEngine.__init__`
36-
- [x] Track states with invocations in `_enter_states()`
37-
- [x] Cancel invocations in `_exit_states()` and `_handle_final_state()`
38-
- [x] Replace TODO in `sync.py` with actual invoke spawn code
39-
- [x] Replace TODO in `sync.py` with finalize + autoforward
40-
- [x] Mirror changes in `async_.py`
41-
- [ ] **Commit** (pending — code written, needs final test pass)
43+
- [x] `invoke_manager` + `states_to_invoke` in BaseEngine
44+
- [x] Track invocations in `_enter_states()`, cancel in `_exit_states()`
45+
- [x] Spawn in sync (daemon thread) and async (asyncio task) engines
46+
- [x] Finalize and autoforward in processing loops
47+
- [x] Fix async `_enter_states` missing `states_to_invoke`
48+
- [x] **Commit:** 0780e4f, 33edf3b
4249

4350
### 5. Event routing (#_parent, invokeid, done.invoke)
44-
- [x] Add `invokeid` field to `TriggerData` in `event_data.py`
45-
- [x] Update `Event.put()` and `Event.build_trigger()` to accept `invokeid`
46-
- [x] Update `StateChart.send()` to accept `invokeid`
47-
- [x] Update `EventDataWrapper.__init__` to read `invokeid` from trigger_data
48-
- [x] Implement `#_parent` target in `create_send_action_callable()`
49-
- [x] Implement `#_child` target
50-
- [x] Implement `#_<invokeid>` target
51-
- [x] Fix `_eval_send_params` duplicate `machine` kwarg bug
52-
- [x] Fix `#_scxml_` vs `#_invokeid` target ordering (test496/521 regression)
53-
- [ ] **Commit** (pending — code written, needs final test pass)
51+
- [x] `invokeid` field on `TriggerData`
52+
- [x] `#_parent`, `#_child`, `#_<invokeid>` send targets
53+
- [x] Fix `_eval_send_params` kwarg conflict
54+
- [x] Fix `#_scxml_` vs `#_invokeid` target ordering
55+
- [x] **Commit:** 0780e4f
5456

5557
### 6. done_invoke_ naming convention and State(invoke=...) API
56-
- [x] Add `invoke` parameter to `State.__init__()`
57-
- [x] Add `_normalize_invoke()` static method
58-
- [x] Add `invocations` property to `InstanceState`
59-
- [x] Add `invoke` to `BaseStateKwargs` in `io/__init__.py`
60-
- [ ] Add `done_invoke_` handler in `factory.py` (not yet implemented)
58+
- [x] `State(invoke=...)` parameter + `_normalize_invoke()` + `InstanceState.invocations`
59+
- [ ] `done_invoke_` handler in `factory.py`
60+
- [ ] Unit tests for Python API
6161
- [ ] **Commit**
6262

6363
### 7. SCXML advanced invoke features
64-
- [x] `src` — load SCXML from external file
65-
- [x] `namelist` / `<param>` — pass initial data to child
66-
- [x] `autoforward` — forward all external events to child
67-
- [x] `<finalize>` — execute before processing child event
68-
- [ ] `typeexpr` — evaluate expression for type (not yet)
69-
- [ ] `srcexpr` — evaluate expression for src (not yet)
64+
- [x] `src` — load SCXML from file
65+
- [x] `namelist` / `<param>` — pass data to child
66+
- [x] `autoforward` / `<finalize>`
67+
- [ ] `srcexpr` / `typeexpr`
68+
- [ ] Fix relative `src` path resolution (test239 failure)
7069
- [ ] **Commit**
7170

72-
### 8. Run W3C invoke tests and fix failures
73-
- [ ] Remove `.fail.md` for passing tests
74-
- [ ] Group failures by root cause and fix
75-
- [ ] Current status (post-rebase, post-bug-fixes):
76-
- Tests that now xpass (need .fail.md removed): test191, test207, test220, test223, test228,
77-
test232, test233, test235, test237, test241, test242, test245, test247, test338, test347,
78-
test422, test554
79-
- Tests still expected to fail: test187, test192, test216, test226, test229, test234, test236,
80-
test240, test243, test244, test276, test530
81-
- [ ] **Commits per group**
71+
### 8. Fix W3C invoke test failures
72+
- [ ] Write unit tests to reproduce specific failure modes
73+
- [ ] Fix root causes identified by unit tests
74+
- [ ] Regenerate fail marks with `--upd-fail`
75+
- Known failure categories:
76+
- **Async timing**: child events not reaching parent before timeout
77+
- **Relative src paths**: `src="file:test239sub1.scxml"` not resolved
78+
- **Cross-session event routing**: `#_parent` send from child thread
79+
- **Session termination**: child onexit not firing on cancel
8280

8381
### 9. Unit tests for Python invoke API
84-
- [ ] Create `tests/test_invoke.py`
85-
- [ ] Test basic: `State(invoke=ChildMachine)`, child terminates, parent gets `done.invoke`
86-
- [ ] Test cancellation: parent exits state, child is cancelled
87-
- [ ] Test cross-engine: sync parent + async child, async parent + sync child
88-
- [ ] Test `done_invoke_` naming convention
89-
- [ ] Test autoforward
90-
- [ ] Test multiple invocations
91-
- [ ] Use `sm_runner` fixture for sync/async coverage
92-
- [ ] **Commit**
82+
- [ ] `tests/test_invoke.py`
83+
- [ ] Basic: `State(invoke=ChildMachine)`, child terminates, `done.invoke`
84+
- [ ] Cancellation: parent exits state, child cancelled
85+
- [ ] `done_invoke_` naming convention
86+
- [ ] Autoforward, multiple invocations
87+
- [ ] Use `sm_runner` fixture
9388

9489
### 10. Documentation and release notes
95-
- [ ] Create `docs/invoke.md` (concept, Python API, SCXML, examples)
96-
- [ ] Add to `docs/index.md` toctree
97-
- [ ] Add section in `docs/releases/3.0.0.md`
98-
- [ ] Reference from `docs/statecharts.md`
99-
- [ ] **Commit**
90+
- [ ] `docs/invoke.md`
91+
- [ ] `docs/index.md` toctree
92+
- [ ] `docs/releases/3.0.0.md`
10093

101-
### 11. Final cleanup and verification
102-
- [ ] `uv run ruff check .` — all clean
103-
- [ ] `uv run ruff format .` — all clean
104-
- [ ] `uv run mypy statemachine/` — no errors
105-
- [ ] `uv run pytest -n auto` — full suite passes
106-
- [ ] Verify W3C invoke tests pass (`.fail.md` removed for passing tests)
107-
- [ ] Fix any regressions
94+
### 11. Final cleanup
95+
- [ ] Linting, mypy, full test suite
96+
- [ ] Regenerate fail marks with `--upd-fail`
10897

10998
---
11099

111-
## Key Files
112-
113-
| File | Status |
114-
|------|--------|
115-
| `statemachine/invoke.py` | Committed + uncommitted changes |
116-
| `statemachine/engines/base.py` | Uncommitted changes |
117-
| `statemachine/engines/sync.py` | Uncommitted changes |
118-
| `statemachine/engines/async_.py` | Uncommitted changes |
119-
| `statemachine/event.py` | Uncommitted changes |
120-
| `statemachine/event_data.py` | Uncommitted changes |
121-
| `statemachine/state.py` | Uncommitted changes |
122-
| `statemachine/statemachine.py` | Uncommitted changes |
123-
| `statemachine/io/__init__.py` | Uncommitted changes |
124-
| `statemachine/io/scxml/actions.py` | Uncommitted changes |
125-
| `statemachine/io/scxml/processor.py` | Uncommitted changes |
126-
| `statemachine/io/scxml/schema.py` | Committed |
127-
| `statemachine/io/scxml/parser.py` | Committed |
128-
129100
## Bugs Fixed
130101

131-
1. **`_eval_send_params` duplicate `machine` kwarg** — function took `machine` as positional
132-
AND received it via `**kwargs`. Fixed by taking only `**kwargs` and extracting `machine` from it.
133-
2. **`#_scxml_` vs `#_invokeid` target ordering**`#_scxml_foo` targets were caught by the
134-
generic `#_` invoke handler before reaching the `#_scxml_``error.communication` handler.
135-
Fixed by checking `#_scxml_` first.
136-
3. **Child `_parent_sm` not set during initial entry** — fixed by setting `_parent_sm` and
137-
`_invokeid` on the child CLASS before instantiation.
102+
1. **`_eval_send_params` duplicate `machine` kwarg** — took `machine` as positional AND via
103+
`**kwargs`. Fixed by taking only `**kwargs`.
104+
2. **`#_scxml_` vs `#_invokeid` target ordering**`#_scxml_foo` caught by generic `#_`
105+
handler. Fixed by checking `#_scxml_` first.
106+
3. **Child `_parent_sm` not set during initial entry** — fixed by setting on child CLASS
107+
before instantiation.
108+
4. **Async `_enter_states` missing `states_to_invoke`** — async override didn't track states
109+
with invocations. Fixed by adding the tracking.

tests/scxml/conftest.py

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,18 @@ def processor(testcase_path: Path):
2525

2626

2727
def compute_testcase_marks(
28-
testcase_path: Path, variant: str = "",
28+
testcase_path: Path, variant: str,
2929
) -> list[pytest.MarkDecorator]:
3030
marks = [pytest.mark.scxml]
3131

32-
# Check variant-specific fail/skip first (e.g., test191.async.fail.md),
33-
# then fall back to the generic one (e.g., test191.fail.md).
32+
# Only variant-specific marks are recognized (e.g., test191.sync.fail.md).
33+
# No generic fallback — each variant is tracked independently.
3434
stem = testcase_path.stem
3535
parent = testcase_path.parent
3636

37-
def _has_mark(suffix: str) -> bool:
38-
if variant and (parent / f"{stem}.{variant}.{suffix}").exists():
39-
return True
40-
return (parent / f"{stem}.{suffix}").exists()
41-
42-
if _has_mark("fail.md"):
37+
if (parent / f"{stem}.{variant}.fail.md").exists():
4338
marks.append(pytest.mark.xfail)
44-
if _has_mark("skip.md"):
39+
if (parent / f"{stem}.{variant}.skip.md").exists():
4540
marks.append(pytest.mark.skip)
4641
return marks
4742

@@ -57,7 +52,7 @@ def pytest_generate_tests(metafunc):
5752
elif func_name.endswith("_sync"):
5853
variant = "sync"
5954
else:
60-
variant = ""
55+
raise ValueError(f"Cannot determine variant from test function name: {func_name}")
6156

6257
metafunc.parametrize(
6358
"testcase_path",

tests/scxml/test_scxml_cases.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,8 @@ def _run_scxml_testcase(
142142
should_generate_debug_diagram,
143143
caplog,
144144
*,
145+
variant: str,
145146
async_mode: bool = False,
146-
variant: str = "",
147147
) -> StateChart:
148148
"""Shared logic for sync and async SCXML test variants.
149149

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

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

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

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

tests/scxml/w3c/mandatory/test215.async.fail.md

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

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

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

tests/scxml/w3c/mandatory/test220.async.fail.md

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

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

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

tests/scxml/w3c/mandatory/test228.async.fail.md

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

0 commit comments

Comments
 (0)