simloom 0.3.0__tar.gz → 0.4.0__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.
- {simloom-0.3.0 → simloom-0.4.0}/CHANGELOG.md +51 -1
- {simloom-0.3.0 → simloom-0.4.0}/PKG-INFO +18 -1
- {simloom-0.3.0 → simloom-0.4.0}/README.md +17 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_cli.py +5 -1
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_consistency.py +131 -15
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_linearizability.py +9 -3
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_version.py +1 -1
- {simloom-0.3.0 → simloom-0.4.0}/.gitignore +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/LICENSE +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/examples/README.md +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/pyproject.toml +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/__init__.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_boundary.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_buggify.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_context.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_debugger.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_errors.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_eventlog.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_explore.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_fingerprint.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_hashseed.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_loop.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_monitors.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_net.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_patches.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_pytest_plugin.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_run.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_sched.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_services.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_shrink.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_soak.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_systematic.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_tape.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_testing.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/_world.py +0 -0
- {simloom-0.3.0 → simloom-0.4.0}/src/simloom/py.typed +0 -0
|
@@ -7,6 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.4.0] - 2026-07-06
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **World/fault-matrix fuzzer.** Generated distributed programs (hosts running
|
|
14
|
+
TCP echo servers and UDP endpoints, clients hitting random peers) while a
|
|
15
|
+
director executes a random fault script — partitions, heals, asymmetric
|
|
16
|
+
blocks, crashes, restarts, connection resets, latency/loss, datagram faults —
|
|
17
|
+
at random virtual times. Properties: run==run==replay byte-identical whatever
|
|
18
|
+
the faults, and errors are only ever user-level (a replay never misaligns).
|
|
19
|
+
2,000-case offline campaign, zero divergences.
|
|
20
|
+
- **Adversarial API-misuse suite + hostile-input hardening.** Every misuse is a
|
|
21
|
+
clean, named error (nested `run()`, coroutine objects, bad bounds, raising
|
|
22
|
+
monitor predicates/protocol callbacks/`__repr__`s, double-completes), the CLI
|
|
23
|
+
reports unreadable logs instead of tracebacking, unhashable register values
|
|
24
|
+
get a clear message, and 100 failing/deadlocking runs leave zero residue.
|
|
25
|
+
- **Unpinned-PYTHONHASHSEED CI job + shrinker properties.** The determinism-
|
|
26
|
+
critical suites now also run with hash randomization ON (any internal
|
|
27
|
+
hash-order dependence surfaces as a divergence), and the shrinker is
|
|
28
|
+
property-tested over fuzzer-generated failures: the minimized tape must still
|
|
29
|
+
fail with the same error class, deterministically (400-case sweep).
|
|
30
|
+
- **Systematic-verifier cross-validation.** `proven_correct` — the most
|
|
31
|
+
dangerous claim simloom makes — is cross-checked against random exploration
|
|
32
|
+
on fuzzer-generated programs: a proof must never coexist with a random
|
|
33
|
+
failure, every reported witness must replay, and an exhaustive search must
|
|
34
|
+
find any bug random finds. 400-case offline campaign, zero contradictions.
|
|
35
|
+
- **asyncio semantics-parity fuzzer.** Generated programs with interleaving-
|
|
36
|
+
independent results must produce exactly what the stock asyncio loop produces
|
|
37
|
+
— values, exception types, timeouts, shields, gathers, cancellation. Offline:
|
|
38
|
+
4,000 programs x 3 sim seeds vs `asyncio.run`, zero divergences.
|
|
39
|
+
- **Random-program determinism fuzzer.** Arbitrary generated async programs
|
|
40
|
+
(queues, locks, events, sleeps, tape draws, task spawning, cancellation) must
|
|
41
|
+
satisfy run==run==replay byte-identically under both schedulers — deadlocking
|
|
42
|
+
programs must deadlock identically. Offline campaign: 20,000 programs
|
|
43
|
+
(~100,000 executions), zero divergences; a randomized slice runs in CI.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
- **Serializability checker: four incompleteness bugs**, found by a
|
|
47
|
+
differential campaign against an independent brute-force reference (450k+
|
|
48
|
+
random histories). The checker was always *sound* (no false accusations) but
|
|
49
|
+
missed ~20% of real violations: (1) incompatible reads — two reads neither of
|
|
50
|
+
which is a prefix of the other; (2) intra-transaction inconsistencies — a
|
|
51
|
+
transaction reading its own future append, shrinking reads, or two different
|
|
52
|
+
pre-transaction states; (3) torn atomic append groups — a read observing part
|
|
53
|
+
of a transaction's appends; (4) rw anti-dependency edges were only generated
|
|
54
|
+
for values in the recovered version order, missing cross-key atomicity cycles
|
|
55
|
+
— now a read precedes the append of *every* value it did not observe. After
|
|
56
|
+
the fixes: zero disagreements over 450k serializability and 320k
|
|
57
|
+
linearizability cases; the differential suite is now part of CI.
|
|
58
|
+
|
|
10
59
|
## [0.3.0] - 2026-07-06
|
|
11
60
|
|
|
12
61
|
### Added
|
|
@@ -211,7 +260,8 @@ explorer, shrinker, and pytest plugin are all present and exercised by a
|
|
|
211
260
|
race (bpo-42130) reproduced deterministically from a seed; the modern stdlib
|
|
212
261
|
survives the identical torture. Launch-post draft in `docs/launch-post.md`.
|
|
213
262
|
|
|
214
|
-
[Unreleased]: https://github.com/mandipadk/simloom/compare/v0.
|
|
263
|
+
[Unreleased]: https://github.com/mandipadk/simloom/compare/v0.4.0...HEAD
|
|
264
|
+
[0.4.0]: https://github.com/mandipadk/simloom/compare/v0.3.0...v0.4.0
|
|
215
265
|
[0.3.0]: https://github.com/mandipadk/simloom/compare/v0.2.0...v0.3.0
|
|
216
266
|
[0.2.0]: https://github.com/mandipadk/simloom/compare/v0.1.0...v0.2.0
|
|
217
267
|
[0.1.0]: https://github.com/mandipadk/simloom/releases/tag/v0.1.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: simloom
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Deterministic simulation testing for asyncio: explore every schedule, inject faults, assert properties, prove correctness, and replay any failure from a seed.
|
|
5
5
|
Project-URL: Homepage, https://github.com/mandipadk/simloom
|
|
6
6
|
Project-URL: Repository, https://github.com/mandipadk/simloom
|
|
@@ -256,6 +256,23 @@ double-vote bug and exploration elects two leaders in one term in ~1 seed of 5;
|
|
|
256
256
|
version survives, with coverage counters proving the faults actually fired.
|
|
257
257
|
→ `examples/toy_raft.py`
|
|
258
258
|
|
|
259
|
+
## The harness is tested harder than your code
|
|
260
|
+
|
|
261
|
+
A correctness tool's worst failure mode is *false confidence*, so simloom attacks itself
|
|
262
|
+
the way it attacks your code. Beyond the 10,000-seed determinism torture, every CI run:
|
|
263
|
+
|
|
264
|
+
- **differentially validates both consistency checkers** against independent brute-force
|
|
265
|
+
references (the offline campaign — 450k+ serializability and 320k+ linearizability
|
|
266
|
+
histories — found and fixed four checker incompleteness bugs before you ever saw them);
|
|
267
|
+
- **fuzzes determinism with generated programs** — random workers, queues, locks,
|
|
268
|
+
cancellation, fault scripts against hosts and the network — every one must satisfy
|
|
269
|
+
`run == run == replay` byte-for-byte (20k programs + 2k fault-scripted worlds offline);
|
|
270
|
+
- **checks semantics parity against stock asyncio** — programs with interleaving-
|
|
271
|
+
independent results must behave identically on the real loop (4k programs offline);
|
|
272
|
+
- **cross-validates every `proven_correct` claim** against random exploration, runs the
|
|
273
|
+
determinism-critical suites with `PYTHONHASHSEED` *unpinned*, property-tests the
|
|
274
|
+
shrinker, and asserts every API misuse is a clean, named error.
|
|
275
|
+
|
|
259
276
|
## Honesty first
|
|
260
277
|
|
|
261
278
|
A determinism claim is only as good as its disclosed limits. simloom raises a loud error
|
|
@@ -220,6 +220,23 @@ double-vote bug and exploration elects two leaders in one term in ~1 seed of 5;
|
|
|
220
220
|
version survives, with coverage counters proving the faults actually fired.
|
|
221
221
|
→ `examples/toy_raft.py`
|
|
222
222
|
|
|
223
|
+
## The harness is tested harder than your code
|
|
224
|
+
|
|
225
|
+
A correctness tool's worst failure mode is *false confidence*, so simloom attacks itself
|
|
226
|
+
the way it attacks your code. Beyond the 10,000-seed determinism torture, every CI run:
|
|
227
|
+
|
|
228
|
+
- **differentially validates both consistency checkers** against independent brute-force
|
|
229
|
+
references (the offline campaign — 450k+ serializability and 320k+ linearizability
|
|
230
|
+
histories — found and fixed four checker incompleteness bugs before you ever saw them);
|
|
231
|
+
- **fuzzes determinism with generated programs** — random workers, queues, locks,
|
|
232
|
+
cancellation, fault scripts against hosts and the network — every one must satisfy
|
|
233
|
+
`run == run == replay` byte-for-byte (20k programs + 2k fault-scripted worlds offline);
|
|
234
|
+
- **checks semantics parity against stock asyncio** — programs with interleaving-
|
|
235
|
+
independent results must behave identically on the real loop (4k programs offline);
|
|
236
|
+
- **cross-validates every `proven_correct` claim** against random exploration, runs the
|
|
237
|
+
determinism-critical suites with `PYTHONHASHSEED` *unpinned*, property-tests the
|
|
238
|
+
shrinker, and asserts every API misuse is a clean, named error.
|
|
239
|
+
|
|
223
240
|
## Honesty first
|
|
224
241
|
|
|
225
242
|
A determinism claim is only as good as its disclosed limits. simloom raises a loud error
|
|
@@ -234,7 +234,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
234
234
|
diff.set_defaults(func=_cmd_diff)
|
|
235
235
|
|
|
236
236
|
args = parser.parse_args(argv)
|
|
237
|
-
|
|
237
|
+
try:
|
|
238
|
+
result: int = args.func(args)
|
|
239
|
+
except OSError as exc:
|
|
240
|
+
print(f"error: {exc}")
|
|
241
|
+
return 2
|
|
238
242
|
return result
|
|
239
243
|
|
|
240
244
|
|
|
@@ -145,6 +145,103 @@ class _Graph:
|
|
|
145
145
|
self.types[(u, v)] = kind
|
|
146
146
|
|
|
147
147
|
|
|
148
|
+
def _is_prefix(shorter: list[Any], longer: list[Any]) -> bool:
|
|
149
|
+
return len(shorter) <= len(longer) and longer[: len(shorter)] == shorter
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _intra_transaction_violation(txn: Transaction) -> str | None:
|
|
153
|
+
"""In a serial order a transaction executes atomically: outside state is
|
|
154
|
+
frozen while it runs. So every read of key k must observe exactly
|
|
155
|
+
``base + (this transaction's own prior appends to k)`` where ``base`` — the
|
|
156
|
+
pre-transaction state — is the same for all of the transaction's reads of k
|
|
157
|
+
and cannot contain the transaction's own (globally unique) values."""
|
|
158
|
+
own_values = {op.value for op in txn.ops if op.f == "append"}
|
|
159
|
+
appended_so_far: dict[str, list[Any]] = {}
|
|
160
|
+
base_of: dict[str, list[Any]] = {}
|
|
161
|
+
for op in txn.ops:
|
|
162
|
+
if op.f == "append":
|
|
163
|
+
appended_so_far.setdefault(op.key, []).append(op.value)
|
|
164
|
+
continue
|
|
165
|
+
observed = list(op.value)
|
|
166
|
+
own = appended_so_far.get(op.key, [])
|
|
167
|
+
if own and observed[len(observed) - len(own) :] != own:
|
|
168
|
+
return (
|
|
169
|
+
f"transaction {txn.index} read {observed!r} from {op.key!r}, which does "
|
|
170
|
+
f"not end with its own prior appends {own!r}"
|
|
171
|
+
)
|
|
172
|
+
base = observed[: len(observed) - len(own)] if own else observed
|
|
173
|
+
if any(value in own_values for value in base):
|
|
174
|
+
return (
|
|
175
|
+
f"transaction {txn.index} read {observed!r} from {op.key!r} containing "
|
|
176
|
+
f"a value it had not appended yet"
|
|
177
|
+
)
|
|
178
|
+
if op.key in base_of and base_of[op.key] != base:
|
|
179
|
+
return (
|
|
180
|
+
f"transaction {txn.index} observed two different pre-transaction states "
|
|
181
|
+
f"for {op.key!r}: {base_of[op.key]!r} and {base!r}"
|
|
182
|
+
)
|
|
183
|
+
base_of[op.key] = base
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _torn_transaction_violation(txns: tuple[Transaction, ...]) -> str | None:
|
|
188
|
+
"""A transaction executes atomically, so its appends to a key land in the
|
|
189
|
+
serial sequence as one contiguous block — and states only exist at
|
|
190
|
+
transaction boundaries. A read observing *any* of a transaction's appends
|
|
191
|
+
to a key must therefore observe *all* of them, contiguously and in order.
|
|
192
|
+
|
|
193
|
+
An external read observing a torn slice of another transaction (or its
|
|
194
|
+
values out of order) can never be explained by any serial order. A
|
|
195
|
+
transaction's *own* mid-flight reads legitimately see a prefix of its own
|
|
196
|
+
appends; those are validated by ``_intra_transaction_violation`` instead.
|
|
197
|
+
"""
|
|
198
|
+
appends_of: dict[tuple[int, str], list[Any]] = {}
|
|
199
|
+
appender: dict[tuple[str, Any], int] = {}
|
|
200
|
+
for txn in txns:
|
|
201
|
+
for op in txn.ops:
|
|
202
|
+
if op.f == "append":
|
|
203
|
+
appends_of.setdefault((txn.index, op.key), []).append(op.value)
|
|
204
|
+
appender[(op.key, op.value)] = txn.index
|
|
205
|
+
for txn in txns:
|
|
206
|
+
for op in txn.ops:
|
|
207
|
+
if op.f != "read":
|
|
208
|
+
continue
|
|
209
|
+
observed = list(op.value)
|
|
210
|
+
for writer in {appender.get((op.key, v)) for v in observed}:
|
|
211
|
+
if writer is None or writer == txn.index:
|
|
212
|
+
continue # unknown value, or an own-read (checked elsewhere)
|
|
213
|
+
block = appends_of[(writer, op.key)]
|
|
214
|
+
positions = [i for i, v in enumerate(observed) if v in set(block)]
|
|
215
|
+
if [observed[i] for i in positions] != block or positions != list(
|
|
216
|
+
range(positions[0], positions[0] + len(block))
|
|
217
|
+
):
|
|
218
|
+
return (
|
|
219
|
+
f"transaction {txn.index} read {observed!r} from {op.key!r}: it "
|
|
220
|
+
f"observes a torn slice of transaction {writer}'s atomic appends "
|
|
221
|
+
f"{block!r}"
|
|
222
|
+
)
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _incompatible_reads_violation(txns: tuple[Transaction, ...]) -> str | None:
|
|
227
|
+
"""A key's list only ever grows, so every read is a prefix of the final
|
|
228
|
+
sequence — any two reads of the same key must be prefix-related."""
|
|
229
|
+
reads_by_key: dict[str, list[list[Any]]] = {}
|
|
230
|
+
for txn in txns:
|
|
231
|
+
for op in txn.ops:
|
|
232
|
+
if op.f == "read":
|
|
233
|
+
reads_by_key.setdefault(op.key, []).append(list(op.value))
|
|
234
|
+
for key, reads in reads_by_key.items():
|
|
235
|
+
reads.sort(key=len)
|
|
236
|
+
for i in range(len(reads) - 1):
|
|
237
|
+
if not _is_prefix(reads[i], reads[i + 1]):
|
|
238
|
+
return (
|
|
239
|
+
f"incompatible reads of {key!r}: {reads[i]!r} and {reads[i + 1]!r} "
|
|
240
|
+
f"(neither is a prefix of the other, impossible for an append-only list)"
|
|
241
|
+
)
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
|
|
148
245
|
def check_serializable(history: History) -> SerializabilityResult:
|
|
149
246
|
"""Check a list-append history for serializability (Elle-style). Returns a
|
|
150
247
|
result whose ``cycle`` (with per-edge ``edge_types``) witnesses any
|
|
@@ -153,40 +250,59 @@ def check_serializable(history: History) -> SerializabilityResult:
|
|
|
153
250
|
if not txns:
|
|
154
251
|
return SerializabilityResult(ok=True, message="empty history is serializable")
|
|
155
252
|
|
|
253
|
+
# 0. Sound structural pre-checks: reads that no serial order can explain,
|
|
254
|
+
# regardless of transaction ordering (found by differential testing
|
|
255
|
+
# against a brute-force reference — see tests/test_adversarial_checkers.py).
|
|
256
|
+
for txn in txns:
|
|
257
|
+
reason = _intra_transaction_violation(txn)
|
|
258
|
+
if reason is not None:
|
|
259
|
+
return SerializabilityResult(ok=False, message=f"serializability violation: {reason}")
|
|
260
|
+
reason = _incompatible_reads_violation(txns)
|
|
261
|
+
if reason is not None:
|
|
262
|
+
return SerializabilityResult(ok=False, message=f"serializability violation: {reason}")
|
|
263
|
+
reason = _torn_transaction_violation(txns)
|
|
264
|
+
if reason is not None:
|
|
265
|
+
return SerializabilityResult(ok=False, message=f"serializability violation: {reason}")
|
|
266
|
+
|
|
156
267
|
# 1. Recover each key's version order from the longest read that observed it,
|
|
157
268
|
# and which transaction appended each (unique) value.
|
|
158
269
|
version_order: dict[str, list[Any]] = {}
|
|
159
270
|
appender: dict[Any, int] = {}
|
|
271
|
+
appends_by_key: dict[str, list[Any]] = {}
|
|
160
272
|
for txn in txns:
|
|
161
273
|
for op in txn.ops:
|
|
162
274
|
if op.f == "append":
|
|
163
275
|
appender[op.value] = txn.index
|
|
276
|
+
appends_by_key.setdefault(op.key, []).append(op.value)
|
|
164
277
|
elif op.f == "read":
|
|
165
278
|
observed = list(op.value)
|
|
166
279
|
if op.key not in version_order or len(observed) > len(version_order[op.key]):
|
|
167
280
|
version_order[op.key] = observed
|
|
168
281
|
|
|
169
282
|
graph = _Graph()
|
|
170
|
-
for
|
|
171
|
-
index_of = {v: i for i, v in enumerate(order)}
|
|
283
|
+
for order in version_order.values():
|
|
172
284
|
# 2. ww edges along the version order.
|
|
173
285
|
for i in range(len(order) - 1):
|
|
174
286
|
a, b = appender.get(order[i]), appender.get(order[i + 1])
|
|
175
287
|
if a is not None and b is not None:
|
|
176
288
|
graph.add(a, b, "ww")
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
289
|
+
# 3. wr and rw edges from every read. The rw rule is deliberately broader
|
|
290
|
+
# than the version order: an append is never un-observed (the list only
|
|
291
|
+
# grows), so a read that does NOT contain value v must precede v's
|
|
292
|
+
# append — even if v was never observed by any read. (The narrow
|
|
293
|
+
# version-order-only rule missed cross-key atomicity cycles; found by
|
|
294
|
+
# differential testing against a brute-force reference.)
|
|
295
|
+
for txn in txns:
|
|
296
|
+
for op in txn.ops:
|
|
297
|
+
if op.f != "read":
|
|
298
|
+
continue
|
|
299
|
+
observed_set = set(op.value)
|
|
300
|
+
for v in op.value: # wr: the writer of an observed value precedes us
|
|
301
|
+
w = appender.get(v)
|
|
302
|
+
if w is not None:
|
|
303
|
+
graph.add(w, txn.index, "wr")
|
|
304
|
+
for v in appends_by_key.get(op.key, ()):
|
|
305
|
+
if v not in observed_set:
|
|
190
306
|
w = appender.get(v)
|
|
191
307
|
if w is not None:
|
|
192
308
|
graph.add(txn.index, w, "rw")
|
|
@@ -194,9 +194,15 @@ def _check_key(
|
|
|
194
194
|
if is_terminal(remaining):
|
|
195
195
|
return None
|
|
196
196
|
marker = (remaining, state)
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
197
|
+
try:
|
|
198
|
+
if marker in seen:
|
|
199
|
+
continue
|
|
200
|
+
seen.add(marker)
|
|
201
|
+
except TypeError as exc:
|
|
202
|
+
raise TypeError(
|
|
203
|
+
f"register values must be hashable for linearizability checking "
|
|
204
|
+
f"(the search memoises on them); got {state!r}"
|
|
205
|
+
) from exc
|
|
200
206
|
if len(seen) > max_states:
|
|
201
207
|
raise RuntimeError(
|
|
202
208
|
f"linearizability search exceeded {max_states} states on key {key!r}; "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|