pyopensoundcontrol 1.0.8__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.
- pyopensoundcontrol-1.0.8/LICENSE +21 -0
- pyopensoundcontrol-1.0.8/MANIFEST.in +2 -0
- pyopensoundcontrol-1.0.8/PKG-INFO +31 -0
- pyopensoundcontrol-1.0.8/README.md +14 -0
- pyopensoundcontrol-1.0.8/pyopensoundcontrol.egg-info/PKG-INFO +31 -0
- pyopensoundcontrol-1.0.8/pyopensoundcontrol.egg-info/SOURCES.txt +13 -0
- pyopensoundcontrol-1.0.8/pyopensoundcontrol.egg-info/dependency_links.txt +1 -0
- pyopensoundcontrol-1.0.8/pyopensoundcontrol.egg-info/requires.txt +1 -0
- pyopensoundcontrol-1.0.8/pyopensoundcontrol.egg-info/top_level.txt +1 -0
- pyopensoundcontrol-1.0.8/pyproject.toml +70 -0
- pyopensoundcontrol-1.0.8/setup.cfg +4 -0
- pyopensoundcontrol-1.0.8/tests/test_call_handler.py +301 -0
- pyopensoundcontrol-1.0.8/tests/test_dispatcher.py +369 -0
- pyopensoundcontrol-1.0.8/tests/test_integration.py +195 -0
- pyopensoundcontrol-1.0.8/tests/test_peer.py +484 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-2026 Morph Tollon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyopensoundcontrol
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: A package to add fully featured OSC support to python
|
|
5
|
+
Author-email: Morph Tollon <code@morphtollon.co.uk>
|
|
6
|
+
Maintainer-email: Morph Tollon <code@morphtollon.co.uk>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Repository, https://github.com/Theatre-Tools/PyOSC
|
|
9
|
+
Project-URL: Homepage, https://github.com/Theatre-Tools/PyOSC
|
|
10
|
+
Project-URL: Issues, https://github.com/Theatre-Tools/PyOSC/issues
|
|
11
|
+
Keywords: osc,sound,midi,music,sockets,transport,Lighting,Theatre
|
|
12
|
+
Requires-Python: <4.0.0,>=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: oscparser<3.0.0,>=2.0.3
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# PyOSC
|
|
19
|
+
|
|
20
|
+
Python library to support OSC (Open Sound Control).
|
|
21
|
+
|
|
22
|
+
## About the Project
|
|
23
|
+
|
|
24
|
+
I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
- Support for both UDP and TCP transport protocols
|
|
28
|
+
- Flexible message handling with customizable handlers
|
|
29
|
+
- Support for both OSC version 1.0 and 1.1
|
|
30
|
+
- Background processing of incoming messages to avoid blocking the main thread
|
|
31
|
+
- Simple API for sending OSC messages to remote hosts
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# PyOSC
|
|
2
|
+
|
|
3
|
+
Python library to support OSC (Open Sound Control).
|
|
4
|
+
|
|
5
|
+
## About the Project
|
|
6
|
+
|
|
7
|
+
I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
- Support for both UDP and TCP transport protocols
|
|
11
|
+
- Flexible message handling with customizable handlers
|
|
12
|
+
- Support for both OSC version 1.0 and 1.1
|
|
13
|
+
- Background processing of incoming messages to avoid blocking the main thread
|
|
14
|
+
- Simple API for sending OSC messages to remote hosts
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyopensoundcontrol
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: A package to add fully featured OSC support to python
|
|
5
|
+
Author-email: Morph Tollon <code@morphtollon.co.uk>
|
|
6
|
+
Maintainer-email: Morph Tollon <code@morphtollon.co.uk>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Repository, https://github.com/Theatre-Tools/PyOSC
|
|
9
|
+
Project-URL: Homepage, https://github.com/Theatre-Tools/PyOSC
|
|
10
|
+
Project-URL: Issues, https://github.com/Theatre-Tools/PyOSC/issues
|
|
11
|
+
Keywords: osc,sound,midi,music,sockets,transport,Lighting,Theatre
|
|
12
|
+
Requires-Python: <4.0.0,>=3.13
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: oscparser<3.0.0,>=2.0.3
|
|
16
|
+
Dynamic: license-file
|
|
17
|
+
|
|
18
|
+
# PyOSC
|
|
19
|
+
|
|
20
|
+
Python library to support OSC (Open Sound Control).
|
|
21
|
+
|
|
22
|
+
## About the Project
|
|
23
|
+
|
|
24
|
+
I had issues getting the support I needed out of any of the other packages, so I decide how hard can it be...
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
- Support for both UDP and TCP transport protocols
|
|
28
|
+
- Flexible message handling with customizable handlers
|
|
29
|
+
- Support for both OSC version 1.0 and 1.1
|
|
30
|
+
- Background processing of incoming messages to avoid blocking the main thread
|
|
31
|
+
- Simple API for sending OSC messages to remote hosts
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
MANIFEST.in
|
|
3
|
+
README.md
|
|
4
|
+
pyproject.toml
|
|
5
|
+
pyopensoundcontrol.egg-info/PKG-INFO
|
|
6
|
+
pyopensoundcontrol.egg-info/SOURCES.txt
|
|
7
|
+
pyopensoundcontrol.egg-info/dependency_links.txt
|
|
8
|
+
pyopensoundcontrol.egg-info/requires.txt
|
|
9
|
+
pyopensoundcontrol.egg-info/top_level.txt
|
|
10
|
+
tests/test_call_handler.py
|
|
11
|
+
tests/test_dispatcher.py
|
|
12
|
+
tests/test_integration.py
|
|
13
|
+
tests/test_peer.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
oscparser<3.0.0,>=2.0.3
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "pyopensoundcontrol"
|
|
3
|
+
version = "1.0.8"
|
|
4
|
+
description = "A package to add fully featured OSC support to python"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Morph Tollon",email = "code@morphtollon.co.uk"}
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
maintainers = [
|
|
10
|
+
{ name = "Morph Tollon", email = "code@morphtollon.co.uk"}
|
|
11
|
+
]
|
|
12
|
+
keywords = ["osc", "sound", "midi", "music", "sockets", "transport", "Lighting", "Theatre"]
|
|
13
|
+
license = "MIT"
|
|
14
|
+
readme = "README.md"
|
|
15
|
+
requires-python = ">=3.13,<4.0.0"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"oscparser (>=2.0.3,<3.0.0)",
|
|
18
|
+
]
|
|
19
|
+
[project.urls]
|
|
20
|
+
Repository = "https://github.com/Theatre-Tools/PyOSC"
|
|
21
|
+
Homepage = "https://github.com/Theatre-Tools/PyOSC"
|
|
22
|
+
Issues = "https://github.com/Theatre-Tools/PyOSC/issues"
|
|
23
|
+
|
|
24
|
+
[tool.poetry.dependencies]
|
|
25
|
+
pydantic = "^2.12.5"
|
|
26
|
+
|
|
27
|
+
[tool.poetry]
|
|
28
|
+
packages = [{ include = "pyosc"}]
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
py-modules = []
|
|
32
|
+
|
|
33
|
+
[build-system]
|
|
34
|
+
requires = ["setuptools"]
|
|
35
|
+
build-backend = "setuptools.build_meta"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pyright (>=1.1.408,<2.0.0)",
|
|
40
|
+
"ruff>=0.14.10",
|
|
41
|
+
"lefthook (>=2.0.13,<3.0.0)",
|
|
42
|
+
"pydantic (>=2.12.5,<3.0.0)",
|
|
43
|
+
"pytest (>=8.0.0,<9.0.0)",
|
|
44
|
+
"pytest-cov (>=4.1.0,<5.0.0)",
|
|
45
|
+
"pytest-timeout (>=2.2.0,<3.0.0)"
|
|
46
|
+
]
|
|
47
|
+
ci = [
|
|
48
|
+
"pydantic (>=2.12.5,<3.0.0)",
|
|
49
|
+
"twine (>=6.2.0,<7.0.0)",
|
|
50
|
+
"pytest (>=8.0.0,<9.0.0)",
|
|
51
|
+
"pytest-cov (>=4.1.0,<5.0.0)"
|
|
52
|
+
]
|
|
53
|
+
docs = [
|
|
54
|
+
"mkdocs-material (>=9.7.1,<10.0.0)",
|
|
55
|
+
"mkdocs-git-revision-date-localized-plugin (>=1.5.0,<2.0.0)",
|
|
56
|
+
"mkdocs-git-committers-plugin-2 (>=2.5.0,<3.0.0)",
|
|
57
|
+
"mike (>=2.1.3,<3.0.0)",
|
|
58
|
+
"markdown (>=3.10,<4.0)"
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E4", "E7", "E9", "W1", "W2", "F", "RUF", "I"]
|
|
63
|
+
|
|
64
|
+
[tool.ruff]
|
|
65
|
+
line-length = 127
|
|
66
|
+
|
|
67
|
+
[tool.pyright]
|
|
68
|
+
venvPath = "."
|
|
69
|
+
venv = ".venv"
|
|
70
|
+
exclude = [".venv"]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Unit tests for the CallHandler class."""
|
|
2
|
+
|
|
3
|
+
import queue
|
|
4
|
+
import time
|
|
5
|
+
import unittest
|
|
6
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
7
|
+
|
|
8
|
+
from oscparser import OSCInt, OSCMessage, OSCString
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from pyosc.call_handler import Call, CallHandler
|
|
12
|
+
from pyosc.dispatcher import Dispatcher
|
|
13
|
+
from pyosc.peer import Peer
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ResponseModel(BaseModel):
|
|
17
|
+
"""Custom response model for testing."""
|
|
18
|
+
|
|
19
|
+
address: str
|
|
20
|
+
args: tuple
|
|
21
|
+
status: str = Field(default="success")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestCall(unittest.TestCase):
|
|
25
|
+
"""Test cases for the Call class."""
|
|
26
|
+
|
|
27
|
+
def test_call_initialization(self):
|
|
28
|
+
"""Test Call object initialization."""
|
|
29
|
+
q = queue.Queue()
|
|
30
|
+
call = Call(q, OSCMessage)
|
|
31
|
+
|
|
32
|
+
self.assertIs(call.queue, q)
|
|
33
|
+
self.assertEqual(call.validator, OSCMessage)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class TestCallHandler(unittest.TestCase):
|
|
37
|
+
"""Test cases for the CallHandler class."""
|
|
38
|
+
|
|
39
|
+
def setUp(self):
|
|
40
|
+
"""Set up test fixtures."""
|
|
41
|
+
self.mock_peer = Mock(spec=Peer)
|
|
42
|
+
self.mock_peer.Dispatcher = Dispatcher()
|
|
43
|
+
self.call_handler = CallHandler(self.mock_peer)
|
|
44
|
+
|
|
45
|
+
def tearDown(self):
|
|
46
|
+
"""Clean up after tests."""
|
|
47
|
+
if hasattr(self, "call_handler"):
|
|
48
|
+
# Clear any remaining queues
|
|
49
|
+
with self.call_handler.queue_lock:
|
|
50
|
+
self.call_handler.queues.clear()
|
|
51
|
+
|
|
52
|
+
def test_initialization(self):
|
|
53
|
+
"""Test CallHandler initialization."""
|
|
54
|
+
self.assertIs(self.call_handler.peer, self.mock_peer)
|
|
55
|
+
self.assertEqual(len(self.call_handler.queues), 0)
|
|
56
|
+
self.assertIsNotNone(self.call_handler.queue_lock)
|
|
57
|
+
|
|
58
|
+
def test_call_basic_success(self):
|
|
59
|
+
"""Test basic call with successful response."""
|
|
60
|
+
message = OSCMessage(address="/test/call", args=(OSCInt(value=42),))
|
|
61
|
+
|
|
62
|
+
# Mock send_message
|
|
63
|
+
def mock_send(msg):
|
|
64
|
+
# Simulate receiving response
|
|
65
|
+
response = OSCMessage(address="/test/call", args=(OSCInt(value=100),))
|
|
66
|
+
self.call_handler(response)
|
|
67
|
+
|
|
68
|
+
self.mock_peer.send_message = mock_send
|
|
69
|
+
|
|
70
|
+
result = self.call_handler.call(message, timeout=1.0)
|
|
71
|
+
|
|
72
|
+
self.assertIsNotNone(result)
|
|
73
|
+
assert result is not None # Type narrowing for pyright
|
|
74
|
+
self.assertIsInstance(result, OSCMessage)
|
|
75
|
+
self.assertEqual(result.address, "/test/call")
|
|
76
|
+
|
|
77
|
+
def test_call_with_custom_return_address(self):
|
|
78
|
+
"""Test call with custom return address."""
|
|
79
|
+
message = OSCMessage(address="/test/request", args=())
|
|
80
|
+
|
|
81
|
+
def mock_send(msg):
|
|
82
|
+
# Send response to custom address
|
|
83
|
+
response = OSCMessage(address="/test/response", args=(OSCString(value="ok"),))
|
|
84
|
+
self.call_handler(response)
|
|
85
|
+
|
|
86
|
+
self.mock_peer.send_message = mock_send
|
|
87
|
+
|
|
88
|
+
result = self.call_handler.call(message, return_address="/test/response", timeout=1.0)
|
|
89
|
+
|
|
90
|
+
self.assertIsNotNone(result)
|
|
91
|
+
assert result is not None # Type narrowing for pyright
|
|
92
|
+
self.assertEqual(result.address, "/test/response")
|
|
93
|
+
|
|
94
|
+
def test_call_with_validator(self):
|
|
95
|
+
"""Test call with custom validator."""
|
|
96
|
+
message = OSCMessage(address="/test/validated", args=())
|
|
97
|
+
|
|
98
|
+
def mock_send(msg):
|
|
99
|
+
response = OSCMessage(
|
|
100
|
+
address="/test/validated",
|
|
101
|
+
args=(OSCString(value="success"),),
|
|
102
|
+
)
|
|
103
|
+
# Manually add status for validation
|
|
104
|
+
response_dict = response.model_dump()
|
|
105
|
+
response_dict["status"] = "success"
|
|
106
|
+
# Simulate the response
|
|
107
|
+
with self.call_handler.queue_lock:
|
|
108
|
+
if "/test/validated" in self.call_handler.queues:
|
|
109
|
+
try:
|
|
110
|
+
validated = ResponseModel.model_validate(response_dict)
|
|
111
|
+
self.call_handler.queues["/test/validated"].queue.put(validated) # type: ignore[arg-type]
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
self.mock_peer.send_message = mock_send
|
|
116
|
+
|
|
117
|
+
# Pre-add the handler for this test
|
|
118
|
+
responseq = queue.Queue()
|
|
119
|
+
with self.call_handler.queue_lock:
|
|
120
|
+
self.call_handler.queues["/test/validated"] = Call(responseq, ResponseModel)
|
|
121
|
+
self.mock_peer.Dispatcher.add_handler("/test/validated", self.call_handler)
|
|
122
|
+
|
|
123
|
+
# Send message and get from queue
|
|
124
|
+
self.mock_peer.send_message(message)
|
|
125
|
+
try:
|
|
126
|
+
result = responseq.get(timeout=1.0)
|
|
127
|
+
self.assertIsNotNone(result)
|
|
128
|
+
self.assertIsInstance(result, ResponseModel)
|
|
129
|
+
finally:
|
|
130
|
+
with self.call_handler.queue_lock:
|
|
131
|
+
self.mock_peer.Dispatcher.remove_handler("/test/validated")
|
|
132
|
+
if "/test/validated" in self.call_handler.queues:
|
|
133
|
+
del self.call_handler.queues["/test/validated"]
|
|
134
|
+
|
|
135
|
+
def test_call_timeout(self):
|
|
136
|
+
"""Test call timeout when no response received."""
|
|
137
|
+
message = OSCMessage(address="/test/timeout", args=())
|
|
138
|
+
|
|
139
|
+
# Don't send any response
|
|
140
|
+
self.mock_peer.send_message = MagicMock()
|
|
141
|
+
|
|
142
|
+
result = self.call_handler.call(message, timeout=0.1)
|
|
143
|
+
|
|
144
|
+
self.assertIsNone(result)
|
|
145
|
+
# Queue should be cleaned up
|
|
146
|
+
self.assertNotIn("/test/timeout", self.call_handler.queues)
|
|
147
|
+
|
|
148
|
+
def test_call_handler_callable(self):
|
|
149
|
+
"""Test CallHandler as callable (message handler)."""
|
|
150
|
+
# Set up a queue for testing
|
|
151
|
+
test_queue = queue.Queue()
|
|
152
|
+
with self.call_handler.queue_lock:
|
|
153
|
+
self.call_handler.queues["/test"] = Call(test_queue, OSCMessage)
|
|
154
|
+
|
|
155
|
+
# Call the handler
|
|
156
|
+
message = OSCMessage(address="/test", args=(OSCInt(value=42),))
|
|
157
|
+
self.call_handler(message)
|
|
158
|
+
|
|
159
|
+
# Check queue received message
|
|
160
|
+
self.assertFalse(test_queue.empty())
|
|
161
|
+
result = test_queue.get(timeout=0.1)
|
|
162
|
+
self.assertIsInstance(result, OSCMessage)
|
|
163
|
+
self.assertEqual(result.address, "/test")
|
|
164
|
+
|
|
165
|
+
def test_call_handler_wrong_address(self):
|
|
166
|
+
"""Test CallHandler ignores messages with wrong address."""
|
|
167
|
+
# Set up queue for /test
|
|
168
|
+
test_queue = queue.Queue()
|
|
169
|
+
with self.call_handler.queue_lock:
|
|
170
|
+
self.call_handler.queues["/test"] = Call(test_queue, OSCMessage)
|
|
171
|
+
|
|
172
|
+
# Send message to different address
|
|
173
|
+
message = OSCMessage(address="/other", args=())
|
|
174
|
+
self.call_handler(message)
|
|
175
|
+
|
|
176
|
+
# Queue should remain empty
|
|
177
|
+
self.assertTrue(test_queue.empty())
|
|
178
|
+
|
|
179
|
+
def test_call_handler_validation_error(self):
|
|
180
|
+
"""Test CallHandler handles validation errors gracefully."""
|
|
181
|
+
|
|
182
|
+
class StrictModel(BaseModel):
|
|
183
|
+
required_field: str
|
|
184
|
+
|
|
185
|
+
test_queue = queue.Queue()
|
|
186
|
+
with self.call_handler.queue_lock:
|
|
187
|
+
self.call_handler.queues["/test"] = Call(test_queue, StrictModel)
|
|
188
|
+
|
|
189
|
+
# Send message that won't validate
|
|
190
|
+
message = OSCMessage(address="/test", args=())
|
|
191
|
+
|
|
192
|
+
# Should not raise exception
|
|
193
|
+
with patch("builtins.print") as mock_print:
|
|
194
|
+
self.call_handler(message)
|
|
195
|
+
# Should print validation error
|
|
196
|
+
mock_print.assert_called()
|
|
197
|
+
|
|
198
|
+
# Queue should remain empty
|
|
199
|
+
self.assertTrue(test_queue.empty())
|
|
200
|
+
|
|
201
|
+
def test_concurrent_calls(self):
|
|
202
|
+
"""Test multiple concurrent calls to different addresses."""
|
|
203
|
+
messages = [OSCMessage(address=f"/test/call{i}", args=(OSCInt(value=i),)) for i in range(5)]
|
|
204
|
+
|
|
205
|
+
results = []
|
|
206
|
+
responses_sent = []
|
|
207
|
+
|
|
208
|
+
def mock_send(msg):
|
|
209
|
+
# Store for later
|
|
210
|
+
responses_sent.append(msg)
|
|
211
|
+
|
|
212
|
+
self.mock_peer.send_message = mock_send
|
|
213
|
+
|
|
214
|
+
# Start calls in threads
|
|
215
|
+
import threading
|
|
216
|
+
|
|
217
|
+
def make_call(msg, idx):
|
|
218
|
+
# Simulate delayed response
|
|
219
|
+
def delayed_response():
|
|
220
|
+
time.sleep(0.05)
|
|
221
|
+
response = OSCMessage(address=msg.address, args=(OSCInt(value=idx * 10),))
|
|
222
|
+
self.call_handler(response)
|
|
223
|
+
|
|
224
|
+
threading.Thread(target=delayed_response, daemon=True).start()
|
|
225
|
+
|
|
226
|
+
result = self.call_handler.call(msg, timeout=1.0)
|
|
227
|
+
results.append((idx, result))
|
|
228
|
+
|
|
229
|
+
threads = []
|
|
230
|
+
for i, msg in enumerate(messages):
|
|
231
|
+
t = threading.Thread(target=make_call, args=(msg, i))
|
|
232
|
+
threads.append(t)
|
|
233
|
+
t.start()
|
|
234
|
+
|
|
235
|
+
# Wait for all threads
|
|
236
|
+
for t in threads:
|
|
237
|
+
t.join()
|
|
238
|
+
|
|
239
|
+
# All calls should succeed
|
|
240
|
+
self.assertEqual(len(results), 5)
|
|
241
|
+
for idx, result in results:
|
|
242
|
+
self.assertIsNotNone(result)
|
|
243
|
+
self.assertIsInstance(result, OSCMessage)
|
|
244
|
+
|
|
245
|
+
def test_queue_lock_thread_safety(self):
|
|
246
|
+
"""Test that queue_lock prevents race conditions."""
|
|
247
|
+
iterations = 100
|
|
248
|
+
errors = []
|
|
249
|
+
|
|
250
|
+
def add_remove_queue():
|
|
251
|
+
for i in range(iterations):
|
|
252
|
+
try:
|
|
253
|
+
with self.call_handler.queue_lock:
|
|
254
|
+
self.call_handler.queues[f"/test/{i}"] = Call(queue.Queue(), OSCMessage)
|
|
255
|
+
with self.call_handler.queue_lock:
|
|
256
|
+
if f"/test/{i}" in self.call_handler.queues:
|
|
257
|
+
del self.call_handler.queues[f"/test/{i}"]
|
|
258
|
+
except Exception as e:
|
|
259
|
+
errors.append(e)
|
|
260
|
+
|
|
261
|
+
import threading
|
|
262
|
+
|
|
263
|
+
threads = [threading.Thread(target=add_remove_queue) for _ in range(5)]
|
|
264
|
+
for t in threads:
|
|
265
|
+
t.start()
|
|
266
|
+
for t in threads:
|
|
267
|
+
t.join()
|
|
268
|
+
|
|
269
|
+
# Should have no errors
|
|
270
|
+
self.assertEqual(len(errors), 0)
|
|
271
|
+
|
|
272
|
+
def test_cleanup_after_successful_call(self):
|
|
273
|
+
"""Test that queues and handlers are cleaned up after successful call."""
|
|
274
|
+
message = OSCMessage(address="/test/cleanup", args=())
|
|
275
|
+
|
|
276
|
+
def mock_send(msg):
|
|
277
|
+
response = OSCMessage(address="/test/cleanup", args=())
|
|
278
|
+
self.call_handler(response)
|
|
279
|
+
|
|
280
|
+
self.mock_peer.send_message = mock_send
|
|
281
|
+
|
|
282
|
+
result = self.call_handler.call(message, timeout=1.0)
|
|
283
|
+
|
|
284
|
+
self.assertIsNotNone(result)
|
|
285
|
+
# Queue should be cleaned up
|
|
286
|
+
self.assertNotIn("/test/cleanup", self.call_handler.queues)
|
|
287
|
+
|
|
288
|
+
def test_cleanup_after_timeout(self):
|
|
289
|
+
"""Test that queues are cleaned up after timeout."""
|
|
290
|
+
message = OSCMessage(address="/test/cleanup", args=())
|
|
291
|
+
self.mock_peer.send_message = MagicMock()
|
|
292
|
+
|
|
293
|
+
result = self.call_handler.call(message, timeout=0.1)
|
|
294
|
+
|
|
295
|
+
self.assertIsNone(result)
|
|
296
|
+
# Queue should be cleaned up
|
|
297
|
+
self.assertNotIn("/test/cleanup", self.call_handler.queues)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
unittest.main()
|