Mercurial > libervia-desktop-kivy
comparison tests/unit/test_plugin_calls.py @ 509:f0ce49b360c8
calls: move webrtc code to core:
WebRTC code which can be used in several frontends has been factorized and moved to
common `frontends.tools`. Test have been updated consequently.
rel 426
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 01 Nov 2023 13:41:07 +0100 |
parents | f6b8300e8234 |
children |
comparison
equal
deleted
inserted
replaced
508:d87b9a6b0b69 | 509:f0ce49b360c8 |
---|---|
16 # You should have received a copy of the GNU Affero General Public License | 16 # You should have received a copy of the GNU Affero General Public License |
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 from unittest.mock import AsyncMock, MagicMock | 19 from unittest.mock import AsyncMock, MagicMock |
20 | 20 |
21 from gi.repository import Gst | |
22 from libervia.backend.core import exceptions | |
23 from libervia.backend.tools.common import data_format | 21 from libervia.backend.tools.common import data_format |
24 import pytest | 22 import pytest |
25 | 23 |
26 from libervia.desktop_kivy import G | 24 from libervia.desktop_kivy import G |
27 from libervia.desktop_kivy.plugins import plugin_wid_calls | 25 from libervia.desktop_kivy.plugins import plugin_wid_calls |
34 host.app.expand = lambda s: s | 32 host.app.expand = lambda s: s |
35 monkeypatch.setattr(G, "_host", host, raising=False) | 33 monkeypatch.setattr(G, "_host", host, raising=False) |
36 return host | 34 return host |
37 | 35 |
38 | 36 |
39 @pytest.fixture(scope="function") | |
40 def webrtc(): | |
41 """Fixture for WebRTC instantiation.""" | |
42 host_mock = MagicMock() | |
43 profile = "test_profile" | |
44 instance = plugin_wid_calls.WebRTC(host_mock, profile) | |
45 | |
46 instance._set_media_types = MagicMock() | |
47 instance.start_pipeline = MagicMock() | |
48 instance.webrtc = MagicMock() | |
49 instance.webrtc.emit = MagicMock() | |
50 | |
51 instance.GstSdp_SDPMessage_new_from_text = MagicMock() | |
52 instance.GstWebRTC_WebRTCSessionDescription_new = MagicMock() | |
53 instance.Gst_Promise_new_with_change_func = MagicMock() | |
54 | |
55 return instance | |
56 | |
57 | |
58 @pytest.fixture | 37 @pytest.fixture |
59 def calls(monkeypatch, host): | 38 def calls(monkeypatch, host): |
60 """Fixture for Call UI instantiation.""" | 39 """Fixture for Call UI instantiation.""" |
61 for attr in ("header_box", "local_video", "remote_video", "screen_manager"): | 40 for attr in ("header_box", "local_video", "remote_video", "screen_manager"): |
62 monkeypatch.setattr( | 41 monkeypatch.setattr(plugin_wid_calls.Calls, attr, MagicMock()) |
63 plugin_wid_calls.Calls, | 42 calls = plugin_wid_calls.Calls(host, "test_peer@example.org", ["test_profile"]) |
64 attr, | |
65 MagicMock() | |
66 ) | |
67 calls = plugin_wid_calls.Calls( | |
68 host, | |
69 "test_peer@example.org", | |
70 ["test_profile"] | |
71 ) | |
72 calls.jid_selector = MagicMock() | 43 calls.jid_selector = MagicMock() |
73 calls.header_input = MagicMock() | 44 calls.header_input = MagicMock() |
74 calls.header_input.text = "fake_jid@domain" | 45 calls.header_input.text = "fake_jid@domain" |
75 calls.webrtc = MagicMock() | 46 calls.webrtc = MagicMock() |
76 calls.webrtc.setup_call = AsyncMock() | 47 calls.webrtc.setup_call = AsyncMock() |
78 calls.end_call = AsyncMock() | 49 calls.end_call = AsyncMock() |
79 | 50 |
80 return calls | 51 return calls |
81 | 52 |
82 | 53 |
83 class TestWebRtc: | |
84 def test_get_payload_types(self, webrtc): | |
85 """The method can identify the correct payload types for video and audio.""" | |
86 fake_sdpmsg = MagicMock() | |
87 fake_media = MagicMock() | |
88 fake_caps = MagicMock() | |
89 fake_structure = MagicMock() | |
90 | |
91 # This side effect will return 'fake_video_encoding' first, then | |
92 # 'fake_audio_encoding'. | |
93 fake_structure.__getitem__.side_effect = [ | |
94 "fake_video_encoding", | |
95 "fake_audio_encoding", | |
96 ] | |
97 fake_caps.get_structure.return_value = fake_structure | |
98 fake_media.get_format.side_effect = ["webrtc-datachannel", "10", "20"] | |
99 fake_media.get_caps_from_media.return_value = fake_caps | |
100 fake_sdpmsg.get_media.return_value = fake_media | |
101 fake_sdpmsg.medias_len.return_value = 1 | |
102 fake_media.formats_len.return_value = 3 | |
103 | |
104 result = webrtc.get_payload_types( | |
105 fake_sdpmsg, "fake_video_encoding", "fake_audio_encoding" | |
106 ) | |
107 expected_result = {"fake_video_encoding": 10, "fake_audio_encoding": 20} | |
108 | |
109 assert result == expected_result | |
110 | |
111 def test_on_accepted_call(self, webrtc): | |
112 """The method correctly sets the remote SDP upon acceptance of an outgoing call.""" | |
113 sdp_str = "mock_sdp_string" | |
114 profile_str = "test_profile" | |
115 | |
116 webrtc.on_accepted_call(sdp_str, profile_str) | |
117 | |
118 # remote description must be set | |
119 assert webrtc.webrtc.emit.call_count == 1 | |
120 assert webrtc.webrtc.emit.call_args[0][0] == "set-remote-description" | |
121 | |
122 @pytest.mark.asyncio | |
123 async def test_answer_call(self, webrtc, monkeypatch): | |
124 """The method correctly answers an incoming call.""" | |
125 mock_setup_call = AsyncMock() | |
126 | |
127 def mock_get_payload_types(sdpmsg, video_encoding, audio_encoding): | |
128 return {"VP8": 96, "OPUS": 97} | |
129 | |
130 monkeypatch.setattr(webrtc, "setup_call", mock_setup_call) | |
131 monkeypatch.setattr(webrtc, "get_payload_types", mock_get_payload_types) | |
132 | |
133 sdp_str = "mock_sdp_string" | |
134 profile_str = "mock_profile" | |
135 | |
136 await webrtc.answer_call(sdp_str, profile_str) | |
137 | |
138 mock_setup_call.assert_called_once_with("responder", audio_pt=97, video_pt=96) | |
139 | |
140 # remote description must be set | |
141 assert webrtc.webrtc.emit.call_count == 1 | |
142 assert webrtc.webrtc.emit.call_args[0][0] == "set-remote-description" | |
143 | |
144 def test_on_remote_decodebin_stream_video(self, webrtc, monkeypatch): | |
145 """The method correctly handles video streams from the remote decodebin.""" | |
146 mock_pipeline = MagicMock() | |
147 monkeypatch.setattr(webrtc, "pipeline", mock_pipeline) | |
148 | |
149 mock_pad = MagicMock() | |
150 mock_caps = MagicMock() | |
151 mock_structure = MagicMock() | |
152 | |
153 mock_pad.has_current_caps.return_value = True | |
154 mock_pad.get_current_caps.return_value = mock_caps | |
155 mock_caps.__len__.return_value = 1 | |
156 mock_caps.__getitem__.return_value = mock_structure | |
157 mock_structure.get_name.return_value = "video/x-h264" | |
158 # We use non-standard resolution as example to trigger the workaround | |
159 mock_structure.get_int.side_effect = lambda x: MagicMock( | |
160 value=990 if x == "width" else 557 | |
161 ) | |
162 | |
163 webrtc.on_remote_decodebin_stream(None, mock_pad) | |
164 | |
165 assert webrtc._remote_video_pad == mock_pad | |
166 mock_pipeline.add.assert_called() | |
167 mock_pipeline.set_state.assert_called() | |
168 mock_pad.link.assert_called() | |
169 | |
170 def test_on_remote_decodebin_stream_audio(self, webrtc, monkeypatch): | |
171 """The method correctly handles audio streams from the remote decodebin.""" | |
172 mock_pipeline = MagicMock() | |
173 monkeypatch.setattr(webrtc, "pipeline", mock_pipeline) | |
174 | |
175 mock_pad = MagicMock() | |
176 mock_caps = MagicMock() | |
177 mock_structure = MagicMock() | |
178 | |
179 mock_pad.has_current_caps.return_value = True | |
180 mock_pad.get_current_caps.return_value = mock_caps | |
181 mock_caps.__len__.return_value = 1 | |
182 mock_caps.__getitem__.return_value = mock_structure | |
183 mock_structure.get_name.return_value = "audio/x-raw" | |
184 | |
185 webrtc.on_remote_decodebin_stream(None, mock_pad) | |
186 | |
187 mock_pipeline.add.assert_called() | |
188 mock_pipeline.set_state.assert_called() | |
189 mock_pad.link.assert_called() | |
190 | |
191 @pytest.mark.asyncio | |
192 async def test_setup_call_correct_role(self, host, webrtc, monkeypatch): | |
193 """Roles are set in setup_call.""" | |
194 monkeypatch.setattr(Gst, "parse_launch", MagicMock()) | |
195 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
196 | |
197 await webrtc.setup_call("initiator") | |
198 assert webrtc.role == "initiator" | |
199 | |
200 await webrtc.setup_call("responder") | |
201 assert webrtc.role == "responder" | |
202 | |
203 with pytest.raises(AssertionError): | |
204 await webrtc.setup_call("invalid_role") | |
205 | |
206 @pytest.mark.asyncio | |
207 async def test_setup_call_test_mode(self, host, webrtc, monkeypatch): | |
208 """Test mode use fake video and audio in setup_call.""" | |
209 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
210 monkeypatch.setattr(webrtc, "test_mode", True) | |
211 await webrtc.setup_call("initiator") | |
212 assert "videotestsrc" in webrtc.gst_pipe_desc | |
213 assert "audiotestsrc" in webrtc.gst_pipe_desc | |
214 | |
215 @pytest.mark.asyncio | |
216 async def test_setup_call_normal_mode(self, host, webrtc, monkeypatch): | |
217 """Normal mode use real video and audio in setup_call.""" | |
218 monkeypatch.setattr(data_format, "deserialise", MagicMock(return_value=[])) | |
219 monkeypatch.setattr(webrtc, "test_mode", False) | |
220 await webrtc.setup_call("initiator") | |
221 assert "v4l2src" in webrtc.gst_pipe_desc | |
222 assert "pulsesrc" in webrtc.gst_pipe_desc | |
223 | |
224 @pytest.mark.asyncio | |
225 async def test_setup_call_with_stun_and_turn(self, host, webrtc, monkeypatch): | |
226 """STUN and TURN server configurations are done in setup_call.""" | |
227 mock_pipeline = MagicMock() | |
228 mock_parse_launch = MagicMock() | |
229 mock_parse_launch.return_value = mock_pipeline | |
230 monkeypatch.setattr(Gst, "parse_launch", mock_parse_launch) | |
231 | |
232 mock_pipeline.get_by_name.return_value = webrtc.webrtc | |
233 | |
234 mock_external_disco = [ | |
235 {"type": "stun", "transport": "udp", "host": "stun.host", "port": "3478"}, | |
236 { | |
237 "type": "turn", | |
238 "transport": "udp", | |
239 "host": "turn.host", | |
240 "port": "3478", | |
241 "username": "user", | |
242 "password": "pass", | |
243 }, | |
244 ] | |
245 | |
246 monkeypatch.setattr( | |
247 data_format, "deserialise", MagicMock(return_value=mock_external_disco) | |
248 ) | |
249 | |
250 mock_emit = AsyncMock() | |
251 monkeypatch.setattr(webrtc.webrtc, "emit", mock_emit) | |
252 | |
253 mock_set_property = AsyncMock() | |
254 monkeypatch.setattr(webrtc.webrtc, "set_property", mock_set_property) | |
255 | |
256 await webrtc.setup_call("initiator") | |
257 | |
258 G.host.a_bridge.external_disco_get.assert_called_once_with("", webrtc.profile) | |
259 mock_set_property.assert_any_call("stun-server", "stun://stun.host:3478") | |
260 mock_emit.assert_called_once_with( | |
261 "add-turn-server", "turn://user:pass@turn.host:3478" | |
262 ) | |
263 | |
264 @pytest.mark.asyncio | |
265 async def test_setup_call_gstreamer_pipeline_failure(self, webrtc, monkeypatch): | |
266 """Test setup_call method handling Gstreamer pipeline failure.""" | |
267 monkeypatch.setattr(Gst, "parse_launch", lambda _: None) | |
268 with pytest.raises(exceptions.InternalError): | |
269 await webrtc.setup_call("initiator") | |
270 | |
271 | |
272 class TestCalls: | 54 class TestCalls: |
273 | |
274 @pytest.mark.asyncio | 55 @pytest.mark.asyncio |
275 async def test_toggle_call_sid_none(self, monkeypatch, calls): | 56 async def test_toggle_call_sid_none(self, monkeypatch, calls): |
276 """Call is started when there is not sid set.""" | 57 """Call is started when there is not sid set.""" |
277 monkeypatch.setattr(calls.webrtc, "sid", None) | 58 monkeypatch.setattr(calls.webrtc, "sid", None) |
278 | 59 |
291 | 72 |
292 calls.end_call.assert_called_once_with({"reason": "terminated"}, calls.profile) | 73 calls.end_call.assert_called_once_with({"reason": "terminated"}, calls.profile) |
293 host.a_bridge.call_end.assert_called_once_with("test_sid", "", calls.profile) | 74 host.a_bridge.call_end.assert_called_once_with("test_sid", "", calls.profile) |
294 assert calls.in_call == False | 75 assert calls.in_call == False |
295 | 76 |
296 | |
297 @pytest.mark.asyncio | 77 @pytest.mark.asyncio |
298 async def test_on_incoming_call_sid_none(self, monkeypatch, host, calls): | 78 async def test_on_incoming_call_sid_none(self, monkeypatch, host, calls): |
299 """Incoming call is accepted if no ongoing call.""" | 79 """Incoming call is accepted if no ongoing call.""" |
300 monkeypatch.setattr(calls.webrtc, "sid", None) | 80 monkeypatch.setattr(calls.webrtc, "sid", None) |
301 fake_action_id = "fake_action_id" | 81 fake_action_id = "fake_action_id" |
305 await calls.on_incoming_call(fake_action_data, fake_action_id, fake_profile) | 85 await calls.on_incoming_call(fake_action_data, fake_action_id, fake_profile) |
306 | 86 |
307 assert calls.in_call == True | 87 assert calls.in_call == True |
308 assert calls.webrtc.sid == "test_sid" | 88 assert calls.webrtc.sid == "test_sid" |
309 host.a_bridge.action_launch.assert_called_once_with( | 89 host.a_bridge.action_launch.assert_called_once_with( |
310 fake_action_id, | 90 fake_action_id, data_format.serialise({"cancelled": False}), fake_profile |
311 data_format.serialise({"cancelled": False}), | |
312 fake_profile | |
313 ) | 91 ) |
314 | 92 |
315 @pytest.mark.asyncio | 93 @pytest.mark.asyncio |
316 async def test_on_incoming_call_sid_set(self, monkeypatch, host, calls): | 94 async def test_on_incoming_call_sid_set(self, monkeypatch, host, calls): |
317 """Incoming call is ignored if there's an ongoing call.""" | 95 """Incoming call is ignored if there's an ongoing call.""" |
327 host.a_bridge.action_launch.assert_not_called() | 105 host.a_bridge.action_launch.assert_not_called() |
328 | 106 |
329 @pytest.mark.asyncio | 107 @pytest.mark.asyncio |
330 async def test_on_call_setup_initiator(self, calls): | 108 async def test_on_call_setup_initiator(self, calls): |
331 """Correct method called if role is 'initiator'.""" | 109 """Correct method called if role is 'initiator'.""" |
332 setup_data = { | 110 setup_data = {"role": "initiator", "sdp": "fake_sdp"} |
333 "role": "initiator", | |
334 "sdp": "fake_sdp" | |
335 } | |
336 profile = "fake_profile" | 111 profile = "fake_profile" |
337 | 112 |
338 await calls.on_call_setup(setup_data, profile) | 113 await calls.on_call_setup(setup_data, profile) |
339 | 114 |
340 calls.webrtc.on_accepted_call.assert_called_once_with(setup_data["sdp"], profile) | 115 calls.webrtc.on_accepted_call.assert_called_once_with(setup_data["sdp"], profile) |
341 | 116 |
342 @pytest.mark.asyncio | 117 @pytest.mark.asyncio |
343 async def test_on_call_setup_responder(self, monkeypatch, calls): | 118 async def test_on_call_setup_responder(self, monkeypatch, calls): |
344 """Correct method called if role is 'responder'.""" | 119 """Correct method called if role is 'responder'.""" |
345 monkeypatch.setattr( | 120 monkeypatch.setattr(calls.webrtc, "answer_call", AsyncMock()) |
346 calls.webrtc, | 121 setup_data = {"role": "responder", "sdp": "fake_sdp"} |
347 "answer_call", | |
348 AsyncMock() | |
349 ) | |
350 setup_data = { | |
351 "role": "responder", | |
352 "sdp": "fake_sdp" | |
353 } | |
354 profile = "fake_profile" | 122 profile = "fake_profile" |
355 | 123 |
356 await calls.on_call_setup(setup_data, profile) | 124 await calls.on_call_setup(setup_data, profile) |
357 | 125 |
358 calls.webrtc.answer_call.assert_called_once_with(setup_data["sdp"], profile) | 126 calls.webrtc.answer_call.assert_called_once_with(setup_data["sdp"], profile) |
359 calls.webrtc.on_accepted_call.assert_not_called() | 127 calls.webrtc.on_accepted_call.assert_not_called() |
360 | 128 |
361 @pytest.mark.asyncio | 129 @pytest.mark.asyncio |
362 async def test_on_call_setup_invalid_role(self, calls): | 130 async def test_on_call_setup_invalid_role(self, calls): |
363 """Nothing is called if role is neither 'initiator' nor 'responder'.""" | 131 """Nothing is called if role is neither 'initiator' nor 'responder'.""" |
364 setup_data = { | 132 setup_data = {"role": "invalid_role", "sdp": "fake_sdp"} |
365 "role": "invalid_role", | |
366 "sdp": "fake_sdp" | |
367 } | |
368 profile = "fake_profile" | 133 profile = "fake_profile" |
369 | 134 |
370 await calls.on_call_setup(setup_data, profile) | 135 await calls.on_call_setup(setup_data, profile) |
371 | 136 |
372 calls.webrtc.answer_call.assert_not_called() | 137 calls.webrtc.answer_call.assert_not_called() |