diff --git a/docs/notes/2.26.x.md b/docs/notes/2.26.x.md index 48835fc3540..2ae79425a7e 100644 --- a/docs/notes/2.26.x.md +++ b/docs/notes/2.26.x.md @@ -36,6 +36,9 @@ Some deprecations have expired and been removed: The default version of the [Pex](https://docs.pex-tool.org/) tool has been updated from 2.32.0 to [2.33.0](https://github.com/pex-tool/pex/releases/tag/v2.33.0). Among many improvements and bug fixes, this unlocks support for pip [25.0.1](https://pip.pypa.io/en/stable/news/#v25-0-1). +#### Shell + +The `experiemental_test_shell_command` target type may now be used with the `test` goal's `--debug` flag to execute the test interactively. #### Terraform diff --git a/src/python/pants/backend/shell/goals/test.py b/src/python/pants/backend/shell/goals/test.py index 53a850c237b..85814d7cce7 100644 --- a/src/python/pants/backend/shell/goals/test.py +++ b/src/python/pants/backend/shell/goals/test.py @@ -14,10 +14,18 @@ ) from pants.backend.shell.util_rules import shell_command from pants.backend.shell.util_rules.shell_command import ShellCommandProcessFromTargetRequest -from pants.core.goals.test import TestExtraEnv, TestFieldSet, TestRequest, TestResult, TestSubsystem +from pants.core.goals.test import ( + TestDebugRequest, + TestExtraEnv, + TestFieldSet, + TestRequest, + TestResult, + TestSubsystem, +) from pants.core.util_rules.environments import EnvironmentField from pants.engine.internals.selectors import Get from pants.engine.process import ( + InteractiveProcess, Process, ProcessCacheScope, ProcessResultWithRetries, @@ -46,6 +54,7 @@ def opt_out(cls, tgt: Target) -> bool: class ShellTestRequest(TestRequest): tool_subsystem = ShellTestSubsystem field_set_type = TestShellCommandFieldSet + supports_debug = True @rule(desc="Test with shell command", level=LogLevel.DEBUG) @@ -88,6 +97,31 @@ async def test_shell_command( ) +@rule(desc="Test with shell command (interactively)", level=LogLevel.DEBUG) +async def test_shell_command_interactively( + batch: ShellTestRequest.Batch[TestShellCommandFieldSet, Any], +) -> TestDebugRequest: + field_set = batch.single_element + wrapped_tgt = await Get( + WrappedTarget, + WrappedTargetRequest(field_set.address, description_of_origin=""), + ) + + shell_process = await Get( + Process, + ShellCommandProcessFromTargetRequest(wrapped_tgt.target), + ) + + # This is probably not strictly necessary given the use of `InteractiveProcess` but good to be correct in any event. + shell_process = dataclasses.replace(shell_process, cache_scope=ProcessCacheScope.PER_SESSION) + + return TestDebugRequest( + InteractiveProcess.from_process( + shell_process, forward_signals_to_process=False, restartable=True + ) + ) + + def rules(): return ( *collect_rules(), diff --git a/src/python/pants/backend/shell/goals/test_test.py b/src/python/pants/backend/shell/goals/test_test.py index 23109cc6447..85202c4c617 100644 --- a/src/python/pants/backend/shell/goals/test_test.py +++ b/src/python/pants/backend/shell/goals/test_test.py @@ -17,11 +17,11 @@ ) from pants.build_graph.address import Address from pants.core.goals import package -from pants.core.goals.test import TestResult, get_filtered_environment +from pants.core.goals.test import TestDebugRequest, TestResult, get_filtered_environment from pants.core.util_rules import archive, source_files from pants.engine.rules import QueryRule from pants.engine.target import Target -from pants.testutil.rule_runner import RuleRunner +from pants.testutil.rule_runner import RuleRunner, mock_console ATTEMPTS_DEFAULT_OPTION = 2 @@ -36,6 +36,7 @@ def rule_runner() -> RuleRunner: *package.rules(), get_filtered_environment, QueryRule(TestResult, (ShellTestRequest.Batch,)), + QueryRule(TestDebugRequest, [ShellTestRequest.Batch]), ], target_types=[ ShellSourcesGeneratorTarget, @@ -95,11 +96,11 @@ def test_shell_command_as_test(rule_runner: RuleRunner) -> None: ) (Path(rule_runner.build_root) / "test.sh").chmod(0o555) + def test_batch_for_target(test_target: Target) -> ShellTestRequest.Batch: + return ShellTestRequest.Batch("", (TestShellCommandFieldSet.create(test_target),), None) + def run_test(test_target: Target) -> TestResult: - input: ShellTestRequest.Batch = ShellTestRequest.Batch( - "", (TestShellCommandFieldSet.create(test_target),), None - ) - return rule_runner.request(TestResult, [input]) + return rule_runner.request(TestResult, [test_batch_for_target(test_target)]) pass_target = rule_runner.get_target(Address("", target_name="pass")) pass_result = run_test(pass_target) @@ -111,3 +112,14 @@ def run_test(test_target: Target) -> TestResult: assert fail_result.exit_code == 1 assert fail_result.stdout_bytes == b"does not contain 'xyzzy'\n" assert len(fail_result.process_results) == ATTEMPTS_DEFAULT_OPTION + + # Check whether interactive execution via the `test` goal's `--debug` flags succeeds. + pass_debug_request = rule_runner.request(TestDebugRequest, [test_batch_for_target(pass_target)]) + with mock_console(rule_runner.options_bootstrapper): + pass_debug_result = rule_runner.run_interactive_process(pass_debug_request.process) + assert pass_debug_result.exit_code == 0 + + fail_debug_request = rule_runner.request(TestDebugRequest, [test_batch_for_target(pass_target)]) + with mock_console(rule_runner.options_bootstrapper): + fail_debug_result = rule_runner.run_interactive_process(fail_debug_request.process) + assert fail_debug_result.exit_code == 0