diff options
author | Gregory P. Smith <gps@google.com> | 2021-05-24 15:29:39 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-05-24 15:29:39 -0700 |
commit | c33bdd51b8d2e0a24b05a17b8e9b2192f0977dc8 (patch) | |
tree | 1eb21d5e1153f19e6c3626f251d6644c1dc9450f /src | |
parent | 2f3d53514ea498517ff004b5fa48a3fd37e39f9e (diff) | |
download | portpicker-c33bdd51b8d2e0a24b05a17b8e9b2192f0977dc8.tar.gz |
Fixes to support Python 3.10, require 3.6+ (#24)
Switch from asyncio.coroutine to async def to support 3.10.
https://www.python.org/dev/peps/pep-0492/
Drop the use of the loop= parameter in portserver for 3.10.
Refactor portserver_test to launch a subprocess instead of mock.
This will wind up as 1.4.0.
Use tox for our testing.
Test on 3.10 in CI.
Require 3.6+.
Add a package.sh.
Diffstat (limited to 'src')
-rw-r--r-- | src/portserver.py | 12 | ||||
-rw-r--r-- | src/tests/portserver_test.py | 122 |
2 files changed, 104 insertions, 30 deletions
diff --git a/src/portserver.py b/src/portserver.py index 43f5567..58b7ecd 100644 --- a/src/portserver.py +++ b/src/portserver.py @@ -117,7 +117,7 @@ def _should_allocate_port(pid): return False try: os.kill(pid, 0) - except ProcessLookupError: + except (ProcessLookupError, OverflowError): log.info('Not allocating a port to a non-existent process') return False return True @@ -227,9 +227,8 @@ class _PortServerRequestHandler(object): for port in ports_to_serve: self._port_pool.add_port_to_free_pool(port) - @asyncio.coroutine - def handle_port_request(self, reader, writer): - client_data = yield from reader.read(100) + async def handle_port_request(self, reader, writer): + client_data = await reader.read(100) self._handle_port_request(client_data, writer) writer.close() @@ -241,6 +240,8 @@ class _PortServerRequestHandler(object): writer: The asyncio Writer for the response to be written to. """ try: + if len(client_data) > 20: + raise ValueError('More than 20 characters in "pid".') pid = int(client_data) except ValueError as error: self._client_request_errors += 1 @@ -349,10 +350,11 @@ def main(): event_loop = asyncio.get_event_loop() event_loop.add_signal_handler(signal.SIGUSR1, request_handler.dump_stats) + old_py_loop = {'loop': event_loop} if sys.version_info < (3, 10) else {} coro = asyncio.start_unix_server( request_handler.handle_port_request, path=config.portserver_unix_socket_address.replace('@', '\0', 1), - loop=event_loop) + **old_py_loop) server_address = config.portserver_unix_socket_address server = event_loop.run_until_complete(coro) diff --git a/src/tests/portserver_test.py b/src/tests/portserver_test.py index c87ad82..394b1b5 100644 --- a/src/tests/portserver_test.py +++ b/src/tests/portserver_test.py @@ -16,11 +16,13 @@ # """Tests for the example portserver.""" -from __future__ import print_function import asyncio import os +import signal import socket +import subprocess import sys +import time import unittest from unittest import mock @@ -129,38 +131,108 @@ class PortserverFunctionsTest(unittest.TestCase): portserver._configure_logging(False) portserver._configure_logging(True) + + _test_socket_addr = f'@TST-{os.getpid()}' + @mock.patch.object( sys, 'argv', ['PortserverFunctionsTest.test_main', - '--portserver_unix_socket_address=@TST-%d' % os.getpid()] + f'--portserver_unix_socket_address={_test_socket_addr}'] ) @mock.patch.object(portserver, '_parse_port_ranges') - @mock.patch.object(asyncio, 'get_event_loop') - def test_main(self, *unused_mocks): + def test_main_no_ports(self, *unused_mocks): portserver._parse_port_ranges.return_value = set() with self.assertRaises(SystemExit): portserver.main() - # Give it at least one port and try again. - portserver._parse_port_ranges.return_value = {self.port} - - @asyncio.coroutine - def mock_coroutine_template(*args, **kwargs): - return mock.Mock() - - mock_start_unix_server = mock.Mock(wraps=mock_coroutine_template) - - with mock.patch.object(asyncio, 'start_unix_server', - mock_start_unix_server): - mock_event_loop = mock.Mock(spec=asyncio.base_events.BaseEventLoop) - asyncio.get_event_loop.return_value = mock_event_loop - mock_event_loop.run_forever.side_effect = KeyboardInterrupt - - portserver.main() - - mock_event_loop.run_until_complete.assert_any_call( - mock.ANY) - mock_event_loop.close.assert_called_once_with() - # NOTE: This could be improved. Tests of main() are often gross. + @unittest.skipUnless(sys.executable, 'Requires a stand alone interpreter') + @unittest.skipUnless(hasattr(socket, 'AF_UNIX'), 'AF_UNIX required') + def test_portserver_binary(self): + """Launch python portserver.py and test it.""" + # Blindly assuming tree layout is src/tests/portserver_test.py + # with src/portserver.py. + portserver_py = os.path.join( + os.path.dirname(os.path.dirname(__file__)), + 'portserver.py') + anon_addr = self._test_socket_addr.replace('@', '\0') + + conn = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + with self.assertRaises( + ConnectionRefusedError, + msg=f'{self._test_socket_addr} should not listen yet.'): + conn.connect(anon_addr) + conn.close() + + server = subprocess.Popen( + [sys.executable, portserver_py, + f'--portserver_unix_socket_address={self._test_socket_addr}'], + stderr=subprocess.PIPE, + ) + try: + # Wait a few seconds for the server to start listening. + start_time = time.monotonic() + while True: + time.sleep(0.05) + try: + conn.connect(anon_addr) + conn.close() + except ConnectionRefusedError: + delta = time.monotonic() - start_time + if delta < 4: + continue + else: + server.kill() + self.fail('Failed to connect to portserver ' + f'{self._test_socket_addr} within ' + f'{delta} seconds. STDERR:\n' + + server.stderr.read().decode('utf-8')) + else: + break + + ports = set() + port = portpicker.get_port_from_port_server( + portserver_address=self._test_socket_addr) + ports.add(port) + port = portpicker.get_port_from_port_server( + portserver_address=self._test_socket_addr) + ports.add(port) + + with subprocess.Popen('exit 0', shell=True) as quick_process: + quick_process.wait() + # This process doesn't exist so it should be a denied alloc. + # We use the pid from the above quick_process under the assumption + # that most OSes try to avoid rapid pid recycling. + denied_port = portpicker.get_port_from_port_server( + portserver_address=self._test_socket_addr, + pid=quick_process.pid) # A now unused pid. + self.assertIsNone(denied_port) + + self.assertEqual(len(ports), 2, msg=ports) + + # Check statistics from portserver + server.send_signal(signal.SIGUSR1) + # TODO implement an I/O timeout + for line in server.stderr: + if b'denied-allocations ' in line: + denied_allocations = int( + line.split(b'denied-allocations ', 2)[1]) + self.assertEqual(1, denied_allocations, msg=line) + elif b'total-allocations ' in line: + total_allocations = int( + line.split(b'total-allocations ', 2)[1]) + self.assertEqual(2, total_allocations, msg=line) + break + + rejected_port = portpicker.get_port_from_port_server( + portserver_address=self._test_socket_addr, + pid=99999999999999999999999999999999999) # Out of range. + self.assertIsNone(rejected_port) + + # Done. shutdown gracefully. + server.send_signal(signal.SIGINT) + server.communicate(timeout=2) + finally: + server.kill() + server.wait() class PortPoolTest(unittest.TestCase): |