feat: property-based testing and extended fuzz targets (Cycle 11)
Add proptest-state-machine for stateful property testing of the runtime, covering FIFO ordering, budget fairness, timer correctness, mailbox bounds, and spawn tracking. Extend cargo-fuzz with stop, restart, and timer actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d58a999db0
commit
9b1518b46c
5 changed files with 1100 additions and 11 deletions
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
## Current Stage: Phase 1 — Research + First Improvement Cycle
|
## Current Stage: Phase 1 — Research + First Improvement Cycle
|
||||||
|
|
||||||
### Status: Cycle 10 COMPLETE
|
### Status: Cycle 11 COMPLETE
|
||||||
|
|
||||||
## Plan Overview
|
## Plan Overview
|
||||||
1. **Phase 0**: Codebase audit — understand current swactor architecture, existing tests, benchmarks ✅
|
1. **Phase 0**: Codebase audit — understand current swactor architecture, existing tests, benchmarks ✅
|
||||||
|
|
@ -124,6 +124,37 @@
|
||||||
- `stop_nonexistent_actor_returns_error` — stop on bad address returns Err
|
- `stop_nonexistent_actor_returns_error` — stop on bad address returns Err
|
||||||
- **Result**: 82 tests pass, all workspace compiles
|
- **Result**: 82 tests pass, all workspace compiles
|
||||||
|
|
||||||
|
### Cycle 11: Property-Based Testing (proptest + fuzz extension)
|
||||||
|
- **Research**: Studied testing approaches across tokio (loom), Erlang (PropEr, QuickCheck, Concuerror),
|
||||||
|
Rust property-based testing (proptest vs quickcheck), cargo-fuzz, and actor-specific testing patterns.
|
||||||
|
- Ranked approaches: #1 proptest-state-machine (perfect fit for deterministic ticks),
|
||||||
|
#2 extend cargo-fuzz, #3 simple proptest, #4 shuttle, #5 loom, #6 DST
|
||||||
|
- Also researched remaining feature gaps: named actors, monitoring/death watch, groups, ask pattern
|
||||||
|
- **Implementation**: Property-based testing suite with proptest-state-machine
|
||||||
|
- Added `proptest` and `proptest-state-machine` to dev-dependencies
|
||||||
|
- New test file: `tests/proptest_runtime.rs` with 7 tests:
|
||||||
|
- `fifo_ordering_for_any_message_sequence` — FIFO preserved for 1-100 random messages
|
||||||
|
- `budget_limits_per_actor_processing` — budget caps per-tick processing for 2-10 actors
|
||||||
|
- `one_shot_timer_fires_at_correct_tick` — timer with delay 1-20 fires at exact right tick
|
||||||
|
- `interval_timer_fires_at_correct_period` — period 1-10, verifies 3 consecutive fires
|
||||||
|
- `bounded_mailbox_never_exceeds_capacity` — capacity 1-20, 1-200 messages, never exceeds
|
||||||
|
- `spawn_n_actors_all_tracked` — 1-50 actors, all unique, all in stats
|
||||||
|
- `swactor_state_machine` — stateful property test: random Spawn/Send/Tick/Stop/CheckStats
|
||||||
|
sequences (up to 40 transitions, 128 cases), verifies runtime invariants after each step
|
||||||
|
- State machine test defines SwactorModel (reference) vs SwactorTest (SUT) with:
|
||||||
|
- Reference model: HashMap<id, alive> tracking expected actor lifecycle
|
||||||
|
- Invariants checked after every transition: worker count, actor placement, mailbox safety
|
||||||
|
- Automatic shrinking finds minimal failing sequences
|
||||||
|
- Extended fuzz targets (fuzz_runtime.rs) with 4 new RawAction variants:
|
||||||
|
- `StopActor` — graceful stop via runtime.stop_actor
|
||||||
|
- `SpawnRestartable` — spawn_restartable with configurable max_restarts
|
||||||
|
- `ScheduleTimer` — one-shot timer via TimerSchedulerActor
|
||||||
|
- `ScheduleInterval` — interval timer via IntervalSchedulerActor
|
||||||
|
- Added 3 new actor types to fuzz: TimerSchedulerActor, IntervalSchedulerActor, RestartableEchoActor
|
||||||
|
- **Bug found**: State machine test immediately caught invariant mismatch: address map tracks spawned
|
||||||
|
actors immediately, but per-worker num_actors lags until first tick. Fixed invariant to use <= check.
|
||||||
|
- **Result**: 95 tests pass (88 behavioral + 7 proptest), fuzz targets compile, zero warnings
|
||||||
|
|
||||||
### Cycle 10: Actor Timers (Tick-Counting)
|
### Cycle 10: Actor Timers (Tick-Counting)
|
||||||
- **Research**: Studied timer/scheduling patterns across Erlang (timer:send_after, erlang:start_timer),
|
- **Research**: Studied timer/scheduling patterns across Erlang (timer:send_after, erlang:start_timer),
|
||||||
Akka (scheduleOnce, scheduler), Actix (ctx.run_later, ctx.run_interval), Kameo (tokio::time::sleep),
|
Akka (scheduleOnce, scheduler), Actix (ctx.run_later, ctx.run_interval), Kameo (tokio::time::sleep),
|
||||||
|
|
@ -226,8 +257,10 @@
|
||||||
- [x] **Cycle 8: Dead actor cleanup** ✅
|
- [x] **Cycle 8: Dead actor cleanup** ✅
|
||||||
- [x] **Cycle 9: Lifecycle hooks + graceful stop** ✅
|
- [x] **Cycle 9: Lifecycle hooks + graceful stop** ✅
|
||||||
- [x] **Cycle 10: Actor timers (tick-counting)** ✅
|
- [x] **Cycle 10: Actor timers (tick-counting)** ✅
|
||||||
- [ ] **Cycle 11: Next improvement**
|
- [x] **Cycle 11: Property-based testing (proptest-state-machine + fuzz extension)** ✅
|
||||||
- Candidates: SmallBox optimization, property-based tests, named actors/registry, actor groups
|
- [ ] **Cycle 12: Next improvement**
|
||||||
|
- Candidates: named actors/registry (small effort, high value), actor monitoring/death watch,
|
||||||
|
actor groups/pub-sub, SmallBox optimization
|
||||||
- Priority messages REJECTED (lifecycle hooks cover 95% of cases)
|
- Priority messages REJECTED (lifecycle hooks cover 95% of cases)
|
||||||
- LIFO slot rejected (0-5% benefit for typical workloads, not worth complexity)
|
- LIFO slot rejected (0-5% benefit for typical workloads, not worth complexity)
|
||||||
|
|
||||||
|
|
|
||||||
392
Cargo.lock
generated
392
Cargo.lock
generated
|
|
@ -29,6 +29,12 @@ version = "1.0.13"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyhow"
|
||||||
|
version = "1.0.101"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5f0e0fee31ef5ed1ba1316088939cea399010ed7731dba877ed44aeb407a75ea"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ascii"
|
name = "ascii"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -47,6 +53,21 @@ version = "1.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-set"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
|
||||||
|
dependencies = [
|
||||||
|
"bit-vec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bit-vec"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.10.0"
|
version = "2.10.0"
|
||||||
|
|
@ -277,7 +298,7 @@ dependencies = [
|
||||||
"crossterm_winapi",
|
"crossterm_winapi",
|
||||||
"mio",
|
"mio",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rustix",
|
"rustix 0.38.44",
|
||||||
"signal-hook",
|
"signal-hook",
|
||||||
"signal-hook-mio",
|
"signal-hook-mio",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|
@ -417,7 +438,7 @@ name = "distribution"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ed25519-dalek",
|
"ed25519-dalek",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"swactor",
|
"swactor",
|
||||||
|
|
@ -441,7 +462,7 @@ checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"curve25519-dalek",
|
"curve25519-dalek",
|
||||||
"ed25519",
|
"ed25519",
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
"serde",
|
"serde",
|
||||||
"sha2",
|
"sha2",
|
||||||
"subtle",
|
"subtle",
|
||||||
|
|
@ -470,12 +491,24 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fiat-crypto"
|
name = "fiat-crypto"
|
||||||
version = "0.2.9"
|
version = "0.2.9"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fnv"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foldhash"
|
name = "foldhash"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
|
|
@ -503,6 +536,31 @@ dependencies = [
|
||||||
"wasi",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.3.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getrandom"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"r-efi",
|
||||||
|
"wasip2",
|
||||||
|
"wasip3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "half"
|
name = "half"
|
||||||
version = "2.7.1"
|
version = "2.7.1"
|
||||||
|
|
@ -549,6 +607,12 @@ version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "id-arena"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ident_case"
|
name = "ident_case"
|
||||||
version = "1.0.1"
|
version = "1.0.1"
|
||||||
|
|
@ -563,6 +627,8 @@ checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.16.1",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -638,6 +704,12 @@ version = "1.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leb128fmt"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.181"
|
version = "0.2.181"
|
||||||
|
|
@ -650,6 +722,12 @@ version = "0.4.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
|
|
@ -837,6 +915,25 @@ version = "1.13.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ppv-lite86"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||||
|
dependencies = [
|
||||||
|
"zerocopy",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "prettyplease"
|
||||||
|
version = "0.2.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
|
|
@ -846,6 +943,34 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proptest"
|
||||||
|
version = "1.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "37566cb3fdacef14c0737f9546df7cfeadbfbc9fef10991038bf5015d0c80532"
|
||||||
|
dependencies = [
|
||||||
|
"bit-set",
|
||||||
|
"bit-vec",
|
||||||
|
"bitflags",
|
||||||
|
"num-traits",
|
||||||
|
"rand",
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_xorshift",
|
||||||
|
"regex-syntax",
|
||||||
|
"rusty-fork",
|
||||||
|
"tempfile",
|
||||||
|
"unarray",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proptest-state-machine"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e943d140e09d07740fb496487c51fb8eb31c70389ac4a2e9dcd8a0d9fdf228d4"
|
||||||
|
dependencies = [
|
||||||
|
"proptest",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyo3"
|
name = "pyo3"
|
||||||
version = "0.23.5"
|
version = "0.23.5"
|
||||||
|
|
@ -917,6 +1042,12 @@ dependencies = [
|
||||||
"swactor",
|
"swactor",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quick-error"
|
||||||
|
version = "1.2.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.44"
|
version = "1.0.44"
|
||||||
|
|
@ -926,13 +1057,57 @@ dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "r-efi"
|
||||||
|
version = "5.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand"
|
||||||
|
version = "0.9.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
|
||||||
|
dependencies = [
|
||||||
|
"rand_chacha",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_chacha"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
|
||||||
|
dependencies = [
|
||||||
|
"ppv-lite86",
|
||||||
|
"rand_core 0.9.5",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand_core"
|
name = "rand_core"
|
||||||
version = "0.6.4"
|
version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_core"
|
||||||
|
version = "0.9.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xorshift"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core 0.9.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1049,16 +1224,41 @@ dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys 0.4.15",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "1.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"errno",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys 0.11.0",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rusty-fork"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"quick-error",
|
||||||
|
"tempfile",
|
||||||
|
"wait-timeout",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ryu"
|
name = "ryu"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
@ -1195,7 +1395,7 @@ version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rand_core",
|
"rand_core 0.6.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1203,7 +1403,7 @@ name = "simulation"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"distribution",
|
"distribution",
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -1285,7 +1485,9 @@ dependencies = [
|
||||||
"criterion",
|
"criterion",
|
||||||
"crossbeam-queue",
|
"crossbeam-queue",
|
||||||
"crossbeam-utils",
|
"crossbeam-utils",
|
||||||
"getrandom",
|
"getrandom 0.2.17",
|
||||||
|
"proptest",
|
||||||
|
"proptest-state-machine",
|
||||||
"serde",
|
"serde",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
@ -1307,6 +1509,19 @@ version = "0.12.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tempfile"
|
||||||
|
version = "3.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0136791f7c95b1f6dd99f9cc786b91bb81c3800b639b3478e561ddb7be95e5f1"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand",
|
||||||
|
"getrandom 0.4.1",
|
||||||
|
"once_cell",
|
||||||
|
"rustix 1.1.3",
|
||||||
|
"windows-sys 0.61.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.1.9"
|
version = "1.1.9"
|
||||||
|
|
@ -1442,6 +1657,12 @@ version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unarray"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.23"
|
version = "1.0.23"
|
||||||
|
|
@ -1477,6 +1698,12 @@ version = "0.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-xid"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unindent"
|
name = "unindent"
|
||||||
version = "0.2.4"
|
version = "0.2.4"
|
||||||
|
|
@ -1495,6 +1722,15 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wait-timeout"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
|
@ -1511,6 +1747,24 @@ version = "0.11.1+wasi-snapshot-preview1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip2"
|
||||||
|
version = "1.0.2+wasi-0.2.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasip3"
|
||||||
|
version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm"
|
name = "wasm"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -1564,6 +1818,40 @@ dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-encoder"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
|
||||||
|
dependencies = [
|
||||||
|
"leb128fmt",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-metadata"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"indexmap",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasmparser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hashbrown 0.15.5",
|
||||||
|
"indexmap",
|
||||||
|
"semver",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.85"
|
version = "0.3.85"
|
||||||
|
|
@ -1702,6 +1990,94 @@ dependencies = [
|
||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||||
|
dependencies = [
|
||||||
|
"wit-bindgen-rust-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-core"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"heck",
|
||||||
|
"indexmap",
|
||||||
|
"prettyplease",
|
||||||
|
"syn",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-component",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-bindgen-rust-macro"
|
||||||
|
version = "0.51.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"prettyplease",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wit-bindgen-core",
|
||||||
|
"wit-bindgen-rust",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-component"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-encoder",
|
||||||
|
"wasm-metadata",
|
||||||
|
"wasmparser",
|
||||||
|
"wit-parser",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wit-parser"
|
||||||
|
version = "0.244.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"id-arena",
|
||||||
|
"indexmap",
|
||||||
|
"log",
|
||||||
|
"semver",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
"unicode-xid",
|
||||||
|
"wasmparser",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.39"
|
version = "0.8.39"
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,8 @@ crossbeam-utils = "0.8.21"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.5", features = ["html_reports"] }
|
criterion = { version = "0.5", features = ["html_reports"] }
|
||||||
|
proptest = "1"
|
||||||
|
proptest-state-machine = "0.3"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "runtime_benchmarks"
|
name = "runtime_benchmarks"
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,51 @@ impl ActorInterface for WrongTypeActor {
|
||||||
fn handle(&mut self, _ctx: &Ctx, _msg: WrongTypeMsg) {}
|
fn handle(&mut self, _ctx: &Ctx, _msg: WrongTypeMsg) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Timer actor: on message, schedules a one-shot timer to deliver the message
|
||||||
|
/// to the given target after `delay` ticks.
|
||||||
|
struct TimerSchedulerActor {
|
||||||
|
target: ActorAddress,
|
||||||
|
delay: u64,
|
||||||
|
}
|
||||||
|
impl ActorInterface for TimerSchedulerActor {
|
||||||
|
type Incoming = FuzzMsg;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||||
|
ctx.send_after_ticks(self.target, msg, self.delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interval timer actor: on start, schedules an interval timer to fire
|
||||||
|
/// to the target every `period` ticks.
|
||||||
|
struct IntervalSchedulerActor {
|
||||||
|
target: ActorAddress,
|
||||||
|
period: u64,
|
||||||
|
}
|
||||||
|
impl ActorInterface for IntervalSchedulerActor {
|
||||||
|
type Incoming = FuzzMsg;
|
||||||
|
type Response = ();
|
||||||
|
fn on_start(&mut self, ctx: &Ctx) {
|
||||||
|
ctx.send_interval_ticks(self.target, FuzzMsg { value: 0, reply_to_idx: None }, self.period);
|
||||||
|
}
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _msg: FuzzMsg) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restartable echo: panics on value=0, otherwise echoes.
|
||||||
|
struct RestartableEchoActor;
|
||||||
|
impl ActorInterface for RestartableEchoActor {
|
||||||
|
type Incoming = FuzzMsg;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: FuzzMsg) {
|
||||||
|
if msg.value == 0 {
|
||||||
|
panic!("fuzz: intentional panic for restart test");
|
||||||
|
}
|
||||||
|
if let Some(idx) = msg.reply_to_idx {
|
||||||
|
let reply = FuzzMsg { value: msg.value, reply_to_idx: None };
|
||||||
|
let _ = ctx.send(INBOX_ADDRS.lock_or_default().get(idx as usize), reply);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Shared Inbox Address Table ─────────────────────────────────────────────
|
// ─── Shared Inbox Address Table ─────────────────────────────────────────────
|
||||||
|
|
||||||
struct InboxAddrs(Vec<ActorAddress>);
|
struct InboxAddrs(Vec<ActorAddress>);
|
||||||
|
|
@ -205,6 +250,14 @@ enum RawAction {
|
||||||
DrainAll,
|
DrainAll,
|
||||||
Tick,
|
Tick,
|
||||||
TickN { n: u8 },
|
TickN { n: u8 },
|
||||||
|
/// Graceful stop an actor
|
||||||
|
StopActor { actor_idx: u8 },
|
||||||
|
/// Spawn a restartable echo actor (max_restarts = n)
|
||||||
|
SpawnRestartable { max_restarts: u8 },
|
||||||
|
/// Schedule a one-shot timer from an actor to an inbox
|
||||||
|
ScheduleTimer { delay: u8 },
|
||||||
|
/// Schedule an interval timer from an actor to an inbox
|
||||||
|
ScheduleInterval { actor_idx: u8, period: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Arbitrary)]
|
#[derive(Debug, Arbitrary)]
|
||||||
|
|
@ -753,6 +806,55 @@ impl FuzzState {
|
||||||
RawAction::DrainAll => { self.drain_all_inboxes(); }
|
RawAction::DrainAll => { self.drain_all_inboxes(); }
|
||||||
RawAction::Tick => { self.tick(); }
|
RawAction::Tick => { self.tick(); }
|
||||||
RawAction::TickN { n } => { self.tick_n((*n).max(1).min(64) as usize); }
|
RawAction::TickN { n } => { self.tick_n((*n).max(1).min(64) as usize); }
|
||||||
|
RawAction::StopActor { actor_idx } => {
|
||||||
|
if let Some(addr) = self.resolve_actor(*actor_idx) {
|
||||||
|
let label = self.actor_label(addr);
|
||||||
|
let _ = self.runtime.stop_actor(addr);
|
||||||
|
self.log(format_args!("[STOP] {label}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawAction::SpawnRestartable { max_restarts } => {
|
||||||
|
let restarts = (*max_restarts).min(5) as u32;
|
||||||
|
if let Ok(addr) = self.runtime.spawn_restartable(
|
||||||
|
RestartableEchoActor,
|
||||||
|
|| RestartableEchoActor,
|
||||||
|
restarts,
|
||||||
|
) {
|
||||||
|
let id = self.actors.len();
|
||||||
|
self.actors.push((addr, ActorKind::Echo));
|
||||||
|
self.total_spawned += 1;
|
||||||
|
self.log(format_args!("[SPAWN] Restartable(max={restarts}) -> actor#{id}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawAction::ScheduleTimer { delay } => {
|
||||||
|
// Create a timer scheduler actor, send it a message to trigger scheduling
|
||||||
|
let delay = (*delay).max(1).min(10) as u64;
|
||||||
|
if self.inboxes.is_empty() { self.new_inbox(); }
|
||||||
|
if let Some(idx) = self.resolve_inbox_idx(0) {
|
||||||
|
let target = *self.inboxes[idx].addr();
|
||||||
|
if let Ok(addr) = self.runtime.spawn(TimerSchedulerActor { target, delay }) {
|
||||||
|
let id = self.actors.len();
|
||||||
|
self.actors.push((addr, ActorKind::Echo));
|
||||||
|
self.total_spawned += 1;
|
||||||
|
self.tick(); // bring actor alive
|
||||||
|
self.send_msg(addr, FuzzMsg { value: 42, reply_to_idx: None });
|
||||||
|
self.log(format_args!("[TIMER] actor#{id} -> inbox#{idx} delay={delay}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RawAction::ScheduleInterval { actor_idx, period } => {
|
||||||
|
let period = (*period).max(1).min(5) as u64;
|
||||||
|
if self.inboxes.is_empty() { self.new_inbox(); }
|
||||||
|
if let Some(idx) = self.resolve_inbox_idx(*actor_idx) {
|
||||||
|
let target = *self.inboxes[idx].addr();
|
||||||
|
if let Ok(addr) = self.runtime.spawn(IntervalSchedulerActor { target, period }) {
|
||||||
|
let id = self.actors.len();
|
||||||
|
self.actors.push((addr, ActorKind::Echo));
|
||||||
|
self.total_spawned += 1;
|
||||||
|
self.log(format_args!("[INTVL] actor#{id} -> inbox#{idx} period={period}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
576
tests/proptest_runtime.rs
Normal file
576
tests/proptest_runtime.rs
Normal file
|
|
@ -0,0 +1,576 @@
|
||||||
|
//! Property-based tests for the swactor runtime.
|
||||||
|
//!
|
||||||
|
//! Uses proptest for randomized testing and proptest-state-machine for
|
||||||
|
//! stateful property testing with automatic shrinking of failing sequences.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use proptest::prelude::*;
|
||||||
|
use proptest_state_machine::{prop_state_machine, ReferenceStateMachine, StateMachineTest};
|
||||||
|
|
||||||
|
use swactor::actor::{ActorAddress, ActorInterface};
|
||||||
|
use swactor::config::{MailboxOverflow, RuntimeConfig};
|
||||||
|
use swactor::runtime::{Ctx, Inbox, Runtime};
|
||||||
|
|
||||||
|
// ─── Shared Actor Types ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct Ping(u64);
|
||||||
|
|
||||||
|
/// Echo: receives Ping, sends Ping back to reply_to address.
|
||||||
|
struct EchoActor {
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
impl ActorInterface for EchoActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, ctx: &Ctx, msg: Ping) {
|
||||||
|
let _ = ctx.send(self.reply_to, msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Counter: tracks message count, replies with count.
|
||||||
|
struct CounterActor {
|
||||||
|
count: u64,
|
||||||
|
reply_to: ActorAddress,
|
||||||
|
}
|
||||||
|
impl ActorInterface for CounterActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, ctx: &Ctx, _msg: Ping) {
|
||||||
|
self.count += 1;
|
||||||
|
let _ = ctx.send(self.reply_to, Ping(self.count));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PanicActor: panics on first message.
|
||||||
|
struct PanicActor;
|
||||||
|
impl ActorInterface for PanicActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {
|
||||||
|
panic!("intentional panic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Noop: discards all messages silently.
|
||||||
|
struct NoopActor;
|
||||||
|
impl ActorInterface for NoopActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Simple Property Tests ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
proptest! {
|
||||||
|
/// FIFO ordering is preserved for any sequence of numbered messages
|
||||||
|
/// sent from a single sender to a single actor.
|
||||||
|
#[test]
|
||||||
|
fn fifo_ordering_for_any_message_sequence(
|
||||||
|
values in proptest::collection::vec(0u64..10_000, 1..100)
|
||||||
|
) {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
||||||
|
let addr = rt.spawn(EchoActor { reply_to: *inbox.addr() }).unwrap();
|
||||||
|
|
||||||
|
// Send all messages
|
||||||
|
for &v in &values {
|
||||||
|
rt.send_to(addr, Ping(v)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tick enough to process all
|
||||||
|
let ticks_needed = (values.len() / 64) + 3; // budget=64 default
|
||||||
|
for _ in 0..ticks_needed { rt.tick(); }
|
||||||
|
|
||||||
|
// Verify FIFO ordering
|
||||||
|
let mut received = Vec::new();
|
||||||
|
while let Some(msg) = inbox.try_recv() {
|
||||||
|
received.push(msg.0);
|
||||||
|
}
|
||||||
|
prop_assert_eq!(&received, &values, "FIFO ordering violated");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Budget fairness: no actor processes more than budget messages per tick
|
||||||
|
/// when multiple actors have pending messages.
|
||||||
|
#[test]
|
||||||
|
fn budget_limits_per_actor_processing(
|
||||||
|
n_actors in 2usize..10,
|
||||||
|
msgs_per in 10usize..100,
|
||||||
|
budget in 1usize..32,
|
||||||
|
) {
|
||||||
|
let config = RuntimeConfig {
|
||||||
|
actor_message_budget: budget,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let rt = Runtime::new(config);
|
||||||
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
||||||
|
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
for _ in 0..n_actors {
|
||||||
|
addrs.push(rt.spawn(CounterActor { count: 0, reply_to: *inbox.addr() }).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
rt.tick(); // on_start
|
||||||
|
|
||||||
|
// Send msgs_per messages to each actor
|
||||||
|
for addr in &addrs {
|
||||||
|
for v in 0..msgs_per as u64 {
|
||||||
|
rt.send_to(*addr, Ping(v)).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Single tick — each actor should process at most `budget` messages
|
||||||
|
rt.tick();
|
||||||
|
|
||||||
|
// Drain inbox to count replies per actor
|
||||||
|
// CounterActor replies with incrementing count, so max reply value = messages processed
|
||||||
|
let mut replies = Vec::new();
|
||||||
|
while let Some(msg) = inbox.try_recv() {
|
||||||
|
replies.push(msg.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Total replies should be at most n_actors * budget
|
||||||
|
prop_assert!(
|
||||||
|
replies.len() <= n_actors * budget,
|
||||||
|
"Too many messages processed: {} > {} (n_actors={}, budget={})",
|
||||||
|
replies.len(), n_actors * budget, n_actors, budget,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot timer fires at exactly the right tick for any delay.
|
||||||
|
#[test]
|
||||||
|
fn one_shot_timer_fires_at_correct_tick(delay in 1u64..20) {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
||||||
|
|
||||||
|
struct TimerActor { target: ActorAddress, delay: u64 }
|
||||||
|
impl ActorInterface for TimerActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
|
||||||
|
fn on_start(&mut self, ctx: &Ctx) {
|
||||||
|
ctx.send_after_ticks(self.target, Ping(42), self.delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _addr = rt.spawn(TimerActor { target: *inbox.addr(), delay }).unwrap();
|
||||||
|
|
||||||
|
// Tick up to the expected fire tick
|
||||||
|
for tick in 1..=(delay + 1) {
|
||||||
|
rt.tick();
|
||||||
|
let msg = inbox.try_recv();
|
||||||
|
if tick <= delay {
|
||||||
|
prop_assert!(msg.is_none(), "Timer fired too early at tick {}", tick);
|
||||||
|
} else {
|
||||||
|
prop_assert!(msg.is_some(), "Timer should have fired at tick {}", tick);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No second fire (one-shot)
|
||||||
|
rt.tick();
|
||||||
|
prop_assert!(inbox.try_recv().is_none(), "One-shot timer fired twice");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interval timer fires at correct periodic ticks for any period.
|
||||||
|
#[test]
|
||||||
|
fn interval_timer_fires_at_correct_period(period in 1u64..10) {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
||||||
|
|
||||||
|
struct IntervalActor { target: ActorAddress, period: u64 }
|
||||||
|
impl ActorInterface for IntervalActor {
|
||||||
|
type Incoming = Ping;
|
||||||
|
type Response = ();
|
||||||
|
fn handle(&mut self, _ctx: &Ctx, _msg: Ping) {}
|
||||||
|
fn on_start(&mut self, ctx: &Ctx) {
|
||||||
|
ctx.send_interval_ticks(self.target, Ping(1), self.period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let _addr = rt.spawn(IntervalActor { target: *inbox.addr(), period }).unwrap();
|
||||||
|
|
||||||
|
// Verify 3 consecutive fires
|
||||||
|
let mut fire_count = 0;
|
||||||
|
// Timer scheduled on tick 1 (on_start). First fire at tick 1+period.
|
||||||
|
for tick in 1..=(period * 3 + 2) {
|
||||||
|
rt.tick();
|
||||||
|
if let Some(_) = inbox.try_recv() {
|
||||||
|
fire_count += 1;
|
||||||
|
// First fire should be at tick (period + 1)
|
||||||
|
// Subsequent fires every `period` ticks after that
|
||||||
|
let expected_tick = period + 1 + (fire_count - 1) * period;
|
||||||
|
prop_assert_eq!(tick, expected_tick,
|
||||||
|
"Fire #{} at wrong tick (period={})", fire_count, period);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prop_assert!(fire_count >= 3, "Expected 3+ fires, got {} (period={})", fire_count, period);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bounded mailbox with DropNewest never exceeds capacity.
|
||||||
|
#[test]
|
||||||
|
fn bounded_mailbox_never_exceeds_capacity(
|
||||||
|
capacity in 1usize..20,
|
||||||
|
msg_count in 1usize..200,
|
||||||
|
) {
|
||||||
|
let config = RuntimeConfig {
|
||||||
|
default_mailbox_capacity: capacity,
|
||||||
|
mailbox_overflow: MailboxOverflow::DropNewest,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let rt = Runtime::new(config);
|
||||||
|
let addr = rt.spawn(NoopActor).unwrap();
|
||||||
|
rt.tick(); // on_start
|
||||||
|
|
||||||
|
for v in 0..msg_count as u64 {
|
||||||
|
rt.send_to(addr, Ping(v)).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let stats = rt.stats();
|
||||||
|
let worker = &stats.workers[0];
|
||||||
|
// Mailbox depth should never exceed capacity
|
||||||
|
prop_assert!(
|
||||||
|
worker.mailbox_depth <= capacity,
|
||||||
|
"Mailbox depth {} exceeds capacity {}",
|
||||||
|
worker.mailbox_depth, capacity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Spawn N actors and verify all get unique addresses and appear in stats.
|
||||||
|
#[test]
|
||||||
|
fn spawn_n_actors_all_tracked(n in 1usize..50) {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
let mut addrs = Vec::new();
|
||||||
|
for _ in 0..n {
|
||||||
|
addrs.push(rt.spawn(NoopActor).unwrap());
|
||||||
|
}
|
||||||
|
rt.tick(); // process spawns
|
||||||
|
|
||||||
|
let stats = rt.stats();
|
||||||
|
prop_assert_eq!(stats.actors.len(), n, "Expected {} actors in stats", n);
|
||||||
|
|
||||||
|
// All addresses should be unique
|
||||||
|
let unique: std::collections::HashSet<_> = addrs.iter().collect();
|
||||||
|
prop_assert_eq!(unique.len(), n, "Duplicate addresses detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── State Machine Test ────────────────────────────────────────────────────
|
||||||
|
//
|
||||||
|
// Reference model tracks expected runtime state. Transitions are random
|
||||||
|
// operations (spawn, send, tick, stop). After each transition, invariants
|
||||||
|
// are checked against the actual runtime.
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct RefState {
|
||||||
|
/// actor_id -> is_alive (not stopped/poisoned)
|
||||||
|
actors: HashMap<usize, bool>,
|
||||||
|
/// actor_id -> messages sent (values, in order)
|
||||||
|
sent_messages: HashMap<usize, Vec<u64>>,
|
||||||
|
/// Number of ticks executed
|
||||||
|
tick_count: u64,
|
||||||
|
/// Next actor ID to assign
|
||||||
|
next_id: usize,
|
||||||
|
/// IDs of actors that will panic on first message
|
||||||
|
panic_actors: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
enum Transition {
|
||||||
|
/// Spawn a new echo actor
|
||||||
|
SpawnEcho,
|
||||||
|
/// Spawn a panic-on-first-message actor
|
||||||
|
SpawnPanic,
|
||||||
|
/// Send a numbered message to actor at index
|
||||||
|
Send { actor_idx: usize, value: u64 },
|
||||||
|
/// Run one tick
|
||||||
|
Tick,
|
||||||
|
/// Run N ticks
|
||||||
|
TickN(u8),
|
||||||
|
/// Stop actor at index gracefully
|
||||||
|
StopActor(usize),
|
||||||
|
/// Check runtime stats match reference
|
||||||
|
CheckStats,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwactorModel;
|
||||||
|
|
||||||
|
impl ReferenceStateMachine for SwactorModel {
|
||||||
|
type State = RefState;
|
||||||
|
type Transition = Transition;
|
||||||
|
|
||||||
|
fn init_state() -> BoxedStrategy<Self::State> {
|
||||||
|
Just(RefState {
|
||||||
|
actors: HashMap::new(),
|
||||||
|
sent_messages: HashMap::new(),
|
||||||
|
tick_count: 0,
|
||||||
|
next_id: 0,
|
||||||
|
panic_actors: Vec::new(),
|
||||||
|
})
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn transitions(state: &Self::State) -> BoxedStrategy<Self::Transition> {
|
||||||
|
let has_actors = !state.actors.is_empty();
|
||||||
|
let has_alive = state.actors.values().any(|&alive| alive);
|
||||||
|
|
||||||
|
if !has_actors {
|
||||||
|
// Must spawn first
|
||||||
|
prop_oneof![
|
||||||
|
3 => Just(Transition::SpawnEcho),
|
||||||
|
1 => Just(Transition::SpawnPanic),
|
||||||
|
]
|
||||||
|
.boxed()
|
||||||
|
} else if !has_alive {
|
||||||
|
// All actors dead, spawn new ones or tick to clean up
|
||||||
|
prop_oneof![
|
||||||
|
3 => Just(Transition::SpawnEcho),
|
||||||
|
1 => Just(Transition::SpawnPanic),
|
||||||
|
1 => Just(Transition::Tick),
|
||||||
|
]
|
||||||
|
.boxed()
|
||||||
|
} else {
|
||||||
|
let n = state.actors.len();
|
||||||
|
prop_oneof![
|
||||||
|
3 => Just(Transition::SpawnEcho),
|
||||||
|
1 => Just(Transition::SpawnPanic),
|
||||||
|
10 => (0..n, 0u64..1000).prop_map(|(idx, val)| Transition::Send {
|
||||||
|
actor_idx: idx,
|
||||||
|
value: val,
|
||||||
|
}),
|
||||||
|
5 => Just(Transition::Tick),
|
||||||
|
2 => (1u8..5).prop_map(Transition::TickN),
|
||||||
|
2 => (0..n).prop_map(Transition::StopActor),
|
||||||
|
1 => Just(Transition::CheckStats),
|
||||||
|
]
|
||||||
|
.boxed()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(mut state: Self::State, transition: &Self::Transition) -> Self::State {
|
||||||
|
match transition {
|
||||||
|
Transition::SpawnEcho => {
|
||||||
|
let id = state.next_id;
|
||||||
|
state.next_id += 1;
|
||||||
|
state.actors.insert(id, true);
|
||||||
|
state.sent_messages.insert(id, Vec::new());
|
||||||
|
}
|
||||||
|
Transition::SpawnPanic => {
|
||||||
|
let id = state.next_id;
|
||||||
|
state.next_id += 1;
|
||||||
|
state.actors.insert(id, true);
|
||||||
|
state.sent_messages.insert(id, Vec::new());
|
||||||
|
state.panic_actors.push(id);
|
||||||
|
}
|
||||||
|
Transition::Send { actor_idx, value } => {
|
||||||
|
let alive_ids: Vec<usize> = state
|
||||||
|
.actors
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, alive)| **alive)
|
||||||
|
.map(|(&id, _)| id)
|
||||||
|
.collect();
|
||||||
|
if !alive_ids.is_empty() {
|
||||||
|
let id = alive_ids[*actor_idx % alive_ids.len()];
|
||||||
|
state
|
||||||
|
.sent_messages
|
||||||
|
.entry(id)
|
||||||
|
.or_default()
|
||||||
|
.push(*value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Transition::Tick => {
|
||||||
|
state.tick_count += 1;
|
||||||
|
}
|
||||||
|
Transition::TickN(n) => {
|
||||||
|
state.tick_count += *n as u64;
|
||||||
|
}
|
||||||
|
Transition::StopActor(idx) => {
|
||||||
|
let alive_ids: Vec<usize> = state
|
||||||
|
.actors
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, alive)| **alive)
|
||||||
|
.map(|(&id, _)| id)
|
||||||
|
.collect();
|
||||||
|
if !alive_ids.is_empty() {
|
||||||
|
let id = alive_ids[*idx % alive_ids.len()];
|
||||||
|
state.actors.insert(id, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Transition::CheckStats => {}
|
||||||
|
}
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preconditions(state: &Self::State, transition: &Self::Transition) -> bool {
|
||||||
|
match transition {
|
||||||
|
Transition::Send { .. } | Transition::StopActor(_) => {
|
||||||
|
state.actors.values().any(|&alive| alive)
|
||||||
|
}
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Concrete System Under Test ────────────────────────────────────────────
|
||||||
|
|
||||||
|
struct SutState {
|
||||||
|
runtime: Runtime,
|
||||||
|
inbox: Inbox<Ping>,
|
||||||
|
/// Maps reference actor_id to actual ActorAddress
|
||||||
|
actor_map: HashMap<usize, ActorAddress>,
|
||||||
|
/// Reference IDs that are panic actors
|
||||||
|
panic_ids: Vec<usize>,
|
||||||
|
/// Tracks which actor IDs are alive (mirrors ref model)
|
||||||
|
alive: HashMap<usize, bool>,
|
||||||
|
/// Next ID for spawn
|
||||||
|
next_id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SwactorTest;
|
||||||
|
|
||||||
|
impl StateMachineTest for SwactorTest {
|
||||||
|
type SystemUnderTest = SutState;
|
||||||
|
type Reference = SwactorModel;
|
||||||
|
|
||||||
|
fn init_test(_ref_state: &RefState) -> Self::SystemUnderTest {
|
||||||
|
let rt = Runtime::new(RuntimeConfig::default());
|
||||||
|
let inbox = rt.new_inbox::<Ping>().unwrap();
|
||||||
|
SutState {
|
||||||
|
runtime: rt,
|
||||||
|
inbox,
|
||||||
|
actor_map: HashMap::new(),
|
||||||
|
panic_ids: Vec::new(),
|
||||||
|
alive: HashMap::new(),
|
||||||
|
next_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply(
|
||||||
|
mut sut: Self::SystemUnderTest,
|
||||||
|
_ref_state: &RefState,
|
||||||
|
transition: Transition,
|
||||||
|
) -> Self::SystemUnderTest {
|
||||||
|
match transition {
|
||||||
|
Transition::SpawnEcho => {
|
||||||
|
let id = sut.next_id;
|
||||||
|
sut.next_id += 1;
|
||||||
|
let addr = sut
|
||||||
|
.runtime
|
||||||
|
.spawn(EchoActor {
|
||||||
|
reply_to: *sut.inbox.addr(),
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
sut.actor_map.insert(id, addr);
|
||||||
|
sut.alive.insert(id, true);
|
||||||
|
}
|
||||||
|
Transition::SpawnPanic => {
|
||||||
|
let id = sut.next_id;
|
||||||
|
sut.next_id += 1;
|
||||||
|
let addr = sut.runtime.spawn(PanicActor).unwrap();
|
||||||
|
sut.actor_map.insert(id, addr);
|
||||||
|
sut.alive.insert(id, true);
|
||||||
|
sut.panic_ids.push(id);
|
||||||
|
}
|
||||||
|
Transition::Send { actor_idx, value } => {
|
||||||
|
let alive_ids: Vec<usize> = sut
|
||||||
|
.alive
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, alive)| **alive)
|
||||||
|
.map(|(&id, _)| id)
|
||||||
|
.collect();
|
||||||
|
if !alive_ids.is_empty() {
|
||||||
|
let id = alive_ids[actor_idx % alive_ids.len()];
|
||||||
|
if let Some(&addr) = sut.actor_map.get(&id) {
|
||||||
|
let _ = sut.runtime.send_to(addr, Ping(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Transition::Tick => {
|
||||||
|
sut.runtime.tick();
|
||||||
|
}
|
||||||
|
Transition::TickN(n) => {
|
||||||
|
for _ in 0..n {
|
||||||
|
sut.runtime.tick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Transition::StopActor(idx) => {
|
||||||
|
let alive_ids: Vec<usize> = sut
|
||||||
|
.alive
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, alive)| **alive)
|
||||||
|
.map(|(&id, _)| id)
|
||||||
|
.collect();
|
||||||
|
if !alive_ids.is_empty() {
|
||||||
|
let id = alive_ids[idx % alive_ids.len()];
|
||||||
|
sut.alive.insert(id, false);
|
||||||
|
if let Some(&addr) = sut.actor_map.get(&id) {
|
||||||
|
let _ = sut.runtime.stop_actor(addr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Transition::CheckStats => {
|
||||||
|
let stats = sut.runtime.stats();
|
||||||
|
assert!(stats.num_workers >= 1);
|
||||||
|
for info in &stats.workers {
|
||||||
|
assert!(info.id < stats.num_workers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sut
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_invariants(sut: &Self::SystemUnderTest, _ref_state: &RefState) {
|
||||||
|
let stats = sut.runtime.stats();
|
||||||
|
|
||||||
|
// Invariant 1: worker count is consistent
|
||||||
|
assert_eq!(stats.workers.len(), stats.num_workers);
|
||||||
|
|
||||||
|
// Invariant 2: all actors in stats are on valid workers
|
||||||
|
for (_, wid) in &stats.actors {
|
||||||
|
assert!(
|
||||||
|
*wid < stats.num_workers,
|
||||||
|
"Actor on worker {} but only {} workers",
|
||||||
|
wid,
|
||||||
|
stats.num_workers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 3: per-worker actor count <= address map count
|
||||||
|
// (workers lag behind address map because they drain spawn queue on tick)
|
||||||
|
let worker_actor_count: usize = stats.workers.iter().map(|w| w.num_actors).sum();
|
||||||
|
assert!(
|
||||||
|
worker_actor_count <= stats.actors.len(),
|
||||||
|
"Worker actor count {} > address map count {}",
|
||||||
|
worker_actor_count,
|
||||||
|
stats.actors.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Invariant 4: inbox can be drained without panic
|
||||||
|
// (type-safety of inbox messages)
|
||||||
|
while let Some(_msg) = sut.inbox.try_recv() {
|
||||||
|
// Just verify no panic on try_recv
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invariant 5: stats queries don't panic
|
||||||
|
let _total_processed: u64 = stats.workers.iter().map(|w| w.messages_processed).sum();
|
||||||
|
let _total_depth: usize = stats.workers.iter().map(|w| w.mailbox_depth).sum();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn teardown(_sut: Self::SystemUnderTest) {
|
||||||
|
// Runtime drops normally
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
prop_state_machine! {
|
||||||
|
#![proptest_config(proptest::test_runner::Config {
|
||||||
|
cases: 128,
|
||||||
|
max_shrink_iters: 10_000,
|
||||||
|
.. proptest::test_runner::Config::default()
|
||||||
|
})]
|
||||||
|
|
||||||
|
/// Given a random sequence of spawn/send/tick/stop operations,
|
||||||
|
/// when applied to a swactor runtime,
|
||||||
|
/// then all invariants (worker consistency, mailbox safety, stats accuracy) hold.
|
||||||
|
#[test]
|
||||||
|
fn swactor_state_machine(sequential 1..40 => SwactorTest);
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue