From 8fadbab0345f2049afe4f9f463d49e441a44da2d Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 00:23:08 -0500 Subject: [PATCH 1/7] enable streaming of integration test output --- .../pants/testutil/pants_integration_test.py | 44 ++++++++++++++++--- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/src/python/pants/testutil/pants_integration_test.py b/src/python/pants/testutil/pants_integration_test.py index c1889b92844..f84bc93eaff 100644 --- a/src/python/pants/testutil/pants_integration_test.py +++ b/src/python/pants/testutil/pants_integration_test.py @@ -9,7 +9,9 @@ import sys from contextlib import contextmanager from dataclasses import dataclass -from typing import Any, Iterator, List, Mapping, Union, cast +from io import BytesIO +from threading import Thread +from typing import Any, Iterator, List, Mapping, TextIO, Union, cast import pytest import toml @@ -67,13 +69,42 @@ class PantsJoinHandle: process: subprocess.Popen workdir: str - def join(self, stdin_data: bytes | str | None = None) -> PantsResult: + def join( + self, stdin_data: bytes | str | None = None, stream_output: bool = False + ) -> PantsResult: """Wait for the pants process to complete, and return a PantsResult for it.""" if stdin_data is not None: stdin_data = ensure_binary(stdin_data) - (stdout, stderr) = self.process.communicate(stdin_data) - if self.process.returncode != PANTS_SUCCEEDED_EXIT_CODE: + def worker(in_stream: BytesIO, buffer: bytearray, out_stream: TextIO) -> None: + while data := in_stream.read1(1024): + buffer.extend(data) + out_stream.write(buffer.decode(errors="ignore")) + out_stream.flush() + + if stream_output: + stdout_buffer = bytearray() + stdout_thread = Thread( + target=worker, args=(self.process.stdout, stdout_buffer, sys.stdout) + ) + stdout_thread.daemon = True + stdout_thread.start() + + stderr_buffer = bytearray() + stderr_thread = Thread( + target=worker, args=(self.process.stderr, stderr_buffer, sys.stderr) + ) + stderr_thread.daemon = True + stderr_thread.start() + + if stdin_data and self.process.stdin: + self.process.stdin.write(stdin_data) + self.process.wait() + stdout, stderr = (bytes(stdout_buffer), bytes(stderr_buffer)) + else: + stdout, stderr = self.process.communicate(stdin_data) + + if self.process.returncode != PANTS_SUCCEEDED_EXIT_CODE or stream_output: render_logs(self.workdir) return PantsResult( @@ -202,6 +233,7 @@ def run_pants_with_workdir( stdin_data: bytes | str | None = None, shell: bool = False, set_pants_ignore: bool = True, + stream_output: bool = False, ) -> PantsResult: handle = run_pants_with_workdir_without_waiting( command, @@ -213,7 +245,7 @@ def run_pants_with_workdir( extra_env=extra_env, set_pants_ignore=set_pants_ignore, ) - return handle.join(stdin_data=stdin_data) + return handle.join(stdin_data=stdin_data, stream_output=stream_output) def run_pants( @@ -224,6 +256,7 @@ def run_pants( config: Mapping | None = None, extra_env: Env | None = None, stdin_data: bytes | str | None = None, + stream_output: bool = False, ) -> PantsResult: """Runs Pants in a subprocess. @@ -244,6 +277,7 @@ def run_pants( config=config, stdin_data=stdin_data, extra_env=extra_env, + stream_output=stream_output, ) From 911bed3c45daf0289cbbb904edb790ff077a4e6a Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 00:37:14 -0500 Subject: [PATCH 2/7] release notes --- docs/notes/2.25.x.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/notes/2.25.x.md b/docs/notes/2.25.x.md index 09991ac124f..09e2c35ac08 100644 --- a/docs/notes/2.25.x.md +++ b/docs/notes/2.25.x.md @@ -209,6 +209,8 @@ The version of Python used by Pants itself is now [3.11](https://docs.python.org The oldest [glibc version](https://www.sourceware.org/glibc/wiki/Glibc%20Timeline) supported by the published Pants wheels is now 2.28. This should have no effect unless you are running on extremely old Linux distributions. See for background context on Python wheels and C libraries. +The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, set the `stream_output` parameter to `True` on specific invocation(s) of the `run_pants_with_workdir` and `run_pants` function, and then run the test with `--debug` so the test is invoked as an interactive process and the pytest `--capture=np` option. + ## Full Changelog From cb95c7d6d367d926916b51e558c310b0ac9bde0f Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 00:44:36 -0500 Subject: [PATCH 3/7] fix release notes --- docs/notes/2.25.x.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notes/2.25.x.md b/docs/notes/2.25.x.md index 09e2c35ac08..48e08406d23 100644 --- a/docs/notes/2.25.x.md +++ b/docs/notes/2.25.x.md @@ -209,7 +209,7 @@ The version of Python used by Pants itself is now [3.11](https://docs.python.org The oldest [glibc version](https://www.sourceware.org/glibc/wiki/Glibc%20Timeline) supported by the published Pants wheels is now 2.28. This should have no effect unless you are running on extremely old Linux distributions. See for background context on Python wheels and C libraries. -The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, set the `stream_output` parameter to `True` on specific invocation(s) of the `run_pants_with_workdir` and `run_pants` function, and then run the test with `--debug` so the test is invoked as an interactive process and the pytest `--capture=np` option. +The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, set the `stream_output` parameter to `True` on specific invocation(s) of the `run_pants_with_workdir` and `run_pants` function, and then run the test with `--debug` so the test is invoked as an interactive process and the pytest `--capture=no` option so pytest does not capture the output during the run. ## Full Changelog From a9e35830d419950f58cc0a882b87f0474ebf1841 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 02:09:36 -0500 Subject: [PATCH 4/7] write only the current data ... --- src/python/pants/testutil/pants_integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/python/pants/testutil/pants_integration_test.py b/src/python/pants/testutil/pants_integration_test.py index f84bc93eaff..cf9acbe7f12 100644 --- a/src/python/pants/testutil/pants_integration_test.py +++ b/src/python/pants/testutil/pants_integration_test.py @@ -79,7 +79,7 @@ def join( def worker(in_stream: BytesIO, buffer: bytearray, out_stream: TextIO) -> None: while data := in_stream.read1(1024): buffer.extend(data) - out_stream.write(buffer.decode(errors="ignore")) + out_stream.write(data.decode(errors="ignore")) out_stream.flush() if stream_output: From f0ba4875d70bf7e3e388fb54e03f2f17e31b0b89 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 19:11:54 -0500 Subject: [PATCH 5/7] Update docs/notes/2.25.x.md Co-authored-by: Huon Wilson --- docs/notes/2.25.x.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/notes/2.25.x.md b/docs/notes/2.25.x.md index 48e08406d23..2fe3be0a2ea 100644 --- a/docs/notes/2.25.x.md +++ b/docs/notes/2.25.x.md @@ -209,7 +209,7 @@ The version of Python used by Pants itself is now [3.11](https://docs.python.org The oldest [glibc version](https://www.sourceware.org/glibc/wiki/Glibc%20Timeline) supported by the published Pants wheels is now 2.28. This should have no effect unless you are running on extremely old Linux distributions. See for background context on Python wheels and C libraries. -The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, set the `stream_output` parameter to `True` on specific invocation(s) of the `run_pants_with_workdir` and `run_pants` function, and then run the test with `--debug` so the test is invoked as an interactive process and the pytest `--capture=no` option so pytest does not capture the output during the run. +The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, adjust specific test(s) to use the new `stream_output` parameter, like `run_pants_with_workdir(..., stream_output=True)` or `run_pants(..., stream_output=True)`, and then run the test `pants test --debug path/to:test -- --capture=no` so the test is invoked as an interactive process and pytest does not capture the output during the run. ## Full Changelog From fc09d6953ff166c00c370c5c269addd7a6b324fa Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 19:16:21 -0500 Subject: [PATCH 6/7] edits --- docs/docs/writing-plugins/the-rules-api/testing-plugins.mdx | 6 ++++++ docs/notes/2.25.x.md | 3 +-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/docs/writing-plugins/the-rules-api/testing-plugins.mdx b/docs/docs/writing-plugins/the-rules-api/testing-plugins.mdx index f3e7cc2875a..67e97b38b0b 100644 --- a/docs/docs/writing-plugins/the-rules-api/testing-plugins.mdx +++ b/docs/docs/writing-plugins/the-rules-api/testing-plugins.mdx @@ -598,3 +598,9 @@ def test_junit_report() -> None: coverage_report = Path(get_buildroot(), "dist", "coverage", "python", "report.json") assert coverage_report.read_text() == "foo" ``` + +### Debugging integration tests + +While developing and debugging integration tests, you can have Pants stream the output for the Pants invocation under test to the console. This is useful, for example, when debugging long-running integration tests which would otherwise show no output while they run. + +To use, adjust specific test(s) to use the `stream_output` parameter, for example, `run_pants_with_workdir(..., stream_output=True)` or `run_pants(..., stream_output=True)`, and then run the test with `pants test --debug path/to:test -- --capture=no` so the test is invoked as an interactive process and pytest does not capture the output during the run. diff --git a/docs/notes/2.25.x.md b/docs/notes/2.25.x.md index 2fe3be0a2ea..9c58074c2f4 100644 --- a/docs/notes/2.25.x.md +++ b/docs/notes/2.25.x.md @@ -209,8 +209,7 @@ The version of Python used by Pants itself is now [3.11](https://docs.python.org The oldest [glibc version](https://www.sourceware.org/glibc/wiki/Glibc%20Timeline) supported by the published Pants wheels is now 2.28. This should have no effect unless you are running on extremely old Linux distributions. See for background context on Python wheels and C libraries. -The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, adjust specific test(s) to use the new `stream_output` parameter, like `run_pants_with_workdir(..., stream_output=True)` or `run_pants(..., stream_output=True)`, and then run the test `pants test --debug path/to:test -- --capture=no` so the test is invoked as an interactive process and pytest does not capture the output during the run. - +The integration testing framework in the `pantsbuild.pants.testutil` package now supports streaming the output of the Pants invocation under test to the console. This is useful when debugging long-running integration tests which would otherwise show no output while they run since the integration test framework previously only captured output to a buffer. To use, adjust specific test(s) to use the new `stream_output` parameter, for example, `run_pants_with_workdir(..., stream_output=True)` or `run_pants(..., stream_output=True)`, and then run the test with `pants test --debug path/to:test -- --capture=no` so the test is invoked as an interactive process and pytest does not capture the output during the run. ## Full Changelog From 378b6e2b1a07fe22897d51378b6638774d3455e3 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Tue, 4 Feb 2025 19:28:49 -0500 Subject: [PATCH 7/7] match stdin behavior used by `.communicate` --- .../pants/testutil/pants_integration_test.py | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/src/python/pants/testutil/pants_integration_test.py b/src/python/pants/testutil/pants_integration_test.py index cf9acbe7f12..9b7bd827969 100644 --- a/src/python/pants/testutil/pants_integration_test.py +++ b/src/python/pants/testutil/pants_integration_test.py @@ -3,6 +3,7 @@ from __future__ import annotations +import errno import glob import os import subprocess @@ -69,12 +70,41 @@ class PantsJoinHandle: process: subprocess.Popen workdir: str + # Write data to the child's stdin pipe and then close the pipe. (Copied from Python source + # at https://github.com/python/cpython/blob/e41ec8e18b078024b02a742272e675ae39778536/Lib/subprocess.py#L1151 + # to handle the same edge cases handled by `subprocess.Popen.communicate`.) + def _stdin_write(self, input: bytes | str | None): + assert self.process.stdin + + if input: + try: + binary_input = ensure_binary(input) + self.process.stdin.write(binary_input) + except BrokenPipeError: + pass # communicate() must ignore broken pipe errors. + except OSError as exc: + if exc.errno == errno.EINVAL: + # bpo-19612, bpo-30418: On Windows, stdin.write() fails + # with EINVAL if the child process exited or if the child + # process is still running but closed the pipe. + pass + else: + raise + + try: + self.process.stdin.close() + except BrokenPipeError: + pass # communicate() must ignore broken pipe errors. + except OSError as exc: + if exc.errno == errno.EINVAL: + pass + else: + raise + def join( self, stdin_data: bytes | str | None = None, stream_output: bool = False ) -> PantsResult: """Wait for the pants process to complete, and return a PantsResult for it.""" - if stdin_data is not None: - stdin_data = ensure_binary(stdin_data) def worker(in_stream: BytesIO, buffer: bytearray, out_stream: TextIO) -> None: while data := in_stream.read1(1024): @@ -97,11 +127,12 @@ def worker(in_stream: BytesIO, buffer: bytearray, out_stream: TextIO) -> None: stderr_thread.daemon = True stderr_thread.start() - if stdin_data and self.process.stdin: - self.process.stdin.write(stdin_data) + self._stdin_write(stdin_data) self.process.wait() stdout, stderr = (bytes(stdout_buffer), bytes(stderr_buffer)) else: + if stdin_data is not None: + stdin_data = ensure_binary(stdin_data) stdout, stderr = self.process.communicate(stdin_data) if self.process.returncode != PANTS_SUCCEEDED_EXIT_CODE or stream_output: