Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 93 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,106 @@
# simloop

Deterministic simulation testing for Python asyncioseeded task scheduling,
Deterministic simulation testing for Python asyncio: seeded scheduling,
virtual time, and a simulated network with fault injection. Any failure
it finds replays exactly from a seed.
simloop finds replays exactly from a seed.

Rust has [madsim](https://github.com/madsim-rs/madsim) and
[turmoil](https://github.com/tokio-rs/turmoil). Python has nothing comparable.
simloop is that missing tool: it runs **real, unmodified asyncio code** on a
simulated event loop where every scheduling decision is drawn from a seeded PRNG
and time is virtual — so a test that passes 999 times and deadlocks once becomes
a test that deadlocks *every* time at seed 4217, in milliseconds, under a debugger.
[turmoil](https://github.com/tokio-rs/turmoil); FoundationDB and
TigerBeetle made the technique famous. simloop brings it to asyncio:
your real, unmodified networking code runs on a simulated loop where
time is virtual, every scheduling decision is seeded, and the network
loses, delays, duplicates, and partitions traffic on command.

## Status
## Install

**Early development.** The deterministic loop core works: seeded scheduling,
virtual time, replay-proving trace hashes, deadlock detection, and seeded
`sim.random` / `sim.uuid4` / `sim.time` shims, with the stdlib coordination
primitives (`Queue`, `Event`, `Lock`, `gather`, `TaskGroup`, `timeout`)
running unchanged on top. The simulated network is in: `asyncio.open_connection`,
`start_server`, and datagram endpoints run unchanged over in-memory transports
with seeded latency, drops, duplication, partitions, and host crashes. See
[the supported subset](docs/supported-api.md) for the exact contract.
```
pip install simloop
```

## Planned
Python 3.12+. No runtime dependencies. The pytest plugin ships in the
same package and activates automatically.

- **SimLoop** — a deterministic `asyncio` event loop: virtual clock, seeded
ready-queue ordering, append-only scheduling trace with replay-proving hashes.
- **pytest plugin** — `@sim_test(seeds=1000)` to explore schedules, and an exact
replay flag for any failing seed.
## Find a bug, then replay it

Design notes live in [`docs/`](docs/).
```python
import asyncio
from simloop import sim_test


@sim_test(seeds=200)
async def test_replies_survive_a_lossy_network():
loop = asyncio.get_running_loop()
loop.net.set_defaults(latency=(0.001, 0.050))

async def serve():
async def handle(reader, writer):
writer.write((await reader.readline()).upper())
writer.close()

server = await asyncio.start_server(handle, port=8080)
async with server:
await server.serve_forever()

loop.net.host("server").create_task(serve())
await asyncio.sleep(1.0)

async with asyncio.timeout(30.0):
reader, writer = await asyncio.open_connection("server", 8080)
writer.write(b"hello\n")
assert await reader.readline() == b"HELLO\n"
```

`@sim_test(seeds=200)` runs the test under 200 seeds, each on a fresh
simulated loop, and stops at the first failure:

```
simloop: failed at seed 41 (41 seeds passed first)
replay: pytest 'tests/test_echo.py::test_replies_survive_a_lossy_network' --simloop-replay=41

last 20 trace events:
[t=1.0312] net seq=812 send driver>server
...
pending tasks by host:
server Task 'Task-2' awaiting serve_forever at ...
```

The replay command reproduces the failure exactly — same scheduling
decisions, same fault decisions, same trace. In CI, crank the search
without touching code:

```
pytest --simloop-seeds=1000
```

## What the simulation gives you

- **Seeded scheduling** — the ready queue's execution order comes from a
per-run PRNG; a seed pins the entire interleaving.
- **Virtual time** — `asyncio.sleep(300)` costs nothing; timeouts fire
in simulated seconds.
- **A simulated network** — `open_connection` / `start_server` /
datagram endpoints run over an in-memory packet core with per-link
latency, drop, and duplication, plus partitions and host crashes:

```python
net = loop.net
net.set_defaults(latency=(0.001, 0.010), drop=0.05)
net.partition({"node1"}, {"node2", "node3"}) # silent blackhole
loop.call_later(5.0, net.heal) # heals in virtual time
net.crash("node2") # no reset, just silence
```

- **Replayable traces** — every scheduling and fault decision lands in
an append-only trace whose hash proves a replay is exact.

## Honest limits

Code that goes through the event-loop API is supported; code that
bypasses it is fenced: threads and executors, raw sockets, subprocesses,
signals, TLS, and `getaddrinfo` raise `SimulationFenceError` rather than
silently breaking determinism. Write-side flow control is not simulated.
The full contract is in [docs/supported-api.md](docs/supported-api.md).

## License

[MIT](LICENSE)
MIT
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pytest_plugins = ["pytester"]
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "simloop"
version = "0.0.1.dev0"
version = "0.1.0"
description = "Deterministic simulation testing for Python asyncio: seeded scheduling, virtual time, replayable failures"
readme = "README.md"
requires-python = ">=3.12"
Expand All @@ -9,7 +9,7 @@ license-files = ["LICENSE"]
authors = [{ name = "Dhruv Kumar Singh", email = "dsingh32@binghamton.edu" }]
keywords = ["asyncio", "testing", "deterministic", "simulation", "dst"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Development Status :: 3 - Alpha",
"Framework :: AsyncIO",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3.12",
Expand All @@ -22,6 +22,9 @@ dependencies = []
[dependency-groups]
dev = ["pytest>=8", "mypy>=1.11"]

[project.entry-points.pytest11]
simloop = "simloop._pytest_plugin"

[build-system]
requires = ["hatchling>=1.27"]
build-backend = "hatchling.build"
Expand Down
6 changes: 5 additions & 1 deletion src/simloop/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
"""simloop — deterministic simulation testing for Python asyncio."""

from simloop._explore import SeedReport, explore, sim_test
from simloop._loop import SimLoop, SimulationDeadlockError, SimulationFenceError
from simloop._net import Host, SimNetwork
from simloop._sim import Sim, sim
from simloop._trace import TraceEvent

__version__ = "0.0.1.dev0"
__version__ = "0.1.0"

__all__ = [
"Host",
"SeedReport",
"Sim",
"SimLoop",
"SimNetwork",
"SimulationDeadlockError",
"SimulationFenceError",
"TraceEvent",
"__version__",
"explore",
"sim",
"sim_test",
]
Loading
Loading