mirror of
https://github.com/microsoft/debugpy.git
synced 2025-12-23 08:48:12 +00:00
* Add parent-session-pid argument Add the ability to specify the parent process id when connecting a new DAP server to the client. This value is used instead of the actual process' parent id so that it can be associated with a specific debug session even if it hasn't been spawned directly by that parent process. * Add tests for new option
250 lines
8.2 KiB
Python
250 lines
8.2 KiB
Python
# Copyright (c) Microsoft Corporation. All rights reserved.
|
|
# Licensed under the MIT License. See LICENSE in the project root
|
|
# for license information.
|
|
|
|
import os
|
|
import pickle
|
|
import pytest
|
|
import subprocess
|
|
import sys
|
|
|
|
# This is used for mocking environment variables
|
|
# See https://docs.python.org/3/library/unittest.mock-examples.html for more info
|
|
from unittest import mock
|
|
|
|
from debugpy.common import log
|
|
from tests.patterns import some
|
|
|
|
@pytest.fixture
|
|
def cli(pyfile):
|
|
@pyfile
|
|
def cli_parser():
|
|
import os
|
|
import pickle
|
|
import sys
|
|
from debugpy.server import cli
|
|
|
|
try:
|
|
cli.parse_args()
|
|
except Exception as exc:
|
|
os.write(1, pickle.dumps(exc))
|
|
sys.exit(1)
|
|
|
|
# Check that sys.argv has the correct type after parsing - there should be no bytes.
|
|
assert all(isinstance(s, str) for s in sys.argv)
|
|
|
|
# We only care about options that correspond to public switches.
|
|
options = {
|
|
name: getattr(cli.options, name)
|
|
for name in [
|
|
"address",
|
|
"config",
|
|
"log_to",
|
|
"log_to_stderr",
|
|
"mode",
|
|
"target",
|
|
"target_kind",
|
|
"wait_for_client",
|
|
"parent_session_pid",
|
|
]
|
|
}
|
|
|
|
# Serialize the command line args and the options to stdout
|
|
serialized_data = pickle.dumps([sys.argv[1:], options])
|
|
os.write(1, serialized_data)
|
|
# Ensure all data is written before process exits
|
|
sys.stdout.flush()
|
|
|
|
def parse(args):
|
|
log.debug("Parsing argv: {0!r}", args)
|
|
try:
|
|
try:
|
|
# Run the CLI parser in a subprocess, and capture its output.
|
|
output = subprocess.check_output(
|
|
[sys.executable, "-u", cli_parser.strpath] + args,
|
|
stderr=subprocess.PIPE # Capture stderr to help with debugging
|
|
)
|
|
|
|
# Deserialize the output and return the parsed argv and options.
|
|
try:
|
|
argv, options = pickle.loads(output)
|
|
except Exception as e:
|
|
log.debug("Failed to deserialize output: {0}, Output was: {1!r}", e, output)
|
|
raise
|
|
except subprocess.CalledProcessError as exc:
|
|
log.debug("Process exited with code {0}. Output: {1!r}, Error: {2!r}",
|
|
exc.returncode, exc.output, exc.stderr)
|
|
raise pickle.loads(exc.output)
|
|
except EOFError:
|
|
# We may have just been shutting down. If so, return an empty argv and options.
|
|
argv, options = [], {}
|
|
|
|
log.debug("Adjusted sys.argv: {0!r}", argv)
|
|
log.debug("Parsed options: {0!r}", options)
|
|
return argv, options
|
|
|
|
return parse
|
|
|
|
|
|
# Test a combination of command line switches
|
|
@pytest.mark.parametrize("target_kind", ["file", "module", "code"])
|
|
@pytest.mark.parametrize("mode", ["listen", "connect"])
|
|
@pytest.mark.parametrize("address", ["8888", "localhost:8888", "[::1]:8888"])
|
|
@pytest.mark.parametrize("wait_for_client", ["", "wait_for_client"])
|
|
@pytest.mark.parametrize("script_args", ["", "script_args"])
|
|
def test_targets(cli, target_kind, mode, address, wait_for_client, script_args):
|
|
expected_options = {
|
|
"mode": mode,
|
|
"target_kind": target_kind,
|
|
"wait_for_client": False
|
|
}
|
|
|
|
args = ["--" + mode, address]
|
|
|
|
host, sep, port = address.rpartition(":")
|
|
host = host.strip("[]")
|
|
if sep:
|
|
expected_options["address"] = (host, int(port))
|
|
else:
|
|
expected_options["address"] = ("127.0.0.1", int(address))
|
|
|
|
if wait_for_client:
|
|
args += ["--wait-for-client"]
|
|
expected_options["wait_for_client"] = True
|
|
|
|
if target_kind == "file":
|
|
target = "spam.py"
|
|
args += [target]
|
|
elif target_kind == "module":
|
|
target = "spam"
|
|
args += ["-m", target]
|
|
elif target_kind == "code":
|
|
target = "123"
|
|
args += ["-c", target]
|
|
else:
|
|
pytest.fail(target_kind)
|
|
expected_options["target"] = target
|
|
|
|
if script_args:
|
|
script_args = [
|
|
"ham",
|
|
"--listen",
|
|
"--wait-for-client",
|
|
"-y",
|
|
"spam",
|
|
"--",
|
|
"--connect",
|
|
"-c",
|
|
"--something",
|
|
"-m",
|
|
]
|
|
args += script_args
|
|
else:
|
|
script_args = []
|
|
|
|
argv, options = cli(args)
|
|
assert argv == script_args
|
|
assert options == some.dict.containing(expected_options)
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
def test_configure_subProcess(cli, value):
|
|
args = ["--listen", "8888", "--configure-subProcess", str(value), "spam.py"]
|
|
_, options = cli(args)
|
|
|
|
assert options["config"]["subProcess"] == value
|
|
|
|
@pytest.mark.parametrize("value", [True, False])
|
|
def test_configure_subProcess_from_environment(cli, value):
|
|
args = ["--listen", "8888", "spam.py"]
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--configure-subProcess " + str(value)}):
|
|
_, options = cli(args)
|
|
|
|
assert options["config"]["subProcess"] == value
|
|
|
|
def test_unsupported_switch(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["--listen", "8888", "--xyz", "123", "spam.py"])
|
|
|
|
assert "unrecognized switch --xyz" in str(ex.value)
|
|
|
|
def test_unsupported_switch_from_environment(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--xyz 123"}):
|
|
cli(["--listen", "8888", "spam.py"])
|
|
|
|
assert "unrecognized switch --xyz" in str(ex.value)
|
|
|
|
def test_unsupported_configure(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["--connect", "127.0.0.1:8888", "--configure-xyz", "123", "spam.py"])
|
|
|
|
assert "unknown property 'xyz'" in str(ex.value)
|
|
|
|
def test_unsupported_configure_from_environment(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--configure-xyz 123"}):
|
|
cli(["--connect", "127.0.0.1:8888", "spam.py"])
|
|
|
|
assert "unknown property 'xyz'" in str(ex.value)
|
|
|
|
def test_address_required(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["-m", "spam"])
|
|
|
|
assert "either --listen or --connect is required" in str(ex.value)
|
|
|
|
def test_missing_target(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["--listen", "8888"])
|
|
|
|
assert "missing target" in str(ex.value)
|
|
|
|
def test_duplicate_switch(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["--listen", "8888", "--listen", "9999", "spam.py"])
|
|
|
|
assert "duplicate switch on command line: --listen" in str(ex.value)
|
|
|
|
def test_duplicate_switch_from_environment(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--listen 8888 --listen 9999"}):
|
|
cli(["spam.py"])
|
|
|
|
assert "duplicate switch from environment: --listen" in str(ex.value)
|
|
|
|
# Test that switches can be read from the environment
|
|
def test_read_switches_from_environment(cli):
|
|
args = ["spam.py"]
|
|
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--connect 5678"}):
|
|
_, options = cli(args)
|
|
|
|
assert options["mode"] == "connect"
|
|
assert options["address"] == ("127.0.0.1", 5678)
|
|
assert options["target"] == "spam.py"
|
|
|
|
# Test that command line switches override environment variables
|
|
def test_override_environment_switch(cli):
|
|
args = ["--connect", "8888", "spam.py"]
|
|
|
|
with mock.patch.dict(os.environ, {"DEBUGPY_EXTRA_ARGV": "--connect 5678"}):
|
|
_, options = cli(args)
|
|
|
|
assert options["mode"] == "connect"
|
|
assert options["address"] == ("127.0.0.1", 8888)
|
|
assert options["target"] == "spam.py"
|
|
|
|
# Test that script args (passed to target) are preserved
|
|
def test_script_args(cli):
|
|
args = ["--listen", "8888", "spam.py", "arg1", "arg2"]
|
|
argv, options = cli(args)
|
|
|
|
assert argv == ["arg1", "arg2"]
|
|
assert options["target"] == "spam.py"
|
|
|
|
# Tests that --parent-session-pid fails with --listen
|
|
def test_script_parent_pid_with_listen_failure(cli):
|
|
with pytest.raises(ValueError) as ex:
|
|
cli(["--listen", "8888", "--parent-session-pid", "1234", "spam.py"])
|
|
|
|
assert "--parent-session-pid requires --connect" in str(ex.value)
|