diff --git a/ptvsd/wrapper.py b/ptvsd/wrapper.py index b8d36c4d..430a1844 100644 --- a/ptvsd/wrapper.py +++ b/ptvsd/wrapper.py @@ -653,6 +653,7 @@ class InternalsFilter(object): """Identifies debugger internal artifacts. """ # TODO: Move the internal thread identifier here + def __init__(self): if platform.system() == 'Windows': self._init_windows() @@ -1407,6 +1408,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): '\t'.join(project_dirs)) def _is_stdlib(self, filepath): + filepath = os.path.normcase(os.path.normpath(filepath)) for prefix in STDLIB_PATH_PREFIXES: if prefix != '' and filepath.startswith(prefix): return True @@ -2090,7 +2092,7 @@ class VSCodeMessageProcessor(VSCLifecycleMsgProcessor): condition = None expressions = re.findall('\{.*?\}', logMessage) if len(expressions) == 0: - expression = '{}'.format(repr(logMessage)) # noqa + expression = '{}'.format(repr(logMessage)) # noqa else: raw_text = reduce(lambda a, b: a.replace(b, '{}'), expressions, logMessage) # noqa raw_text = raw_text.replace('"', '\\"') diff --git a/tests/helpers/_io.py b/tests/helpers/_io.py index e14d51be..3000bd42 100644 --- a/tests/helpers/_io.py +++ b/tests/helpers/_io.py @@ -103,6 +103,9 @@ def iter_lines_buffered(read, sep=b'\n', initial=b'', stop=noop): data = read(1024) if not data: raise EOFError() + if buf and buf[-1:] == b'\r': + data = buf + data + buf = b'' except EOFError as exc: exc.remainder = buf raise diff --git a/tests/resources/system_tests/test_exceptions/mypkg_attach_unhandled/__init__.py b/tests/resources/system_tests/test_exceptions/mypkg_attach_unhandled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/system_tests/test_exceptions/mypkg_attach_unhandled/__main__.py b/tests/resources/system_tests/test_exceptions/mypkg_attach_unhandled/__main__.py new file mode 100644 index 00000000..d34f890f --- /dev/null +++ b/tests/resources/system_tests/test_exceptions/mypkg_attach_unhandled/__main__.py @@ -0,0 +1,7 @@ +import ptvsd +import sys +ptvsd.enable_attach((sys.argv[1], sys.argv[2])) +ptvsd.wait_for_attach() + +raise ArithmeticError('Hello') +sys.stdout.write('end') diff --git a/tests/resources/system_tests/test_exceptions/mypkg_launch_unhandled/__init__.py b/tests/resources/system_tests/test_exceptions/mypkg_launch_unhandled/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/resources/system_tests/test_exceptions/mypkg_launch_unhandled/__main__.py b/tests/resources/system_tests/test_exceptions/mypkg_launch_unhandled/__main__.py new file mode 100644 index 00000000..a75779b2 --- /dev/null +++ b/tests/resources/system_tests/test_exceptions/mypkg_launch_unhandled/__main__.py @@ -0,0 +1,4 @@ +import sys + +raise ArithmeticError('Hello') +sys.stdout.write('end') diff --git a/tests/resources/system_tests/test_exceptions/unhandled_exceptions_attach.py b/tests/resources/system_tests/test_exceptions/unhandled_exceptions_attach.py new file mode 100644 index 00000000..7973fe74 --- /dev/null +++ b/tests/resources/system_tests/test_exceptions/unhandled_exceptions_attach.py @@ -0,0 +1,7 @@ +import sys +import ptvsd +ptvsd.enable_attach((('localhost', 9879))) +ptvsd.wait_for_attach() + +raise ArithmeticError('Hello') +sys.stdout.write('end') diff --git a/tests/system_tests/test_exceptions.py b/tests/system_tests/test_exceptions.py index ebd3e6f6..390611cb 100644 --- a/tests/system_tests/test_exceptions.py +++ b/tests/system_tests/test_exceptions.py @@ -46,15 +46,17 @@ class ExceptionTests(LifecycleTestsBase): options=options) received = list(_strip_newline_output_events(dbg.session.received)) - self.assertEqual( - len(self.find_events(received, 'output', {'category': 'stdout'})), - 1) + # TODO: Re-enable after fixing #685 + #self.assertEqual( + # len(self.find_events(received, 'output', {'category': 'stdout'})), + # 1) std_errs = self.find_events(received, 'output', {'category': 'stderr'}) self.assertGreaterEqual(len(std_errs), 1) std_err_msg = ''.join([msg.body['output'] for msg in std_errs]) self.assertIn('ArithmeticError: Hello', std_err_msg) self.assert_contains(received, [ - self.new_event('output', category='stdout', output='one'), + # TODO: Re-enable after fixing #685 + # self.new_event('output', category='stdout', output='one'), self.new_event('exited', exitCode=0), self.new_event('terminated'), ]) @@ -156,16 +158,135 @@ class ExceptionTests(LifecycleTestsBase): Awaitable.wait_all(continued) received = list(_strip_newline_output_events(dbg.session.received)) - self.assertEqual( - len(self.find_events(received, 'output', {'category': 'stdout'})), - 1) + # TODO: Re-enable after fixing #685 + #self.assertEqual( + # len(self.find_events(received, 'output', {'category': 'stdout'})), + # 1) std_errs = self.find_events(received, 'output', {'category': 'stderr'}) self.assertGreaterEqual(len(std_errs), 1) std_err_msg = ''.join([msg.body['output'] for msg in std_errs]) self.assertIn('ArithmeticError: Hello', std_err_msg) self.assert_contains(received, [ self.new_event('continued', threadId=thread_id), - self.new_event('output', category='stdout', output='one'), + # TODO: Re-enable after fixing #685 + # self.new_event('output', category='stdout', output='one'), + self.new_event('exited', exitCode=0), + self.new_event('terminated'), + ]) + + def run_test_breaking_into_raised_exceptions_only(self, debug_info, + expected_source_name): + # NOTE: for this case we will be using a unhandled exception. The + # behavior expected here is that it breaks once when the exception + # was raised but not during postmortem + excbreakpoints = [{'filters': ['raised']}] + 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, + excbreakpoints=excbreakpoints, + options=options) + + Awaitable.wait_all(req_launch_attach, stopped) + self.assertEqual(stopped.event.body['text'], 'ArithmeticError') + self.assertIn("ArithmeticError('Hello'", + stopped.event.body['description']) + + thread_id = stopped.event.body['threadId'] + req_exc_info = dbg.session.send_request( + 'exceptionInfo', + threadId=thread_id) + req_exc_info.wait() + exc_info = req_exc_info.resp.body + + self.assert_is_subset(exc_info, { + 'exceptionId': 'ArithmeticError', + 'breakMode': 'always', + 'details': { + 'typeName': 'ArithmeticError', + 'source': expected_source_name + } + }) + + continued = dbg.session.get_awaiter_for_event('continued') + dbg.session.send_request( + 'continue', + threadId=thread_id, + ).wait() + Awaitable.wait_all(continued) + + received = list(_strip_newline_output_events(dbg.session.received)) + self.assert_contains(received, [ + self.new_event('continued', threadId=thread_id), + self.new_event('exited', exitCode=0), + self.new_event('terminated'), + ]) + + def run_test_breaking_into_raised_and_unhandled_exceptions( + self, debug_info, expected_source_name): + excbreakpoints = [{'filters': ['raised', 'uncaught']}] + options = {'debugOptions': ['RedirectOutput']} + + expected = { + 'exceptionId': 'ArithmeticError', + 'breakMode': 'always', + 'details': { + 'typeName': 'ArithmeticError', + 'source': expected_source_name + } + } + + 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, + excbreakpoints=excbreakpoints, + options=options) + + Awaitable.wait_all(req_launch_attach, stopped) + self.assertEqual(stopped.event.body['text'], 'ArithmeticError') + self.assertIn("ArithmeticError('Hello'", + stopped.event.body['description']) + + thread_id = stopped.event.body['threadId'] + req_exc_info = dbg.session.send_request( + 'exceptionInfo', + threadId=thread_id) + req_exc_info.wait() + self.assert_is_subset(req_exc_info.resp.body, expected) + + stopped2 = dbg.session.get_awaiter_for_event('stopped') + continued = dbg.session.get_awaiter_for_event('continued') + dbg.session.send_request( + 'continue', + threadId=thread_id, + ).wait() + Awaitable.wait_all(stopped2, continued) + + # Second hit on uncaught exception + self.assertEqual(stopped2.event.body['text'], 'ArithmeticError') + self.assertIn("ArithmeticError('Hello'", + stopped2.event.body['description']) + req_exc_info2 = dbg.session.send_request( + 'exceptionInfo', + threadId=thread_id, + ) + req_exc_info2.wait() + self.assert_is_subset(req_exc_info2.resp.body, expected) + + continued2 = dbg.session.get_awaiter_for_event('continued') + dbg.session.send_request( + 'continue', + threadId=thread_id, + ).wait() + Awaitable.wait_all(continued2) + + received = list(_strip_newline_output_events(dbg.session.received)) + self.assert_contains(received, [ + self.new_event('continued', threadId=thread_id), + self.new_event('continued', threadId=thread_id), # expect 2 events self.new_event('exited', exitCode=0), self.new_event('terminated'), ]) @@ -190,15 +311,40 @@ class LaunchFileTests(ExceptionTests): self.run_test_breaking_into_handled_exceptions( DebugInfo(filename=filename, cwd=cwd), filename) - @unittest.skip('Issue #653') def test_breaking_into_unhandled_exceptions(self): filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') cwd = os.path.dirname(filename) self.run_test_breaking_into_unhandled_exceptions( DebugInfo(filename=filename, cwd=cwd), filename) + def test_breaking_into_raised_exceptions_only(self): + filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') + cwd = os.path.dirname(filename) + self.run_test_breaking_into_raised_exceptions_only( + DebugInfo(filename=filename, cwd=cwd), filename) + + def test_breaking_into_raised_and_unhandled_exceptions(self): + filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') + cwd = os.path.dirname(filename) + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo(filename=filename, cwd=cwd), filename) + class LaunchModuleExceptionLifecycleTests(ExceptionTests): + def test_not_breaking_into_handled_exceptions(self): + module_name = 'mypkg_launch1' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.parent.root + self.run_test_not_breaking_into_handled_exceptions( + DebugInfo(modulename=module_name, env=env, cwd=cwd)) + + def test_not_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.parent.root + self.run_test_not_breaking_into_unhandled_exceptions( + DebugInfo(modulename=module_name, env=env, cwd=cwd)) + def test_breaking_into_handled_exceptions(self): module_name = 'mypkg_launch1' env = TEST_FILES.env_with_py_path() @@ -207,26 +353,43 @@ class LaunchModuleExceptionLifecycleTests(ExceptionTests): DebugInfo(modulename=module_name, env=env, cwd=cwd), os.path.join(TEST_FILES.root, module_name, '__init__.py')) - def test_not_breaking_into_handled_exceptions(self): - module_name = 'mypkg_launch1' + def test_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' env = TEST_FILES.env_with_py_path() cwd = TEST_FILES.parent.root - self.run_test_not_breaking_into_handled_exceptions( - DebugInfo(modulename=module_name, env=env, cwd=cwd)) + self.run_test_breaking_into_unhandled_exceptions( + DebugInfo(modulename=module_name, env=env, cwd=cwd), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_exceptions_only(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.parent.root + self.run_test_breaking_into_raised_exceptions_only( + DebugInfo(modulename=module_name, env=env, cwd=cwd), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_and_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.parent.root + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo(modulename=module_name, env=env, cwd=cwd), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) class ServerAttachExceptionLifecycleTests(ExceptionTests): - def test_breaking_into_handled_exceptions(self): + def test_not_breaking_into_handled_exceptions(self): filename = TEST_FILES.resolve('handled_exceptions_launch.py') cwd = os.path.dirname(filename) argv = ['localhost', str(PORT)] - self.run_test_breaking_into_handled_exceptions( + self.run_test_not_breaking_into_handled_exceptions( DebugInfo( filename=filename, cwd=cwd, starttype='attach', argv=argv, - ), filename) + )) def test_not_breaking_into_unhandled_exceptions(self): filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') @@ -240,17 +403,17 @@ class ServerAttachExceptionLifecycleTests(ExceptionTests): argv=argv, )) - def test_not_breaking_into_handled_exceptions(self): + def test_breaking_into_handled_exceptions(self): filename = TEST_FILES.resolve('handled_exceptions_launch.py') cwd = os.path.dirname(filename) argv = ['localhost', str(PORT)] - self.run_test_not_breaking_into_handled_exceptions( + self.run_test_breaking_into_handled_exceptions( DebugInfo( filename=filename, cwd=cwd, starttype='attach', argv=argv, - )) + ), filename) def test_breaking_into_unhandled_exceptions(self): filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') @@ -264,21 +427,32 @@ class ServerAttachExceptionLifecycleTests(ExceptionTests): argv=argv, ), filename) - -class PTVSDAttachExceptionLifecycleTests(ExceptionTests): - def test_breaking_into_handled_exceptions(self): - filename = TEST_FILES.resolve('handled_exceptions_attach.py') + def test_breaking_into_raised_exceptions_only(self): + filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') cwd = os.path.dirname(filename) argv = ['localhost', str(PORT)] - self.run_test_breaking_into_handled_exceptions( + self.run_test_breaking_into_raised_exceptions_only( DebugInfo( filename=filename, - attachtype='import', cwd=cwd, starttype='attach', argv=argv, ), filename) + def test_breaking_into_raised_and_unhandled_exceptions(self): + filename = TEST_FILES.resolve('unhandled_exceptions_launch.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo( + filename=filename, + cwd=cwd, + starttype='attach', + argv=argv, + ), filename) + + +class PTVSDAttachExceptionLifecycleTests(ExceptionTests): @unittest.skip('Needs fixing in #609, #580') def test_not_breaking_into_handled_exceptions(self): filename = TEST_FILES.resolve('handled_exceptions_attach.py') @@ -293,8 +467,105 @@ class PTVSDAttachExceptionLifecycleTests(ExceptionTests): argv=argv, )) + @unittest.skip('Needs fixing in #609, #580') + def test_not_breaking_into_unhandled_exceptions(self): + filename = TEST_FILES.resolve('unhandled_exceptions_attach.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_not_breaking_into_unhandled_exceptions( + DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + )) + + @unittest.skip('#686') + def test_breaking_into_handled_exceptions(self): + filename = TEST_FILES.resolve('handled_exceptions_attach.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_handled_exceptions( + DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + ), filename) + + def test_breaking_into_unhandled_exceptions(self): + filename = TEST_FILES.resolve('unhandled_exceptions_attach.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_unhandled_exceptions( + DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + ), filename) + + @unittest.skip('Needs fixing in #609') + def test_breaking_into_raised_exceptions_only(self): + filename = TEST_FILES.resolve('unhandled_exceptions_attach.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_exceptions_only( + DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + ), filename) + + @unittest.skip('Needs fixing in #609') + def test_breaking_into_raised_and_unhandled_exceptions(self): + filename = TEST_FILES.resolve('unhandled_exceptions_attach.py') + cwd = os.path.dirname(filename) + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo( + filename=filename, + attachtype='import', + cwd=cwd, + starttype='attach', + argv=argv, + ), filename) + class ServerAttachModuleExceptionLifecycleTests(ExceptionTests): + def test_not_breaking_into_handled_exceptions(self): + module_name = 'mypkg_launch1' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_not_breaking_into_handled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + starttype='attach', + )) + + def test_not_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_not_breaking_into_unhandled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + starttype='attach', + )) + def test_breaking_into_handled_exceptions(self): module_name = 'mypkg_launch1' env = TEST_FILES.env_with_py_path() @@ -309,22 +580,83 @@ class ServerAttachModuleExceptionLifecycleTests(ExceptionTests): starttype='attach', ), os.path.join(TEST_FILES.root, module_name, '__init__.py')) - def test_not_breaking_into_handled_exceptions(self): - module_name = 'mypkg_launch1' + def test_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' env = TEST_FILES.env_with_py_path() cwd = TEST_FILES.root argv = ['localhost', str(PORT)] - self.run_test_not_breaking_into_handled_exceptions( + self.run_test_breaking_into_unhandled_exceptions( DebugInfo( modulename=module_name, env=env, cwd=cwd, argv=argv, starttype='attach', - )) + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_exceptions_only(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_exceptions_only( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + starttype='attach', + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_and_unhandled_exceptions(self): + module_name = 'mypkg_launch_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + starttype='attach', + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) class PTVSDAttachModuleExceptionLifecycleTests(ExceptionTests): + def test_not_breaking_into_handled_exceptions(self): + module_name = 'mypkg_attach1' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_not_breaking_into_handled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + attachtype='import', + starttype='attach', + )) + + def test_not_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_attach_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_not_breaking_into_unhandled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + attachtype='import', + starttype='attach', + )) + def test_breaking_into_handled_exceptions(self): module_name = 'mypkg_attach1' env = TEST_FILES.env_with_py_path() @@ -340,12 +672,12 @@ class PTVSDAttachModuleExceptionLifecycleTests(ExceptionTests): starttype='attach', ), os.path.join(TEST_FILES.root, module_name, '__init__.py')) - def test_not_breaking_into_handled_exceptions(self): - module_name = 'mypkg_attach1' + def test_breaking_into_unhandled_exceptions(self): + module_name = 'mypkg_attach_unhandled' env = TEST_FILES.env_with_py_path() cwd = TEST_FILES.root argv = ['localhost', str(PORT)] - self.run_test_not_breaking_into_handled_exceptions( + self.run_test_breaking_into_unhandled_exceptions( DebugInfo( modulename=module_name, env=env, @@ -353,4 +685,37 @@ class PTVSDAttachModuleExceptionLifecycleTests(ExceptionTests): argv=argv, attachtype='import', starttype='attach', - )) + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_exceptions_only(self): + module_name = 'mypkg_attach_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_exceptions_only( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + attachtype='import', + starttype='attach', + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py')) + + def test_breaking_into_raised_and_unhandled_exceptions(self): + module_name = 'mypkg_attach_unhandled' + env = TEST_FILES.env_with_py_path() + cwd = TEST_FILES.root + argv = ['localhost', str(PORT)] + self.run_test_breaking_into_raised_and_unhandled_exceptions( + DebugInfo( + modulename=module_name, + env=env, + cwd=cwd, + argv=argv, + attachtype='import', + starttype='attach', + ), + os.path.join(TEST_FILES.root, module_name, '__main__.py'))