Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ The tests are run concurrently, and the default number of workers is 8. You can

While tox is the recommended way to run the test suite, pytest can also be invoked directly from the root of the repository. This requires packages in tests/requirements.txt to be installed first.

Using a venv created by tox in the '.tox' folder can make it easier to get the pytest configuration correct. Debugpy needs to be installed into the venv for the tests to run, so using the tox generated .venv makes that easier.

#### Keeping logs on test success

There's an internal setting `debugpy_log_passed` that if set to true will not erase the logs after a successful test run. Just search for this in the code and remove the code that deletes the logs on success.
Expand Down
17 changes: 17 additions & 0 deletions src/debugpy/adapter/launchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,23 @@ def on_launcher_connected(sock):
request_args["cwd"] = cwd
if shell_expand_args:
request_args["argsCanBeInterpretedByShell"] = True

# VS Code debugger extension may pass us an argument indicating the
# quoting character to use in the terminal. Otherwise default based on platform.
default_quote = '"' if os.name != "nt" else "'"
quote_char = arguments["terminalQuoteCharacter"] if "terminalQuoteCharacter" in arguments else default_quote

# VS code doesn't quote arguments if `argsCanBeInterpretedByShell` is true,
# so we need to do it ourselves for the arguments up to the call to the adapter.
args = request_args["args"]
for i in range(len(args)):
if args[i] == "--":
break
s = args[i]
if " " in s and not ((s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'"))):
s = f"{quote_char}{s}{quote_char}"
args[i] = s

try:
# It is unspecified whether this request receives a response immediately, or only
# after the spawned command has completed running, so do not block waiting for it.
Expand Down
26 changes: 23 additions & 3 deletions tests/debug/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,9 +366,29 @@ def _make_env(self, base_env, codecov=True):
return env

def _make_python_cmdline(self, exe, *args):
return [
str(s.strpath if isinstance(s, py.path.local) else s) for s in [exe, *args]
]
def normalize(s, strip_quotes=False):
# Convert py.path.local to string
if isinstance(s, py.path.local):
s = s.strpath
else:
s = str(s)
# Strip surrounding quotes if requested (for launcher args only)
if strip_quotes and len(s) >= 2 and (s[0] == s[-1] == '"' or s[0] == s[-1] == "'"):
s = s[1:-1]
return s

# Strip quotes from exe and args before '--', but not from debuggee args after '--'
# (exe and launcher paths may be quoted when argsCanBeInterpretedByShell is set)
result = [normalize(exe, strip_quotes=True)]
found_separator = False
for arg in args:
if arg == "--":
found_separator = True
result.append(arg)
else:
# Strip quotes before '--', but not after (debuggee args)
result.append(normalize(arg, strip_quotes=not found_separator))
return result

def spawn_debuggee(self, args, cwd=None, exe=sys.executable, setup=None):
assert self.debuggee is None
Expand Down
37 changes: 36 additions & 1 deletion tests/debugpy/test_args.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# Licensed under the MIT License. See LICENSE in the project root
# for license information.

import os
import sys
import pytest

from debugpy.common import log
Expand Down Expand Up @@ -35,7 +37,8 @@ def code_to_debug():
@pytest.mark.parametrize("target", targets.all)
@pytest.mark.parametrize("run", runners.all_launch)
@pytest.mark.parametrize("expansion", ["preserve", "expand"])
def test_shell_expansion(pyfile, target, run, expansion):
@pytest.mark.parametrize("python_with_space", [False, True])
def test_shell_expansion(pyfile, tmpdir, target, run, expansion, python_with_space):
if expansion == "expand" and run.console == "internalConsole":
pytest.skip('Shell expansion is not supported for "internalConsole"')

Expand All @@ -57,14 +60,34 @@ def expand(args):
args[i] = arg[1:]
log.info("After expansion: {0}", args)

captured_run_in_terminal_args = []

class Session(debug.Session):
def run_in_terminal(self, args, cwd, env):
captured_run_in_terminal_args.append(args[:]) # Capture a copy of the args
expand(args)
return super().run_in_terminal(args, cwd, env)

argslist = ["0", "$1", "2"]
args = argslist if expansion == "preserve" else " ".join(argslist)

with Session() as session:
# Create a Python wrapper with a space in the path if requested
if python_with_space:
# Create a directory with a space in the name
python_dir = tmpdir / "python with space"
python_dir.mkdir()

if sys.platform == "win32":
wrapper = python_dir / "python.cmd"
wrapper.write(f'@echo off\n"{sys.executable}" %*')
else:
wrapper = python_dir / "python.sh"
wrapper.write(f'#!/bin/sh\nexec "{sys.executable}" "$@"')
os.chmod(wrapper.strpath, 0o777)

session.config["python"] = wrapper.strpath

backchannel = session.open_backchannel()
with run(session, target(code_to_debug, args=args)):
pass
Expand All @@ -73,3 +96,15 @@ def run_in_terminal(self, args, cwd, env):

expand(argslist)
assert argv == [some.str] + argslist

# Verify that the python executable path is correctly quoted if it contains spaces
if python_with_space and captured_run_in_terminal_args:
terminal_args = captured_run_in_terminal_args[0]
log.info("Captured runInTerminal args: {0}", terminal_args)

# Check if the python executable (first arg) contains a space
python_arg = terminal_args[0]
assert "python with space" in python_arg, \
f"Expected 'python with space' in python path: {python_arg}"
if expansion == "expand":
assert (python_arg.startswith('"') or python_arg.startswith("'")), f"Python_arg is not quoted: {python_arg}"
3 changes: 1 addition & 2 deletions tests/debugpy/test_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import pytest

from tests import code, debug, log, net, test_data
from tests.debug import runners, targets
from tests.debug import targets
from tests.patterns import some

pytestmark = pytest.mark.timeout(60)
Expand All @@ -25,7 +25,6 @@ class lines:


@pytest.fixture
@pytest.mark.parametrize("run", [runners.launch, runners.attach_connect["cli"]])
def start_django(run):
def start(session, multiprocess=False):
# No clean way to kill Django server, expect non-zero exit code
Expand Down
3 changes: 1 addition & 2 deletions tests/debugpy/test_flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import sys

from tests import code, debug, log, net, test_data
from tests.debug import runners, targets
from tests.debug import targets
from tests.patterns import some

pytestmark = pytest.mark.timeout(60)
Expand All @@ -27,7 +27,6 @@ class lines:


@pytest.fixture
@pytest.mark.parametrize("run", [runners.launch, runners.attach_connect["cli"]])
def start_flask(run):
def start(session, multiprocess=False):
# No clean way to kill Flask server, expect non-zero exit code
Expand Down
Loading