diff --git a/ptvsd/__main__.py b/ptvsd/__main__.py index ee9654f9d..f3d97fd51 100644 --- a/ptvsd/__main__.py +++ b/ptvsd/__main__.py @@ -17,19 +17,23 @@ def run_module(address, modname, *extra, **kwargs): """Run pydevd for the given module.""" + run = kwargs.pop('_run', _run) + prog = kwargs.pop('_prog', sys.argv[0]) filename = modname + ':' - argv = _run_argv(address, filename, *extra) + argv = _run_argv(address, filename, extra, _prog=prog) argv.insert(argv.index('--file'), '--module') - _run(argv, **kwargs) + run(argv, **kwargs) def run_file(address, filename, *extra, **kwargs): """Run pydevd for the given Python file.""" - argv = _run_argv(address, filename, *extra) - _run(argv, **kwargs) + run = kwargs.pop('_run', _run) + prog = kwargs.pop('_prog', sys.argv[0]) + argv = _run_argv(address, filename, extra, _prog=prog) + run(argv, **kwargs) -def _run_argv(address, filename, *extra): +def _run_argv(address, filename, extra, _prog=sys.argv[0]): """Convert the given values to an argv that pydevd.main() supports.""" if '--' in extra: pydevd = list(extra[:extra.index('--')]) @@ -42,7 +46,7 @@ def _run_argv(address, filename, *extra): #if host is None: # host = '127.0.0.1' argv = [ - sys.argv[0], + _prog, '--port', str(port), ] if host is not None: @@ -54,7 +58,7 @@ def _run_argv(address, filename, *extra): ] + extra -def _run(argv, **kwargs): +def _run(argv, _pydevd=pydevd, _install=ptvsd.wrapper.install, **kwargs): """Start pydevd with the given commandline args.""" #print(' '.join(argv)) @@ -72,14 +76,14 @@ def _run(argv, **kwargs): # imports of the "pydevd" module then return the wrong module. We # work around this by avoiding lazy imports of the "pydevd" module. # We also replace the __main__ module with the "pydevd" module here. - if sys.modules['__main__'].__file__ != pydevd.__file__: + if sys.modules['__main__'].__file__ != _pydevd.__file__: sys.modules['__main___orig'] = sys.modules['__main__'] - sys.modules['__main__'] = pydevd + sys.modules['__main__'] = _pydevd - ptvsd.wrapper.install(pydevd, **kwargs) + _install(_pydevd, **kwargs) sys.argv[:] = argv try: - pydevd.main() + _pydevd.main() except SystemExit as ex: ptvsd.wrapper.ptvsd_sys_exit_code = int(ex.code) raise @@ -135,7 +139,10 @@ def parse_args(argv=None): supported, pydevd, script = _group_args(argv) args = _parse_args(prog, supported) - return args, pydevd + ['--'] + script + extra = pydevd + if script: + extra += ['--'] + script + return args, extra def _group_args(argv): @@ -176,9 +183,9 @@ def _group_args(argv): if arg == '--client': arg = '--host' elif arg == '--file': - if nextarg is None: + if nextarg is None: # The filename is missing... pydevd.append(arg) - continue + continue # This will get handled later. if nextarg.endswith(':') and '--module' in pydevd: pydevd.remove('--module') arg = '-m' diff --git a/ptvsd/debugger.py b/ptvsd/debugger.py index ec6717868..ff6df4775 100644 --- a/ptvsd/debugger.py +++ b/ptvsd/debugger.py @@ -2,6 +2,8 @@ # Licensed under the MIT License. See LICENSE in the project root # for license information. +import sys + from ptvsd.__main__ import run_module, run_file @@ -11,11 +13,27 @@ # TODO: not needed? DONT_DEBUG = [] +LOCALHOST = 'localhost' + +RUNNERS = { + 'module': run_module, # python -m spam + 'script': run_file, # python spam.py + 'code': run_file, # python -c 'print("spam")' + None: run_file, # catchall +} + -def debug(filename, port_num, debug_id, debug_options, run_as, **kwargs): +def debug(filename, port_num, debug_id, debug_options, run_as, + _runners=RUNNERS, _extra=None, *args, **kwargs): # TODO: docstring - address = ('localhost', port_num) - if run_as == 'module': - run_module(address, filename, **kwargs) - else: - run_file(address, filename, **kwargs) + if _extra is None: + _extra = sys.argv[1:] + address = (LOCALHOST, port_num) + try: + run = _runners[run_as] + except KeyError: + # TODO: fail? + run = _runners[None] + if _extra: + args = _extra + list(args) + run(address, filename, *args, **kwargs) diff --git a/tests/ptvsd/test___main__.py b/tests/ptvsd/test___main__.py new file mode 100644 index 000000000..ece74e6ae --- /dev/null +++ b/tests/ptvsd/test___main__.py @@ -0,0 +1,412 @@ +import contextlib +from io import StringIO +import sys +import unittest + +from _pydevd_bundle import pydevd_comm + +import ptvsd.wrapper +from ptvsd.__main__ import run_module, run_file, parse_args + +if sys.version_info < (3,): + from io import BytesIO as StringIO # noqa + + +@contextlib.contextmanager +def captured_stdio(out=None, err=None): + if out is None: + if err is None: + out = err = StringIO() + elif err is False: + out = StringIO() + elif err is None and out is False: + err = StringIO() + if out is False: + out = None + if err is False: + err = None + + orig = sys.stdout, sys.stderr + if out is not None: + sys.stdout = out + if err is not None: + sys.stderr = err + try: + yield out, err + finally: + sys.stdout, sys.stderr = orig + + +class FakePyDevd(object): + + def __init__(self, __file__, handle_main): + self.__file__ = __file__ + self.handle_main = handle_main + + def main(self): + self.handle_main() + + +class RunBase(object): + + def setUp(self): + super(RunBase, self).setUp() + self.argv = None + self.kwargs = None + + def _run(self, argv, **kwargs): + self.argv = argv + self.kwargs = kwargs + + +class RunModuleTests(RunBase, unittest.TestCase): + + def test_local(self): + addr = (None, 8888) + run_module(addr, 'spam', _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--module', + '--file', 'spam:', + ]) + self.assertEqual(self.kwargs, {}) + + def test_remote(self): + addr = ('1.2.3.4', 8888) + run_module(addr, 'spam', _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', '1.2.3.4', + '--module', + '--file', 'spam:', + ]) + self.assertEqual(self.kwargs, {}) + + def test_extra(self): + addr = (None, 8888) + run_module(addr, 'spam', '--vm_type', 'xyz', '--', '--DEBUG', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--vm_type', 'xyz', + '--module', + '--file', 'spam:', + '--DEBUG', + ]) + self.assertEqual(self.kwargs, {}) + + def test_executable(self): + addr = (None, 8888) + run_module(addr, 'spam', _run=self._run) + + self.assertEqual(self.argv, [ + sys.argv[0], + '--port', '8888', + '--module', + '--file', 'spam:', + ]) + self.assertEqual(self.kwargs, {}) + + +class RunScriptTests(RunBase, unittest.TestCase): + + def test_local(self): + addr = (None, 8888) + run_file(addr, 'spam.py', _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--file', 'spam.py', + ]) + self.assertEqual(self.kwargs, {}) + + def test_remote(self): + addr = ('1.2.3.4', 8888) + run_file(addr, 'spam.py', _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', '1.2.3.4', + '--file', 'spam.py', + ]) + self.assertEqual(self.kwargs, {}) + + def test_extra(self): + addr = (None, 8888) + run_file(addr, 'spam.py', '--vm_type', 'xyz', '--', '--DEBUG', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--vm_type', 'xyz', + '--file', 'spam.py', + '--DEBUG', + ]) + self.assertEqual(self.kwargs, {}) + + def test_executable(self): + addr = (None, 8888) + run_file(addr, 'spam.py', _run=self._run) + + self.assertEqual(self.argv, [ + sys.argv[0], + '--port', '8888', + '--file', 'spam.py', + ]) + self.assertEqual(self.kwargs, {}) + + +class IntegratedRunTests(unittest.TestCase): + + def setUp(self): + super(IntegratedRunTests, self).setUp() + self.__main__ = sys.modules['__main__'] + self.argv = sys.argv + ptvsd.wrapper.ptvsd_sys_exit_code = 0 + self.start_server = pydevd_comm.start_server + self.start_client = pydevd_comm.start_client + + self.pydevd = None + self.kwargs = None + self.maincalls = 0 + self.mainexc = None + + def tearDown(self): + sys.argv[:] = self.argv + sys.modules['__main__'] = self.__main__ + sys.modules.pop('__main___orig', None) + ptvsd.wrapper.ptvsd_sys_exit_code = 0 + pydevd_comm.start_server = self.start_server + pydevd_comm.start_client = self.start_client + # We shouldn't need to restore __main__.start_*. + super(IntegratedRunTests, self).tearDown() + + def _install(self, pydevd, **kwargs): + self.pydevd = pydevd + self.kwargs = kwargs + + def _main(self): + self.maincalls += 1 + if self.mainexc is not None: + raise self.mainexc + + def test_run(self): + pydevd = FakePyDevd('pydevd/pydevd.py', self._main) + addr = (None, 8888) + run_file(addr, 'spam.py', _pydevd=pydevd, _install=self._install) + + self.assertEqual(self.pydevd, pydevd) + self.assertEqual(self.kwargs, {}) + self.assertEqual(self.maincalls, 1) + self.assertEqual(sys.argv, [ + sys.argv[0], + '--port', '8888', + '--file', 'spam.py', + ]) + self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 0) + + def test_failure(self): + self.mainexc = RuntimeError('boom!') + pydevd = FakePyDevd('pydevd/pydevd.py', self._main) + addr = (None, 8888) + with self.assertRaises(RuntimeError) as cm: + run_file(addr, 'spam.py', _pydevd=pydevd, _install=self._install) + exc = cm.exception + + self.assertEqual(self.pydevd, pydevd) + self.assertEqual(self.kwargs, {}) + self.assertEqual(self.maincalls, 1) + self.assertEqual(sys.argv, [ + sys.argv[0], + '--port', '8888', + '--file', 'spam.py', + ]) + self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 0) + self.assertIs(exc, self.mainexc) + + def test_exit(self): + self.mainexc = SystemExit(1) + pydevd = FakePyDevd('pydevd/pydevd.py', self._main) + addr = (None, 8888) + with self.assertRaises(SystemExit): + run_file(addr, 'spam.py', _pydevd=pydevd, _install=self._install) + + self.assertEqual(self.pydevd, pydevd) + self.assertEqual(self.kwargs, {}) + self.assertEqual(self.maincalls, 1) + self.assertEqual(sys.argv, [ + sys.argv[0], + '--port', '8888', + '--file', 'spam.py', + ]) + self.assertEqual(ptvsd.wrapper.ptvsd_sys_exit_code, 1) + + def test_installed(self): + pydevd = FakePyDevd('pydevd/pydevd.py', self._main) + addr = (None, 8888) + run_file(addr, 'spam.py', _pydevd=pydevd) + + self.assertIs(pydevd_comm.start_server, ptvsd.wrapper.start_server) + self.assertIs(pydevd_comm.start_client, ptvsd.wrapper.start_client) + self.assertIs(pydevd.start_server, ptvsd.wrapper.start_server) + self.assertIs(pydevd.start_client, ptvsd.wrapper.start_client) + __main__ = sys.modules['__main__'] + self.assertIs(__main__.start_server, ptvsd.wrapper.start_server) + self.assertIs(__main__.start_client, ptvsd.wrapper.start_client) + + +class ParseArgsTests(unittest.TestCase): + + def test_module(self): + args, extra = parse_args([ + 'eggs', + '--port', '8888', + '-m', 'spam', + ]) + + self.assertEqual(vars(args), { + 'kind': 'module', + 'name': 'spam', + 'address': (None, 8888), + }) + self.assertEqual(extra, []) + + def test_script(self): + args, extra = parse_args([ + 'eggs', + '--port', '8888', + 'spam.py', + ]) + + self.assertEqual(vars(args), { + 'kind': 'script', + 'name': 'spam.py', + 'address': (None, 8888), + }) + self.assertEqual(extra, []) + + def test_remote(self): + args, extra = parse_args([ + 'eggs', + '--host', '1.2.3.4', + '--port', '8888', + 'spam.py', + ]) + + self.assertEqual(vars(args), { + 'kind': 'script', + 'name': 'spam.py', + 'address': ('1.2.3.4', 8888), + }) + self.assertEqual(extra, []) + + def test_extra(self): + args, extra = parse_args([ + 'eggs', + '--DEBUG', + '--port', '8888', + '--vm_type', '???', + 'spam.py', + '--xyz', '123', + 'abc', + '--cmd-line', + '--', + 'foo', + '--server', + '--bar' + ]) + + self.assertEqual(vars(args), { + 'kind': 'script', + 'name': 'spam.py', + 'address': (None, 8888), + }) + self.assertEqual(extra, [ + '--DEBUG', + '--vm_type', '???', + '--', + '--xyz', '123', + 'abc', + '--cmd-line', + 'foo', + '--server', + '--bar', + ]) + + def test_unsupported_arg(self): + with self.assertRaises(SystemExit): + with captured_stdio(): + parse_args([ + 'eggs', + '--port', '8888', + '--xyz', '123', + 'spam.py', + ]) + + def test_backward_compatibility_host(self): + args, extra = parse_args([ + 'eggs', + '--client', '1.2.3.4', + '--port', '8888', + '-m', 'spam', + ]) + + self.assertEqual(vars(args), { + 'kind': 'module', + 'name': 'spam', + 'address': ('1.2.3.4', 8888), + }) + self.assertEqual(extra, []) + + def test_backward_compatibility_module(self): + args, extra = parse_args([ + 'eggs', + '--port', '8888', + '--module', + '--file', 'spam:', + ]) + + self.assertEqual(vars(args), { + 'kind': 'module', + 'name': 'spam', + 'address': (None, 8888), + }) + self.assertEqual(extra, []) + + def test_backward_compatibility_script(self): + args, extra = parse_args([ + 'eggs', + '--port', '8888', + '--file', 'spam.py', + ]) + + self.assertEqual(vars(args), { + 'kind': 'script', + 'name': 'spam.py', + 'address': (None, 8888), + }) + self.assertEqual(extra, []) + + def test_pseudo_backward_compatibility(self): + args, extra = parse_args([ + 'eggs', + '--port', '8888', + '--module', + '--file', 'spam', + ]) + + self.assertEqual(vars(args), { + 'kind': 'script', + 'name': 'spam', + 'address': (None, 8888), + }) + self.assertEqual(extra, ['--module']) diff --git a/tests/ptvsd/test_debugger.py b/tests/ptvsd/test_debugger.py new file mode 100644 index 000000000..5fceacbe7 --- /dev/null +++ b/tests/ptvsd/test_debugger.py @@ -0,0 +1,190 @@ +import sys +import unittest + +from ptvsd.debugger import debug, LOCALHOST + + +class DebugTests(unittest.TestCase): + + def setUp(self): + super(DebugTests, self).setUp() + + def _make_run(kind): + def run(addr, name, *args, **kwargs): + self._run(kind, addr, name, *args, **kwargs) + return run + self.runners = {} + for kind in ('module', 'script', 'code', None): + self.runners[kind] = _make_run(kind) + self.kind = None + self.args = None + self.kwargs = None + + def _run(self, kind, *args, **kwargs): + self.kind = kind + self.args = args + self.kwargs = kwargs + + def test_module(self): + filename = 'spam' + _, port = addr = (LOCALHOST, 8888) + debug_id = 1 + debug_options = {'x': 'y'} + debug(filename, port, debug_id, debug_options, 'module', + _runners=self.runners, _extra=()) + + self.assertEqual(self.kind, 'module') + self.assertEqual(self.args, (addr, filename)) + self.assertEqual(self.kwargs, {}) + + def test_script(self): + filename = 'spam.py' + _, port = addr = (LOCALHOST, 8888) + debug_id = 1 + debug_options = {'x': 'y'} + debug(filename, port, debug_id, debug_options, 'script', + _runners=self.runners, _extra=()) + + self.assertEqual(self.kind, 'script') + self.assertEqual(self.args, (addr, filename)) + self.assertEqual(self.kwargs, {}) + + def test_code(self): + filename = "print('spam')" + _, port = addr = (LOCALHOST, 8888) + debug_id = 1 + debug_options = {'x': 'y'} + debug(filename, port, debug_id, debug_options, 'code', + _runners=self.runners, _extra=()) + + self.assertEqual(self.kind, 'code') + self.assertEqual(self.args, (addr, filename)) + self.assertEqual(self.kwargs, {}) + + def test_unsupported(self): + filename = 'spam' + _, port = addr = (LOCALHOST, 8888) + debug_id = 1 + debug_options = {'x': 'y'} + debug(filename, port, debug_id, debug_options, '???', + _runners=self.runners, _extra=()) + + self.assertIs(self.kind, None) + self.assertEqual(self.args, (addr, filename)) + self.assertEqual(self.kwargs, {}) + + def test_extra_sys_argv(self): + filename = 'spam.py' + _, port = addr = (LOCALHOST, 8888) + debug_id = 1 + debug_options = {'x': 'y'} + extra = ['--eggs', 'abc'] + debug(filename, port, debug_id, debug_options, 'script', + _runners=self.runners, _extra=extra) + + self.assertEqual(self.args, (addr, filename, '--eggs', 'abc')) + + +class IntegrationTests(unittest.TestCase): + + def setUp(self): + super(IntegrationTests, self).setUp() + self.argv = None + self.kwargs = None + self._sys_argv = list(sys.argv) + + def tearDown(self): + sys.argv[:] = self._sys_argv + super(IntegrationTests, self).tearDown() + + def _run(self, argv, **kwargs): + self.argv = argv + self.kwargs = kwargs + + def test_module(self): + filename = 'spam' + port = 8888 + debug_id = 1 + debug_options = {'x': 'y'} + sys.argv = [filename] + debug(filename, port, debug_id, debug_options, 'module', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', LOCALHOST, + '--module', + '--file', 'spam:', + ]) + self.assertEqual(self.kwargs, {}) + + def test_script(self): + filename = 'spam.py' + port = 8888 + debug_id = 1 + debug_options = {'x': 'y'} + sys.argv = [filename] + debug(filename, port, debug_id, debug_options, 'script', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', LOCALHOST, + '--file', 'spam.py', + ]) + self.assertEqual(self.kwargs, {}) + + def test_code(self): + filename = "print('spam')" + port = 8888 + debug_id = 1 + debug_options = {'x': 'y'} + sys.argv = [filename] + debug(filename, port, debug_id, debug_options, 'code', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', LOCALHOST, + '--file', filename, + ]) + self.assertEqual(self.kwargs, {}) + + def test_unsupported(self): + filename = 'spam' + port = 8888 + debug_id = 1 + debug_options = {'x': 'y'} + sys.argv = [filename] + debug(filename, port, debug_id, debug_options, '???', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', LOCALHOST, + '--file', 'spam', + ]) + self.assertEqual(self.kwargs, {}) + + def test_extra_sys_argv(self): + filename = 'spam.py' + port = 8888 + debug_id = 1 + debug_options = {'x': 'y'} + sys.argv = [filename, '--abc', 'xyz', '42'] + debug(filename, port, debug_id, debug_options, 'script', + _run=self._run, _prog='eggs') + + self.assertEqual(self.argv, [ + 'eggs', + '--port', '8888', + '--client', LOCALHOST, + '--file', 'spam.py', + '--abc', 'xyz', + '42', + ]) + self.assertEqual(self.kwargs, {})