From 1a9112262b2a192553bff3ffdfa6d843eebb54fd Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 5 Jul 2018 14:52:43 -0700 Subject: [PATCH] Add delay waiting for socket server to start (#594) Add delay waiting for socket server to start and port to be free --- tests/helpers/debugadapter.py | 60 +++++++++++++++------------ tests/helpers/debugclient.py | 41 ++++++++++++------ tests/system_tests/__init__.py | 49 +++++++++++++++------- tests/system_tests/test_exceptions.py | 6 +-- 4 files changed, 99 insertions(+), 57 deletions(-) diff --git a/tests/helpers/debugadapter.py b/tests/helpers/debugadapter.py index 27f160bb..551a3b61 100644 --- a/tests/helpers/debugadapter.py +++ b/tests/helpers/debugadapter.py @@ -89,6 +89,19 @@ def wait_for_socket_server(addr, timeout=3.0, **kwargs): raise ConnectionRefusedError('Timeout waiting for connection') +def wait_for_port_to_free(port, timeout=3.0): + start_time = time.time() + while True: + try: + sock = socket.create_connection(('localhost', port)) + sock.close() + except Exception: + return + time.sleep(0.1) + if time.time() - start_time > timeout: + raise ConnectionRefusedError('Timeout waiting for port to be free') + + class DebugAdapter(Closeable): VERBOSE = False @@ -105,38 +118,32 @@ class DebugAdapter(Closeable): argv = list(argv) cls._ensure_addr(argv, addr) return Proc.start_python_module( - 'ptvsd', - argv, - env=env_vars, - cwd=cwd, - **kwds - ) + 'ptvsd', argv, env=env_vars, cwd=cwd, **kwds) + return cls._start(new_proc, argv, **kwargs) @classmethod - def start_wrapper_script(cls, filename, argv, env=None, cwd=None, **kwargs): # noqa + def start_wrapper_script(cls, filename, argv, env=None, cwd=None, + **kwargs): # noqa def new_proc(argv, addr, **kwds): env_vars = _copy_env(verbose=cls.VERBOSE, env=env) return Proc.start_python_script( - filename, - argv, - env=env_vars, - cwd=cwd, - **kwds - ) + filename, argv, env=env_vars, cwd=cwd, **kwds) + return cls._start(new_proc, argv, **kwargs) @classmethod - def start_wrapper_module(cls, modulename, argv, env=None, cwd=None, **kwargs): # noqa + def start_wrapper_module(cls, + modulename, + argv, + env=None, + cwd=None, + **kwargs): # noqa def new_proc(argv, addr, **kwds): env_vars = _copy_env(verbose=cls.VERBOSE, env=env) return Proc.start_python_module( - modulename, - argv, - env=env_vars, - cwd=cwd, - **kwds - ) + modulename, argv, env=env_vars, cwd=cwd, **kwds) + return cls._start(new_proc, argv, **kwargs) # specific factory cases @@ -169,7 +176,12 @@ class DebugAdapter(Closeable): return adapter @classmethod - def _start_as(cls, addr, name, kind='script', extra=None, server=False, + def _start_as(cls, + addr, + name, + kind='script', + extra=None, + server=False, **kwargs): argv = [] if server: @@ -192,11 +204,7 @@ class DebugAdapter(Closeable): # TODO: Handle this case somehow? assert 'ptvsd.enable_attach' in content adapter = cls.start_wrapper_script( - filename, - argv=argv, - addr=addr, - **kwargs - ) + filename, argv=argv, addr=addr, **kwargs) wait_for_socket_server(addr, **kwargs) return adapter diff --git a/tests/helpers/debugclient.py b/tests/helpers/debugclient.py index 9f656100..b976a2ce 100644 --- a/tests/helpers/debugclient.py +++ b/tests/helpers/debugclient.py @@ -8,14 +8,15 @@ from . import Closeable from .debugadapter import DebugAdapter, wait_for_socket_server from .debugsession import DebugSession - # TODO: Add a helper function to start a remote debugger for testing # remote debugging? class _LifecycleClient(Closeable): - - def __init__(self, addr=None, port=8888, breakpoints=None, + def __init__(self, + addr=None, + port=8888, + breakpoints=None, connecttimeout=1.0): super(_LifecycleClient, self).__init__() self._addr = Address.from_raw(addr, defaultport=port) @@ -101,12 +102,19 @@ class _LifecycleClient(Closeable): if self._adapter is not None: self._adapter.close() - def _launch(self, argv, script=None, wait_for_connect=None, - detachable=True, env=None, cwd=None, **kwargs): + def _launch(self, + argv, + script=None, + wait_for_connect=None, + detachable=True, + env=None, + cwd=None, + **kwargs): if script is not None: + def start(*args, **kwargs): - return DebugAdapter.start_wrapper_script(script, - *args, **kwargs) + return DebugAdapter.start_wrapper_script( + script, *args, **kwargs) else: start = DebugAdapter.start new_addr = Address.as_server if detachable else Address.as_client @@ -138,7 +146,6 @@ class DebugClient(_LifecycleClient): class EasyDebugClient(DebugClient): - def start_detached(self, argv): """Start an adapter in a background process.""" if self.closed: @@ -151,7 +158,12 @@ class EasyDebugClient(DebugClient): self._adapter = DebugAdapter.start(argv, port=self._port) return self._adapter - def host_local_debugger(self, argv, script=None, env=None, cwd=None, **kwargs): # noqa + def host_local_debugger(self, + argv, + script=None, + env=None, + cwd=None, + **kwargs): # noqa if self.closed: raise RuntimeError('debug client closed') if self._adapter is not None: @@ -161,6 +173,7 @@ class EasyDebugClient(DebugClient): def run(): self._session = DebugSession.create_server(addr, **kwargs) + t = new_hidden_thread( target=run, name='test.client', @@ -172,16 +185,17 @@ class EasyDebugClient(DebugClient): if t.is_alive(): warnings.warn('timed out waiting for connection') if self._session is None: - raise RuntimeError('unable to connect') + raise RuntimeError('unable to connect after {} secs'.format( + self._connecttimeout)) # The adapter will close when the connection does. + self._launch( argv, script=script, wait_for_connect=wait, detachable=False, env=env, - cwd=cwd - ) + cwd=cwd) return self._adapter, self._session @@ -208,7 +222,8 @@ class EasyDebugClient(DebugClient): assert self._session is None argv = [ - '-m', module, + '-m', + module, ] + list(argv) if kwargs.pop('nodebug', False): argv.insert(0, '--nodebug') diff --git a/tests/system_tests/__init__.py b/tests/system_tests/__init__.py index 118458bd..4467d123 100644 --- a/tests/system_tests/__init__.py +++ b/tests/system_tests/__init__.py @@ -2,11 +2,12 @@ import contextlib import os import ptvsd import signal +import time import unittest from collections import namedtuple from ptvsd.socket import Address -from tests.helpers.debugadapter import DebugAdapter +from tests.helpers.debugadapter import DebugAdapter, wait_for_port_to_free from tests.helpers.debugclient import EasyDebugClient as DebugClient from tests.helpers.script import find_line from tests.helpers.threading import get_locked_and_waiter @@ -17,7 +18,7 @@ from tests.helpers.vsc import parse_message, VSCMessages, Response, Event # noq ROOT = os.path.dirname(os.path.dirname(ptvsd.__file__)) PORT = 9876 CONNECT_TIMEOUT = 3.0 - +DELAY_WAITING_FOR_SOCKETS = 1.0 DebugInfo = namedtuple('DebugInfo', 'port starttype argv filename modulename env cwd attachtype') # noqa DebugInfo.__new__.__defaults__ = (9876, 'launch', []) + ((None, ) * (len(DebugInfo._fields) - 3)) # noqa @@ -218,6 +219,7 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): addr = Address('localhost', debug_info.port) cwd = debug_info.cwd env = debug_info.env + wait_for_port_to_free(debug_info.port) def _kill_proc(pid): """If debugger does not end gracefully, then kill proc and @@ -229,6 +231,25 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): import time time.sleep(1) # wait for socket connections to die out. # noqa + def _wrap_and_reraise(ex, session): + messages = [] + try: + messages = [str(msg) for msg in + _strip_newline_output_events(session.received)] + except Exception: + pass + + messages = os.linesep.join(messages) + try: + raise Exception(messages) from ex + except Exception: + print(messages) + raise ex + + def _handle_exception(ex, adapter, session): + _kill_proc(adapter.pid) + _wrap_and_reraise(ex, session) + if debug_info.attachtype == 'import' and \ debug_info.modulename is not None: argv = debug_info.argv @@ -238,13 +259,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): env=env, cwd=cwd) as adapter: with DebugClient() as editor: + time.sleep(DELAY_WAITING_FOR_SOCKETS) session = editor.attach_socket(addr, adapter) try: yield Debugger(session=session, adapter=adapter) adapter.wait() - except Exception: - _kill_proc(adapter.pid) - raise + except Exception as ex: + _handle_exception(ex, adapter, session) elif debug_info.attachtype == 'import' and \ debug_info.starttype == 'attach' and \ debug_info.filename is not None: @@ -256,13 +277,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): env=env, cwd=cwd) as adapter: with DebugClient() as editor: + time.sleep(DELAY_WAITING_FOR_SOCKETS) session = editor.attach_socket(addr, adapter) try: yield Debugger(session=session, adapter=adapter) adapter.wait() - except Exception: - _kill_proc(adapter.pid) - raise + except Exception as ex: + _handle_exception(ex, adapter, session) elif debug_info.starttype == 'attach': if debug_info.modulename is None: name = debug_info.filename @@ -279,13 +300,13 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): env=env, cwd=cwd) as adapter: with DebugClient() as editor: + time.sleep(DELAY_WAITING_FOR_SOCKETS) session = editor.attach_socket(addr, adapter) try: yield Debugger(session=session, adapter=adapter) adapter.wait() - except Exception: - _kill_proc(adapter.pid) - raise + except Exception as ex: + _handle_exception(ex, adapter, session) else: if debug_info.filename is None: argv = ["-m", debug_info.modulename] + debug_info.argv @@ -294,14 +315,14 @@ class LifecycleTestsBase(TestsBase, unittest.TestCase): with DebugClient( port=debug_info.port, connecttimeout=CONNECT_TIMEOUT) as editor: + time.sleep(DELAY_WAITING_FOR_SOCKETS) adapter, session = editor.host_local_debugger( argv, cwd=cwd, env=env) try: yield Debugger(session=session, adapter=adapter) adapter.wait() - except Exception: - _kill_proc(adapter.pid) - raise + except Exception as ex: + _handle_exception(ex, adapter, session) @property def messages(self): diff --git a/tests/system_tests/test_exceptions.py b/tests/system_tests/test_exceptions.py index 932aee4e..c0f130f0 100644 --- a/tests/system_tests/test_exceptions.py +++ b/tests/system_tests/test_exceptions.py @@ -45,6 +45,7 @@ class LaunchExceptionLifecycleTests(LifecycleTestsBase): options = {"debugOptions": ["RedirectOutput"]} with self.start_debugging(debug_info) as dbg: + stopped = dbg.session.get_awaiter_for_event('stopped') (_, req_launch_attach, _, _, _, _) = lifecycle_handshake( dbg.session, debug_info.starttype, @@ -52,10 +53,7 @@ class LaunchExceptionLifecycleTests(LifecycleTestsBase): options=options, threads=True) - req_launch_attach.wait() - - stopped = dbg.session.get_awaiter_for_event('stopped') - stopped.wait() + Awaitable.wait_all(req_launch_attach, stopped) self.assertEqual(stopped.event.body["text"], "ArithmeticError") self.assertIn("ArithmeticError('Hello'", stopped.event.body["description"])