baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,204 @@
|
||||
"""状态机服务测试(纯内存:use_transaction=False 跳过 DB)。"""
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.core.workflow import StateMachine, StateTransitionError, TransitionContext
|
||||
|
||||
|
||||
class Stub:
|
||||
def __init__(self, state="draft"):
|
||||
self.state = state
|
||||
self.saved = []
|
||||
|
||||
def save(self, update_fields=None):
|
||||
self.saved.append((self.state, update_fields))
|
||||
|
||||
|
||||
def make_sm():
|
||||
return StateMachine(
|
||||
states=("draft", "confirmed", "posted", "closed", "cancelled"),
|
||||
initial="draft",
|
||||
transitions={
|
||||
"draft": {"confirm": "confirmed", "cancel": "cancelled"},
|
||||
"confirmed": {"post": "posted", "cancel": "cancelled"},
|
||||
"posted": {"close": "closed"},
|
||||
"closed": {},
|
||||
"cancelled": {},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_get_next_states():
|
||||
sm = make_sm()
|
||||
assert sm.get_next_states("draft") == ["confirmed", "cancelled"]
|
||||
assert sm.get_next_states("posted") == ["closed"]
|
||||
assert sm.get_next_states("closed") == []
|
||||
|
||||
|
||||
def test_can_transition():
|
||||
sm = make_sm()
|
||||
obj = Stub()
|
||||
assert sm.can_transition(obj, "confirm") is True
|
||||
assert sm.can_transition(obj, "post") is False
|
||||
|
||||
|
||||
def test_transition_changes_state():
|
||||
sm = make_sm()
|
||||
obj = Stub("draft")
|
||||
ctx = sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert obj.state == "confirmed"
|
||||
assert obj.saved == [] # state machine 不自动 save
|
||||
assert isinstance(ctx, TransitionContext)
|
||||
assert ctx.from_state == "draft"
|
||||
assert ctx.to_state == "confirmed"
|
||||
assert ctx.event == "confirm"
|
||||
|
||||
|
||||
def test_invalid_event_raises():
|
||||
sm = make_sm()
|
||||
obj = Stub("posted")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
|
||||
|
||||
def test_invalid_from_state_raises():
|
||||
sm = make_sm()
|
||||
obj = Stub("unknown_state")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
|
||||
|
||||
def test_guard_blocks_transition():
|
||||
blocked = []
|
||||
|
||||
def guard(ctx):
|
||||
blocked.append(ctx)
|
||||
return False # 显式拒绝
|
||||
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
guards={("draft", "confirmed", "confirm"): guard},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition(obj, "confirm", user="alice", use_transaction=False)
|
||||
assert blocked
|
||||
assert obj.state == "draft" # 状态未被改
|
||||
|
||||
|
||||
def test_guard_raising_also_blocks():
|
||||
"""guard 抛异常也视为拒绝。"""
|
||||
|
||||
class BusinessError(Exception):
|
||||
pass
|
||||
|
||||
def guard(ctx):
|
||||
raise BusinessError("nope")
|
||||
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
guards={("draft", "confirmed", "confirm"): guard},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
with pytest.raises(BusinessError):
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert obj.state == "draft"
|
||||
|
||||
|
||||
def test_guard_returns_ok_passes():
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
guards={("draft", "confirmed", "confirm"): lambda ctx: None},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert obj.state == "confirmed"
|
||||
|
||||
|
||||
def test_action_runs_after_state_change():
|
||||
fired = []
|
||||
|
||||
def action(ctx):
|
||||
fired.append((ctx.from_state, ctx.to_state))
|
||||
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
actions={("draft", "confirmed", "confirm"): action},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert fired == [("draft", "confirmed")]
|
||||
|
||||
|
||||
def test_hooks_runs_after_action():
|
||||
fired = []
|
||||
|
||||
def hook(ctx):
|
||||
fired.append(ctx.event)
|
||||
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
hooks={"confirm": [hook]},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert fired == ["confirm"]
|
||||
|
||||
|
||||
def test_extra_passed_in_context():
|
||||
seen = []
|
||||
|
||||
def action(ctx):
|
||||
seen.append(ctx.extra)
|
||||
|
||||
sm = StateMachine(
|
||||
states=("draft", "confirmed"),
|
||||
initial="draft",
|
||||
transitions={"draft": {"confirm": "confirmed"}},
|
||||
actions={("draft", "confirmed", "confirm"): action},
|
||||
)
|
||||
obj = Stub("draft")
|
||||
sm.transition(obj, "confirm", extra={"warehouse_id": 1}, use_transaction=False)
|
||||
assert seen == [{"warehouse_id": 1}]
|
||||
|
||||
|
||||
def test_full_workflow_chain():
|
||||
sm = make_sm()
|
||||
obj = Stub("draft")
|
||||
sm.transition(obj, "confirm", use_transaction=False)
|
||||
assert obj.state == "confirmed"
|
||||
sm.transition(obj, "post", use_transaction=False)
|
||||
assert obj.state == "posted"
|
||||
sm.transition(obj, "close", use_transaction=False)
|
||||
assert obj.state == "closed"
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition(obj, "close", use_transaction=False)
|
||||
|
||||
|
||||
def test_initial_must_be_in_states():
|
||||
with pytest.raises(ValueError):
|
||||
StateMachine(
|
||||
states=("draft",),
|
||||
initial="wrong",
|
||||
transitions={},
|
||||
)
|
||||
|
||||
|
||||
def test_cancel_branch():
|
||||
sm = make_sm()
|
||||
obj = Stub("confirmed")
|
||||
sm.transition(obj, "cancel", use_transaction=False)
|
||||
assert obj.state == "cancelled"
|
||||
# cancelled 是终态
|
||||
with pytest.raises(StateTransitionError):
|
||||
sm.transition(obj, "post", use_transaction=False)
|
||||
Reference in New Issue
Block a user