bec-ipython-client 4.3.1__tar.gz → 4.3.3__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 (47) hide show
  1. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/PKG-INFO +1 -1
  2. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/pyproject.toml +1 -1
  3. bec_ipython_client-4.3.3/tests/end-2-end/test_procedures_e2e.py +322 -0
  4. bec_ipython_client-4.3.1/tests/end-2-end/test_procedures_e2e.py +0 -192
  5. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/.gitignore +0 -0
  6. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/__init__.py +0 -0
  7. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/beamline_mixin.py +0 -0
  8. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/bec_magics.py +0 -0
  9. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/bec_startup.py +0 -0
  10. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/__init__.py +0 -0
  11. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/device_progress.py +0 -0
  12. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/ipython_live_updates.py +0 -0
  13. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/live_table.py +0 -0
  14. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/move_device.py +0 -0
  15. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/callbacks/utils.py +0 -0
  16. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/gui_client.py +0 -0
  17. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/high_level_interfaces/__init__.py +0 -0
  18. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/high_level_interfaces/bec_hli.py +0 -0
  19. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/high_level_interfaces/spec_hli.py +0 -0
  20. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/main.py +0 -0
  21. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/SLS/__init__.py +0 -0
  22. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/SLS/sls_info.py +0 -0
  23. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/XTreme/__init__.py +0 -0
  24. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/XTreme/x-treme.py +0 -0
  25. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/__init__.py +0 -0
  26. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/plugins/flomni/flomni_config.yaml +0 -0
  27. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/prettytable.py +0 -0
  28. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/progressbar.py +0 -0
  29. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/bec_ipython_client/signals.py +0 -0
  30. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/demo.py +0 -0
  31. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/conftest.py +0 -0
  32. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_beamline_mixins.py +0 -0
  33. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_bec_client.py +0 -0
  34. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_device_progress.py +0 -0
  35. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_gui_client.py +0 -0
  36. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_ipython_live_updates.py +0 -0
  37. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_live_table.py +0 -0
  38. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_move_callback.py +0 -0
  39. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_pretty_table.py +0 -0
  40. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/client_tests/test_signals.py +0 -0
  41. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/conftest.py +0 -0
  42. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/_ensure_requirements_container.py +0 -0
  43. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/test_acl_e2e.py +0 -0
  44. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/test_actors_e2e.py +0 -0
  45. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/test_scan_workflows_lib_e2e.py +0 -0
  46. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/test_scans_e2e.py +0 -0
  47. {bec_ipython_client-4.3.1 → bec_ipython_client-4.3.3}/tests/end-2-end/test_scans_lib_e2e.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: bec_ipython_client
3
- Version: 4.3.1
3
+ Version: 4.3.3
4
4
  Summary: BEC IPython client
5
5
  Project-URL: Bug Tracker, https://github.com/bec-project/bec/issues
6
6
  Project-URL: Homepage, https://github.com/bec-project/bec
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "bec_ipython_client"
7
- version = "4.3.1"
7
+ version = "4.3.3"
8
8
  description = "BEC IPython client"
9
9
  requires-python = ">=3.11"
10
10
  classifiers = [
@@ -0,0 +1,322 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import time
5
+ from importlib.metadata import version
6
+ from typing import Callable, Generator
7
+ from unittest.mock import patch
8
+ from uuid import uuid4
9
+
10
+ import pytest
11
+ from pytest_redis import factories as redis_factories
12
+ from redis.exceptions import NoPermissionError
13
+
14
+ from bec_lib import messages
15
+ from bec_lib.endpoints import MessageEndpoints
16
+ from bec_lib.logger import bec_logger
17
+ from bec_lib.procedures.helper import FrontendProcedureHelper, ProcedureState
18
+ from bec_lib.redis_connector import RedisConnector
19
+ from bec_lib.utils.user_acls_test import BECAccessDemo
20
+ from bec_server.procedures.constants import PROCEDURE
21
+ from bec_server.procedures.container_utils import (
22
+ _run_and_capture_error,
23
+ get_backend,
24
+ podman_available,
25
+ )
26
+ from bec_server.procedures.container_worker import ContainerProcedureWorker
27
+ from bec_server.procedures.manager import ProcedureManager
28
+ from bec_server.procedures.subprocess_worker import SubProcessWorker
29
+
30
+ logger = bec_logger.logger
31
+
32
+ # pylint: disable=protected-access
33
+
34
+ # Random order disabled for this module so that the test for building the worker container runs first
35
+ # and we can use lower timeouts for the remaining tests
36
+ pytestmark = pytest.mark.random_order(disabled=True)
37
+
38
+
39
+ # Isolate test managers from the scan server's manager, which subscribes to the same requests.
40
+ proc_redis = redis_factories.redis_proc()
41
+
42
+
43
+ @pytest.fixture
44
+ def procedure_manager(proc_redis) -> Generator[ProcedureManager, None, None]:
45
+ """Provide a manager whose request channels are isolated from running BEC services."""
46
+ manager = ProcedureManager(f"{proc_redis.host}:{proc_redis.port}", SubProcessWorker)
47
+ try:
48
+ yield manager
49
+ finally:
50
+ manager.shutdown()
51
+
52
+
53
+ def _wait_while(cond: Callable[[], bool], timeout_s):
54
+ start = time.monotonic()
55
+ while cond():
56
+ if (time.monotonic() - start) > timeout_s:
57
+ raise TimeoutError()
58
+ time.sleep(0.01)
59
+
60
+
61
+ @pytest.mark.timeout(100)
62
+ def test_building_worker_image():
63
+ podman_utils = get_backend()
64
+ build = podman_utils.build_worker_image()
65
+ assert len(build._command_output.splitlines()[-1]) == 64 # type: ignore
66
+ assert podman_utils.image_exists(f"bec_procedure_worker:v{version('bec_lib')}")
67
+
68
+
69
+ @pytest.mark.timeout(30)
70
+ def test_procedure_runner_spawns_worker(procedure_manager):
71
+ manager = procedure_manager
72
+ assert manager.active_workers() == []
73
+ status = manager._helper.request.procedure(
74
+ "sleep", args_kwargs=((), {"time_s": 0.1}), queue="test"
75
+ )
76
+ _wait_while(lambda: not manager.active_workers(), 10)
77
+ status.wait(timeout_s=10, raise_on_failure=True)
78
+ assert status.state == ProcedureState.SUCCESS
79
+ # A completed execution leaves the queue-owned worker available for reuse.
80
+ assert len(manager.active_workers()) == 1
81
+
82
+
83
+ @pytest.mark.timeout(30)
84
+ def test_happy_path_container_procedure_runner(procedure_manager):
85
+ test_args = (1, 2, 3)
86
+ test_kwargs = {"a": "b", "c": "d"}
87
+ manager = procedure_manager
88
+ status = manager._helper.request.procedure(
89
+ "_log_msg_args", args_kwargs=(test_args, test_kwargs)
90
+ )
91
+ status.wait(timeout_s=10, raise_on_failure=True)
92
+ assert status.state == ProcedureState.SUCCESS
93
+ manager.shutdown()
94
+ assert any("Executing procedure _log_msg_args" in line for line in manager._logs)
95
+ expected = (
96
+ f"Builtin procedure log_message_args_kwargs called with args: {test_args} "
97
+ f"and kwargs: {test_kwargs}"
98
+ )
99
+ assert any(expected in line for line in manager._logs)
100
+
101
+
102
+ @pytest.mark.timeout(30)
103
+ def test_personal_procedure_submission_acl(proc_redis):
104
+ """Only the submitting ACL user can publish or subscribe to their request channel."""
105
+ server = f"{proc_redis.host}:{proc_redis.port}"
106
+ manager = ProcedureManager(server, SubProcessWorker)
107
+ alice = RedisConnector(server)
108
+ bob = RedisConnector(server)
109
+ try:
110
+ access = BECAccessDemo(manager._conn)
111
+ access.add_account("alice", None, "user")
112
+ access.add_account("bob", None, "user")
113
+ alice.authenticate(username="alice", password=None)
114
+ bob.authenticate(username="bob", password=None)
115
+ with (
116
+ patch.object(manager, "_process_queue_request") as process,
117
+ patch.object(manager, "_ack", wraps=manager._ack) as acknowledge,
118
+ ):
119
+ helper = FrontendProcedureHelper(alice)
120
+ helper.request.procedure("sleep")
121
+ _wait_while(lambda: process.call_count < 1, 5)
122
+ received = process.call_args.args[0]
123
+ assert received.topic == MessageEndpoints.procedure_request("alice").endpoint
124
+ assert received.value.metadata["client_info"]["acl_user"] == "alice"
125
+
126
+ # A later login must change both the personal channel and the supplied identity.
127
+ alice.authenticate(username="bob", password=None)
128
+ helper.request.procedure("sleep")
129
+ _wait_while(lambda: process.call_count < 2, 5)
130
+ assert process.call_args.args[0].value.metadata["client_info"]["acl_user"] == "bob"
131
+
132
+ forged = messages.ProcedureRequestMessage(
133
+ identifier="sleep", metadata={"client_info": {"acl_user": "alice"}}
134
+ )
135
+ bob.send(MessageEndpoints.procedure_request("bob"), forged)
136
+ _wait_while(lambda: not acknowledge.called, 5)
137
+ acknowledge.assert_called_once_with(
138
+ False, "Username in topic does not match client info.", forged.execution_id
139
+ )
140
+ assert process.call_count == 2
141
+ with pytest.raises(NoPermissionError):
142
+ bob.send(MessageEndpoints.procedure_request("alice"), forged)
143
+ with bob._managed_connection._redis_conn.pubsub() as subscription:
144
+ subscription.subscribe(MessageEndpoints.procedure_request("alice").endpoint)
145
+ with pytest.raises(NoPermissionError):
146
+ subscription.get_message(timeout=1)
147
+ finally:
148
+ alice.shutdown()
149
+ bob.shutdown()
150
+ manager.shutdown()
151
+
152
+
153
+ @pytest.mark.timeout(120)
154
+ @pytest.mark.parametrize(
155
+ "worker_type", [ContainerProcedureWorker, SubProcessWorker], ids=["podman", "subprocess"]
156
+ )
157
+ def test_procedure_execution(proc_redis, monkeypatch, worker_type):
158
+ """Execute a real procedure with either worker backend against isolated Redis.
159
+
160
+ Podman requires the worker image and a host-networked ``local_bec`` pod. For a remote
161
+ Podman VM, BEC_TEST_CONTAINER_REDIS_HOST can supply a container-reachable host.
162
+ """
163
+ uses_container = worker_type is ContainerProcedureWorker
164
+ if uses_container and not podman_available():
165
+ pytest.skip("Podman is not available")
166
+ host, port = proc_redis.host, proc_redis.port
167
+ queue = f"procedure_test_{uuid4().hex}"
168
+ if uses_container and (container_host := os.environ.get("BEC_TEST_CONTAINER_REDIS_HOST")):
169
+ worker_environment = ContainerProcedureWorker._worker_environment
170
+ monkeypatch.setattr(
171
+ ContainerProcedureWorker,
172
+ "_worker_environment",
173
+ lambda worker: worker_environment(worker)
174
+ | {"redis_server": f"{container_host}:{port}"},
175
+ )
176
+ manager = ProcedureManager(f"{host}:{port}", worker_type)
177
+ try:
178
+ status = manager._helper.request.procedure(
179
+ "_log_msg_args", args_kwargs=(("procedure regression",), {}), queue=queue
180
+ )
181
+ status.wait(timeout_s=90)
182
+ if not status.done or status.state != ProcedureState.SUCCESS:
183
+ logs = manager._conn.xrange(MessageEndpoints.procedure_logs(queue), "-", "+") or []
184
+ output = "".join(entry["data"].data for entry in logs)
185
+ pytest.fail(
186
+ f"Procedure state: {status.state.value}; error: {status.error}\n"
187
+ f"Worker output:\n{output}"
188
+ )
189
+ _wait_while(lambda: bool(manager.active_workers()), PROCEDURE.WORKER.QUEUE_TIMEOUT_S + 10)
190
+ logs = manager._conn.xrange(MessageEndpoints.procedure_logs(queue), "-", "+") or []
191
+ assert any("procedure regression" in entry["data"].data for entry in logs)
192
+ assert any("procedure regression" in line for line in manager._logs)
193
+ finally:
194
+ try:
195
+ manager.shutdown()
196
+ finally:
197
+ if uses_container:
198
+ container_name = f"bec_procedure_{PROCEDURE.BEC_VERSION}_{queue}"
199
+ _run_and_capture_error("podman", "rm", "--force", "--ignore", container_name)
200
+
201
+
202
+ @pytest.mark.timeout(120)
203
+ def test_queue_pool_credentials_and_reuse(proc_redis):
204
+ """Use only the submitted identity in clients, with service control confined to the parent."""
205
+ from bec_lib.script_executor import upload_script
206
+
207
+ server = f"{proc_redis.host}:{proc_redis.port}"
208
+ admin = RedisConnector(server)
209
+ alice = RedisConnector(server)
210
+ manager = None
211
+ try:
212
+ access = BECAccessDemo(admin)
213
+ access.add_account("procedure_service", "service-secret", "admin")
214
+ access.add_account("procedure_alice", None, "user")
215
+ admin._managed_connection._redis_conn.acl_setuser(
216
+ "procedure_alice", enabled=True, reset_passwords=True, passwords=["+first-token"]
217
+ )
218
+ admin.authenticate(username="procedure_service", password="service-secret")
219
+ admin._managed_connection._redis_conn.acl_setuser(
220
+ "procedure_alice", enabled=True, passwords=["+second-token"]
221
+ )
222
+ admin._managed_connection._redis_conn.acl_setuser("default", enabled=False)
223
+ alice.authenticate(username="procedure_alice", password="first-token")
224
+ manager = ProcedureManager(
225
+ server,
226
+ SubProcessWorker,
227
+ redis_credentials={"username": "procedure_service", "token": "service-secret"},
228
+ )
229
+ helper = FrontendProcedureHelper(alice)
230
+
231
+ def execute(queue):
232
+ script_id = upload_script(
233
+ alice,
234
+ "import os, sys\n"
235
+ "assert 'credentials_file' not in os.environ\n"
236
+ "assert self.connector.username == 'procedure_alice'\n"
237
+ "try:\n"
238
+ " self.connector._managed_connection._redis_conn.acl_list()\n"
239
+ "except Exception:\n"
240
+ " pass\n"
241
+ "else:\n"
242
+ " raise AssertionError('Procedure obtained service privileges')\n"
243
+ "print('stdout marker', end='', flush=True)\n"
244
+ "sys.stderr.write('stderr marker')\n"
245
+ "sys.stderr.flush()\n",
246
+ )
247
+ status = helper.request.procedure(
248
+ "run_script", args_kwargs=((script_id,), {}), queue=queue
249
+ )
250
+ status.wait(timeout_s=30, raise_on_failure=True)
251
+ assert status.state == ProcedureState.SUCCESS
252
+ _wait_while(lambda: manager._queues[queue].current is not None, 5)
253
+ return status
254
+
255
+ execute("credential_queue")
256
+ (first_id,) = manager._queues["credential_queue"].workers
257
+ first_pid = manager._queues["credential_queue"].workers[first_id].worker._process.pid
258
+ execute("credential_queue")
259
+ assert list(manager._queues["credential_queue"].workers) == [first_id]
260
+ alice.authenticate(username="procedure_alice", password="second-token")
261
+ execute("credential_queue")
262
+ assert len(manager._queues["credential_queue"].workers) == 2
263
+ (second_id,) = set(manager._queues["credential_queue"].workers) - {first_id}
264
+ assert (
265
+ manager._queues["credential_queue"].workers[second_id].worker._process.pid != first_pid
266
+ )
267
+ alice.authenticate(username="procedure_alice", password="first-token")
268
+ execute("credential_queue")
269
+ assert len(manager._queues["credential_queue"].workers) == 2
270
+ assert (
271
+ manager._queues["credential_queue"].workers[first_id].worker._process.pid == first_pid
272
+ )
273
+ execute("different_queue")
274
+ assert not set(manager._queues["different_queue"].workers) & set(
275
+ manager._queues["credential_queue"].workers
276
+ )
277
+ logs = (
278
+ manager._conn.xrange(MessageEndpoints.procedure_logs("credential_queue"), "-", "+")
279
+ or []
280
+ )
281
+ text = "".join(entry["data"].data for entry in logs)
282
+ assert "stdout marker" in text and "stderr marker" in text
283
+ assert all(
284
+ secret not in text for secret in ("first-token", "second-token", "service-secret")
285
+ )
286
+ finally:
287
+ if manager is not None:
288
+ manager.shutdown()
289
+ BECAccessDemo(admin).set_default_limited(False)
290
+ alice.shutdown()
291
+ admin.shutdown()
292
+
293
+
294
+ @pytest.mark.timeout(60)
295
+ def test_invalid_procedure_token_cannot_fall_back_to_default(proc_redis):
296
+ """Invalid forwarded credentials must fail even when default has full privileges."""
297
+ server = f"{proc_redis.host}:{proc_redis.port}"
298
+ manager = ProcedureManager(server, SubProcessWorker)
299
+ try:
300
+ BECAccessDemo(manager._conn).add_account("invalid_token_alice", None, "user")
301
+ manager._conn._managed_connection._redis_conn.acl_setuser(
302
+ "invalid_token_alice", enabled=True, reset_passwords=True, passwords=["+correct"]
303
+ )
304
+ request = messages.ProcedureRequestMessage(
305
+ identifier="_log_msg_args",
306
+ args_kwargs=(("must not execute",), {}),
307
+ queue="invalid_credentials",
308
+ metadata={
309
+ "client_info": {"acl_user": "invalid_token_alice", "acl_token": "incorrect-token"}
310
+ },
311
+ )
312
+ manager._conn.send(MessageEndpoints.procedure_request("invalid_token_alice"), request)
313
+ _wait_while(lambda: not manager._helper.get.unhandled_queue("invalid_credentials"), 30)
314
+ assert (
315
+ manager._helper.get.unhandled_queue("invalid_credentials")[0].execution_id
316
+ == request.execution_id
317
+ )
318
+ output = "\n".join(manager._logs)
319
+ assert "must not execute" not in output
320
+ assert "incorrect-token" not in output
321
+ finally:
322
+ manager.shutdown()
@@ -1,192 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import time
5
- from dataclasses import dataclass
6
- from importlib.metadata import version
7
- from typing import TYPE_CHECKING, Callable, Generator
8
- from unittest.mock import patch
9
- from uuid import uuid4
10
-
11
- import pytest
12
- from pytest_redis import factories as redis_factories
13
-
14
- from bec_ipython_client.main import BECIPythonClient
15
- from bec_lib import messages
16
- from bec_lib.endpoints import MessageEndpoints
17
- from bec_lib.logger import bec_logger
18
- from bec_lib.procedures.helper import ProcedureState
19
- from bec_server.procedures.constants import _CONTAINER, _WORKER, PROCEDURE
20
- from bec_server.procedures.container_utils import (
21
- _run_and_capture_error,
22
- get_backend,
23
- podman_available,
24
- )
25
- from bec_server.procedures.container_worker import ContainerProcedureWorker
26
- from bec_server.procedures.manager import ProcedureManager
27
- from bec_server.procedures.subprocess_worker import SubProcessWorker
28
-
29
- if TYPE_CHECKING:
30
- from pytest_bec_e2e.plugin import LogTestTool
31
-
32
- logger = bec_logger.logger
33
-
34
- # pylint: disable=protected-access
35
-
36
- # Random order disabled for this module so that the test for building the worker container runs first
37
- # and we can use lower timeouts for the remaining tests
38
- pytestmark = pytest.mark.random_order(disabled=True)
39
-
40
-
41
- @dataclass(frozen=True)
42
- class PATCHED_CONSTANTS:
43
- WORKER = _WORKER()
44
- CONTAINER = _CONTAINER()
45
- MANAGER_SHUTDOWN_TIMEOUT_S = 2
46
- BEC_VERSION = version("bec_lib")
47
- REDIS_HOST = "localhost"
48
-
49
-
50
- @pytest.fixture
51
- def client_logtool_and_manager(
52
- bec_ipython_client_fixture_with_logtool: tuple[BECIPythonClient, "LogTestTool"],
53
- ) -> Generator[tuple[BECIPythonClient, "LogTestTool", ProcedureManager], None, None]:
54
- client, logtool = bec_ipython_client_fixture_with_logtool
55
- manager = ProcedureManager(f"{client.connector.host}:{client.connector.port}", SubProcessWorker)
56
- try:
57
- yield client, logtool, manager
58
- finally:
59
- manager.shutdown()
60
-
61
-
62
- def _wait_while(cond: Callable[[], bool], timeout_s):
63
- start = time.monotonic()
64
- while cond():
65
- if (time.monotonic() - start) > timeout_s:
66
- raise TimeoutError()
67
- time.sleep(0.01)
68
-
69
-
70
- @pytest.mark.timeout(100)
71
- def test_building_worker_image():
72
- podman_utils = get_backend()
73
- build = podman_utils.build_worker_image()
74
- assert len(build._command_output.splitlines()[-1]) == 64 # type: ignore
75
- assert podman_utils.image_exists(f"bec_procedure_worker:v{version('bec_lib')}")
76
-
77
-
78
- @pytest.mark.timeout(100)
79
- @patch("bec_server.procedures.manager.procedure_registry.is_registered", lambda _: True)
80
- @patch("bec_server.procedures.oop_worker_base.PROCEDURE", PATCHED_CONSTANTS())
81
- @patch("bec_server.procedures.container_worker.PROCEDURE", PATCHED_CONSTANTS())
82
- def test_procedure_runner_spawns_worker(
83
- client_logtool_and_manager: tuple[BECIPythonClient, "LogTestTool", ProcedureManager],
84
- ):
85
- client, _, manager = client_logtool_and_manager
86
- assert manager._active_workers == {}
87
- endpoint = MessageEndpoints.procedure_request()
88
- msg = messages.ProcedureRequestMessage(
89
- identifier="sleep", args_kwargs=((), {"time_s": 0.1}), queue="test"
90
- )
91
-
92
- logs = []
93
-
94
- def cb(worker: SubProcessWorker):
95
- nonlocal logs
96
- logs = worker.logs()
97
-
98
- manager.add_callback("test", cb)
99
- client.connector.send(endpoint, msg)
100
-
101
- _wait_while(lambda: manager._active_workers == {}, 5)
102
- _wait_while(lambda: manager._active_workers != {}, 90)
103
-
104
-
105
- @pytest.mark.timeout(100)
106
- @patch("bec_server.procedures.manager.procedure_registry.is_registered", lambda _: True)
107
- @patch("bec_server.procedures.oop_worker_base.PROCEDURE", PATCHED_CONSTANTS())
108
- @patch("bec_server.procedures.container_worker.PROCEDURE", PATCHED_CONSTANTS())
109
- def test_happy_path_container_procedure_runner(
110
- client_logtool_and_manager: tuple[BECIPythonClient, "LogTestTool", ProcedureManager],
111
- ):
112
- test_args = (1, 2, 3)
113
- test_kwargs = {"a": "b", "c": "d"}
114
- client, logtool, manager = client_logtool_and_manager
115
- assert manager._active_workers == {}
116
- conn = client.connector
117
- endpoint = MessageEndpoints.procedure_request()
118
- msg = messages.ProcedureRequestMessage(
119
- identifier="_log_msg_args", args_kwargs=(test_args, test_kwargs)
120
- )
121
- conn.send(endpoint, msg)
122
-
123
- _wait_while(lambda: manager._active_workers == {}, 5)
124
- _wait_while(lambda: manager._active_workers != {}, 90)
125
-
126
- logtool.fetch()
127
- assert logtool.is_present_in_any_message("procedure accepted: True, message:")
128
- assert logtool.is_present_in_any_message(
129
- "Procedure worker started container for queue primary"
130
- ), f"Log content relating to procedures: {manager._logs}"
131
-
132
- res, msg = logtool.are_present_in_order(
133
- [
134
- "Procedure worker 'primary' status update: IDLE",
135
- "Procedure worker 'primary' status update: RUNNING",
136
- "Procedure worker 'primary' status update: IDLE",
137
- "Procedure worker 'primary' status update: FINISHED",
138
- ]
139
- )
140
- assert res, f"failed on {msg}"
141
-
142
- assert logtool.is_present_in_any_message(
143
- f"Builtin procedure log_message_args_kwargs called with args: {test_args} and kwargs: {test_kwargs}"
144
- )
145
-
146
-
147
- # A separate Redis prevents the scan server's manager from also consuming this request.
148
- proc_redis = redis_factories.redis_proc()
149
-
150
-
151
- @pytest.mark.timeout(120)
152
- @pytest.mark.parametrize(
153
- "worker_type", [ContainerProcedureWorker, SubProcessWorker], ids=["podman", "subprocess"]
154
- )
155
- def test_procedure_execution(proc_redis, monkeypatch, worker_type):
156
- """Execute a real procedure with either worker backend against isolated Redis.
157
-
158
- Podman requires the worker image and a host-networked ``local_bec`` pod. For a remote
159
- Podman VM, BEC_TEST_CONTAINER_REDIS_HOST can supply a container-reachable host.
160
- """
161
- uses_container = worker_type is ContainerProcedureWorker
162
- if uses_container and not podman_available():
163
- pytest.skip("Podman is not available")
164
- host, port = proc_redis.host, proc_redis.port
165
- queue = f"procedure_test_{uuid4().hex}"
166
- if uses_container and (container_host := os.environ.get("BEC_TEST_CONTAINER_REDIS_HOST")):
167
- worker_environment = ContainerProcedureWorker._worker_environment
168
- monkeypatch.setattr(
169
- ContainerProcedureWorker,
170
- "_worker_environment",
171
- lambda worker: worker_environment(worker)
172
- | {"redis_server": f"{container_host}:{port}"},
173
- )
174
- manager = ProcedureManager(f"{host}:{port}", worker_type)
175
- try:
176
- status = manager._helper.request.procedure(
177
- "_log_msg_args", args_kwargs=(("procedure regression",), {}), queue=queue
178
- )
179
- status.wait(timeout_s=90, raise_on_failure=True)
180
- assert status.done, "Procedure remained scheduled"
181
- assert status.state == ProcedureState.SUCCESS
182
- _wait_while(lambda: bool(manager.active_workers()), 20)
183
- logs = manager._conn.xrange(MessageEndpoints.procedure_logs(queue), "-", "+") or []
184
- assert any("procedure regression" in entry["data"].data for entry in logs)
185
- assert any("procedure regression" in line for line in manager._logs)
186
- finally:
187
- try:
188
- manager.shutdown()
189
- finally:
190
- if uses_container:
191
- container_name = f"bec_procedure_{PROCEDURE.BEC_VERSION}_{queue}"
192
- _run_and_capture_error("podman", "rm", "--force", "--ignore", container_name)