vantage6-node 5.0.3rc6__tar.gz → 5.1.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 (33) hide show
  1. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/PKG-INFO +5 -10
  2. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/pyproject.toml +5 -13
  3. vantage6_node-5.1.0/tests_node/test_algorithm_policies.py +80 -0
  4. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_container_manager.py +154 -1
  5. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_network_isolation.py +15 -4
  6. vantage6_node-5.1.0/tests_node/test_node_status_emit.py +51 -0
  7. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_pod_status_mapping.py +26 -0
  8. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/test_share_algorithm_logs.py +3 -8
  9. vantage6_node-5.1.0/tests_node/test_socket_transport.py +46 -0
  10. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/__init__.py +119 -60
  11. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/globals.py +4 -0
  12. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/container_manager.py +107 -46
  13. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/jobpod_state_to_run_status_mapper.py +30 -7
  14. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/socket.py +1 -9
  15. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/.gitignore +0 -0
  16. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/Makefile +0 -0
  17. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/README.md +0 -0
  18. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/dev_start.sh +0 -0
  19. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests/docker/__init__.py +0 -0
  20. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/tests_node/__init__.py +0 -0
  21. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/utest.py +0 -0
  22. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/dev_start.py +0 -0
  23. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/cli/__init__.py +0 -0
  24. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/cli/node.py +0 -0
  25. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/enum.py +0 -0
  26. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/data_classes.py +0 -0
  27. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/exceptions.py +0 -0
  28. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/network_isolation.py +0 -0
  29. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/run_io.py +0 -0
  30. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/k8s/session_manager.py +0 -0
  31. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/proxy_server.py +0 -0
  32. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/util/__init__.py +0 -0
  33. {vantage6_node-5.0.3rc6 → vantage6_node-5.1.0}/vantage6/node/util/colorer.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vantage6-node
3
- Version: 5.0.3rc6
3
+ Version: 5.1.0
4
4
  Summary: vantage6 node
5
5
  Author: Vantage6 Team
6
6
  Maintainer-email: Frank Martin <f.martin@iknl.nl>, Bart van Beusekom <b.vanbeusekom@iknl.nl>
7
- License: MIT
7
+ License: Apache-2.0
8
8
  Requires-Python: >=3.13
9
9
  Requires-Dist: click==8.1.3
10
10
  Requires-Dist: flask==3.1.3
@@ -12,17 +12,12 @@ Requires-Dist: gevent==25.4.2
12
12
  Requires-Dist: jinja2==3.1.6
13
13
  Requires-Dist: kubernetes==36.0.2
14
14
  Requires-Dist: nvidia-ml-py==12.535.133
15
- Requires-Dist: parquet-tools==0.2.16
16
15
  Requires-Dist: psutil==7.0.0
17
16
  Requires-Dist: python-socketio==5.16.2
18
17
  Requires-Dist: requests==2.33.1
19
- Requires-Dist: vantage6-client==5.0.3rc6
20
- Requires-Dist: vantage6-common==5.0.3rc6
21
- Requires-Dist: vantage6==5.0.3rc6
22
- Provides-Extra: dev
23
- Requires-Dist: black; extra == 'dev'
24
- Requires-Dist: coverage==7.10.2; extra == 'dev'
25
- Requires-Dist: pre-commit; extra == 'dev'
18
+ Requires-Dist: vantage6-client==5.1.0
19
+ Requires-Dist: vantage6-common==5.1.0
20
+ Requires-Dist: vantage6==5.1.0
26
21
  Description-Content-Type: text/markdown
27
22
 
28
23
  <h1 align="center">
@@ -4,10 +4,10 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vantage6-node"
7
- version = "5.0.3rc6"
7
+ version = "5.1.0"
8
8
  description = "vantage6 node"
9
9
  readme = "README.md"
10
- license = {text = "MIT"}
10
+ license = {text = "Apache-2.0"}
11
11
  authors = [
12
12
  {name = "Vantage6 Team"}
13
13
  ]
@@ -23,20 +23,12 @@ dependencies = [
23
23
  "jinja2==3.1.6",
24
24
  "kubernetes==36.0.2",
25
25
  "nvidia-ml-py==12.535.133",
26
- "parquet-tools==0.2.16",
27
26
  "psutil==7.0.0",
28
27
  "python-socketio==5.16.2",
29
28
  "requests==2.33.1",
30
- "vantage6==5.0.3rc6",
31
- "vantage6-client==5.0.3rc6",
32
- "vantage6-common==5.0.3rc6",
33
- ]
34
-
35
- [project.optional-dependencies]
36
- dev = [
37
- "coverage==7.10.2",
38
- "black",
39
- "pre-commit",
29
+ "vantage6==5.1.0",
30
+ "vantage6-client==5.1.0",
31
+ "vantage6-common==5.1.0",
40
32
  ]
41
33
 
42
34
  [project.scripts]
@@ -0,0 +1,80 @@
1
+ import unittest
2
+ from unittest.mock import MagicMock, patch
3
+
4
+ from vantage6.common.globals import NodePolicy
5
+
6
+ from vantage6.node import Node
7
+
8
+
9
+ class TestCheckAlgorithmPolicies(unittest.TestCase):
10
+ """
11
+ Tests for Node.check_algorithm_policies().
12
+
13
+ Node.__init__ connects to HQ and Kubernetes, so these tests bypass it (via __new__)
14
+ and set up only the attributes the check reads: `config` and `log`.
15
+ """
16
+
17
+ def _node(self, config: dict) -> Node:
18
+ node = Node.__new__(Node)
19
+ node.config = config
20
+ node.log = MagicMock()
21
+ return node
22
+
23
+ def _assert_exits(self, config: dict):
24
+ node = self._node(config)
25
+ with (
26
+ patch("vantage6.node.sys.exit", side_effect=SystemExit(1)) as mock_exit,
27
+ self.assertRaises(SystemExit),
28
+ ):
29
+ node.check_algorithm_policies()
30
+ mock_exit.assert_called_once_with(1)
31
+ node.log.error.assert_called_once()
32
+
33
+ def _assert_starts(self, config: dict) -> Node:
34
+ node = self._node(config)
35
+ with patch("vantage6.node.sys.exit") as mock_exit:
36
+ node.check_algorithm_policies()
37
+ mock_exit.assert_not_called()
38
+ return node
39
+
40
+ def test_production_without_policies_exits(self):
41
+ self._assert_exits({"production": True})
42
+
43
+ def test_production_is_default(self):
44
+ self._assert_exits({})
45
+
46
+ def test_production_with_empty_policies_exits(self):
47
+ self._assert_exits(
48
+ {
49
+ "policies": {
50
+ NodePolicy.ALLOWED_ALGORITHMS.value: [],
51
+ NodePolicy.ALLOWED_ALGORITHM_STORES.value: [],
52
+ NodePolicy.ALLOWED_USERS.value: [1],
53
+ }
54
+ }
55
+ )
56
+
57
+ def test_production_with_allowed_algorithms_starts(self):
58
+ node = self._assert_starts(
59
+ {
60
+ "production": True,
61
+ "policies": {NodePolicy.ALLOWED_ALGORITHMS.value: ["^some/image$"]},
62
+ }
63
+ )
64
+ node.log.warning.assert_not_called()
65
+
66
+ def test_production_with_allowed_stores_starts(self):
67
+ node = self._assert_starts(
68
+ {
69
+ "production": True,
70
+ "policies": {
71
+ NodePolicy.ALLOWED_ALGORITHM_STORES.value: ["https://store.example"]
72
+ },
73
+ }
74
+ )
75
+ node.log.warning.assert_not_called()
76
+
77
+ def test_non_production_without_policies_warns(self):
78
+ node = self._assert_starts({"production": False})
79
+ node.log.warning.assert_called()
80
+ node.log.error.assert_not_called()
@@ -1,9 +1,12 @@
1
1
  import logging
2
2
  import os
3
+ import tempfile
3
4
  import unittest
5
+ from pathlib import Path
4
6
  from unittest.mock import MagicMock, patch
5
7
 
6
- from vantage6.common.globals import NodePolicy
8
+ from vantage6.common.enum import AlgorithmStepType, TaskDatabaseType
9
+ from vantage6.common.globals import ContainerEnvNames, NodePolicy
7
10
 
8
11
  from vantage6.node.k8s.container_manager import ContainerManager
9
12
 
@@ -215,5 +218,155 @@ class TestGetImagePullPolicy:
215
218
  assert manager._get_image_pull_policy() == "Always"
216
219
 
217
220
 
221
+ class TestDatabaseMountMode(unittest.TestCase):
222
+ """
223
+ Tests that the configured database mount mode is applied to algorithm pods
224
+ (GHSA-27gf-8rcw-jqgh): 'ro' (default) databases are mounted read-only and file
225
+ databases in 'copy' mode are copied, so algorithms never get write access to the
226
+ original.
227
+ """
228
+
229
+ HOST_DATA_DIR = "/host/tasks"
230
+
231
+ def setUp(self):
232
+ tmp_dir = tempfile.TemporaryDirectory()
233
+ self.addCleanup(tmp_dir.cleanup)
234
+ self.db_dir = Path(tmp_dir.name) / "databases"
235
+ self.tasks_dir = Path(tmp_dir.name) / "tasks"
236
+ self.db_dir.mkdir()
237
+ (self.db_dir / "file_db.csv").write_text("a,b\n1,2\n")
238
+ (self.db_dir / "dir_db.parquet").mkdir()
239
+
240
+ for name, value in [
241
+ ("DATABASE_BASE_PATH", str(self.db_dir)),
242
+ ("TASK_FILES_ROOT", str(self.tasks_dir)),
243
+ ]:
244
+ patcher = patch(f"vantage6.node.k8s.container_manager.{name}", value)
245
+ patcher.start()
246
+ self.addCleanup(patcher.stop)
247
+
248
+ self.manager = ContainerManager.__new__(ContainerManager)
249
+ self.manager.log = _get_null_logger()
250
+ self.manager.host_data_dir = self.HOST_DATA_DIR
251
+
252
+ def _get_databases(self, env: dict) -> dict:
253
+ with patch.dict(os.environ, env, clear=True):
254
+ return self.manager._get_database_metadata()
255
+
256
+ @staticmethod
257
+ def _file_db_env(mount_mode: str | None = None) -> dict:
258
+ env = {
259
+ "DATABASE_LABELS": "file_db",
260
+ "DATABASE_FILE_DB_URI": "/data/file_db.csv",
261
+ "DATABASE_FILE_DB_TYPE": "csv",
262
+ }
263
+ if mount_mode is not None:
264
+ env["DATABASE_FILE_DB_MOUNT_MODE"] = mount_mode
265
+ return env
266
+
267
+ def test_read_only_file_is_not_copied(self):
268
+ db = self._get_databases(self._file_db_env("ro"))["file_db"]
269
+
270
+ self.assertEqual(db.mount_mode, "ro")
271
+ self.assertEqual(db.uri, "/data/file_db.csv")
272
+ self.assertEqual(db.local_uri, str(self.db_dir / "file_db.csv"))
273
+ self.assertFalse((self.tasks_dir / "databases").exists())
274
+
275
+ def test_mount_mode_defaults_to_read_only(self):
276
+ db = self._get_databases(self._file_db_env())["file_db"]
277
+
278
+ self.assertEqual(db.mount_mode, "ro")
279
+ self.assertEqual(db.uri, "/data/file_db.csv")
280
+ self.assertFalse((self.tasks_dir / "databases").exists())
281
+
282
+ def test_copy_mode_copies_file(self):
283
+ db = self._get_databases(self._file_db_env("copy"))["file_db"]
284
+
285
+ self.assertEqual(db.mount_mode, "copy")
286
+ self.assertEqual(db.uri, f"{self.HOST_DATA_DIR}/databases/file_db.csv")
287
+ self.assertEqual(db.local_uri, str(self.db_dir / "file_db.csv"))
288
+ self.assertEqual(
289
+ (self.tasks_dir / "databases" / "file_db.csv").read_text(), "a,b\n1,2\n"
290
+ )
291
+
292
+ def test_mount_mode_is_case_insensitive(self):
293
+ db = self._get_databases(self._file_db_env("RO"))["file_db"]
294
+ self.assertEqual(db.mount_mode, "ro")
295
+
296
+ def test_copy_mode_directory_is_not_copied(self):
297
+ env = {
298
+ "DATABASE_LABELS": "dir_db",
299
+ "DATABASE_DIR_DB_URI": "/data/dir_db",
300
+ "DATABASE_DIR_DB_TYPE": "parquet",
301
+ "DATABASE_DIR_DB_MOUNT_MODE": "copy",
302
+ }
303
+ db = self._get_databases(env)["dir_db"]
304
+
305
+ self.assertEqual(db.mount_mode, "copy")
306
+ self.assertEqual(db.uri, "/data/dir_db")
307
+ self.assertFalse((self.tasks_dir / "databases").exists())
308
+
309
+ def test_invalid_mount_mode_raises(self):
310
+ with self.assertRaises(ValueError):
311
+ self._get_databases(self._file_db_env("rw"))
312
+
313
+ def test_mount_mode_not_passed_to_algorithm_env(self):
314
+ env = self._file_db_env("ro")
315
+ env["DATABASE_FILE_DB_SOME_KEY"] = "some_value"
316
+ db = self._get_databases(env)["file_db"]
317
+
318
+ self.assertEqual(db.env, {"SOME_KEY": "some_value"})
319
+
320
+ def _create_extraction_mounts(self, env: dict, label: str):
321
+ self.manager.databases = self._get_databases(env)
322
+ run_io = MagicMock()
323
+ run_io.action = AlgorithmStepType.DATA_EXTRACTION
324
+ run_io.run_id = 1
325
+ run_io.create_files.return_value = ("1/input", "1/output")
326
+ run_io.session_file_manager.session_folder = "session"
327
+ run_io.session_file_manager.session_state_file_name = "state.parquet"
328
+ databases_to_use = [{"label": label, "type": TaskDatabaseType.SOURCE}]
329
+
330
+ with patch.object(self.manager, "_validate_source_database"):
331
+ return self.manager._create_volume_mounts(run_io, b"", databases_to_use)
332
+
333
+ def _get_extraction_db_mount(self, mount_mode: str):
334
+ volumes, vol_mounts, _, _ = self._create_extraction_mounts(
335
+ self._file_db_env(mount_mode), "file_db"
336
+ )
337
+
338
+ volume = next(v for v in volumes if v.name == "task-1-db-file_db")
339
+ vol_mount = next(m for m in vol_mounts if m.name == "task-1-db-file_db")
340
+ return volume, vol_mount
341
+
342
+ def test_read_only_database_mounted_read_only(self):
343
+ volume, vol_mount = self._get_extraction_db_mount("ro")
344
+
345
+ self.assertTrue(vol_mount.read_only)
346
+ self.assertEqual(volume.host_path.path, "/data/file_db.csv")
347
+
348
+ def test_copy_database_mounts_copy_read_write(self):
349
+ volume, vol_mount = self._get_extraction_db_mount("copy")
350
+
351
+ self.assertFalse(vol_mount.read_only)
352
+ self.assertEqual(
353
+ volume.host_path.path, f"{self.HOST_DATA_DIR}/databases/file_db.csv"
354
+ )
355
+ self.assertEqual(vol_mount.mount_path, str(self.db_dir / "file_db.csv"))
356
+
357
+ def test_service_database_ignores_default_read_only(self):
358
+ env = {
359
+ "DATABASE_LABELS": "pg_db",
360
+ "DATABASE_PG_DB_URI": "postgresql://host:5432/db",
361
+ "DATABASE_PG_DB_TYPE": "sql",
362
+ }
363
+ volumes, _, env_vars, _ = self._create_extraction_mounts(env, "pg_db")
364
+
365
+ self.assertFalse(any(v.name == "task-1-db-pg_db" for v in volumes))
366
+ self.assertEqual(
367
+ env_vars[ContainerEnvNames.DATABASE_URI.value], "postgresql://host:5432/db"
368
+ )
369
+
370
+
218
371
  if __name__ == "__main__":
219
372
  unittest.main()
@@ -3,6 +3,8 @@ from unittest.mock import MagicMock, patch
3
3
  import pytest
4
4
  from kubernetes import client as k8s_client
5
5
 
6
+ from vantage6.common.globals import NodePolicy
7
+
6
8
  from vantage6.node import Node
7
9
  from vantage6.node.k8s.network_isolation import (
8
10
  _probe_pod_reached_target,
@@ -69,10 +71,13 @@ class TestValidateAlgorithmIsolation:
69
71
  assert "verified" in message
70
72
 
71
73
 
74
+ _ALGORITHM_POLICY = {NodePolicy.ALLOWED_ALGORITHMS.value: ["^some/image$"]}
75
+
76
+
72
77
  class TestNodeProductionGating:
73
78
  def test_production_true_exits_on_isolation_failure(self):
74
79
  ctx = MagicMock()
75
- ctx.config = {"production": True}
80
+ ctx.config = {"production": True, "policies": _ALGORITHM_POLICY}
76
81
 
77
82
  with (
78
83
  patch("vantage6.node.validate_required_env_vars"),
@@ -84,7 +89,9 @@ class TestNodeProductionGating:
84
89
  False,
85
90
  "probe failed",
86
91
  )
87
- with patch("vantage6.node.exit", side_effect=SystemExit(1)) as mock_exit:
92
+ with patch(
93
+ "vantage6.node.sys.exit", side_effect=SystemExit(1)
94
+ ) as mock_exit:
88
95
  with (
89
96
  patch.object(Node, "_setup_node_client", return_value=MagicMock()),
90
97
  pytest.raises(SystemExit),
@@ -94,7 +101,11 @@ class TestNodeProductionGating:
94
101
 
95
102
  def test_production_false_warns_on_isolation_failure(self, caplog):
96
103
  ctx = MagicMock()
97
- ctx.config = {"production": False, "debug": {}}
104
+ ctx.config = {
105
+ "production": False,
106
+ "debug": {},
107
+ "policies": _ALGORITHM_POLICY,
108
+ }
98
109
 
99
110
  def _connect_socket(self):
100
111
  self.socketIO = MagicMock()
@@ -109,7 +120,7 @@ class TestNodeProductionGating:
109
120
  False,
110
121
  "probe failed",
111
122
  )
112
- with patch("vantage6.node.exit") as mock_exit:
123
+ with patch("vantage6.node.sys.exit") as mock_exit:
113
124
  with (
114
125
  patch.object(Node, "_setup_node_client", return_value=MagicMock()),
115
126
  patch.object(Node, "authenticate"),
@@ -0,0 +1,51 @@
1
+ import unittest
2
+ from unittest.mock import Mock, patch
3
+
4
+ from socketio.exceptions import BadNamespaceError
5
+
6
+ from vantage6.common.enum import RunStatus
7
+
8
+ from vantage6.node import Node
9
+
10
+
11
+ class TestEmitAlgorithmStatusChange(unittest.TestCase):
12
+ def setUp(self):
13
+ self.node = Node.__new__(Node)
14
+ self.node.log = Mock()
15
+ self.node.socketIO = Mock()
16
+ self.node.socketIO.namespaces = {"/tasks": "sid"}
17
+
18
+ def _emit(self):
19
+ with patch("vantage6.node.time.sleep"):
20
+ self.node._Node__emit_algorithm_status_change(
21
+ run_id=20, status=RunStatus.ACTIVE
22
+ )
23
+
24
+ def test_emit_when_connected(self):
25
+ self._emit()
26
+
27
+ self.node.socketIO.emit.assert_called_once_with(
28
+ "algorithm_status_change",
29
+ data={"run_id": 20, "status": RunStatus.ACTIVE.value},
30
+ namespace="/tasks",
31
+ )
32
+ self.node.log.warning.assert_not_called()
33
+
34
+ def test_skip_when_namespace_never_connects(self):
35
+ self.node.socketIO.namespaces = {}
36
+
37
+ self._emit()
38
+
39
+ self.node.socketIO.emit.assert_not_called()
40
+ self.node.log.warning.assert_called_once()
41
+
42
+ def test_no_exception_when_namespace_drops_during_emit(self):
43
+ self.node.socketIO.emit.side_effect = BadNamespaceError("/tasks")
44
+
45
+ self._emit()
46
+
47
+ self.node.log.warning.assert_called_once()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ unittest.main()
@@ -96,6 +96,32 @@ class TestPodStatus(unittest.TestCase):
96
96
  RunStatus.NO_DOCKER_IMAGE,
97
97
  )
98
98
 
99
+ def test_image_being_pulled(self):
100
+ self.mock_pod.status.phase = "Pending"
101
+ self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
102
+ self.mock_pod.status.container_statuses[
103
+ 0
104
+ ].state.waiting.reason = "ContainerCreating"
105
+ self.assertEqual(
106
+ compute_run_pod_status(
107
+ log=self.silent_logger, task_namespace="", label="", pod=self.mock_pod
108
+ ),
109
+ RunStatus.PULLING_IMAGE,
110
+ )
111
+
112
+ def test_pod_still_initializing(self):
113
+ self.mock_pod.status.phase = "Pending"
114
+ self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
115
+ self.mock_pod.status.container_statuses[
116
+ 0
117
+ ].state.waiting.reason = "PodInitializing"
118
+ self.assertEqual(
119
+ compute_run_pod_status(
120
+ log=self.silent_logger, task_namespace="", label="", pod=self.mock_pod
121
+ ),
122
+ RunStatus.INITIALIZING,
123
+ )
124
+
99
125
  def test_waiting_reason_not_ready_err(self):
100
126
  self.mock_pod.status.phase = "Pending"
101
127
  self.mock_pod.status.container_statuses[0].state = self.waiting_container_state
@@ -33,7 +33,7 @@ class TestContainerManagerLogSharing(unittest.TestCase):
33
33
  with patch(
34
34
  "vantage6.node.k8s.container_manager.watch.Watch", return_value=fake_watch
35
35
  ):
36
- manager._ContainerManager__log_stream(run_io=run_io, task_id=99)
36
+ manager._ContainerManager__log_stream(run_io=run_io)
37
37
 
38
38
  manager.socket_io.emit.assert_not_called()
39
39
 
@@ -46,16 +46,11 @@ class TestContainerManagerLogSharing(unittest.TestCase):
46
46
  with patch(
47
47
  "vantage6.node.k8s.container_manager.watch.Watch", return_value=fake_watch
48
48
  ):
49
- manager._ContainerManager__log_stream(run_io=run_io, task_id=99)
49
+ manager._ContainerManager__log_stream(run_io=run_io)
50
50
 
51
51
  manager.socket_io.emit.assert_called_once_with(
52
52
  "algorithm_log",
53
- {
54
- "collaboration_id": 7,
55
- "run_id": 42,
56
- "task_id": 99,
57
- "log": "hello from algorithm",
58
- },
53
+ {"run_id": 42, "log": "hello from algorithm"},
59
54
  namespace="/tasks",
60
55
  )
61
56
 
@@ -0,0 +1,46 @@
1
+ import unittest
2
+ from unittest.mock import Mock, patch
3
+
4
+ from engineio.payload import Payload
5
+
6
+ from vantage6.common.globals import SOCKETIO_MAX_DECODE_PACKETS
7
+
8
+ from vantage6.node import Node
9
+
10
+
11
+ class TestSocketTransport(unittest.TestCase):
12
+ def setUp(self):
13
+ # the packet limit is a class attribute, so reset it to the library default
14
+ self.addCleanup(
15
+ setattr, Payload, "max_decode_packets", Payload.max_decode_packets
16
+ )
17
+ Payload.max_decode_packets = 16
18
+
19
+ def _make_node(self, config: dict) -> Node:
20
+ node = Node.__new__(Node)
21
+ node.log = Mock()
22
+ node.config = config
23
+ node.debug = {}
24
+ node.client = Mock()
25
+ node.client.hq_url = "http://hq.example"
26
+ node.client.headers = {"Authorization": "Bearer token"}
27
+ return node
28
+
29
+ def _connect(self, config: dict) -> Mock:
30
+ node = self._make_node(config)
31
+ with patch("vantage6.node.SocketIO") as socketio_cls:
32
+ socketio_cls.return_value.connected = True
33
+ node.connect_to_socket()
34
+ return socketio_cls.return_value.connect
35
+
36
+ def test_websocket_only_by_default(self):
37
+ connect = self._connect({})
38
+ self.assertEqual(connect.call_args.kwargs["transports"], ["websocket"])
39
+
40
+ def test_http_polling_allowed(self):
41
+ connect = self._connect({"allow_http_polling": True})
42
+ self.assertIsNone(connect.call_args.kwargs["transports"])
43
+
44
+ def test_max_decode_packets_raised(self):
45
+ self._connect({})
46
+ self.assertEqual(Payload.max_decode_packets, SOCKETIO_MAX_DECODE_PACKETS)