forked from auracaster/bumble_mirror
open_tcp_server_transport: allow explicit sock as input.
When a user doesn't need an exact port, but cares more about getting SOME unused port, they can do: * Create a socket outside with port=None or port=0. * Use socket.getsockname()[1] to get the allocated port and pass to the TCP client somehow. * Use the created socket to create a TCP server transport. Use-case: unit-testing embedded software that implements a BLE host. The controller will be a Bumble controller, connected to the host via a TCP channel. * The host will have a TCP-client HCI transport for testing. * The pytest setup code will allocate the TCP server and pass the port number to the host. Also add some unittests with python mock.
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,8 @@ dist/
|
|||||||
docs/mkdocs/site
|
docs/mkdocs/site
|
||||||
test-results.xml
|
test-results.xml
|
||||||
__pycache__
|
__pycache__
|
||||||
|
# Vim
|
||||||
|
.*.sw*
|
||||||
# generated by setuptools_scm
|
# generated by setuptools_scm
|
||||||
bumble/_version.py
|
bumble/_version.py
|
||||||
.vscode/launch.json
|
.vscode/launch.json
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
import socket
|
||||||
|
|
||||||
from .common import Transport, StreamPacketSource
|
from .common import Transport, StreamPacketSource
|
||||||
|
|
||||||
@@ -28,6 +29,12 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# A pass-through function to ease mock testing.
|
||||||
|
async def _create_server(*args, **kw_args):
|
||||||
|
await asyncio.get_running_loop().create_server(*args, **kw_args)
|
||||||
|
|
||||||
|
|
||||||
async def open_tcp_server_transport(spec: str) -> Transport:
|
async def open_tcp_server_transport(spec: str) -> Transport:
|
||||||
'''
|
'''
|
||||||
Open a TCP server transport.
|
Open a TCP server transport.
|
||||||
@@ -38,7 +45,22 @@ async def open_tcp_server_transport(spec: str) -> Transport:
|
|||||||
|
|
||||||
Example: _:9001
|
Example: _:9001
|
||||||
'''
|
'''
|
||||||
|
local_host, local_port = spec.split(':')
|
||||||
|
return await _open_tcp_server_transport_impl(
|
||||||
|
host=local_host if local_host != '_' else None, port=int(local_port)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def open_tcp_server_transport_with_socket(sock: socket.socket) -> Transport:
|
||||||
|
'''
|
||||||
|
Open a TCP server transport with an existing socket.
|
||||||
|
|
||||||
|
One reason to use this variant is to let python pick an unused port.
|
||||||
|
'''
|
||||||
|
return await _open_tcp_server_transport_impl(sock=sock)
|
||||||
|
|
||||||
|
|
||||||
|
async def _open_tcp_server_transport_impl(**kwargs) -> Transport:
|
||||||
class TcpServerTransport(Transport):
|
class TcpServerTransport(Transport):
|
||||||
async def close(self):
|
async def close(self):
|
||||||
await super().close()
|
await super().close()
|
||||||
@@ -77,13 +99,10 @@ async def open_tcp_server_transport(spec: str) -> Transport:
|
|||||||
else:
|
else:
|
||||||
logger.debug('no client, dropping packet')
|
logger.debug('no client, dropping packet')
|
||||||
|
|
||||||
local_host, local_port = spec.split(':')
|
|
||||||
packet_source = StreamPacketSource()
|
packet_source = StreamPacketSource()
|
||||||
packet_sink = TcpServerPacketSink()
|
packet_sink = TcpServerPacketSink()
|
||||||
await asyncio.get_running_loop().create_server(
|
await _create_server(
|
||||||
lambda: TcpServerProtocol(packet_source, packet_sink),
|
lambda: TcpServerProtocol(packet_source, packet_sink), **kwargs
|
||||||
host=local_host if local_host != '_' else None,
|
|
||||||
port=int(local_port),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return TcpServerTransport(packet_source, packet_sink)
|
return TcpServerTransport(packet_source, packet_sink)
|
||||||
|
|||||||
64
tests/transport_tcp_server_test.py
Normal file
64
tests/transport_tcp_server_test.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
# Copyright 2024 Google LLC
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import socket
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import ANY, patch
|
||||||
|
|
||||||
|
from bumble.transport.tcp_server import (
|
||||||
|
open_tcp_server_transport,
|
||||||
|
open_tcp_server_transport_with_socket,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenTcpServerTransportTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.patcher = patch('bumble.transport.tcp_server._create_server')
|
||||||
|
self.mock_create_server = self.patcher.start()
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.patcher.stop()
|
||||||
|
|
||||||
|
def test_open_with_spec(self):
|
||||||
|
asyncio.run(open_tcp_server_transport('localhost:32100'))
|
||||||
|
self.mock_create_server.assert_awaited_once_with(
|
||||||
|
ANY, host='localhost', port=32100
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_open_with_port_only_spec(self):
|
||||||
|
asyncio.run(open_tcp_server_transport('_:32100'))
|
||||||
|
self.mock_create_server.assert_awaited_once_with(ANY, host=None, port=32100)
|
||||||
|
|
||||||
|
def test_open_with_socket(self):
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
asyncio.run(open_tcp_server_transport_with_socket(sock=sock))
|
||||||
|
self.mock_create_server.assert_awaited_once_with(ANY, sock=sock)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
not os.environ.get('PYTEST_NOSKIP', 0),
|
||||||
|
reason='''\
|
||||||
|
Not hermetic. Should only run manually with
|
||||||
|
$ PYTEST_NOSKIP=1 pytest tests
|
||||||
|
''',
|
||||||
|
)
|
||||||
|
def test_open_with_real_socket():
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.bind(('localhost', 0))
|
||||||
|
port = sock.getsockname()[1]
|
||||||
|
assert port != 0
|
||||||
|
asyncio.run(open_tcp_server_transport_with_socket(sock=sock))
|
||||||
Reference in New Issue
Block a user