Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add --debug-adapter flag to test goal #15799

Merged
merged 13 commits into from Jun 14, 2022
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions build-support/bin/_generate_all_lockfiles_helper.py
Expand Up @@ -29,6 +29,7 @@
from pants.backend.python.lint.pylint.subsystem import Pylint
from pants.backend.python.lint.pyupgrade.subsystem import PyUpgrade
from pants.backend.python.lint.yapf.subsystem import Yapf
from pants.backend.python.subsystems.debugpy import DebugPy
from pants.backend.python.subsystems.ipython import IPython
from pants.backend.python.subsystems.lambdex import Lambdex
from pants.backend.python.subsystems.pytest import PyTest
Expand Down Expand Up @@ -104,6 +105,7 @@ def jvm(cls, tool: type[JvmToolBase], *, backend: str | None = None) -> DefaultT
DefaultTool.python(Pylint, backend="pants.backend.python.lint.pylint", source_plugins=True),
DefaultTool.python(Yapf, backend="pants.backend.python.lint.yapf"),
DefaultTool.python(PyUpgrade, backend="pants.backend.experimental.python.lint.pyupgrade"),
DefaultTool.python(DebugPy),
DefaultTool.python(IPython),
DefaultTool.python(Setuptools),
DefaultTool.python(SetuptoolsSCM),
Expand Down
15 changes: 14 additions & 1 deletion src/python/pants/backend/go/goals/test.py
Expand Up @@ -28,7 +28,13 @@
from pants.backend.go.util_rules.import_analysis import ImportConfig, ImportConfigRequest
from pants.backend.go.util_rules.link import LinkedGoBinary, LinkGoBinaryRequest
from pants.backend.go.util_rules.tests_analysis import GeneratedTestMain, GenerateTestMainRequest
from pants.core.goals.test import TestDebugRequest, TestFieldSet, TestResult, TestSubsystem
from pants.core.goals.test import (
TestDebugAdaptorRequest,
TestDebugRequest,
TestFieldSet,
TestResult,
TestSubsystem,
)
from pants.core.target_types import FileSourceField
from pants.core.util_rules.source_files import SourceFiles, SourceFilesRequest
from pants.engine.fs import EMPTY_FILE_DIGEST, AddPrefix, Digest, MergeDigests
Expand Down Expand Up @@ -324,6 +330,13 @@ async def generate_go_tests_debug_request(field_set: GoTestFieldSet) -> TestDebu
raise NotImplementedError("This is a stub.")


@rule
async def generate_go_tests_debug_adaptor_request(
field_set: GoTestFieldSet,
) -> TestDebugAdaptorRequest:
raise NotImplementedError("This is a stub.")


def rules():
return [
*collect_rules(),
Expand Down
15 changes: 14 additions & 1 deletion src/python/pants/backend/helm/test/unittest.py
Expand Up @@ -18,7 +18,13 @@
)
from pants.backend.helm.util_rules.chart import HelmChart, HelmChartRequest
from pants.backend.helm.util_rules.tool import HelmProcess
from pants.core.goals.test import TestDebugRequest, TestFieldSet, TestResult, TestSubsystem
from pants.core.goals.test import (
TestDebugAdaptorRequest,
TestDebugRequest,
TestFieldSet,
TestResult,
TestSubsystem,
)
from pants.core.target_types import ResourceSourceField
from pants.core.util_rules.source_files import SourceFilesRequest
from pants.core.util_rules.stripped_source_files import StrippedSourceFiles
Expand Down Expand Up @@ -142,6 +148,13 @@ async def generate_helm_unittest_debug_request(field_set: HelmUnitTestFieldSet)
raise NotImplementedError("Can not debug Helm unit tests")


@rule
async def generate_helm_unittest_debug_adaptor_request(
field_set: HelmUnitTestFieldSet,
) -> TestDebugAdaptorRequest:
raise NotImplementedError("Can not debug Helm unit tests")


def rules():
return [
*collect_rules(),
Expand Down
63 changes: 59 additions & 4 deletions src/python/pants/backend/python/goals/pytest_runner.py
Expand Up @@ -4,15 +4,17 @@
import logging
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional
from typing import Optional, Tuple

from pants.backend.python.goals.coverage_py import (
CoverageConfig,
CoverageSubsystem,
PytestCoverageData,
)
from pants.backend.python.subsystems.debugpy import DebugPy
from pants.backend.python.subsystems.pytest import PyTest, PythonTestFieldSet
from pants.backend.python.subsystems.setup import PythonSetup
from pants.backend.python.target_types import MainSpecification
from pants.backend.python.util_rules.interpreter_constraints import InterpreterConstraints
from pants.backend.python.util_rules.local_dists import LocalDistsPex, LocalDistsPexRequest
from pants.backend.python.util_rules.pex import Pex, PexRequest, VenvPex, VenvPexProcess
Expand All @@ -25,6 +27,7 @@
BuildPackageDependenciesRequest,
BuiltPackageDependencies,
RuntimePackageDependenciesField,
TestDebugAdaptorRequest,
TestDebugRequest,
TestExtraEnv,
TestFieldSet,
Expand Down Expand Up @@ -58,6 +61,7 @@
from pants.engine.unions import UnionMembership, UnionRule, union
from pants.option.global_options import GlobalOptions
from pants.util.logging import LogLevel
from pants.util.strutil import softwrap

logger = logging.getLogger()

Expand Down Expand Up @@ -138,6 +142,9 @@ async def run_all_setup_plugins(
class TestSetupRequest:
field_set: PythonTestFieldSet
is_debug: bool
main: Optional[MainSpecification] = None # Default's to pytest.main
prepend_argv: Tuple[str, ...] = ()
additional_pexes: Tuple[Pex, ...] = ()


@dataclass(frozen=True)
Expand Down Expand Up @@ -227,9 +234,9 @@ async def setup_pytest_for_target(
PexRequest(
output_filename="pytest_runner.pex",
interpreter_constraints=interpreter_constraints,
main=pytest.main,
main=request.main or pytest.main,
internal_only=True,
pex_path=[pytest_pex, requirements_pex, local_dists.pex],
pex_path=[*request.additional_pexes, pytest_pex, requirements_pex, local_dists.pex],
),
)
config_files_get = Get(
Expand Down Expand Up @@ -314,7 +321,12 @@ async def setup_pytest_for_target(
Process,
VenvPexProcess(
pytest_runner_pex,
argv=(*pytest.args, *coverage_args, *field_set_source_files.files),
argv=(
*request.prepend_argv,
*pytest.args,
*coverage_args,
*field_set_source_files.files,
),
extra_env=extra_env,
input_digest=input_digest,
output_directories=(_EXTRA_OUTPUT_DIR,),
Expand Down Expand Up @@ -381,6 +393,49 @@ async def debug_python_test(field_set: PythonTestFieldSet) -> TestDebugRequest:
)


@rule(desc="Set up debugpy to run an interactive Pytest session", level=LogLevel.DEBUG)
async def debugpy_python_test(
field_set: PythonTestFieldSet,
debugpy: DebugPy,
pytest: PyTest,
) -> TestDebugAdaptorRequest:
debugpy_pex = await Get(Pex, PexRequest, debugpy.to_pex_request())
if pytest.main.spec != "pytest":
logger.warn(
thejcannon marked this conversation as resolved.
Show resolved Hide resolved
softwrap(
"""
Ignoring custom [pytest].console_script/entry_point when using --use-debug-adaptor.
`debugpy` (Python's Debug Adaptor) doesn't support this use-case yet.
"""
)
)

setup = await Get(
TestSetup,
TestSetupRequest(
field_set,
is_debug=True,
main=debugpy.main,
prepend_argv=(
"--listen",
f"{debugpy.host}:{debugpy.port}",
"--wait-for-client",
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we could not have the enforcement all implementations want this, and instead make the user specify --debugpy-args=... but this is much nicer to the user.

# @TODO: Techincally we should use `pytest.main`, however `debugpy` doesn't support
# launching an entry_point.
# https://github.com/microsoft/debugpy/issues/955
"-m",
"pytest",
),
additional_pexes=(debugpy_pex,),
),
)
return TestDebugAdaptorRequest(
InteractiveProcess.from_process(
setup.process, forward_signals_to_process=False, restartable=True
)
)


# -----------------------------------------------------------------------------------------
# `runtime_package_dependencies` plugin
# -----------------------------------------------------------------------------------------
Expand Down
Expand Up @@ -29,6 +29,7 @@
)
from pants.backend.python.util_rules import local_dists, pex_from_targets
from pants.core.goals.test import (
TestDebugAdaptorRequest,
TestDebugRequest,
TestResult,
build_runtime_package_dependencies,
Expand Down Expand Up @@ -67,6 +68,7 @@ def rule_runner() -> RuleRunner:
*setuptools_rules(),
QueryRule(TestResult, (PythonTestFieldSet,)),
QueryRule(TestDebugRequest, (PythonTestFieldSet,)),
QueryRule(TestDebugAdaptorRequest, (PythonTestFieldSet,)),
],
target_types=[
PexBinary,
Expand Down Expand Up @@ -578,3 +580,37 @@ def is_applicable(tgt_name: str, fp: str) -> bool:

assert not is_applicable("t1", "test_skip_me.py")
assert is_applicable("t2", "test_foo.py")


def test_debug_adaptor_request_argv(rule_runner: RuleRunner) -> None:
rule_runner.write_files(
{
f"{PACKAGE}/test_foo.py": "",
f"{PACKAGE}/BUILD": dedent(
"""\
python_tests(name='tests')
"""
),
}
)

args = [
"--backend-packages=pants.backend.python",
f"--source-root-patterns={SOURCE_ROOT}",
]
rule_runner.set_options(args, env_inherit={"PATH", "PYENV_ROOT", "HOME"})
tgt = rule_runner.get_target(
Address(PACKAGE, target_name="tests", relative_file_path="test_foo.py")
)
inputs = [PythonTestFieldSet.create(tgt)]
request = rule_runner.request(TestDebugAdaptorRequest, inputs)
assert request.process is not None
assert request.process.argv == (
"./pytest_runner.pex_pex_shim.sh",
"--listen",
"127.0.0.1:5678",
"--wait-for-client",
"-m",
"pytest",
"tests/python/pants_test/test_foo.py",
)
3 changes: 2 additions & 1 deletion src/python/pants/backend/python/register.py
Expand Up @@ -28,7 +28,7 @@
from pants.backend.python.macros.poetry_requirements import PoetryRequirementsTargetGenerator
from pants.backend.python.macros.python_artifact import PythonArtifact
from pants.backend.python.macros.python_requirements import PythonRequirementsTargetGenerator
from pants.backend.python.subsystems import ipython, pytest, python_native_code, setuptools
from pants.backend.python.subsystems import debugpy, ipython, pytest, python_native_code, setuptools
from pants.backend.python.target_types import (
PexBinariesGeneratorTarget,
PexBinary,
Expand Down Expand Up @@ -61,6 +61,7 @@ def rules():
return (
*ancestor_files.rules(),
*coverage_py.rules(),
*debugpy.rules(),
*dependency_inference_rules.rules(),
*export.rules(),
*ipython.rules(),
Expand Down