diff --git a/Makefile b/Makefile index f20be6c34..fa3f7948c 100644 --- a/Makefile +++ b/Makefile @@ -46,7 +46,7 @@ ci-lint: depends lint ci-test: depends # For now we use --quickpy2. $(PYTHON) -m tests -v --full --no-network --quick-py2 - $(PYTHON) setup.py test + $(PYTHON) -m pytest -vv .PHONY: ci-coverage ci-coverage: depends diff --git a/ptvsd/messaging.py b/ptvsd/messaging.py index 0a7915f8c..4d0dd5824 100644 --- a/ptvsd/messaging.py +++ b/ptvsd/messaging.py @@ -262,8 +262,6 @@ class JsonMessageChannel(object): def __init__(self, stream, handlers=None, name='vsc_messaging'): self.stream = stream - self.send_callback = lambda channel, message: None - self.receive_callback = lambda channel, message: None self._lock = threading.Lock() self._stop = threading.Event() self._seq_iter = itertools.count(1) @@ -293,7 +291,6 @@ def _send_message(self, type, rest={}): with self._lock: yield seq self.stream.write_json(message) - self.send_callback(self, message) def send_request(self, command, arguments=None): d = {'command': command} @@ -327,7 +324,6 @@ def _send_response(self, request_seq, success, command, error_message, body): pass def on_message(self, message): - self.receive_callback(self, message) seq = message['seq'] typ = message['type'] if typ == 'request': @@ -359,7 +355,7 @@ def on_request(self, seq, command, arguments): request = Request(self, seq, command, arguments) try: response_body = handler(request) - except Exception as ex: + except RequestFailure as ex: self._send_response(seq, False, command, str(ex), None) else: if isinstance(response_body, Exception): @@ -387,6 +383,14 @@ def on_response(self, seq, request_seq, success, command, error_message, body): body = RequestFailure(error_message) return request._handle_response(seq, command, body) + def on_disconnect(self): + # There's no more incoming messages, so any requests that are still pending + # must be marked as failed to unblock anyone waiting on them. + with self._lock: + for request in self._requests.values(): + request._handle_response(None, request.command, EOFError('No response')) + getattr(self._handlers, 'disconnect', lambda: None)() + def _process_incoming_messages(self): try: while True: @@ -401,17 +405,17 @@ def _process_incoming_messages(self): traceback.print_exc(file=sys.__stderr__) raise finally: - # There's no more incoming messages, so any requests that are still pending - # must be marked as failed to unblock anyone waiting on them. - with self._lock: - for request in self._requests.values(): - request._handle_response(None, request.command, EOFError('No response')) - + try: + self.on_disconnect() + except Exception: + print('Error while processing disconnect', file=sys.__stderr__) + traceback.print_exc(file=sys.__stderr__) + raise class MessageHandlers(object): """A simple delegating message handlers object for use with JsonMessageChannel. For every argument provided, the object has an attribute with the corresponding - name and value. Example: + name and value. """ def __init__(self, **kwargs): diff --git a/ptvsd/multiproc.py b/ptvsd/multiproc.py index 28a0909c6..e2bc048c0 100644 --- a/ptvsd/multiproc.py +++ b/ptvsd/multiproc.py @@ -8,8 +8,10 @@ import itertools import os import re +import signal import socket import sys +import threading import time import traceback @@ -27,8 +29,15 @@ from _pydevd_bundle.pydevd_comm import get_global_debugger +subprocess_lock = threading.Lock() + subprocess_listener_socket = None +subprocesses = {} +"""List of known subprocesses. Keys are process IDs, values are JsonMessageChannel +instances; subprocess_lock must be used to synchronize access. +""" + subprocess_queue = queue.Queue() """A queue of incoming 'ptvsd_subprocess' notifications. Whenenever a new request is received, a tuple of (subprocess_request, subprocess_response) is placed in the @@ -66,6 +75,7 @@ def listen_for_subprocesses(): subprocess_listener_socket = create_server('localhost', 0) atexit.register(stop_listening_for_subprocesses) + atexit.register(kill_subprocesses) new_hidden_thread('SubprocessListener', _subprocess_listener).start() @@ -80,6 +90,18 @@ def stop_listening_for_subprocesses(): subprocess_listener_socket = None +def kill_subprocesses(): + with subprocess_lock: + pids = list(subprocesses.keys()) + for pid in pids: + with subprocess_lock: + subprocesses.pop(pid, None) + try: + os.kill(pid, signal.SIGTERM) + except Exception: + pass + + def subprocess_listener_port(): if subprocess_listener_socket is None: return None @@ -100,6 +122,8 @@ def _subprocess_listener(): def _handle_subprocess(n, stream): class Handlers(object): + _pid = None + def ptvsd_subprocess_request(self, request): # When child process is spawned, the notification it sends only # contains information about itself and its immediate parent. @@ -110,12 +134,21 @@ def ptvsd_subprocess_request(self, request): 'rootStartRequest': root_start_request, }) + self._pid = arguments['processId'] + with subprocess_lock: + subprocesses[self._pid] = channel + debug('ptvsd_subprocess: %r' % arguments) response = {'incomingConnection': False} subprocess_queue.put((arguments, response)) subprocess_queue.join() return response + def disconnect(self): + if self._pid is not None: + with subprocess_lock: + subprocesses.pop(self._pid, None) + name = 'SubprocessListener-%d' % n channel = JsonMessageChannel(stream, Handlers(), name) channel.start() @@ -151,6 +184,10 @@ def notify_root(port): traceback.print_exc() sys.exit(0) + # Keep the channel open until we exit - root process uses open channels to keep + # track of which subprocesses are alive and which are not. + atexit.register(lambda: channel.close()) + if not response['incomingConnection']: debugger = get_global_debugger() while debugger is None: diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index bc57f11df..bdebb7483 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -1154,6 +1154,8 @@ def on_configurationDone(self, request, args): self._notify_ready() def on_disconnect(self, request, args): + multiproc.kill_subprocesses() + debugger_attached.clear() self._restart_debugger = args.get('restart', False) diff --git a/pytests/conftest.py b/pytests/conftest.py index 00c1a8ffe..f6d697f82 100644 --- a/pytests/conftest.py +++ b/pytests/conftest.py @@ -11,6 +11,7 @@ import types from . import helpers +from .helpers.printer import wait_for_output from .helpers.session import DebugSession @@ -29,9 +30,11 @@ def pytest_runtest_makereport(item, call): @pytest.hookimpl(hookwrapper=True, tryfirst=True) def pytest_pyfunc_call(pyfuncitem): - # Resets the timestamp zero for every new test. + # Resets the timestamp to zero for every new test, and ensures that + # all output is printed after the test. helpers.timestamp_zero = helpers.clock() yield + wait_for_output() @pytest.fixture diff --git a/pytests/func/test_multiproc.py b/pytests/func/test_multiproc.py index c255a1769..45afa26a9 100644 --- a/pytests/func/test_multiproc.py +++ b/pytests/func/test_multiproc.py @@ -10,7 +10,7 @@ from pytests.helpers.pattern import ANY from pytests.helpers.session import DebugSession -from pytests.helpers.timeline import Event, Request +from pytests.helpers.timeline import Event, Request, Response @pytest.mark.timeout(60) @@ -137,6 +137,7 @@ def child(q): assert debug_session.read_json() == 'done' + @pytest.mark.timeout(60) @pytest.mark.skipif(sys.version_info < (3, 0) and (platform.system() != 'Windows'), reason='Bug #935') @@ -152,8 +153,7 @@ def parent(): import os import subprocess import sys - argv = [sys.executable] - argv += [sys.argv[1], '--arg1', '--arg2', '--arg3'] + argv = [sys.executable, sys.argv[1], '--arg1', '--arg2', '--arg3'] env = os.environ.copy() process = subprocess.Popen(argv, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) process.wait() @@ -180,10 +180,11 @@ def parent(): 'arguments': root_start_request.arguments, } }) + child_pid = child_subprocess.body['processId'] child_port = child_subprocess.body['port'] debug_session.proceed() - child_session = DebugSession(method='attach_socket', ptvsd_port=child_port) + child_session = DebugSession(method='attach_socket', ptvsd_port=child_port, pid=child_pid) child_session.ignore_unobserved = debug_session.ignore_unobserved child_session.connect() child_session.handshake() @@ -192,4 +193,56 @@ def parent(): child_argv = debug_session.read_json() assert child_argv == [child, '--arg1', '--arg2', '--arg3'] + child_session.wait_for_exit() + debug_session.wait_for_exit() + + +@pytest.mark.timeout(60) +@pytest.mark.skipif(sys.version_info < (3, 0) and (platform.system() != 'Windows'), + reason='Bug #935') +def test_autokill(debug_session, pyfile): + @pyfile + def child(): + while True: + pass + + @pyfile + def parent(): + import backchannel + import os + import subprocess + import sys + argv = [sys.executable, sys.argv[1]] + env = os.environ.copy() + subprocess.Popen(argv, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + backchannel.read_json() + + debug_session.multiprocess = True + debug_session.program_args += [child] + debug_session.prepare_to_run(filename=parent, backchannel=True) + debug_session.start_debugging() + + child_subprocess = debug_session.wait_for_next(Event('ptvsd_subprocess')) + child_pid = child_subprocess.body['processId'] + child_port = child_subprocess.body['port'] + + debug_session.proceed() + + child_session = DebugSession(method='attach_socket', ptvsd_port=child_port, pid=child_pid) + child_session.expected_returncode = ANY + child_session.connect() + child_session.handshake() + child_session.start_debugging() + + if debug_session.method == 'launch': + # In launch scenario, terminate the parent process by disconnecting from it. + debug_session.expected_returncode = ANY + disconnect = debug_session.send_request('disconnect', {}) + debug_session.wait_for_next(Response(disconnect)) + else: + # In attach scenario, just let the parent process run to completion. + debug_session.expected_returncode = 0 + debug_session.write_json(None) + debug_session.wait_for_exit() + child_session.wait_for_exit() diff --git a/pytests/helpers/__init__.py b/pytests/helpers/__init__.py index 34e521cf2..2b15ffd15 100644 --- a/pytests/helpers/__init__.py +++ b/pytests/helpers/__init__.py @@ -23,24 +23,6 @@ def timestamp(): return clock() - timestamp_zero -print_lock = threading.Lock() -real_print = print - -def print(*args, **kwargs): - """Like builtin print(), but synchronized using a global lock, - and adds a timestamp - """ - from . import colors - timestamped = kwargs.pop('timestamped', True) - with print_lock: - if timestamped: - t = timestamp() - real_print(colors.LIGHT_BLACK, end='') - real_print('@%09.6f: ' % t, end='') - real_print(colors.RESET, end='') - real_print(*args, **kwargs) - - def dump_stacks(): """Dump the stacks of all threads except the current thread""" current_ident = threading.current_thread().ident @@ -72,3 +54,6 @@ def dumper(): thread = threading.Thread(target=dumper) thread.daemon = True thread.start() + + +from .printer import print diff --git a/pytests/helpers/colors.py b/pytests/helpers/colors.py index 38a80bbe5..294339219 100644 --- a/pytests/helpers/colors.py +++ b/pytests/helpers/colors.py @@ -4,14 +4,15 @@ from __future__ import print_function, with_statement, absolute_import -import platform - -if platform.system() != 'Linux': - # pytest-timeout seems to be buggy wrt colorama when capturing output. - # - # TODO: re-enable after enabling proper ANSI sequence handling: +if True: + # On Win32, colorama is not active when pytest-timeout dumps captured output + # on timeout, and ANSI sequences aren't properly interpreted. + # TODO: re-enable on Windows after enabling proper ANSI sequence handling: # https://docs.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences + # + # Azure Pipelines doesn't support ANSI sequences at all. + # TODO: re-enable on all platforms after adding Azure Pipelines detection. RESET = '' BLACK = '' diff --git a/pytests/helpers/printer.py b/pytests/helpers/printer.py new file mode 100644 index 000000000..73a9b32f4 --- /dev/null +++ b/pytests/helpers/printer.py @@ -0,0 +1,43 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See LICENSE in the project root +# for license information. + +from __future__ import print_function, with_statement, absolute_import + +__all__ = ['print', 'wait_for_output'] + +import threading +from ptvsd.compat import queue +from pytests.helpers import timestamp, colors + + +real_print = print +print_queue = queue.Queue() + + +def print(*args, **kwargs): + """Like builtin print(), but synchronized across multiple threads, + and adds a timestamp. + """ + timestamped = kwargs.pop('timestamped', True) + t = timestamp() if timestamped else None + print_queue.put((t, args, kwargs)) + + +def wait_for_output(): + print_queue.join() + + +def print_worker(): + while True: + t, args, kwargs = print_queue.get() + if t is not None: + t = colors.LIGHT_BLACK + ('@%09.6f:' % t) + colors.RESET + args = (t,) + args + real_print(*args, **kwargs) + print_queue.task_done() + + +print_thread = threading.Thread(target=print_worker, name='printer') +print_thread.daemon = True +print_thread.start() diff --git a/pytests/helpers/session.py b/pytests/helpers/session.py index acae16af3..f30873f1d 100644 --- a/pytests/helpers/session.py +++ b/pytests/helpers/session.py @@ -21,6 +21,7 @@ from . import colors, debuggee, print, watchdog from .messaging import LoggingJsonStream from .pattern import ANY +from .printer import wait_for_output from .timeline import Timeline, Event, Response from collections import namedtuple @@ -214,6 +215,8 @@ def wait_for_disconnect(self, close=True): if close: self.timeline.close() + wait_for_output() + def wait_for_termination(self): print(colors.LIGHT_MAGENTA + 'Waiting for ptvsd#%d to terminate' % self.ptvsd_port + colors.RESET) @@ -230,6 +233,7 @@ def wait_for_termination(self): self.expect_realized(Event('exited') >> Event('terminated', {})) self.timeline.close() + wait_for_output() def wait_for_exit(self): """Waits for the spawned ptvsd process to exit. If it doesn't exit within @@ -237,6 +241,9 @@ def wait_for_exit(self): exits, validates its return code to match expected_returncode. """ + if not self.is_running: + return + assert self.psutil_process is not None def kill(): diff --git a/pytests/helpers/timeline.py b/pytests/helpers/timeline.py index a0f9c636a..cb72e4d8e 100644 --- a/pytests/helpers/timeline.py +++ b/pytests/helpers/timeline.py @@ -201,36 +201,36 @@ def has_been_realized(occ): # First time wait_until() calls us, we have to check the whole timeline. # On subsequent calls, we only need to check the newly added occurrence. if check_past: - if explain: - print( - colors.LIGHT_MAGENTA + 'Testing ' + colors.RESET + - colors.color_repr(expectation) + - colors.LIGHT_MAGENTA + ' against timeline up to and including ' + colors.RESET + - colors.color_repr(occ) - ) + # if explain: + # print( + # colors.LIGHT_MAGENTA + 'Testing ' + colors.RESET + + # colors.color_repr(expectation) + + # colors.LIGHT_MAGENTA + ' against timeline up to and including ' + colors.RESET + + # colors.color_repr(occ) + # ) reasons.update(expectation.test_at_or_before(occ) or {}) del check_past[:] else: - if explain: - print( - colors.LIGHT_MAGENTA + 'Testing ' + colors.RESET + - colors.color_repr(expectation) + - colors.LIGHT_MAGENTA + ' against ' + colors.RESET + - colors.color_repr(occ) - ) + # if explain: + # print( + # colors.LIGHT_MAGENTA + 'Testing ' + colors.RESET + + # colors.color_repr(expectation) + + # colors.LIGHT_MAGENTA + ' against ' + colors.RESET + + # colors.color_repr(occ) + # ) reasons.update(expectation.test_at(occ) or {}) if reasons: if observe: self.expect_realized(expectation, explain=explain, observe=observe) return True - else: - if explain: - print( - colors.color_repr(occ) + - colors.LIGHT_MAGENTA + ' did not realize ' + colors.RESET + - colors.color_repr(expectation) - ) + # else: + # if explain: + # print( + # colors.color_repr(occ) + + # colors.LIGHT_MAGENTA + ' did not realize ' + colors.RESET + + # colors.color_repr(expectation) + # ) self.wait_until(has_been_realized, freeze) diff --git a/pytests/test_messaging.py b/pytests/test_messaging.py index 56b2765ff..4cf275bc0 100644 --- a/pytests/test_messaging.py +++ b/pytests/test_messaging.py @@ -135,7 +135,7 @@ def request(self, request): def pause_request(self, request): assert request.command == 'pause' requests_received.append((request.channel, request.arguments)) - raise RuntimeError('pause error') + raise RequestFailure('pause error') input, input_exhausted = self.iter_with_event(REQUESTS) output = [] diff --git a/tests/highlevel/test_live_pydevd.py b/tests/highlevel/test_live_pydevd.py deleted file mode 100644 index b26657e0e..000000000 --- a/tests/highlevel/test_live_pydevd.py +++ /dev/null @@ -1,525 +0,0 @@ -# -*- coding: utf-8 -*- - -import contextlib -import os -import sys -from textwrap import dedent -import time -import traceback -import unittest - -import ptvsd -from ptvsd.wrapper import INITIALIZE_RESPONSE # noqa -from tests.helpers._io import captured_stdio -from tests.helpers.pydevd._live import LivePyDevd -from tests.helpers.script import set_lock, find_line -from tests.helpers.workspace import Workspace, PathEntry - -from . import ( - VSCFixture, - VSCTest, -) - - -class Fixture(VSCFixture): - def __init__(self, source, new_fake=None): - self._pydevd = LivePyDevd(source) - super(Fixture, self).__init__( - new_fake=new_fake, - start_adapter=self._pydevd.start, - ) - - @property - def _proc(self): - return self._pydevd.binder.ptvsd.proc - - @property - def binder(self): - return self._pydevd.binder - - @property - def thread(self): - return self._pydevd.thread - - def install_sig_handler(self): - self._pydevd._ptvsd.install_sig_handler() - - -class TestBase(VSCTest): - - FIXTURE = Fixture - - FILENAME = None - SOURCE = '' - - def setUp(self): - super(TestBase, self).setUp() - self._pathentry = PathEntry() - # Tests run a new thread in the same process and use that - # thread as the main thread -- the current thread which is - # managing the test should be invisible to the debugger. - import threading - threading.current_thread().is_pydev_daemon_thread = True - - self._filename = None - if self.FILENAME is not None: - self.set_source_file(self.FILENAME, self.SOURCE) - - def tearDown(self): - super(TestBase, self).tearDown() - self._pathentry.cleanup() - - @property - def pathentry(self): - return self._pathentry - - @property - def workspace(self): - try: - return vars(self)['_workspace'] - #return self._workspace - except KeyError: - self._workspace = Workspace() - self.addCleanup(self._workspace.cleanup) - return self._workspace - - @property - def filename(self): - return None if self._filename is None else self._filePath - - def _new_fixture(self, new_daemon): - self.assertIsNotNone(self._filename) - return self.FIXTURE(self._filename, new_daemon) - - def set_source_file(self, filename, content=None): - self.assertIsNone(self._fix) - if content is not None: - filename = self.pathentry.write(filename, content=content) - self.pathentry.install() - self._filePath = filename - self._filename = 'file:' + filename - - def set_module(self, name, content=None): - self.assertIsNone(self._fix) - if content is not None: - self.write_module(name, content) - self.pathentry.install() - self._filename = 'module:' + name - - -################################## -# lifecycle tests - - -class LifecycleTests(TestBase, unittest.TestCase): - - FILENAME = 'spam.py' - - # Give some time for thread notification to arrive before finishing. - SOURCE = 'import time;time.sleep(.5)' - - @contextlib.contextmanager - def running(self): - addr = (None, 8888) - with self.fake.start(addr): - #with self.fix.install_sig_handler(): - yield - - def test_launch(self): - addr = (None, 8888) - with self.fake.start(addr): - with self.vsc.wait_for_event('initialized'): - # initialize - req_initialize = self.send_request('initialize', { - 'adapterID': 'spam', - }) - - # attach - req_attach = self.send_request('attach') - - # configuration - with self.vsc.wait_for_event('thread'): - req_config = self.send_request('configurationDone') - - # Normal ops would go here. - - # end - with self.wait_for_events(['exited', 'terminated']): - self.fix.binder.done() - # TODO: Send a "disconnect" request? - self.fix.binder.wait_until_done() - received = self.vsc.received - - self.assert_vsc_received( - received, - [ - self.new_event( - 'output', - category='telemetry', - output='ptvsd', - data={'version': ptvsd.__version__}), - self.new_response(req_initialize, **INITIALIZE_RESPONSE), - self.new_event('initialized'), - self.new_response(req_attach), - self.new_response(req_config), - self.new_event( - 'process', - **dict( - name=sys.argv[0], - systemProcessId=os.getpid(), - isLocalProcess=True, - startMethod='attach', - )), - self.new_event('thread', reason='started', threadId=1), - self.new_event('thread', reason='exited', threadId=1), - #self.new_event('exited', exitCode=0), - #self.new_event('terminated'), - ]) - - -################################## -# "normal operation" tests - - -class VSCFlowTest(TestBase): - @contextlib.contextmanager - def launched(self, port=8888, **kwargs): - kwargs.setdefault('process', False) - kwargs.setdefault('disconnect', False) - with self.lifecycle.launched(port=port, hide=True, **kwargs): - yield - self.fix.binder.done(close=False) - - try: - self.fix.binder.wait_until_done() - except Exception: - formatted_ex = traceback.format_exc() - if hasattr(self, 'vsc') and hasattr(self.vsc, 'received'): - message = """ - Session Messages: - ----------------- - {} - - Original Error: - --------------- - {}""".format(os.linesep.join(self.vsc.received), formatted_ex) - raise Exception(message) - else: - raise - - -class BreakpointTests(VSCFlowTest, unittest.TestCase): - - FILENAME = 'spam.py' - SOURCE = dedent(""" - from __future__ import print_function - - class MyError(RuntimeError): - pass - - #class Counter(object): - # def __init__(self, start=0): - # self._next = start - # def __repr__(self): - # return '{}(start={})'.format(type(self).__name__, self._next) - # def __int__(self): - # return self._next - 1 - # __index__ = __int__ - # def __iter__(self): - # return self - # def __next__(self): - # value = self._next - # self._next += 1 - # def peek(self): - # return self._next - # def inc(self, diff=1): - # self._next += diff - - # - def inc(value, count=1): - # - result = value + count - return result - - # - x = 1 - # - x = inc(x) - # - y = inc(x, 2) - # - z = inc(3) - # - print(x, y, z) - # - raise MyError('ka-boom') - """) - - def _set_lock(self, label=None, script=None): - if script is None: - if not os.path.exists(self.filename): - script = self.SOURCE - lockfile = self.workspace.lockfile() - return set_lock(self.filename, lockfile, label, script) - - def test_no_breakpoints(self): - self.lifecycle.requests = [] - config = { - 'breakpoints': [], - 'excbreakpoints': [], - } - with captured_stdio() as (stdout, _): - with self.launched(config=config): - # Allow the script to run to completion. - time.sleep(1.) - out = stdout.getvalue() - - for req, _ in self.lifecycle.requests: - self.assertNotEqual(req['command'], 'setBreakpoints') - self.assertNotEqual(req['command'], 'setExceptionBreakpoints') - self.assertIn('2 4 4', out) - self.assertIn('ka-boom', out) - - def test_breakpoints_single_file(self): - done1, _ = self._set_lock('d') - done2, script = self._set_lock('h') - lineno = find_line(script, 'b') - self.lifecycle.requests = [] # Trigger capture. - config = { - 'breakpoints': [{ - 'source': { - 'path': self.filename - }, - 'breakpoints': [ - { - 'line': lineno - }, - ], - }], - 'excbreakpoints': [], - } - with captured_stdio(out=True, err=True) as (stdout, stderr): - #with self.wait_for_event('exited', timeout=3): - with self.launched(config=config): - with self.fix.hidden(): - _, tid = self.get_threads(self.thread.name) - with self.wait_for_event('stopped'): - done1() - with self.wait_for_event('stopped'): - with self.wait_for_event('continued'): - req_continue1 = self.send_request( - 'continue', { - 'threadId': tid, - }) - with self.wait_for_event('stopped'): - with self.wait_for_event('continued'): - req_continue2 = self.send_request( - 'continue', { - 'threadId': tid, - }) - with self.wait_for_event('continued'): - req_continue_last = self.send_request( - 'continue', { - 'threadId': tid, - }) - - # Allow the script to run to completion. - received = self.vsc.received - done2() - out = stdout.getvalue() - err = stderr.getvalue() - - got = [] - for req, resp in self.lifecycle.requests: - if req['command'] == 'setBreakpoints': - got.append(req['arguments']) - self.assertNotEqual(req['command'], 'setExceptionBreakpoints') - self.assertEqual(got, config['breakpoints']) - - self.assert_contains(received, [ - self.new_event('stopped', - reason='breakpoint', - threadId=tid, - text=None, - description=None, - preserveFocusHint=False, - ), - self.new_response(req_continue1, allThreadsContinued=True), - self.new_event('continued', threadId=tid), - self.new_event('stopped', - reason='breakpoint', - threadId=tid, - text=None, - description=None, - preserveFocusHint=False, - ), - self.new_response(req_continue2, allThreadsContinued=True), - self.new_event('continued', threadId=tid), - self.new_event('stopped', - reason='breakpoint', - threadId=tid, - text=None, - description=None, - preserveFocusHint=False, - ), - self.new_response(req_continue_last, allThreadsContinued=True), - self.new_event('continued', threadId=tid), - ]) - self.assertIn('2 4 4', out) - self.assertIn('ka-boom', err) - - def test_exception_breakpoints(self): - self.vsc.PRINT_RECEIVED_MESSAGES = True - done, script = self._set_lock('h') - self.lifecycle.requests = [] # Trigger capture. - config = { - 'breakpoints': [], - 'excbreakpoints': [ - { - 'filters': ['raised'] - }, - ], - } - with captured_stdio() as (stdout, _): - with self.launched(config=config): - with self.fix.hidden(): - _, tid = self.get_threads(self.thread.name) - with self.wait_for_event('stopped'): - done() - with self.wait_for_event('continued'): - req_continue_last = self.send_request('continue', { - 'threadId': tid, - }) - # Allow the script to run to completion. - received = self.vsc.received - out = stdout.getvalue() - - got = [] - for req, resp in self.lifecycle.requests: - if req['command'] == 'setExceptionBreakpoints': - got.append(req['arguments']) - self.assertNotEqual(req['command'], 'setBreakpoints') - self.assertEqual(got, config['excbreakpoints']) - self.assert_contains(received, [ - self.new_event('stopped', **dict( - reason='exception', - threadId=tid, - text='__main__.MyError', - description='ka-boom', - preserveFocusHint=False)), - self.new_response(req_continue_last, allThreadsContinued=True), - self.new_event('continued', **dict(threadId=tid, )), - ]) - self.assertIn('2 4 4', out) - self.assertIn('ka-boom', out) - - -@unittest.skip('Needs fixing when running with code coverage') -class UnicodeBreakpointTests(BreakpointTests): - FILENAME = u'汉语a2.py' - - -class LogpointTests(TestBase, unittest.TestCase): - FILENAME = 'spam.py' - SOURCE = """ - a = 1 - b = 2 - c = 3 - d = 4 - """ - - @contextlib.contextmanager - def closing(self, exit=True): - def handle_msg(msg, _): - with self.wait_for_event('output'): - self.req_disconnect = self.send_request('disconnect') - - with self.wait_for_event('terminated', handler=handle_msg): - if exit: - with self.wait_for_event('exited'): - yield - else: - yield - - @contextlib.contextmanager - def running(self): - addr = (None, 8888) - with self.fake.start(addr): - yield - - def test_basic(self): - with open(self.filename) as scriptfile: - script = scriptfile.read() - donescript, wait = self.workspace.lockfile().wait_for_script() - done, waitscript = self.workspace.lockfile().wait_in_script() - with open(self.filename, 'w') as scriptfile: - scriptfile.write(script + donescript + waitscript) - addr = (None, 8888) - with self.fake.start(addr): - with self.vsc.wait_for_event('output'): - pass - - with self.vsc.wait_for_event('initialized'): - req_initialize = self.send_request('initialize', { - 'adapterID': 'spam', - }) - req_attach = self.send_request( - 'attach', {'debugOptions': ['RedirectOutput']}) - req_breakpoints = self.send_request( - 'setBreakpoints', { - 'source': { - 'path': self.filename - }, - 'breakpoints': [ - { - 'line': '4', - 'logMessage': '{a}+{b}=3' - }, - ], - }) - with self.vsc.wait_for_event('output'): # 1+2=3 - with self.vsc.wait_for_event('thread'): - req_config = self.send_request('configurationDone') - - wait() - received = self.vsc.received - done() - - self.fix.binder.done(close=False) - self.fix.binder.wait_until_done() - with self.closing(): - self.fix.binder.ptvsd.close() - - self.assert_vsc_received(received, [ - self.new_event( - 'output', - category='telemetry', - output='ptvsd', - data={'version': ptvsd.__version__}), - self.new_response(req_initialize, **INITIALIZE_RESPONSE), - self.new_event('initialized'), - self.new_response(req_attach), - self.new_response( - req_breakpoints, - **dict(breakpoints=[{ - 'id': 1, - 'verified': True, - 'line': '4' - }])), - self.new_response(req_config), - self.new_event( - 'process', - **dict( - name=sys.argv[0], - systemProcessId=os.getpid(), - isLocalProcess=True, - startMethod='attach', - )), - self.new_event('thread', reason='started', threadId=1), - self.new_event( - 'output', - **dict( - category='stdout', - output='1+2=3' + os.linesep, - )), - ])