loopmini 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ tags: ['v*']
7
+ pull_request:
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v7
14
+ - uses: dtolnay/rust-toolchain@stable
15
+ - uses: actions/setup-python@v7
16
+ with:
17
+ python-version: '3.12'
18
+ - run: pip install -e '.[dev]'
19
+ - run: pytest -q
20
+
21
+ build:
22
+ needs: test
23
+ strategy:
24
+ matrix:
25
+ os: [ubuntu-latest, macos-latest]
26
+ runs-on: ${{ matrix.os }}
27
+ steps:
28
+ - uses: actions/checkout@v7
29
+ - uses: PyO3/maturin-action@v1
30
+ with:
31
+ args: --release --out dist -i python3.10 -i python3.11 -i python3.12 -i python3.13
32
+ manylinux: auto
33
+ - uses: actions/upload-artifact@v7
34
+ with:
35
+ name: wheels-${{ matrix.os }}
36
+ path: dist
37
+
38
+ sdist:
39
+ runs-on: ubuntu-latest
40
+ steps:
41
+ - uses: actions/checkout@v7
42
+ - uses: PyO3/maturin-action@v1
43
+ with:
44
+ command: sdist
45
+ args: -o dist
46
+ - uses: actions/upload-artifact@v7
47
+ with:
48
+ name: wheels-sdist
49
+ path: dist
50
+
51
+ publish:
52
+ if: startsWith(github.ref, 'refs/tags/v')
53
+ needs: [build, sdist]
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ id-token: write
57
+ contents: write
58
+ steps:
59
+ - uses: actions/checkout@v7
60
+ - uses: actions/download-artifact@v8
61
+ with:
62
+ path: dist
63
+ merge-multiple: true
64
+ - uses: softprops/action-gh-release@v3
65
+ with:
66
+ files: dist/*
67
+ generate_release_notes: true
68
+ - uses: pypa/gh-action-pypi-publish@release/v1
69
+ with:
70
+ packages-dir: dist/
@@ -0,0 +1,16 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.so
4
+ *.egg-info/
5
+ tags
6
+ target/
7
+ dist/
8
+ build/
9
+ .venv/
10
+ venv/
11
+ .env
12
+ .DS_Store
13
+ .ipynb_checkpoints/
14
+ Cargo.lock
15
+ .pytest_cache/
16
+ .sesskey
@@ -0,0 +1,271 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "bitflags"
7
+ version = "2.13.1"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
10
+
11
+ [[package]]
12
+ name = "cfg-if"
13
+ version = "1.0.4"
14
+ source = "registry+https://github.com/rust-lang/crates.io-index"
15
+ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
16
+
17
+ [[package]]
18
+ name = "concurrent-queue"
19
+ version = "2.5.0"
20
+ source = "registry+https://github.com/rust-lang/crates.io-index"
21
+ checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
22
+ dependencies = [
23
+ "crossbeam-utils",
24
+ ]
25
+
26
+ [[package]]
27
+ name = "crossbeam-utils"
28
+ version = "0.8.22"
29
+ source = "registry+https://github.com/rust-lang/crates.io-index"
30
+ checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
31
+
32
+ [[package]]
33
+ name = "errno"
34
+ version = "0.3.14"
35
+ source = "registry+https://github.com/rust-lang/crates.io-index"
36
+ checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
37
+ dependencies = [
38
+ "libc",
39
+ "windows-sys",
40
+ ]
41
+
42
+ [[package]]
43
+ name = "heck"
44
+ version = "0.5.0"
45
+ source = "registry+https://github.com/rust-lang/crates.io-index"
46
+ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
47
+
48
+ [[package]]
49
+ name = "hermit-abi"
50
+ version = "0.5.2"
51
+ source = "registry+https://github.com/rust-lang/crates.io-index"
52
+ checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
53
+
54
+ [[package]]
55
+ name = "libc"
56
+ version = "0.2.189"
57
+ source = "registry+https://github.com/rust-lang/crates.io-index"
58
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
59
+
60
+ [[package]]
61
+ name = "linux-raw-sys"
62
+ version = "0.12.1"
63
+ source = "registry+https://github.com/rust-lang/crates.io-index"
64
+ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
65
+
66
+ [[package]]
67
+ name = "loopmini"
68
+ version = "0.1.1"
69
+ dependencies = [
70
+ "polling",
71
+ "pyo3",
72
+ "tokio",
73
+ ]
74
+
75
+ [[package]]
76
+ name = "mio"
77
+ version = "1.2.2"
78
+ source = "registry+https://github.com/rust-lang/crates.io-index"
79
+ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
80
+ dependencies = [
81
+ "libc",
82
+ "wasi",
83
+ "windows-sys",
84
+ ]
85
+
86
+ [[package]]
87
+ name = "once_cell"
88
+ version = "1.21.4"
89
+ source = "registry+https://github.com/rust-lang/crates.io-index"
90
+ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
91
+
92
+ [[package]]
93
+ name = "pin-project-lite"
94
+ version = "0.2.17"
95
+ source = "registry+https://github.com/rust-lang/crates.io-index"
96
+ checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
97
+
98
+ [[package]]
99
+ name = "polling"
100
+ version = "3.11.0"
101
+ source = "registry+https://github.com/rust-lang/crates.io-index"
102
+ checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218"
103
+ dependencies = [
104
+ "cfg-if",
105
+ "concurrent-queue",
106
+ "hermit-abi",
107
+ "pin-project-lite",
108
+ "rustix",
109
+ "windows-sys",
110
+ ]
111
+
112
+ [[package]]
113
+ name = "portable-atomic"
114
+ version = "1.15.0"
115
+ source = "registry+https://github.com/rust-lang/crates.io-index"
116
+ checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85"
117
+
118
+ [[package]]
119
+ name = "proc-macro2"
120
+ version = "1.0.107"
121
+ source = "registry+https://github.com/rust-lang/crates.io-index"
122
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
123
+ dependencies = [
124
+ "unicode-ident",
125
+ ]
126
+
127
+ [[package]]
128
+ name = "pyo3"
129
+ version = "0.29.2"
130
+ source = "registry+https://github.com/rust-lang/crates.io-index"
131
+ checksum = "4688ddedf473e32662b9b067670129a8afb8c18e351482c70d62ba4a88171e8b"
132
+ dependencies = [
133
+ "libc",
134
+ "once_cell",
135
+ "portable-atomic",
136
+ "pyo3-build-config",
137
+ "pyo3-ffi",
138
+ "pyo3-macros",
139
+ ]
140
+
141
+ [[package]]
142
+ name = "pyo3-build-config"
143
+ version = "0.29.2"
144
+ source = "registry+https://github.com/rust-lang/crates.io-index"
145
+ checksum = "f41027e41b4bd03f6e60f9f417fe24a6341a6bb744edd62b6f709f2a52ea30e9"
146
+ dependencies = [
147
+ "target-lexicon",
148
+ ]
149
+
150
+ [[package]]
151
+ name = "pyo3-ffi"
152
+ version = "0.29.2"
153
+ source = "registry+https://github.com/rust-lang/crates.io-index"
154
+ checksum = "e591a95526fead067432c3b3a33fc74770b87b1e04e73671090d9c2055a2b327"
155
+ dependencies = [
156
+ "libc",
157
+ "pyo3-build-config",
158
+ ]
159
+
160
+ [[package]]
161
+ name = "pyo3-macros"
162
+ version = "0.29.2"
163
+ source = "registry+https://github.com/rust-lang/crates.io-index"
164
+ checksum = "73225868fc1cd84eef2c3c230ddb91273bf1de46aeb8a4248da76d32a0924a1c"
165
+ dependencies = [
166
+ "proc-macro2",
167
+ "pyo3-macros-backend",
168
+ "quote",
169
+ "syn",
170
+ ]
171
+
172
+ [[package]]
173
+ name = "pyo3-macros-backend"
174
+ version = "0.29.2"
175
+ source = "registry+https://github.com/rust-lang/crates.io-index"
176
+ checksum = "571575aa3749fa6216757dd47d2a3e7ef360f329a40f0666a9fbd14889024952"
177
+ dependencies = [
178
+ "heck",
179
+ "proc-macro2",
180
+ "quote",
181
+ "syn",
182
+ ]
183
+
184
+ [[package]]
185
+ name = "quote"
186
+ version = "1.0.47"
187
+ source = "registry+https://github.com/rust-lang/crates.io-index"
188
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
189
+ dependencies = [
190
+ "proc-macro2",
191
+ ]
192
+
193
+ [[package]]
194
+ name = "rustix"
195
+ version = "1.1.4"
196
+ source = "registry+https://github.com/rust-lang/crates.io-index"
197
+ checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
198
+ dependencies = [
199
+ "bitflags",
200
+ "errno",
201
+ "libc",
202
+ "linux-raw-sys",
203
+ "windows-sys",
204
+ ]
205
+
206
+ [[package]]
207
+ name = "socket2"
208
+ version = "0.6.5"
209
+ source = "registry+https://github.com/rust-lang/crates.io-index"
210
+ checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
211
+ dependencies = [
212
+ "libc",
213
+ "windows-sys",
214
+ ]
215
+
216
+ [[package]]
217
+ name = "syn"
218
+ version = "2.0.119"
219
+ source = "registry+https://github.com/rust-lang/crates.io-index"
220
+ checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
221
+ dependencies = [
222
+ "proc-macro2",
223
+ "quote",
224
+ "unicode-ident",
225
+ ]
226
+
227
+ [[package]]
228
+ name = "target-lexicon"
229
+ version = "0.13.5"
230
+ source = "registry+https://github.com/rust-lang/crates.io-index"
231
+ checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
232
+
233
+ [[package]]
234
+ name = "tokio"
235
+ version = "1.53.1"
236
+ source = "registry+https://github.com/rust-lang/crates.io-index"
237
+ checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
238
+ dependencies = [
239
+ "libc",
240
+ "mio",
241
+ "pin-project-lite",
242
+ "socket2",
243
+ "windows-sys",
244
+ ]
245
+
246
+ [[package]]
247
+ name = "unicode-ident"
248
+ version = "1.0.24"
249
+ source = "registry+https://github.com/rust-lang/crates.io-index"
250
+ checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
251
+
252
+ [[package]]
253
+ name = "wasi"
254
+ version = "0.11.1+wasi-snapshot-preview1"
255
+ source = "registry+https://github.com/rust-lang/crates.io-index"
256
+ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
257
+
258
+ [[package]]
259
+ name = "windows-link"
260
+ version = "0.2.1"
261
+ source = "registry+https://github.com/rust-lang/crates.io-index"
262
+ checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
263
+
264
+ [[package]]
265
+ name = "windows-sys"
266
+ version = "0.61.2"
267
+ source = "registry+https://github.com/rust-lang/crates.io-index"
268
+ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
269
+ dependencies = [
270
+ "windows-link",
271
+ ]
@@ -0,0 +1,23 @@
1
+ [package]
2
+ name = "loopmini"
3
+ version = "0.1.1"
4
+ edition = "2021"
5
+ license = "Apache-2.0"
6
+ description = "Rust-backed asyncio event loop"
7
+ repository = "https://github.com/AnswerDotAI/loopmini"
8
+ readme = "README.md"
9
+
10
+ [lib]
11
+ name = "loopmini"
12
+ crate-type = ["cdylib", "rlib"]
13
+
14
+ [dependencies]
15
+ pyo3 = { version = ">=0.28", features = ["py-clone"] }
16
+ polling = "3"
17
+ tokio = { version = "1", features = ["rt", "time", "net"] }
18
+
19
+ [dev-dependencies]
20
+ tokio = { version = "1", features = ["rt-multi-thread"] }
21
+
22
+ [features]
23
+ extension-module = ["pyo3/extension-module"]
loopmini-0.1.1/DEV.md ADDED
@@ -0,0 +1,204 @@
1
+ # Developer guide
2
+
3
+ ## Why this exists
4
+
5
+ Kernmini's Rust engine (see kernmini `meta/ROUGH.md`, "Separate project idea: a
6
+ Tokio-backed asyncio loop" and the 2026-08-28 review) needs Python kernels to
7
+ run arbitrary user asyncio code. loopmini tests whether a Rust reactor can host
8
+ that loop with full asyncio compatibility. The compatibility bar is the full
9
+ public asyncio surface, because solveit users run arbitrary packages; the
10
+ strategy for meeting it is maximal reuse of CPython's own asyncio machinery,
11
+ with Rust owning only what Python cannot express well.
12
+
13
+ ## Crate/Python split
14
+
15
+ A key goal is that Rust crates and PyO3 wrappers share code: the same reactor
16
+ must be drivable by a pure-Rust kernel (as kernmini's native crate will be).
17
+ So the crate has three layers:
18
+
19
+ - `src/reactor.rs`: `Reactor<H>`, a PyO3-free, Tokio-free core generic over
20
+ the handle type. Ready queue, timer map, fd interest map (oneshot `polling`
21
+ sources, re-armed on delivery), thread-safe `schedule_ts` + notify, and the
22
+ turn phases: `next_timeout` / `poll` / `process` / `take_batch` /
23
+ `requeue_front`. `poll` holds no locks, so a driver may block in it with the
24
+ GIL released. The crate builds as an rlib, so a Rust consumer instantiates
25
+ `Reactor<RustHandle>` directly.
26
+ - `src/tokio_core.rs`: the reactor hosted on a Tokio current-thread runtime.
27
+ Tokio waits on the kqueue's own fd (a kqueue is pollable) and a zero-timeout
28
+ drain then collects events, so the kqueue-native level/oneshot semantics
29
+ that asyncio's `add_reader` contract needs survive Tokio's edge-triggered
30
+ driver. Rust futures spawned on the runtime advance during every blocking
31
+ poll, with the GIL released: the shared-reactor path for kernmini's engine.
32
+ Zero-timeout turns skip the runtime entirely, because entering it rounds the
33
+ wait up to the timer driver's ~1ms tick.
34
+ - `src/pyreactor.rs`: `PyReactor`, the Python-facing pyclass: the scheduling
35
+ and readiness methods plus the canonical dispatch loop (`check_signals` each
36
+ turn, `py.detach` around `poll`, EINTR retry, and the injected-exception
37
+ requeue rule, which must exist exactly once). `loopmini._core` registers it
38
+ on an owned runtime; kernmini's Python feature compiles the same struct and constructs
39
+ it with `PyReactor::with_handle` on kernmini's runtime. Rust has no stable
40
+ dylib ABI, so runtimes and reactors never cross extension boundaries: each
41
+ extension compiles the crate in, and the Python-visible reactor methods are
42
+ the only shared surface. `Loop(reactor=...)` accepts such a foreign reactor.
43
+ - `src/lib.rs`: module registration and the public re-exports (`ReactorCore`,
44
+ `TokioCore`, `PyReactor`) for embedding crates.
45
+
46
+ Python (`python/loopmini/loop.py`) implements `asyncio.AbstractEventLoop` by
47
+ delegating scheduling to the reactor and reusing stock `Handle`, `TimerHandle`,
48
+ `Task`, `Future`, and `wrap_future`. That reuse is a design decision, not a
49
+ shortcut: it buys exact contextvars, cancellation, and introspection semantics
50
+ and removes the most version-sensitive surface. `run_forever` on the main
51
+ thread routes signals through `signal.set_wakeup_fd` into an fd the reactor
52
+ watches, matching `BaseSelectorEventLoop`.
53
+
54
+ ## Implemented surface
55
+
56
+ Beyond the scheduling core: TCP transports (`transports.py`: `SockTransport`
57
+ speaking both `Protocol` and `BufferedProtocol`, with `TCP_NODELAY` set as the
58
+ standard loop does, and `MiniServer` with the accept loop),
59
+ `create_connection`/`create_server` and their Unix-socket twins,
60
+ `connect_accepted_socket`, UDP via `DatagramTransport` and
61
+ `create_datagram_endpoint`, `start_tls` (including the gh-142352
62
+ buffered-StreamReader move), TLS client and server via stock `asyncio.sslproto`
63
+ over our transports, `getaddrinfo`/`getnameinfo` through the executor, and
64
+ `add_signal_handler`/`remove_signal_handler` dispatched from the
65
+ `set_wakeup_fd` socketpair (main-thread loops only, as in the standard loop),
66
+ pipe transports (`connect_read_pipe`/`connect_write_pipe`), and
67
+ `subprocess_exec`/`subprocess_shell` (`subproc.py`: Popen spawned in the
68
+ executor since fork/exec blocks, one reaper thread per child as in asyncio's
69
+ default ThreadedChildWatcher, and pipe transports over the child's fds).
70
+
71
+ Contracts learned from the oracles, kept working by them: `remove_reader`
72
+ cancels the stored `Handle`, so an already-queued readiness callback for a
73
+ removed fd never fires (anyio's futures assume it); `connection_lost` is
74
+ scheduled exactly once however `close()`, `abort()`, and fatal errors overlap
75
+ (anyio's `aclose` does close-then-abort, and a missed schedule leaks the
76
+ socket); a cancelled connect attempt closes its socket; and a raised
77
+ `create_connection` error must not keep a referrer to the attempt-error list.
78
+ `MiniServer` mirrors 3.13 `Server` exactly (test_server pins it):
79
+ `wait_closed()` returns only once the server is closed *and* the last client
80
+ transport is gone, `close_clients()`/`abort_clients()` act on a `WeakSet` of
81
+ attached transports, a cancelled `serve_forever` closes the server and its
82
+ clients before re-raising, and a Unix socket path is unlinked at close only
83
+ when its inode still matches the one bound (`cleanup_socket=False` disables).
84
+ A cancelled timer leaves the reactor's timer map at once: `schedule_at`
85
+ returns a `(deadline µs, seq)` key, the `TimerHandle` subclass carries it, and
86
+ `_timer_handle_cancelled` removes the entry. Retention until the original
87
+ deadline would grow memory for an hour per cancelled `wait_for(..., 3600)`.
88
+ From the aiohttp suite: `loop.time()` must be anchored to `time.monotonic`
89
+ (connectors compare the two directly), so the reactor clock carries a
90
+ constant offset captured at loop creation; and a transport must not hold an
91
+ explicit `memoryview` export of its write buffer across a `send` that can
92
+ raise, because the raised exception's traceback keeps the export alive and
93
+ the buffer can then never be resized (send the bytearray itself, as the
94
+ standard loop does).
95
+
96
+ ## Deliberately not implemented yet
97
+
98
+ Sendfile and Windows.
99
+ `AbstractEventLoop` raises `NotImplementedError` for these, which is the
100
+ honest signal while the compatibility ladder is climbed. Next rungs and their
101
+ oracles (anyio/aiohttp/httpx/websockets test suites) are listed in kernmini
102
+ `meta/ROUGH.md`.
103
+
104
+ ## Tests
105
+
106
+ The tiers, so the inner loop stays seconds:
107
+
108
+ - `pytest -q` per change (~2s).
109
+ - `pytest -m oracle -n auto` when a feature lands, not per edit.
110
+ - `pytest -m bench -s` and `pytest -m soak -s` only when performance or
111
+ stability is the question.
112
+ - `chkstyle` once, at the final PR stage, never per edit.
113
+
114
+ `pytest -q`. Ten integration stories, deliberately few: scheduling/tasks/
115
+ threads (timers, contextvars, gather, TaskGroup, timeout, cancellation,
116
+ cross-thread wakeup, to_thread), socket I/O through the reactor (accept/
117
+ connect/backpressure on a 5MB payload), asyncio streams echo with drain
118
+ backpressure, a background task surviving between `run_until_complete` calls
119
+ (the kernel-persistence story), KeyboardInterrupt injection with the loop
120
+ reused afterwards, uvicorn serving an ASGI app fetched by urllib and by
121
+ httpx-over-anyio, TLS echo against a throwaway openssl cert, a subprocess
122
+ round-trip (exec and shell, streams and communicate), and the
123
+ interrupt-torture story: window-scoped `PyThreadState_SetAsyncExc` injection
124
+ under stream/timer/cross-thread load, mirroring kernmini's
125
+ `sync_execution_context` contract (0.5s by default; `LOOPMINI_TORTURE_SECONDS`
126
+ extends it), and the KI-at-`_run`-entry orphan repro that pins the
127
+ traceback-depth requeue rule. Rust-side unit tests should exist only for
128
+ reactor invariants Python stories cannot reach.
129
+
130
+ Two CPython 3.13 facts the torture test depends on, discovered the hard way:
131
+ an async-injected exception raised at an eval-breaker check escapes the
132
+ raising frame even past a same-frame `try/except` (open regression
133
+ [gh-139622](https://github.com/python/cpython/issues/139622); 3.12 and 3.14
134
+ unaffected), so the guard must sit in a parent frame; and a busy window
135
+ shorter than the GIL switch interval (5ms) is invisible to a sampling thread,
136
+ because the GIL is only ever released while the window is closed.
137
+
138
+ ## The compatibility oracles
139
+
140
+ Four external suites are the conformance measure, run in-repo through
141
+ `tests/test_oracle.py` (marked `oracle`, deselected by default):
142
+
143
+ ```bash
144
+ pytest -m oracle -n auto # all four suites, one worker per module
145
+ pytest -m oracle -k uvloop # one suite; add "and test_tcp" etc. for one module
146
+ ```
147
+
148
+ `tests/oracle_util.py` fetches and caches what each suite needs under
149
+ `~/.cache/loopmini-oracle` (override with `LOOPMINI_ORACLE_CACHE`):
150
+
151
+ - CPython's own test_asyncio (uvloop's strategy). This uv-managed Python ships
152
+ without the stdlib `test` package, so the matching source tarball is fetched
153
+ and its functional modules run under a loopmini event-loop policy. Suites
154
+ asserting standard-loop internals (base_events, selector_events,
155
+ unix_events) are not fair oracles and are excluded.
156
+ - anyio's test suite, via its sanctioned alternative-loop mechanism: the sdist
157
+ matching the installed version is unpacked and a loopmini entry added to
158
+ `asyncio_params` in its conftest. Runs as a subprocess; requires the anyio
159
+ test deps (`trustme`, `blockbuster`) in the venv. blockbuster's
160
+ blocking-call detector allowlists Popen's blocking os.read only under
161
+ `asyncio/base_events.py`, which is one reason the Popen spawn goes through
162
+ the executor.
163
+ - uvloop's test suite, from a source clone (`LOOPMINI_UVLOOP_REPO`, default
164
+ `~/aai-ws/links/uvloop`): its loop-parameterized test classes are cloned
165
+ onto loopmini with `implementation='asyncio'`, so branchy tests take the
166
+ standard-loop expectation paths. `test_tcp` needs pyOpenSSL and is skipped
167
+ without it.
168
+ - aiohttp's test suite: the sdist is unpacked, uvloop is stubbed to loopmini in
169
+ its conftest so `--aiohttp-loop=uvloop` selects it, and its blockbuster
170
+ fixture gains an allowlist entry for `MiniServer.close` (the same
171
+ unlink-if-unchanged stat asyncio's `_stop_serving` is allowlisted for).
172
+ Runs the pure-Python aiohttp (`AIOHTTP_NO_EXTENSIONS=1`), with a private
173
+ `--basetemp` because its permission tests leave chmod-000 dirs that
174
+ pytest's numbered-dir sweeper cannot remove, fatal under aiohttp's
175
+ `filterwarnings = error`. Deselected, with reasons in `test_oracle.py`:
176
+ tests that fail identically on the standard loop in this venv, and one
177
+ test that mock-patches `loop.time()`, which only steers loops whose timer
178
+ arithmetic reads Python-level time each turn (real uvloop fails it too).
179
+
180
+ Status on 2026-08-28: all green; 25 oracle tests, the first three suites in
181
+ ~17s under xdist and aiohttp's ~4,200 tests in a further ~2 minutes.
182
+
183
+ ## The soak gate
184
+
185
+ `pytest -m soak -s` runs a kernel-shaped workload on one loopmini loop for
186
+ `LOOPMINI_SOAK_SECONDS` (default 30): a fasthtml app on uvicorn under
187
+ threaded httpx load, a websocket echo server with a bot-style client, a
188
+ housekeeping tick, and periodic cells submitted from another thread, as a
189
+ kernel would. It asserts zero errors, bounded fd growth, and that every
190
+ component made progress. It watches stability, not speed.
191
+
192
+ ## Benchmarks
193
+
194
+ `pytest -m bench -s` prints loopmini vs the standard loop on loop-bound
195
+ microbenchmarks; informational, never gating. Numbers on 2026-08-28 (M-series
196
+ macOS): call_soon 1.10x, sleep0 1.12x, tcp_echo 1.10x, task spawn 2.48x
197
+ (stdlib=1.0). The spawn gap is per-task PyO3 boundary crossings (two to three
198
+ `schedule` calls per task lifecycle at ~0.5µs each); loop startup is at parity.
199
+ Closing it would need handles represented Rust-side, which is later-rung work.
200
+
201
+ ## Style and releases
202
+
203
+ fastai style (`chkstyle` before committing). Releases via fastship; the tree
204
+ carries the next version.