forked from marimo-team/marimo
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_comm.py
More file actions
316 lines (248 loc) · 9.87 KB
/
Copy pathtest_comm.py
File metadata and controls
316 lines (248 loc) · 9.87 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
from __future__ import annotations
import sys
from unittest.mock import MagicMock, patch
import pytest
from marimo._plugins.ui._impl.anywidget.init import CommLifecycleItem
from marimo._plugins.ui._impl.comm import (
MarimoComm,
MarimoCommManager,
)
from marimo._runtime.commands import (
ModelCommand,
ModelCustomMessage,
ModelUpdateMessage,
)
from marimo._types.ids import WidgetModelId
@pytest.fixture
def comm_manager():
return MarimoCommManager()
@pytest.fixture
def comm(comm_manager: MarimoCommManager) -> MarimoComm:
comm_id = WidgetModelId("test-comm")
with patch("marimo._plugins.ui._impl.comm.broadcast_notification"):
c = MarimoComm(
comm_id=comm_id,
comm_manager=comm_manager,
target_name="test_target",
)
yield c # type: ignore[misc]
# Ensure the comm is closed so __del__ doesn't fire broadcast_notification
# during garbage collection in a later test's patch scope.
with patch("marimo._plugins.ui._impl.comm.broadcast_notification"):
c._closed = True
def test_comm_manager_register_unregister(
comm_manager: MarimoCommManager, comm: MarimoComm
):
# comm is already registered during __init__
comm_id = comm.comm_id
assert comm_id in comm_manager.comms
assert comm_manager.comms[comm_id] == comm
# Test unregistration
unregistered_comm = comm_manager.unregister_comm(comm)
assert unregistered_comm == comm
assert comm_id not in comm_manager.comms
def test_comm_manager_receive_unknown_message(
comm_manager: MarimoCommManager,
):
with patch("marimo._plugins.ui._impl.comm.LOGGER") as mock_logger:
command = ModelCommand(
model_id=WidgetModelId("unknown"),
message=ModelUpdateMessage(state={}, buffer_paths=[]),
buffers=[],
)
comm_manager.receive_comm_message(command)
mock_logger.debug.assert_called_once()
def test_comm_initialization(comm: MarimoComm):
assert comm.comm_id == WidgetModelId("test-comm")
assert comm.target_name == "test_target"
assert comm.kernel == "marimo"
assert not comm._closed
assert comm._msg_callback is None
assert comm._close_callback is None
def test_comm_open(comm: MarimoComm):
with patch.object(comm, "_broadcast") as mock_broadcast:
comm.open(data={"test": "data"})
mock_broadcast.assert_called_once_with({"test": "data"}, [])
assert not comm._closed
def test_comm_send(comm: MarimoComm):
with patch.object(comm, "_broadcast") as mock_broadcast:
comm.send(data={"test": "data"})
mock_broadcast.assert_called_once_with({"test": "data"}, [])
def test_comm_close(comm: MarimoComm):
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
comm.close(data={"test": "data"})
mock_broadcast.assert_called_once()
assert comm._closed
def test_comm_close_already_closed(comm: MarimoComm):
comm._closed = True
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
comm.close()
mock_broadcast.assert_not_called()
def test_comm_on_msg(comm: MarimoComm):
callback = MagicMock()
comm.on_msg(callback)
assert comm._msg_callback == callback
def test_comm_on_close(comm: MarimoComm):
callback = MagicMock()
comm.on_close(callback)
assert comm._close_callback == callback
def test_comm_handle_msg(comm: MarimoComm):
callback = MagicMock()
comm.on_msg(callback)
msg = {"test": "message"}
comm.handle_msg(msg)
callback.assert_called_once_with(msg)
def test_comm_handle_msg_no_callback(comm: MarimoComm):
with patch("marimo._plugins.ui._impl.comm.LOGGER") as mock_logger:
msg = {"test": "message"}
comm.handle_msg(msg)
mock_logger.warning.assert_called_once()
def test_comm_handle_close(comm: MarimoComm):
callback = MagicMock()
comm.on_close(callback)
msg = {"test": "message"}
comm.handle_close(msg)
callback.assert_called_once_with(msg)
def test_comm_handle_close_no_callback(comm: MarimoComm):
with patch("marimo._plugins.ui._impl.comm.LOGGER") as mock_logger:
msg = {"test": "message"}
comm.handle_close(msg)
mock_logger.debug.assert_called()
def test_comm_broadcast(comm: MarimoComm):
"""Test that _broadcast sends a ModelLifecycleNotification."""
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
comm._broadcast({"method": "update", "state": {"key": "value"}}, [])
mock_broadcast.assert_called_once()
notification = mock_broadcast.call_args[0][0]
assert notification.model_id == comm.comm_id
def test_comm_broadcast_echo_update(comm: MarimoComm):
"""echo_update should still contribute to replay state."""
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
comm._broadcast(
{"method": "echo_update", "state": {"key": "value"}},
[],
)
mock_broadcast.assert_called_once()
notification = mock_broadcast.call_args[0][0]
assert notification.model_id == comm.comm_id
assert notification.message.state == {"key": "value"}
def test_comm_manager_receive_update_message(
comm_manager: MarimoCommManager, comm: MarimoComm
):
"""Test receiving an update message through the comm manager."""
callback = MagicMock()
comm.on_msg(callback)
comm.ui_element_id = "test-element"
command = ModelCommand(
model_id=comm.comm_id,
message=ModelUpdateMessage(
state={"key": "value"},
buffer_paths=[],
),
buffers=[],
)
result = comm_manager.receive_comm_message(command)
assert result == ("test-element", {"key": "value"})
callback.assert_called_once()
def test_comm_manager_receive_custom_message(
comm_manager: MarimoCommManager, comm: MarimoComm
):
"""Test receiving a custom message through the comm manager."""
callback = MagicMock()
comm.on_msg(callback)
command = ModelCommand(
model_id=comm.comm_id,
message=ModelCustomMessage(
content={"custom": "data"},
),
buffers=[],
)
result = comm_manager.receive_comm_message(command)
assert result == (None, None)
callback.assert_called_once()
def test_comm_lifecycle_item_dispose_closes_comm(
comm_manager: MarimoCommManager, comm: MarimoComm
):
"""CommLifecycleItem.dispose() should close the comm."""
item = CommLifecycleItem(comm)
assert not comm._closed
assert comm.comm_id in comm_manager.comms
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
result = item.dispose(context=MagicMock(), deletion=False)
assert result is True
assert comm._closed
assert comm.comm_id not in comm_manager.comms
mock_broadcast.assert_called_once()
class _CustomBytes:
"""Simulates obstore.Bytes — implements buffer protocol but not a
subclass of bytes/memoryview/bytearray."""
def __init__(self, data: bytes):
self._data = data
def __buffer__(self, flags: int = 0) -> memoryview:
return memoryview(self._data)
class TestBroadcastBufferTypes:
"""Broadcast through the comm with various buffer types and verify the
notification carries the right data after a serialize/deserialize
roundtrip."""
PAYLOAD = b"RIFF\x00\x01binary\xff\xfe"
def _roundtrip_buffer(self, comm, buf):
"""Broadcast a buffer through the comm and return the deserialized
notification."""
from marimo._messaging.notification import ModelLifecycleNotification
from marimo._messaging.serde import (
deserialize_kernel_message,
serialize_kernel_message,
)
with patch(
"marimo._plugins.ui._impl.comm.broadcast_notification"
) as mock_broadcast:
comm._broadcast(
{"method": "update", "state": {}, "buffer_paths": []},
[buf],
)
notification = mock_broadcast.call_args[0][0]
assert isinstance(notification, ModelLifecycleNotification)
# Full roundtrip through JSON serialization
raw = serialize_kernel_message(notification)
return deserialize_kernel_message(raw)
def test_bytes_buffer(self, comm):
result = self._roundtrip_buffer(comm, self.PAYLOAD)
assert result.message.buffers == [self.PAYLOAD]
def test_memoryview_buffer(self, comm):
result = self._roundtrip_buffer(comm, memoryview(self.PAYLOAD))
assert result.message.buffers == [self.PAYLOAD]
def test_bytearray_buffer(self, comm):
result = self._roundtrip_buffer(comm, bytearray(self.PAYLOAD))
assert result.message.buffers == [self.PAYLOAD]
@pytest.mark.skipif(
sys.version_info < (3, 12),
reason="__buffer__ dunder requires Python 3.12+",
)
def test_custom_buffer_protocol(self, comm):
"""Custom buffer-protocol objects (like obstore.Bytes) survive the
roundtrip."""
result = self._roundtrip_buffer(comm, _CustomBytes(self.PAYLOAD))
assert result.message.buffers == [self.PAYLOAD]
def test_unsupported_type_raises(self):
from marimo._plugins.ui._impl.comm import _ensure_bytes
with pytest.raises(TypeError):
_ensure_bytes("not bytes")
def test_comm_lifecycle_item_dispose_idempotent(comm: MarimoComm):
"""Calling dispose twice should not error (comm.close is idempotent)."""
item = CommLifecycleItem(comm)
with patch("marimo._plugins.ui._impl.comm.broadcast_notification"):
item.dispose(context=MagicMock(), deletion=False)
# Second dispose — comm is already closed, should be a no-op
result = item.dispose(context=MagicMock(), deletion=True)
assert result is True
assert comm._closed