aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGregory P. Smith <gps@google.com>2021-05-24 15:29:39 -0700
committerGitHub <noreply@github.com>2021-05-24 15:29:39 -0700
commitc33bdd51b8d2e0a24b05a17b8e9b2192f0977dc8 (patch)
tree1eb21d5e1153f19e6c3626f251d6644c1dc9450f /src
parent2f3d53514ea498517ff004b5fa48a3fd37e39f9e (diff)
downloadportpicker-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.py12
-rw-r--r--src/tests/portserver_test.py122
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):