hydo-assert 0.2.0__tar.gz → 0.3.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.
Files changed (24) hide show
  1. hydo_assert-0.3.1/PKG-INFO +295 -0
  2. hydo_assert-0.3.1/README.md +273 -0
  3. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/pyproject.toml +1 -1
  4. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/__init__.py +1 -1
  5. hydo_assert-0.3.1/src/hydo_assert.egg-info/PKG-INFO +295 -0
  6. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert.egg-info/SOURCES.txt +2 -1
  7. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/tests/test_cases.py +2 -2
  8. hydo_assert-0.3.1/tests/test_readme.py +82 -0
  9. hydo_assert-0.2.0/PKG-INFO +0 -238
  10. hydo_assert-0.2.0/README.md +0 -216
  11. hydo_assert-0.2.0/src/hydo_assert.egg-info/PKG-INFO +0 -238
  12. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/LICENSE +0 -0
  13. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/setup.cfg +0 -0
  14. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/_buggify.py +0 -0
  15. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/_core.py +0 -0
  16. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/_eventually.py +0 -0
  17. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/_json.py +0 -0
  18. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/_sink.py +0 -0
  19. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/history.py +0 -0
  20. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert/py.typed +0 -0
  21. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert.egg-info/dependency_links.txt +0 -0
  22. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/src/hydo_assert.egg-info/top_level.txt +0 -0
  23. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/tests/test_history.py +0 -0
  24. {hydo_assert-0.2.0 → hydo_assert-0.3.1}/tests/test_hydo_assert.py +0 -0
@@ -0,0 +1,295 @@
1
+ Metadata-Version: 2.4
2
+ Name: hydo-assert
3
+ Version: 0.3.1
4
+ Summary: Assertion SDK for programs tested under hydo, a deterministic hypervisor (always/sometimes properties, BUGGIFY fault points, bounded liveness, operation histories)
5
+ License-Expression: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/kakoc/hydo
7
+ Project-URL: Documentation, https://github.com/kakoc/hydo/tree/main/sdk/python#readme
8
+ Project-URL: Repository, https://github.com/kakoc/hydo
9
+ Project-URL: Issues, https://github.com/kakoc/hydo/issues
10
+ Keywords: testing,assertions,deterministic,fault-injection,simulation,hydo
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Topic :: Software Development :: Testing
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.8
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Dynamic: license-file
22
+
23
+ # hydo-assert for Python
24
+
25
+ [hydo](https://github.com/kakoc/hydo) runs a whole system in a deterministic virtual machine,
26
+ injects faults (process kills, network faults, power cuts, disk errors) and replays every
27
+ failure bit for bit. This package lets a Python program that runs under hydo say, from the
28
+ inside, what must be true and what should happen. hydo collects what the program reports, and
29
+ an exploration (many runs with different faults) reports each property as passed, failed (with
30
+ a schedule that replays the failure) or never exercised.
31
+
32
+ The package uses only the standard library and needs Python 3.8 or later. Outside hydo it does
33
+ almost nothing: the records go to standard error (or nowhere), and fault points stay off.
34
+
35
+ ## Install
36
+
37
+ ```sh
38
+ pip install hydo-assert
39
+ ```
40
+
41
+ The distribution is `hydo-assert`; the module is `hydo_assert`. In a guest image without pip,
42
+ copy the folder `hydo_assert` onto the program's `PYTHONPATH`.
43
+
44
+ ## A complete program
45
+
46
+ A small key/value store that declares three properties and checks them:
47
+
48
+ ```python
49
+ import hydo_assert as ha
50
+
51
+ READ_BACK = ha.always("kv.read_back", "every acknowledged write reads back with its value")
52
+ OVERWRITE = ha.sometimes("kv.overwrite", "a write replaced an existing value")
53
+ LOST_REPLY = ha.buggify("kv.lost_reply", "the write lands but its reply is lost")
54
+
55
+
56
+ def main():
57
+ ha.catalog() # list every property, also the ones that never run
58
+ store = {}
59
+ ha.setup_complete() # the system is ready: hydo may start injecting faults
60
+
61
+ for key, value in [("a", 1), ("b", 2), ("a", 3)]:
62
+ old = store.get(key)
63
+ store[key] = value
64
+ OVERWRITE.check(old is not None, {"key": key})
65
+ if LOST_REPLY.fire({"key": key}):
66
+ continue # the client never hears back (only when hydo turns this point on)
67
+ acked = store.get(key) == value
68
+ READ_BACK.check(acked, {"key": key, "value": value})
69
+
70
+
71
+ if __name__ == "__main__":
72
+ main()
73
+ ```
74
+
75
+ Run it with `HYDO_ASSERT_OUT=stderr` to see what it reports. It writes these lines (under hydo
76
+ they go to the guest's console, where hydo reads them):
77
+
78
+ ```text
79
+ HYDO-ASSERT-DECL {"t":"always","id":"kv.read_back","msg":"every acknowledged write reads back with its value"}
80
+ HYDO-ASSERT-DECL {"t":"sometimes","id":"kv.overwrite","msg":"a write replaced an existing value"}
81
+ HYDO-ASSERT-DECL {"t":"sometimes","id":"buggify.kv.lost_reply","msg":"fault point fired: the write lands but its reply is lost"}
82
+ HYDO-SETUP-COMPLETE
83
+ HYDO-ASSERT {"t":"always","id":"kv.read_back","ok":true,"msg":"every acknowledged write reads back with its value","details":{"key":"a","value":1}}
84
+ HYDO-ASSERT {"t":"sometimes","id":"kv.overwrite","ok":true,"msg":"a write replaced an existing value","details":{"key":"a"}}
85
+ ```
86
+
87
+ A process writes each outcome of a property once: the first pass and the first failure, with
88
+ the details of that evaluation. That is why the later passes of `kv.read_back` write nothing.
89
+
90
+ ## How to
91
+
92
+ ### Declare properties and check them
93
+
94
+ A property has an id (stable across versions of your program) and a message for people. Its
95
+ kind says what hydo expects:
96
+
97
+ | kind | the property holds when | hydo reports a failure when |
98
+ |---|---|---|
99
+ | `always` | the condition is true every time, and it is checked at least once | it is false once, or never checked |
100
+ | `always_or_unreachable` | the condition is true every time it is checked | it is false once |
101
+ | `sometimes` | the condition is true at least once in an exploration | it is never true |
102
+ | `reachable` | the code is reached at least once in an exploration | it is never reached |
103
+ | `unreachable` | the code is never reached | it is reached |
104
+
105
+ Declare properties at module level, so that the catalog lists them once the module is
106
+ imported:
107
+
108
+ ```python
109
+ import hydo_assert as ha
110
+
111
+ STORED = ha.always("kv.stored", "every acknowledged write is stored")
112
+ PARSES = ha.always_or_unreachable("kv.log_parses", "every complete log line parses")
113
+ RETRIED = ha.sometimes("kv.retried", "a client retried a request")
114
+ RECOVERED = ha.reachable("kv.recovered", "the server replayed a non-empty log")
115
+ UNKNOWN = ha.unreachable("kv.unknown_command", "the server got a command it does not know")
116
+
117
+ STORED.check(True, {"key": "a"}) # returns the condition
118
+ PARSES.check(True) # the details are optional
119
+ RETRIED.check(False) # a false `sometimes` writes nothing
120
+ RECOVERED.hit({"entries": 3})
121
+ command = "GET"
122
+ if command not in ("GET", "SET"):
123
+ UNKNOWN.hit({"command": command})
124
+ ```
125
+
126
+ ### Compare two values
127
+
128
+ `cmp` checks `left op right` and puts both values and the operator into the details, so a
129
+ failure shows them:
130
+
131
+ ```python
132
+ import hydo_assert as ha
133
+
134
+ LOG_SIZE = ha.always("kv.log_size", "the log stays under its cap")
135
+ size = 12
136
+ LOG_SIZE.cmp(size, "<=", 10000) # details: {"left":12,"op":"<=","right":10000}
137
+ ```
138
+
139
+ The operator is one of `<`, `<=`, `==`, `!=`, `>=` and `>`.
140
+
141
+ ### Check that something happens in time (eventually)
142
+
143
+ An `eventually` property says that after this point, the condition becomes true within a
144
+ deadline. For example: after a restart, the server answers within 5 seconds. Time inside a hydo
145
+ guest is deterministic, so the verdict is the same on every replay.
146
+
147
+ ```python
148
+ import time
149
+
150
+ import hydo_assert as ha
151
+
152
+ BACK = ha.eventually("kv.back", "the server answers within 5 s of its start", 5.0) # seconds
153
+ started = time.monotonic()
154
+
155
+
156
+ def answers():
157
+ return time.monotonic() - started >= 0.02 # stands for a real request
158
+
159
+
160
+ assert BACK.await_cond(answers, poll=0.005) # polls every 5 ms
161
+
162
+ # Or mark the start and the end yourself:
163
+ watch = BACK.begin()
164
+ # ... the server comes up ...
165
+ watch.done() # or watch.expire() when you know it will not
166
+ ```
167
+
168
+ `await_cond` blocks the thread. In asyncio code use `await_async`, whose condition may be a
169
+ plain function or a coroutine function:
170
+
171
+ ```python
172
+ import asyncio
173
+
174
+ import hydo_assert as ha
175
+
176
+ BACK = ha.eventually("kv.back_async", "the server answers within 5 s", 5.0)
177
+
178
+
179
+ async def answers():
180
+ return True
181
+
182
+
183
+ async def main():
184
+ assert await BACK.await_async(answers, 0.1)
185
+
186
+
187
+ asyncio.run(main())
188
+ ```
189
+
190
+ ### Add a fault point (BUGGIFY)
191
+
192
+ A fault point is a place where your program misbehaves on purpose: it drops a reply, fails a
193
+ write, waits too long. Normally the point does nothing (`fire` returns false). hydo can turn
194
+ points on for a run, so that faults happen where only your program can make them. FoundationDB
195
+ calls this technique BUGGIFY.
196
+
197
+ ```python
198
+ import time
199
+
200
+ import hydo_assert as ha
201
+
202
+ LOST_REPLY = ha.buggify("kv.lost_reply_point", "the write lands but its reply is lost")
203
+ SLOW_DISK = ha.buggify("kv.slow_disk", "a write takes 10 ms longer")
204
+
205
+ reply = "OK"
206
+ if LOST_REPLY.fire():
207
+ reply = None # misbehave: the client never gets the reply
208
+ if SLOW_DISK.fire({"key": "a"}):
209
+ time.sleep(0.01)
210
+ print(reply)
211
+ ```
212
+
213
+ To let hydo turn the points on, add `buggify: true` to the project's `hydo.yaml` and explore
214
+ with `hydo explore --buggify`. Each run then gets a seed that turns some points on, and a point
215
+ that is on fires on some of its calls. To try it outside hydo, set
216
+ `HYDO_BUGGIFY=<seed in hex>,<points on, per mille>,<calls that fire, per mille>`; for example
217
+ `HYDO_BUGGIFY=2a,1000,1000` makes every point fire every time.
218
+
219
+ ### Record an operation history
220
+
221
+ hydo can check a whole history of operations after a run. It checks that every read is
222
+ consistent with the writes before it (linearizability per key), and that no list append goes
223
+ missing. Your workload's
224
+ client records each operation twice: when it starts (`invoke`) and when the outcome is known.
225
+
226
+ ```python
227
+ from hydo_assert import history as hh
228
+
229
+ kv = hh.History("kv")
230
+ write = kv.invoke(3, "write", "k1", hh.enc("v1")) # client 3, before the request
231
+ # ... send the request, then record the outcome:
232
+ write.ok() # it happened; fail(): it did not; info(): the outcome is unknown
233
+ read = kv.invoke(3, "read", "k1")
234
+ read.ok("k1", hh.value_or_nil("v1"))
235
+ ```
236
+
237
+ Then name the history in the project's `hydo.yaml`:
238
+
239
+ ```yaml
240
+ checks:
241
+ - id: kv.linearizable
242
+ type: linearizable
243
+ model: register
244
+ history: kv
245
+ msg: every read sees the latest acknowledged write
246
+ ```
247
+
248
+ `hydo guide checks` describes the checks and their options.
249
+
250
+ ### Choose where the records go
251
+
252
+ Under hydo the records go to the guest's console (`/dev/ttyS0` for properties, `/dev/console`
253
+ for histories), which hydo reads even when the program's standard output goes to a log file.
254
+ Outside hydo, where the console cannot be opened, they go to standard error. Two variables
255
+ change this, and each is read once, at the first record:
256
+
257
+ - `HYDO_ASSERT_OUT`: a path of an existing file, `stderr`, `off`, or `logging` (each record
258
+ logged at INFO on the logger `hydo_assert`) for the properties;
259
+ - `HYDO_HIST_OUT`: a path (created if missing), `stderr`, or `off` for the histories.
260
+
261
+ ### Run a Python program in a hydo guest
262
+
263
+ Set `PYTHONHASHSEED=0` so that the program does not depend on a random hash seed, and
264
+ `PYTHONDONTWRITEBYTECODE=1` because the guest's root file system is read-only. A complete
265
+ project with a `hydo.yaml` is the python-kv example:
266
+ <https://github.com/kakoc/hydo/tree/main/hydo/onboard/examples/python-kv>.
267
+
268
+ ## Reference
269
+
270
+ - The API: the docstrings of `hydo_assert` and `hydo_assert.history` (`help(hydo_assert)`).
271
+ - The records, and how hydo judges each property type:
272
+ <https://github.com/kakoc/hydo/blob/main/hydo/ASSERTIONS.md>.
273
+ - The history format and the checkers:
274
+ <https://github.com/kakoc/hydo/blob/main/hydo/HISTORIES.md>.
275
+ - The same SDK for Rust, JavaScript and Go writes the same records, byte for byte:
276
+ <https://github.com/kakoc/hydo/tree/main/sdk>.
277
+
278
+ Rules of this package:
279
+
280
+ - The catalog (`catalog()`) lists the properties in the order they were declared, which for
281
+ module-level declarations is the import order. Call it once, early, in the process whose
282
+ console hydo reads.
283
+ - Details are encoded as Go's `encoding/json` encodes the same values: keys in UTF-8 byte
284
+ order, a tuple or a set as an array, `bytes` as base64, any other object as `str(obj)`.
285
+ Details that are not a mapping are wrapped as `{"value": ...}`. A record whose details hold a
286
+ NaN or an infinity is not written. The caller's dict is never changed.
287
+ - Every call is safe from threads and from asyncio code: one lock, never held across a blocking
288
+ call or an `await`. After `os.fork()` the child keeps the parent's record of what was written.
289
+ - A history's process is an int or a token without spaces; its arguments are tokens (encode
290
+ free-form values with `enc`). Versions are int64 values. A process has at most one operation
291
+ in flight.
292
+
293
+ ## License
294
+
295
+ Apache-2.0.
@@ -0,0 +1,273 @@
1
+ # hydo-assert for Python
2
+
3
+ [hydo](https://github.com/kakoc/hydo) runs a whole system in a deterministic virtual machine,
4
+ injects faults (process kills, network faults, power cuts, disk errors) and replays every
5
+ failure bit for bit. This package lets a Python program that runs under hydo say, from the
6
+ inside, what must be true and what should happen. hydo collects what the program reports, and
7
+ an exploration (many runs with different faults) reports each property as passed, failed (with
8
+ a schedule that replays the failure) or never exercised.
9
+
10
+ The package uses only the standard library and needs Python 3.8 or later. Outside hydo it does
11
+ almost nothing: the records go to standard error (or nowhere), and fault points stay off.
12
+
13
+ ## Install
14
+
15
+ ```sh
16
+ pip install hydo-assert
17
+ ```
18
+
19
+ The distribution is `hydo-assert`; the module is `hydo_assert`. In a guest image without pip,
20
+ copy the folder `hydo_assert` onto the program's `PYTHONPATH`.
21
+
22
+ ## A complete program
23
+
24
+ A small key/value store that declares three properties and checks them:
25
+
26
+ ```python
27
+ import hydo_assert as ha
28
+
29
+ READ_BACK = ha.always("kv.read_back", "every acknowledged write reads back with its value")
30
+ OVERWRITE = ha.sometimes("kv.overwrite", "a write replaced an existing value")
31
+ LOST_REPLY = ha.buggify("kv.lost_reply", "the write lands but its reply is lost")
32
+
33
+
34
+ def main():
35
+ ha.catalog() # list every property, also the ones that never run
36
+ store = {}
37
+ ha.setup_complete() # the system is ready: hydo may start injecting faults
38
+
39
+ for key, value in [("a", 1), ("b", 2), ("a", 3)]:
40
+ old = store.get(key)
41
+ store[key] = value
42
+ OVERWRITE.check(old is not None, {"key": key})
43
+ if LOST_REPLY.fire({"key": key}):
44
+ continue # the client never hears back (only when hydo turns this point on)
45
+ acked = store.get(key) == value
46
+ READ_BACK.check(acked, {"key": key, "value": value})
47
+
48
+
49
+ if __name__ == "__main__":
50
+ main()
51
+ ```
52
+
53
+ Run it with `HYDO_ASSERT_OUT=stderr` to see what it reports. It writes these lines (under hydo
54
+ they go to the guest's console, where hydo reads them):
55
+
56
+ ```text
57
+ HYDO-ASSERT-DECL {"t":"always","id":"kv.read_back","msg":"every acknowledged write reads back with its value"}
58
+ HYDO-ASSERT-DECL {"t":"sometimes","id":"kv.overwrite","msg":"a write replaced an existing value"}
59
+ HYDO-ASSERT-DECL {"t":"sometimes","id":"buggify.kv.lost_reply","msg":"fault point fired: the write lands but its reply is lost"}
60
+ HYDO-SETUP-COMPLETE
61
+ HYDO-ASSERT {"t":"always","id":"kv.read_back","ok":true,"msg":"every acknowledged write reads back with its value","details":{"key":"a","value":1}}
62
+ HYDO-ASSERT {"t":"sometimes","id":"kv.overwrite","ok":true,"msg":"a write replaced an existing value","details":{"key":"a"}}
63
+ ```
64
+
65
+ A process writes each outcome of a property once: the first pass and the first failure, with
66
+ the details of that evaluation. That is why the later passes of `kv.read_back` write nothing.
67
+
68
+ ## How to
69
+
70
+ ### Declare properties and check them
71
+
72
+ A property has an id (stable across versions of your program) and a message for people. Its
73
+ kind says what hydo expects:
74
+
75
+ | kind | the property holds when | hydo reports a failure when |
76
+ |---|---|---|
77
+ | `always` | the condition is true every time, and it is checked at least once | it is false once, or never checked |
78
+ | `always_or_unreachable` | the condition is true every time it is checked | it is false once |
79
+ | `sometimes` | the condition is true at least once in an exploration | it is never true |
80
+ | `reachable` | the code is reached at least once in an exploration | it is never reached |
81
+ | `unreachable` | the code is never reached | it is reached |
82
+
83
+ Declare properties at module level, so that the catalog lists them once the module is
84
+ imported:
85
+
86
+ ```python
87
+ import hydo_assert as ha
88
+
89
+ STORED = ha.always("kv.stored", "every acknowledged write is stored")
90
+ PARSES = ha.always_or_unreachable("kv.log_parses", "every complete log line parses")
91
+ RETRIED = ha.sometimes("kv.retried", "a client retried a request")
92
+ RECOVERED = ha.reachable("kv.recovered", "the server replayed a non-empty log")
93
+ UNKNOWN = ha.unreachable("kv.unknown_command", "the server got a command it does not know")
94
+
95
+ STORED.check(True, {"key": "a"}) # returns the condition
96
+ PARSES.check(True) # the details are optional
97
+ RETRIED.check(False) # a false `sometimes` writes nothing
98
+ RECOVERED.hit({"entries": 3})
99
+ command = "GET"
100
+ if command not in ("GET", "SET"):
101
+ UNKNOWN.hit({"command": command})
102
+ ```
103
+
104
+ ### Compare two values
105
+
106
+ `cmp` checks `left op right` and puts both values and the operator into the details, so a
107
+ failure shows them:
108
+
109
+ ```python
110
+ import hydo_assert as ha
111
+
112
+ LOG_SIZE = ha.always("kv.log_size", "the log stays under its cap")
113
+ size = 12
114
+ LOG_SIZE.cmp(size, "<=", 10000) # details: {"left":12,"op":"<=","right":10000}
115
+ ```
116
+
117
+ The operator is one of `<`, `<=`, `==`, `!=`, `>=` and `>`.
118
+
119
+ ### Check that something happens in time (eventually)
120
+
121
+ An `eventually` property says that after this point, the condition becomes true within a
122
+ deadline. For example: after a restart, the server answers within 5 seconds. Time inside a hydo
123
+ guest is deterministic, so the verdict is the same on every replay.
124
+
125
+ ```python
126
+ import time
127
+
128
+ import hydo_assert as ha
129
+
130
+ BACK = ha.eventually("kv.back", "the server answers within 5 s of its start", 5.0) # seconds
131
+ started = time.monotonic()
132
+
133
+
134
+ def answers():
135
+ return time.monotonic() - started >= 0.02 # stands for a real request
136
+
137
+
138
+ assert BACK.await_cond(answers, poll=0.005) # polls every 5 ms
139
+
140
+ # Or mark the start and the end yourself:
141
+ watch = BACK.begin()
142
+ # ... the server comes up ...
143
+ watch.done() # or watch.expire() when you know it will not
144
+ ```
145
+
146
+ `await_cond` blocks the thread. In asyncio code use `await_async`, whose condition may be a
147
+ plain function or a coroutine function:
148
+
149
+ ```python
150
+ import asyncio
151
+
152
+ import hydo_assert as ha
153
+
154
+ BACK = ha.eventually("kv.back_async", "the server answers within 5 s", 5.0)
155
+
156
+
157
+ async def answers():
158
+ return True
159
+
160
+
161
+ async def main():
162
+ assert await BACK.await_async(answers, 0.1)
163
+
164
+
165
+ asyncio.run(main())
166
+ ```
167
+
168
+ ### Add a fault point (BUGGIFY)
169
+
170
+ A fault point is a place where your program misbehaves on purpose: it drops a reply, fails a
171
+ write, waits too long. Normally the point does nothing (`fire` returns false). hydo can turn
172
+ points on for a run, so that faults happen where only your program can make them. FoundationDB
173
+ calls this technique BUGGIFY.
174
+
175
+ ```python
176
+ import time
177
+
178
+ import hydo_assert as ha
179
+
180
+ LOST_REPLY = ha.buggify("kv.lost_reply_point", "the write lands but its reply is lost")
181
+ SLOW_DISK = ha.buggify("kv.slow_disk", "a write takes 10 ms longer")
182
+
183
+ reply = "OK"
184
+ if LOST_REPLY.fire():
185
+ reply = None # misbehave: the client never gets the reply
186
+ if SLOW_DISK.fire({"key": "a"}):
187
+ time.sleep(0.01)
188
+ print(reply)
189
+ ```
190
+
191
+ To let hydo turn the points on, add `buggify: true` to the project's `hydo.yaml` and explore
192
+ with `hydo explore --buggify`. Each run then gets a seed that turns some points on, and a point
193
+ that is on fires on some of its calls. To try it outside hydo, set
194
+ `HYDO_BUGGIFY=<seed in hex>,<points on, per mille>,<calls that fire, per mille>`; for example
195
+ `HYDO_BUGGIFY=2a,1000,1000` makes every point fire every time.
196
+
197
+ ### Record an operation history
198
+
199
+ hydo can check a whole history of operations after a run. It checks that every read is
200
+ consistent with the writes before it (linearizability per key), and that no list append goes
201
+ missing. Your workload's
202
+ client records each operation twice: when it starts (`invoke`) and when the outcome is known.
203
+
204
+ ```python
205
+ from hydo_assert import history as hh
206
+
207
+ kv = hh.History("kv")
208
+ write = kv.invoke(3, "write", "k1", hh.enc("v1")) # client 3, before the request
209
+ # ... send the request, then record the outcome:
210
+ write.ok() # it happened; fail(): it did not; info(): the outcome is unknown
211
+ read = kv.invoke(3, "read", "k1")
212
+ read.ok("k1", hh.value_or_nil("v1"))
213
+ ```
214
+
215
+ Then name the history in the project's `hydo.yaml`:
216
+
217
+ ```yaml
218
+ checks:
219
+ - id: kv.linearizable
220
+ type: linearizable
221
+ model: register
222
+ history: kv
223
+ msg: every read sees the latest acknowledged write
224
+ ```
225
+
226
+ `hydo guide checks` describes the checks and their options.
227
+
228
+ ### Choose where the records go
229
+
230
+ Under hydo the records go to the guest's console (`/dev/ttyS0` for properties, `/dev/console`
231
+ for histories), which hydo reads even when the program's standard output goes to a log file.
232
+ Outside hydo, where the console cannot be opened, they go to standard error. Two variables
233
+ change this, and each is read once, at the first record:
234
+
235
+ - `HYDO_ASSERT_OUT`: a path of an existing file, `stderr`, `off`, or `logging` (each record
236
+ logged at INFO on the logger `hydo_assert`) for the properties;
237
+ - `HYDO_HIST_OUT`: a path (created if missing), `stderr`, or `off` for the histories.
238
+
239
+ ### Run a Python program in a hydo guest
240
+
241
+ Set `PYTHONHASHSEED=0` so that the program does not depend on a random hash seed, and
242
+ `PYTHONDONTWRITEBYTECODE=1` because the guest's root file system is read-only. A complete
243
+ project with a `hydo.yaml` is the python-kv example:
244
+ <https://github.com/kakoc/hydo/tree/main/hydo/onboard/examples/python-kv>.
245
+
246
+ ## Reference
247
+
248
+ - The API: the docstrings of `hydo_assert` and `hydo_assert.history` (`help(hydo_assert)`).
249
+ - The records, and how hydo judges each property type:
250
+ <https://github.com/kakoc/hydo/blob/main/hydo/ASSERTIONS.md>.
251
+ - The history format and the checkers:
252
+ <https://github.com/kakoc/hydo/blob/main/hydo/HISTORIES.md>.
253
+ - The same SDK for Rust, JavaScript and Go writes the same records, byte for byte:
254
+ <https://github.com/kakoc/hydo/tree/main/sdk>.
255
+
256
+ Rules of this package:
257
+
258
+ - The catalog (`catalog()`) lists the properties in the order they were declared, which for
259
+ module-level declarations is the import order. Call it once, early, in the process whose
260
+ console hydo reads.
261
+ - Details are encoded as Go's `encoding/json` encodes the same values: keys in UTF-8 byte
262
+ order, a tuple or a set as an array, `bytes` as base64, any other object as `str(obj)`.
263
+ Details that are not a mapping are wrapped as `{"value": ...}`. A record whose details hold a
264
+ NaN or an infinity is not written. The caller's dict is never changed.
265
+ - Every call is safe from threads and from asyncio code: one lock, never held across a blocking
266
+ call or an `await`. After `os.fork()` the child keeps the parent's record of what was written.
267
+ - A history's process is an int or a token without spaces; its arguments are tokens (encode
268
+ free-form values with `enc`). Versions are int64 values. A process has at most one operation
269
+ in flight.
270
+
271
+ ## License
272
+
273
+ Apache-2.0.
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
5
5
 
6
6
  [project]
7
7
  name = "hydo-assert"
8
- version = "0.2.0"
8
+ version = "0.3.1"
9
9
  description = "Assertion SDK for programs tested under hydo, a deterministic hypervisor (always/sometimes properties, BUGGIFY fault points, bounded liveness, operation histories)"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.8"
@@ -45,7 +45,7 @@ from ._eventually import Liveness, Watch, eventually
45
45
  #: The details type: any mapping (a plain dict). The Go SDK calls it D.
46
46
  D = dict
47
47
 
48
- __version__ = "0.2.0"
48
+ __version__ = "0.3.1"
49
49
 
50
50
  __all__ = [
51
51
  "Assertion",