From 29fa1e123fa78eda3da6085db14f4e212384ade4 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Thu, 22 Feb 2018 00:46:56 +0000 Subject: [PATCH] Expand the testing helpers. --- tests/helpers/protocol.py | 21 +-- tests/ptvsd/highlevel/__init__.py | 179 +++++++++++++++++++++---- tests/ptvsd/highlevel/test_messages.py | 35 +++-- 3 files changed, 187 insertions(+), 48 deletions(-) diff --git a/tests/helpers/protocol.py b/tests/helpers/protocol.py index 102ec481..cdc43389 100644 --- a/tests/helpers/protocol.py +++ b/tests/helpers/protocol.py @@ -148,16 +148,9 @@ class Daemon(object): self._handlers.append(entry) return handler - def reset(self, force=False): + def reset(self, *initial, **kwargs): """Clear the recorded messages.""" - if self._failures: - raise RuntimeError('have failures ({!r})'.format(self._failures)) - if self._handlers: - if force: - self._handlers = [] - else: - raise RuntimeError('have pending handlers') - self._received = [] + self._reset(initial, **kwargs) # internal methods @@ -242,3 +235,13 @@ class Daemon(object): #if self._listener.is_alive(): # raise RuntimeError('timed out') self._listener = None + + def _reset(self, initial, force=False): + if self._failures: + raise RuntimeError('have failures ({!r})'.format(self._failures)) + if self._handlers: + if force: + self._handlers = [] + else: + raise RuntimeError('have pending handlers') + self._received = list(self._protocol.parse_each(initial)) diff --git a/tests/ptvsd/highlevel/__init__.py b/tests/ptvsd/highlevel/__init__.py index 51c209a0..4e1aefb5 100644 --- a/tests/ptvsd/highlevel/__init__.py +++ b/tests/ptvsd/highlevel/__init__.py @@ -4,6 +4,8 @@ import platform from _pydevd_bundle.pydevd_comm import ( CMD_VERSION, + CMD_LIST_THREADS, + CMD_THREAD_SUSPEND, ) from tests.helpers.pydevd import FakePyDevd @@ -49,6 +51,24 @@ class PyDevdMessages(object): msg = (cmdid, seq, text) return self.protocol.parse(msg) + def format_threads(self, *threads): + text = '' + for thread in threads: # (tid, tname) + text += ''.format(*thread) + text += '' + return text + + def format_frames(self, thread, reason, *frames): + tid, _ = thread # (tid, tname) + text = '' + text += ''.format(tid, reason) + fmt = '' + for frame in frames: # (fid, func, filename, line) + text += fmt.format(*frame) + text += '' + text += '' + return text + class VSCMessages(object): @@ -83,7 +103,7 @@ class VSCMessages(object): def new_failure(self, req, err, seq=None, **body): """Return a new VSC response message.""" - return self._new_response(req, err, body) + return self._new_response(req, err, body=body) def _new_response(self, req, err=None, seq=None, body=None): if seq is None: @@ -133,11 +153,13 @@ class VSCLifecycle(object): def launch(self, **kwargs): """Initialize the debugger protocol and then launch.""" - self._handshake('launch', **kwargs) + with self._fix.hidden(): + self._handshake('launch', **kwargs) def attach(self, **kwargs): """Initialize the debugger protocol and then attach.""" - self._handshake('attach', **kwargs) + with self._fix.hidden(): + self._handshake('attach', **kwargs) def disconnect(self, **reqargs): self._send_request('disconnect', reqargs) @@ -156,26 +178,29 @@ class VSCLifecycle(object): start() yield - def _handshake(self, command, config=None, reset=True, **kwargs): + def _handshake(self, command, threads=None, config=None, + default_threads=True, reset=True, + **kwargs): initargs = dict( kwargs.pop('initargs', None) or {}, disconnect=kwargs.pop('disconnect', True), ) - with self._fix.vsc.wait_for_event('initialized'): + with self._fix.wait_for_event('initialized'): self._initialize(**initargs) - self._send_request(command, **kwargs) - next(self._fix.vsc_msgs.event_seq) + self._fix.send_request(command, **kwargs) + + self._fix.set_threads(*threads or (), + **dict(default_threads=default_threads)) self._handle_config(**config or {}) - self._send_request('configurationDone') - next(self._fix.vsc_msgs.event_seq) + with self._fix.wait_for_event('process'): + self._fix.send_request('configurationDone') next(self._fix.debugger_msgs.request_seq) # CMD_RUN - assert self._fix.vsc.failures == [], self._fix.vsc.failures - assert self._fix.debugger.failures == [], self._fix.debugger.failures if reset: - self._fix.vsc.reset() - self._fix.debugger.reset() + self._fix.reset() + else: + self._fix.assert_no_failures() def _initialize(self, **reqargs): """ @@ -184,8 +209,8 @@ class VSCLifecycle(object): def handle_response(resp, _): self._capabilities = resp.data['body'] version = self._fix.debugger.VERSION - self._set_debugger_response(CMD_VERSION, version) - self._send_request( + self._fix.set_debugger_response(CMD_VERSION, version) + self._fix.send_request( 'initialize', dict(self.MIN_INITIALIZE_ARGS, **reqargs), handle_response, @@ -193,12 +218,12 @@ class VSCLifecycle(object): def _handle_config(self, breakpoints=None, excbreakpoints=None): if breakpoints: - self._send_request( + self._fix.send_request( 'setBreakpoints', self._parse_breakpoints(breakpoints), ) if excbreakpoints: - self._send_request( + self._fix.send_request( 'setExceptionBreakpoints', self._parse_breakpoints(excbreakpoints), ) @@ -207,14 +232,6 @@ class VSCLifecycle(object): for bp in breakpoints or (): raise NotImplementedError - def _send_request(self, *args, **kwargs): - self._fix.send_request(*args, **kwargs) - next(self._fix.vsc_msgs.response_seq) - - def _set_debugger_response(self, *args, **kwargs): - self._fix.set_debugger_response(*args, **kwargs) - next(self._fix.debugger_msgs.request_seq) - class HighlevelFixture(object): @@ -232,6 +249,8 @@ class HighlevelFixture(object): self.vsc_msgs = VSCMessages() self.debugger_msgs = PyDevdMessages() + self._hidden = False + @property def vsc(self): try: @@ -256,6 +275,18 @@ class HighlevelFixture(object): self._lifecycle = VSCLifecycle(self) return self._lifecycle + @contextlib.contextmanager + def hidden(self): + vsc = self.vsc.received + debugger = self.debugger.received + self._hidden = True + try: + yield + finally: + self._hidden = False + self.vsc.reset(*vsc) + self.debugger.reset(*debugger) + def new_fake(self, debugger=None, handler=None): """Return a new fake VSC that may be used in tests.""" if debugger is None: @@ -264,13 +295,87 @@ class HighlevelFixture(object): return vsc, debugger def send_request(self, command, args=None, handle_response=None): - req = self.vsc_msgs.new_request(command, **args or {}) - with self.vsc.wait_for_response(req, handler=handle_response): + kwargs = dict(args or {}, handler=handle_response) + with self._wait_for_response(command, **kwargs) as req: self.vsc.send_request(req) return req + @contextlib.contextmanager + def _wait_for_response(self, command, *args, **kwargs): + handler = kwargs.pop('handler', None) + req = self.vsc_msgs.new_request(command, *args, **kwargs) + with self.vsc.wait_for_response(req, handler=handler): + yield req + if self._hidden: + next(self.vsc_msgs.response_seq) + + @contextlib.contextmanager + def wait_for_event(self, event, *args, **kwargs): + with self.vsc.wait_for_event(event, *args, **kwargs): + yield + if self._hidden: + next(self.vsc_msgs.event_seq) + def set_debugger_response(self, cmdid, payload): self.debugger.add_pending_response(cmdid, payload) + if self._hidden: + next(self.debugger_msgs.request_seq) + + def send_debugger_event(self, cmdid, payload): + event = self.debugger_msgs.new_event(cmdid, payload) + self.debugger.send_event(event) + + def set_threads(self, *threads, **kwargs): + return self._set_threads(threads, **kwargs) + + def _set_threads(self, threads, default_threads=True): + request = {t[1]: t for t in threads} + response = {t: None for t in threads} + if default_threads: + threads = self._add_default_threads(threads) + text = self.debugger_msgs.format_threads(*threads) + self.set_debugger_response(CMD_LIST_THREADS, text) + self.send_request('threads') + + for tinfo in self.vsc.received[-1].data['body']['threads']: + try: + thread = request[tinfo['name']] + except KeyError: + continue + response[thread] = tinfo['id'] + return response + + def _add_default_threads(self, threads): + defaults = { + 'MainThread', + 'ptvsd.Server', + 'pydevd.thread1', + 'pydevd.thread2', + } + seen = set() + for thread in threads: + tid, tname = thread + seen.add(tid) + if tname in defaults: + defaults.remove(tname) + ids = (id for id in itertools.count(1) if id not in seen) + allthreads = [] + for tname in defaults: + tid = next(ids) + thread = tid, tname + allthreads.append(thread) + allthreads.extend(threads) + return allthreads + + def suspend(self, thread, reason, *stack): + with self.wait_for_event('stopped'): + self.send_debugger_event( + CMD_THREAD_SUSPEND, + self.debugger_msgs.format_frames(thread, reason, *stack), + ) + + #def set_variables(self, ...): + # ... @contextlib.contextmanager def disconnect_when_done(self): @@ -278,7 +383,15 @@ class HighlevelFixture(object): yield finally: self.send_request('disconnect') - #self.vsc._received.pop(-1) + + def assert_no_failures(self): + assert self.vsc.failures == [], self.vsc.failures + assert self.debugger.failures == [], self.debugger.failures + + def reset(self): + self.assert_no_failures() + self.vsc.reset() + self.debugger.reset() class HighlevelTest(object): @@ -324,6 +437,16 @@ class HighlevelTest(object): expected = list(self.vsc.protocol.parse_each(expected)) self.assertEqual(received, expected) + def assert_vsc_failure(self, received, expected, req): + received = list(self.vsc.protocol.parse_each(received)) + expected = list(self.vsc.protocol.parse_each(expected)) + self.assertEqual(received[:-1], expected) + + failure = received[-1] + expected = self.vsc.protocol.parse( + self.fix.vsc_msgs.new_failure(req, failure.data['message'])) + self.assertEqual(failure, expected) + def assert_received(self, daemon, expected): """Ensure that the received messages match the expected ones.""" received = list(daemon.protocol.parse_each(daemon.received)) diff --git a/tests/ptvsd/highlevel/test_messages.py b/tests/ptvsd/highlevel/test_messages.py index d9dc8898..cc1fe3d1 100644 --- a/tests/ptvsd/highlevel/test_messages.py +++ b/tests/ptvsd/highlevel/test_messages.py @@ -115,8 +115,8 @@ class InitializeTests(LifecycleTest, unittest.TestCase): self.new_event(1, 'initialized'), ]) self.assert_received(self.debugger, [ - self.debugger_msgs.new_request(CMD_VERSION, - *['1.1', OS_ID, 'ID']), + self.new_debugger_request(CMD_VERSION, + *['1.1', OS_ID, 'ID']), ]) @@ -129,8 +129,8 @@ class NormalRequestTest(RunningTest): PYDEVD_REQ = None PYDEVD_CMD = None - def launched(self, port=8888): - return super(NormalRequestTest, self).launched(port) + def launched(self, port=8888, **kwargs): + return super(NormalRequestTest, self).launched(port, **kwargs) def set_debugger_response(self, *args, **kwargs): if self.PYDEVD_REQ is None: @@ -145,6 +145,7 @@ class NormalRequestTest(RunningTest): def send_request(self, **args): self.req = self.fix.send_request(self.COMMAND, args) + return self.req def expected_response(self, **body): return self.new_response( @@ -165,14 +166,10 @@ class ThreadsTests(NormalRequestTest, unittest.TestCase): PYDEVD_REQ = CMD_LIST_THREADS def pydevd_payload(self, *threads): - text = '' - for tid, tname in threads: - text += ''.format(tname, tid) - text += '' - return text + return self.debugger_msgs.format_threads(*threads) - def test_basic(self): - with self.launched(): + def test_few(self): + with self.launched(default_threads=False): self.set_debugger_response( (10, 'spam'), (11, 'pydevd.eggs'), @@ -195,6 +192,22 @@ class ThreadsTests(NormalRequestTest, unittest.TestCase): self.expected_pydevd_request(), ]) + def test_none(self): + with self.launched(default_threads=False): + self.set_debugger_response() + self.send_request() + received = self.vsc.received + + self.assert_vsc_received(received, [ + self.expected_response( + threads=[], + ), + # no events + ]) + self.assert_received(self.debugger, [ + self.expected_pydevd_request(), + ]) + # TODO: finish! @unittest.skip('not finished')