libbeachcomber 0.9.0__tar.gz → 0.10.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.
Files changed (30) hide show
  1. libbeachcomber-0.10.0/PKG-INFO +233 -0
  2. libbeachcomber-0.10.0/README.md +209 -0
  3. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/conformance_runner.py +76 -113
  4. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/libbeachcomber/__init__.py +20 -21
  5. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/libbeachcomber/_native.py +19 -37
  6. libbeachcomber-0.10.0/libbeachcomber/client.py +605 -0
  7. libbeachcomber-0.10.0/libbeachcomber/discovery.py +73 -0
  8. libbeachcomber-0.10.0/libbeachcomber/exceptions.py +143 -0
  9. libbeachcomber-0.10.0/libbeachcomber/types.py +102 -0
  10. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/pyproject.toml +1 -1
  11. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/tests/conftest.py +6 -65
  12. libbeachcomber-0.10.0/tests/test_api_surface.py +89 -0
  13. libbeachcomber-0.10.0/tests/test_client_options.py +57 -0
  14. libbeachcomber-0.10.0/tests/test_closed.py +28 -0
  15. libbeachcomber-0.10.0/tests/test_discovery.py +91 -0
  16. libbeachcomber-0.10.0/tests/test_exceptions.py +60 -0
  17. libbeachcomber-0.10.0/tests/test_integration.py +289 -0
  18. libbeachcomber-0.9.0/PKG-INFO +0 -145
  19. libbeachcomber-0.9.0/README.md +0 -121
  20. libbeachcomber-0.9.0/libbeachcomber/client.py +0 -583
  21. libbeachcomber-0.9.0/libbeachcomber/discovery.py +0 -82
  22. libbeachcomber-0.9.0/libbeachcomber/exceptions.py +0 -149
  23. libbeachcomber-0.9.0/libbeachcomber/result.py +0 -59
  24. libbeachcomber-0.9.0/libbeachcomber/types.py +0 -75
  25. libbeachcomber-0.9.0/tests/test_discovery.py +0 -77
  26. libbeachcomber-0.9.0/tests/test_exceptions.py +0 -76
  27. libbeachcomber-0.9.0/tests/test_integration.py +0 -157
  28. libbeachcomber-0.9.0/tests/test_result.py +0 -87
  29. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/.gitignore +0 -0
  30. {libbeachcomber-0.9.0 → libbeachcomber-0.10.0}/tests/__init__.py +0 -0
@@ -0,0 +1,233 @@
1
+ Metadata-Version: 2.5
2
+ Name: libbeachcomber
3
+ Version: 0.10.0
4
+ Summary: Python client SDK for the beachcomber (comb) shell-state daemon
5
+ Project-URL: Homepage, https://github.com/NavistAu/beachcomber
6
+ Project-URL: Repository, https://github.com/NavistAu/beachcomber
7
+ Project-URL: Issues, https://github.com/NavistAu/beachcomber/issues
8
+ Author: NavistAu
9
+ License: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: POSIX
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: System :: Shells
20
+ Requires-Python: >=3.9
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=7; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # beachcomber Python SDK
26
+
27
+ Python client for the [beachcomber](https://github.com/NavistAu/beachcomber) (`comb`) shell-state daemon.
28
+ It follows the SDK API table ([`docs/sdk-api.md`](../../docs/sdk-api.md)): the
29
+ same methods, options, value shapes and error kinds as every other SDK, which
30
+ `tests/test_api_surface.py` checks against [`sdks/api.json`](../api.json).
31
+
32
+ ## Requirements
33
+
34
+ - Python 3.9+
35
+ - No external dependencies (stdlib `ctypes` binds directly to the native
36
+ `libbeachcomber.{so,dylib}` — no subprocess, no socket code in this SDK)
37
+ - A running `comb` daemon (or `autostart` left enabled, the default)
38
+ - `libbeachcomber.{so,dylib}` discoverable — see "Library discovery" below
39
+
40
+ ## Installation
41
+
42
+ ```sh
43
+ pip install libbeachcomber
44
+ ```
45
+
46
+ Or with `uv`:
47
+
48
+ ```sh
49
+ uv add libbeachcomber
50
+ ```
51
+
52
+ ## Quick start
53
+
54
+ ```python
55
+ import os
56
+
57
+ from libbeachcomber import Client
58
+
59
+ client = Client()
60
+
61
+ # What `comb get git.branch` prints in that directory; None on a miss
62
+ branch = client.get("git.branch", cwd="/path/to/repo") # "main"
63
+
64
+ # A full provider (a dict)
65
+ git = client.get("git", cwd="/path/to/repo")
66
+ print(git["branch"], git["dirty"])
67
+
68
+ # The caller's environment for env.* refs; force a run, and wait for it
69
+ client.get("aws.profile", cwd="/path/to/repo", env=dict(os.environ))
70
+ client.get("git.branch", cwd="/path/to/repo", force=True, wait="forever")
71
+ # Answer at once with what is stored, however stale
72
+ client.get("git.branch", cwd="/path/to/repo", wait="0")
73
+
74
+ # The template itself, unrendered: a facet, read like any key
75
+ client.get("aws:template.profile")
76
+
77
+ # Force recomputation
78
+ client.refresh("git", path="/path/to/repo")
79
+
80
+ # Daemon status
81
+ status = client.status()
82
+ ```
83
+
84
+ ## `get`
85
+
86
+ `client.get(key, cwd=None, env=None, force=False, wait=None)` returns the
87
+ value `comb get` prints for this key, cwd and env — the bare value, `None`
88
+ on a miss. The daemon holds the templates (`<provider>:template`, seeded
89
+ from configuration or `put`) and the client renders them: a plain field is
90
+ its cached value, a templated one is rendered, and a bare provider name is
91
+ the whole merged namespace. `cwd=None` is the global entry. The unrendered
92
+ halves are facets, read like any key: `p:template.x` is the template as
93
+ stored, `p:put.x` and `p:source.x` those layers' values.
94
+
95
+ `force=True` re-executes every Source the read demands, as `comb get
96
+ --force`. `wait` is how long the read waits for a run it needs before it
97
+ answers what is stored, as `comb get --wait`: a duration (`"0"`, `"50ms"`,
98
+ `"2s"`) or `"forever"`; anything else is a `ParseError`. Left out, a stored
99
+ record waits its Source's budget and a cold read waits for its run. The read
100
+ may take `timeout_ms` plus the wait (`"forever"` has no deadline), so a cold
101
+ read with no `wait` can outlast the default 250 ms and raise `TimeoutError`;
102
+ its run goes on, and the next read has the value.
103
+
104
+ ## Sessions
105
+
106
+ For multiple queries use a session to reuse a single connection:
107
+
108
+ ```python
109
+ with client.session() as session:
110
+ session.set_context("/path/to/repo")
111
+ branch = session.get("git.branch") # reads at the session's directory
112
+ dirty = session.get("git.dirty")
113
+ hostname = session.get("hostname")
114
+ ```
115
+
116
+ ## Eval
117
+
118
+ `client.eval(template_str, cwd=None, env=None)` renders a template that is
119
+ stored nowhere — the same value `comb eval` prints; `eval("{{ p.f }}")` and
120
+ `get("p.f")` agree. The template accepts a single `{{ expr }}` tag, or
121
+ literal text/several tags — the former keeps the expression's natural type,
122
+ the latter is always a string (a source with no tags at all is its literal
123
+ text).
124
+
125
+ `exec()` inside a template runs only on a client created with
126
+ `Client(exec=True)`; it is off by default, and a template that calls it
127
+ then fails with `exec() is disabled in this context`.
128
+
129
+ ## Watch
130
+
131
+ `client.watch(key, cwd=None, env=None)` streams `get`'s value for that key,
132
+ cwd and env, again each time it changes:
133
+
134
+ ```python
135
+ with client.watch("git.branch", cwd="/path/to/repo") as stream:
136
+ for event in stream: # ends when the stream does
137
+ print(event.data) # None is a miss, not the end
138
+ ```
139
+
140
+ `stream.next_event(timeout_ms=None)` answers the next `WatchEvent`, or
141
+ `None` once the stream has ended; a `timeout_ms` that runs out with no event
142
+ raises `TimeoutError`, and the stream stays open. `stream.cancel()` ends it
143
+ from any thread, and `stream.close()` ends it and frees it.
144
+
145
+ `status()`, `hello()` and `introspect(subject, duration_secs=None)` are the
146
+ client's; a session has none of them. `status()` answers typed `CacheRow`s,
147
+ `hello()` a `HelloInfo`, and `introspect()` an `IntrospectResponse` whose
148
+ `daemon` is a typed `DaemonHealth` (with its `ReaperStatus` and `Verdict`s)
149
+ for the `daemon` subject and whose `other` is the JSON for the rest.
150
+
151
+ ## Client options
152
+
153
+ ```python
154
+ client = Client(socket_path="/tmp/beachcomber-1000/sock", timeout_ms=500, connect_timeout_ms=0)
155
+ ```
156
+
157
+ | Option | Default | Meaning |
158
+ |---|---|---|
159
+ | `socket_path` | `$BEACHCOMBER_SOCKET`, else `/tmp/beachcomber-<uid>/sock` | The daemon's socket. |
160
+ | `timeout_ms` | 250 | How long one read or write on the socket may take; at least 1. |
161
+ | `connect_timeout_ms` | 1500 | How long connecting keeps retrying before it gives up; 0 is one attempt. |
162
+ | `autostart` | on | Start a daemon when none answers an auto-discovered socket. |
163
+ | `exec` | off | Whether a template this client renders may call `exec()`. |
164
+
165
+ The defaults are the library's (`bc_client_new`): this SDK passes on the
166
+ options you give and adds none of its own.
167
+
168
+ ## Library discovery
169
+
170
+ This SDK is a `ctypes` binding over `libbeachcomber`'s C ABI — no socket
171
+ code or wire-protocol framing lives in Python; the native library owns the
172
+ connection (including the daemon socket's own auto-discovery and
173
+ autostart). It is loaded on first use.
174
+
175
+ The library is found by the one rule every SDK follows
176
+ ([`docs/sdk-api.md`, "Library discovery"](../../docs/sdk-api.md#library-discovery)). The file is
177
+ `libbeachcomber.dylib` on macOS and `libbeachcomber.so` on every other
178
+ platform, and the SDK tries, in order:
179
+
180
+ 1. `$BEACHCOMBER_LIB`, when it is set and not empty. It is then the only
181
+ candidate: a path set on purpose that does not load is an error, not a
182
+ reason to load some other library.
183
+ 2. Beside `comb`: `<dir>/../lib/<file>`, where `<dir>` is the directory of
184
+ the first executable `comb` on `$PATH`, its symlinks resolved. A Homebrew
185
+ `comb` is a symlink into the Cellar, and the library sits in the Cellar
186
+ beside it.
187
+ 3. The system loader, given the bare file name.
188
+
189
+ When none loads, `LibraryNotFoundError` names every candidate and why each
190
+ failed; there is no fallback to spawning `comb` as a subprocess. The tests
191
+ check this against the shared fixture, `sdks/discovery.json`.
192
+
193
+ ## Socket discovery
194
+
195
+ Once the library is loaded, the daemon socket path itself is resolved by
196
+ `libbeachcomber` (not this SDK): `$BEACHCOMBER_SOCKET` if set, else a
197
+ stable per-user default. Pass `Client(socket_path=...)` to override it
198
+ directly.
199
+
200
+ ## Exceptions
201
+
202
+ Every exception is one `CombError` subclass per error kind, carrying
203
+ `.kind` (the slug, matching the C ABI envelope's `error.kind`), `.message`
204
+ (the ABI's message, unchanged — `str(exc)` is the same) and
205
+ `.library_version` (the loaded library's `bc_version()`), so callers should
206
+ not need to string-match `str(exc)`.
207
+
208
+ | Exception | `.kind` | When raised |
209
+ |---|---|---|
210
+ | `DaemonNotRunningError` | `daemon_not_running` | No daemon answers, and none was started |
211
+ | `ConnectionFailedError` | `connection_failed` | An explicit `socket_path` could not be connected to |
212
+ | `IoError` | `io_error` | A read or write on the socket failed |
213
+ | `ParseError` | `parse_error` | A request, a response or a template could not be read |
214
+ | `ServerError` | `server_error` | The daemon refused the request, or a template failed to render |
215
+ | `TimeoutError` | `timeout` | A read or write ran out `timeout_ms`, or `next_event` ran out its `timeout_ms` |
216
+ | `BusyError` | `busy` | A session or watch is in use by another caller |
217
+ | `PanicError` | `panic` | The library panicked; the panic was caught at the boundary |
218
+ | `LibraryNotFoundError` | `library_not_found` | No `libbeachcomber` was found where discovery looks |
219
+ | `LibrarySymbolError` | `library_symbol` | The library found lacks a function this SDK calls |
220
+ | `UnsupportedError` | `unsupported` | A transport cannot do this operation (never raised here: this SDK has one transport) |
221
+ | `CombError` | varies | Base class for all of them |
222
+
223
+ ## Running tests
224
+
225
+ ```sh
226
+ cd sdks/python
227
+ uv run --with pytest python -m pytest # unit and integration tests
228
+ COMB_BIN=/path/to/comb python3 conformance_runner.py # the shared fixtures under tests/conformance/
229
+ ```
230
+
231
+ The integration tests spawn a `comb daemon` and load `libbeachcomber` from
232
+ the workspace's debug build (`../../target/debug/`, made by `cargo build`)
233
+ unless `BEACHCOMBER_LIB` / `COMB_BIN` are set.
@@ -0,0 +1,209 @@
1
+ # beachcomber Python SDK
2
+
3
+ Python client for the [beachcomber](https://github.com/NavistAu/beachcomber) (`comb`) shell-state daemon.
4
+ It follows the SDK API table ([`docs/sdk-api.md`](../../docs/sdk-api.md)): the
5
+ same methods, options, value shapes and error kinds as every other SDK, which
6
+ `tests/test_api_surface.py` checks against [`sdks/api.json`](../api.json).
7
+
8
+ ## Requirements
9
+
10
+ - Python 3.9+
11
+ - No external dependencies (stdlib `ctypes` binds directly to the native
12
+ `libbeachcomber.{so,dylib}` — no subprocess, no socket code in this SDK)
13
+ - A running `comb` daemon (or `autostart` left enabled, the default)
14
+ - `libbeachcomber.{so,dylib}` discoverable — see "Library discovery" below
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ pip install libbeachcomber
20
+ ```
21
+
22
+ Or with `uv`:
23
+
24
+ ```sh
25
+ uv add libbeachcomber
26
+ ```
27
+
28
+ ## Quick start
29
+
30
+ ```python
31
+ import os
32
+
33
+ from libbeachcomber import Client
34
+
35
+ client = Client()
36
+
37
+ # What `comb get git.branch` prints in that directory; None on a miss
38
+ branch = client.get("git.branch", cwd="/path/to/repo") # "main"
39
+
40
+ # A full provider (a dict)
41
+ git = client.get("git", cwd="/path/to/repo")
42
+ print(git["branch"], git["dirty"])
43
+
44
+ # The caller's environment for env.* refs; force a run, and wait for it
45
+ client.get("aws.profile", cwd="/path/to/repo", env=dict(os.environ))
46
+ client.get("git.branch", cwd="/path/to/repo", force=True, wait="forever")
47
+ # Answer at once with what is stored, however stale
48
+ client.get("git.branch", cwd="/path/to/repo", wait="0")
49
+
50
+ # The template itself, unrendered: a facet, read like any key
51
+ client.get("aws:template.profile")
52
+
53
+ # Force recomputation
54
+ client.refresh("git", path="/path/to/repo")
55
+
56
+ # Daemon status
57
+ status = client.status()
58
+ ```
59
+
60
+ ## `get`
61
+
62
+ `client.get(key, cwd=None, env=None, force=False, wait=None)` returns the
63
+ value `comb get` prints for this key, cwd and env — the bare value, `None`
64
+ on a miss. The daemon holds the templates (`<provider>:template`, seeded
65
+ from configuration or `put`) and the client renders them: a plain field is
66
+ its cached value, a templated one is rendered, and a bare provider name is
67
+ the whole merged namespace. `cwd=None` is the global entry. The unrendered
68
+ halves are facets, read like any key: `p:template.x` is the template as
69
+ stored, `p:put.x` and `p:source.x` those layers' values.
70
+
71
+ `force=True` re-executes every Source the read demands, as `comb get
72
+ --force`. `wait` is how long the read waits for a run it needs before it
73
+ answers what is stored, as `comb get --wait`: a duration (`"0"`, `"50ms"`,
74
+ `"2s"`) or `"forever"`; anything else is a `ParseError`. Left out, a stored
75
+ record waits its Source's budget and a cold read waits for its run. The read
76
+ may take `timeout_ms` plus the wait (`"forever"` has no deadline), so a cold
77
+ read with no `wait` can outlast the default 250 ms and raise `TimeoutError`;
78
+ its run goes on, and the next read has the value.
79
+
80
+ ## Sessions
81
+
82
+ For multiple queries use a session to reuse a single connection:
83
+
84
+ ```python
85
+ with client.session() as session:
86
+ session.set_context("/path/to/repo")
87
+ branch = session.get("git.branch") # reads at the session's directory
88
+ dirty = session.get("git.dirty")
89
+ hostname = session.get("hostname")
90
+ ```
91
+
92
+ ## Eval
93
+
94
+ `client.eval(template_str, cwd=None, env=None)` renders a template that is
95
+ stored nowhere — the same value `comb eval` prints; `eval("{{ p.f }}")` and
96
+ `get("p.f")` agree. The template accepts a single `{{ expr }}` tag, or
97
+ literal text/several tags — the former keeps the expression's natural type,
98
+ the latter is always a string (a source with no tags at all is its literal
99
+ text).
100
+
101
+ `exec()` inside a template runs only on a client created with
102
+ `Client(exec=True)`; it is off by default, and a template that calls it
103
+ then fails with `exec() is disabled in this context`.
104
+
105
+ ## Watch
106
+
107
+ `client.watch(key, cwd=None, env=None)` streams `get`'s value for that key,
108
+ cwd and env, again each time it changes:
109
+
110
+ ```python
111
+ with client.watch("git.branch", cwd="/path/to/repo") as stream:
112
+ for event in stream: # ends when the stream does
113
+ print(event.data) # None is a miss, not the end
114
+ ```
115
+
116
+ `stream.next_event(timeout_ms=None)` answers the next `WatchEvent`, or
117
+ `None` once the stream has ended; a `timeout_ms` that runs out with no event
118
+ raises `TimeoutError`, and the stream stays open. `stream.cancel()` ends it
119
+ from any thread, and `stream.close()` ends it and frees it.
120
+
121
+ `status()`, `hello()` and `introspect(subject, duration_secs=None)` are the
122
+ client's; a session has none of them. `status()` answers typed `CacheRow`s,
123
+ `hello()` a `HelloInfo`, and `introspect()` an `IntrospectResponse` whose
124
+ `daemon` is a typed `DaemonHealth` (with its `ReaperStatus` and `Verdict`s)
125
+ for the `daemon` subject and whose `other` is the JSON for the rest.
126
+
127
+ ## Client options
128
+
129
+ ```python
130
+ client = Client(socket_path="/tmp/beachcomber-1000/sock", timeout_ms=500, connect_timeout_ms=0)
131
+ ```
132
+
133
+ | Option | Default | Meaning |
134
+ |---|---|---|
135
+ | `socket_path` | `$BEACHCOMBER_SOCKET`, else `/tmp/beachcomber-<uid>/sock` | The daemon's socket. |
136
+ | `timeout_ms` | 250 | How long one read or write on the socket may take; at least 1. |
137
+ | `connect_timeout_ms` | 1500 | How long connecting keeps retrying before it gives up; 0 is one attempt. |
138
+ | `autostart` | on | Start a daemon when none answers an auto-discovered socket. |
139
+ | `exec` | off | Whether a template this client renders may call `exec()`. |
140
+
141
+ The defaults are the library's (`bc_client_new`): this SDK passes on the
142
+ options you give and adds none of its own.
143
+
144
+ ## Library discovery
145
+
146
+ This SDK is a `ctypes` binding over `libbeachcomber`'s C ABI — no socket
147
+ code or wire-protocol framing lives in Python; the native library owns the
148
+ connection (including the daemon socket's own auto-discovery and
149
+ autostart). It is loaded on first use.
150
+
151
+ The library is found by the one rule every SDK follows
152
+ ([`docs/sdk-api.md`, "Library discovery"](../../docs/sdk-api.md#library-discovery)). The file is
153
+ `libbeachcomber.dylib` on macOS and `libbeachcomber.so` on every other
154
+ platform, and the SDK tries, in order:
155
+
156
+ 1. `$BEACHCOMBER_LIB`, when it is set and not empty. It is then the only
157
+ candidate: a path set on purpose that does not load is an error, not a
158
+ reason to load some other library.
159
+ 2. Beside `comb`: `<dir>/../lib/<file>`, where `<dir>` is the directory of
160
+ the first executable `comb` on `$PATH`, its symlinks resolved. A Homebrew
161
+ `comb` is a symlink into the Cellar, and the library sits in the Cellar
162
+ beside it.
163
+ 3. The system loader, given the bare file name.
164
+
165
+ When none loads, `LibraryNotFoundError` names every candidate and why each
166
+ failed; there is no fallback to spawning `comb` as a subprocess. The tests
167
+ check this against the shared fixture, `sdks/discovery.json`.
168
+
169
+ ## Socket discovery
170
+
171
+ Once the library is loaded, the daemon socket path itself is resolved by
172
+ `libbeachcomber` (not this SDK): `$BEACHCOMBER_SOCKET` if set, else a
173
+ stable per-user default. Pass `Client(socket_path=...)` to override it
174
+ directly.
175
+
176
+ ## Exceptions
177
+
178
+ Every exception is one `CombError` subclass per error kind, carrying
179
+ `.kind` (the slug, matching the C ABI envelope's `error.kind`), `.message`
180
+ (the ABI's message, unchanged — `str(exc)` is the same) and
181
+ `.library_version` (the loaded library's `bc_version()`), so callers should
182
+ not need to string-match `str(exc)`.
183
+
184
+ | Exception | `.kind` | When raised |
185
+ |---|---|---|
186
+ | `DaemonNotRunningError` | `daemon_not_running` | No daemon answers, and none was started |
187
+ | `ConnectionFailedError` | `connection_failed` | An explicit `socket_path` could not be connected to |
188
+ | `IoError` | `io_error` | A read or write on the socket failed |
189
+ | `ParseError` | `parse_error` | A request, a response or a template could not be read |
190
+ | `ServerError` | `server_error` | The daemon refused the request, or a template failed to render |
191
+ | `TimeoutError` | `timeout` | A read or write ran out `timeout_ms`, or `next_event` ran out its `timeout_ms` |
192
+ | `BusyError` | `busy` | A session or watch is in use by another caller |
193
+ | `PanicError` | `panic` | The library panicked; the panic was caught at the boundary |
194
+ | `LibraryNotFoundError` | `library_not_found` | No `libbeachcomber` was found where discovery looks |
195
+ | `LibrarySymbolError` | `library_symbol` | The library found lacks a function this SDK calls |
196
+ | `UnsupportedError` | `unsupported` | A transport cannot do this operation (never raised here: this SDK has one transport) |
197
+ | `CombError` | varies | Base class for all of them |
198
+
199
+ ## Running tests
200
+
201
+ ```sh
202
+ cd sdks/python
203
+ uv run --with pytest python -m pytest # unit and integration tests
204
+ COMB_BIN=/path/to/comb python3 conformance_runner.py # the shared fixtures under tests/conformance/
205
+ ```
206
+
207
+ The integration tests spawn a `comb daemon` and load `libbeachcomber` from
208
+ the workspace's debug build (`../../target/debug/`, made by `cargo build`)
209
+ unless `BEACHCOMBER_LIB` / `COMB_BIN` are set.