#5607 add methods Send Input To Process, Read Output From Process, Process Output Should Contain - #5612
#5607 add methods Send Input To Process, Read Output From Process, Process Output Should Contain#5612IlfirinPL wants to merge 7 commits into
Conversation
|
I took a quick look at the code and I'm sorry to say that it doesn't look too good. Especially
There are also various problems with the code style, test coverage, etc., but I don't want to spend time going through these problems in detail if the above issues aren't resolved. A problem is that I'm not sure is it even possible to fix the problems reliably. |
themavik
left a comment
There was a problem hiding this comment.
Reviewed the changes — the implementation looks correct and follows the existing patterns.
themavik
left a comment
There was a problem hiding this comment.
this could break if Python changes the NameError hint text—the stderr assertions match the full "Did you mean..." string.
nit: read_output_from_process slurps the whole stdout/stderr file each call; fine for interactive tests, noisy if someone points it at a huge log.
There was a problem hiding this comment.
Pull request overview
Adds new Process library keywords intended to support interactive processes by writing to stdin, reading output during execution, and asserting on that output, along with acceptance tests.
Changes:
- Added
Send Input To Process,Read Output From Process, andProcess Output Should Containkeywords toProcess.py. - Added an interactive acceptance test suite (
interactive.robot) covering the new keywords. - Added a helper keyword in
process_resource.robotto start an interactive Python process with redirected stdio.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
src/robot/libraries/Process.py |
Implements new interactive-process keywords for stdin writing, output reading, and output assertions. |
atest/testdata/standard_libraries/process/process_resource.robot |
Adds a resource keyword to start an interactive Python process used by the new atests. |
atest/testdata/standard_libraries/process/interactive.robot |
New atest suite validating interactive send/read/contain functionality. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| Read STDERR | ||
| Send Input To Process error | ||
| ${stderr} Read Output From Process stream=stderr lines=-1 | ||
| Should Contain ${stderr} NameError: name 'error' is not defined. Did you mean: 'OSError'? |
There was a problem hiding this comment.
The assertion checks the exact NameError message including the Did you mean: ... suggestion, which varies across Python versions and environments. To avoid brittle atests, assert only the stable portion (e.g. NameError: name 'error' is not defined) rather than the full suggested text.
| Send Input To Process error | ||
| ${stdout} ${stderr} Read Output From Process stream=both lines=-1 | ||
| Should Be Equal ${stdout} Hello | ||
| Should Contain ${stderr} NameError: name 'error' is not defined. Did you mean: 'OSError'? |
There was a problem hiding this comment.
Same brittleness as above: the Did you mean: ... part of NameError messages is not stable across Python versions. Prefer asserting only the stable substring.
| # Encode input data if it's a string | ||
| if isinstance(input_data, str): | ||
| data_to_send = input_data | ||
| if end_of_line is not None: | ||
| data_to_send += end_of_line | ||
| input_bytes = console_encode(data_to_send, force=True) | ||
| elif isinstance(input_data, bytes): | ||
| input_bytes = input_data | ||
| if end_of_line is not None: | ||
| input_bytes += console_encode(end_of_line, force=True) | ||
| else: | ||
| input_bytes = ( | ||
| console_encode(end_of_line, force=True) | ||
| if end_of_line is not None | ||
| else b"" | ||
| ) |
There was a problem hiding this comment.
send_input_to_process claims to encode strings using the process output_encoding, but it currently always uses console_encode(..., force=True) without passing the process’ configured encoding. This will send incorrectly encoded bytes when the process was started with output_encoding other than CONSOLE (e.g. SYSTEM, UTF-8). Use the encoding stored in the associated ProcessResult (or configuration) when encoding both input_data and end_of_line.
| # Encode input data if it's a string | |
| if isinstance(input_data, str): | |
| data_to_send = input_data | |
| if end_of_line is not None: | |
| data_to_send += end_of_line | |
| input_bytes = console_encode(data_to_send, force=True) | |
| elif isinstance(input_data, bytes): | |
| input_bytes = input_data | |
| if end_of_line is not None: | |
| input_bytes += console_encode(end_of_line, force=True) | |
| else: | |
| input_bytes = ( | |
| console_encode(end_of_line, force=True) | |
| if end_of_line is not None | |
| else b"" | |
| ) | |
| # Determine encoding configured for this process, if any. | |
| encoding = ( | |
| getattr(process, "encoding", None) | |
| or getattr(process, "output_encoding", None) | |
| ) | |
| def _encode(text: str) -> bytes: | |
| """Encode text according to the process output encoding.""" | |
| if encoding in (None, "CONSOLE"): | |
| # Preserve existing behavior when no encoding is configured | |
| # or when console encoding is explicitly requested. | |
| return console_encode(text, force=True) | |
| if encoding in ("SYSTEM", LOCALE_ENCODING): | |
| return system_encode(text) | |
| try: | |
| return text.encode(encoding, errors="replace") | |
| except (LookupError, TypeError): | |
| # Fallback to console encoding if the encoding is invalid | |
| # or not a usable value. | |
| return console_encode(text, force=True) | |
| # Encode input data if it's a string | |
| if isinstance(input_data, str): | |
| data_to_send = input_data | |
| if end_of_line is not None: | |
| data_to_send += end_of_line | |
| input_bytes = _encode(data_to_send) | |
| elif isinstance(input_data, bytes): | |
| input_bytes = input_data | |
| if end_of_line is not None: | |
| input_bytes += _encode(end_of_line) | |
| else: | |
| input_bytes = _encode(end_of_line) if end_of_line is not None else b"" |
| def send_input_to_process( | ||
| self, | ||
| input_data: "str | bytes | None" = None, | ||
| handle: Handle = None, | ||
| end_of_line: "str | None" = "\n", | ||
| ) -> None: |
There was a problem hiding this comment.
The positional argument order of send_input_to_process makes Send Input To Process ${handle} text interpret ${handle} as input_data. Because most Process keywords take handle as the first argument (and the linked issue example shows handle first), consider moving handle to be the first argument (or otherwise supporting the common “handle first” call style) to avoid a confusing API.
| else: | ||
| if stream in ("stderr", "both"): | ||
| raise RuntimeError( | ||
| "Process not started with stderr redirected to file. " | ||
| "Use: Start Process | cmd | stderr=/path/file |" | ||
| ) |
There was a problem hiding this comment.
Same issue as above for stderr: the validation uses the original (potentially mixed/upper-case) stream value. Normalize stream before these checks so stream=STDERR and similar behave correctly.
| Send Input To Process print('Hello') | ||
| ${stdout} Read Output From Process | ||
| Should Be Equal ${stdout} Hello | ||
|
|
There was a problem hiding this comment.
This test assumes the latest stdout line equals exactly Hello, but the interactive Python process may write additional lines (banner/prompt) into the redirected stdout file. To make the test robust, consider reading more lines (lines=-1) and asserting Should Contain for Hello, or starting Python in quiet mode (e.g. -q) so the output is deterministic.
|
|
||
| Read Output From Process stderr | ||
| Send Input To Process error | ||
| Process Output Should Contain NameError: name 'error' is not defined. Did you mean: 'OSError'? stream=stderr lines=-1 |
There was a problem hiding this comment.
Same brittleness as above: this exact NameError message (including suggestions) varies across Python versions/environments. Prefer matching only the stable part.
| else: | ||
| if stream in ("stdout", "both"): | ||
| raise RuntimeError( | ||
| "Process not started with stdout redirected to file. " | ||
| "Use: Start Process | cmd | stdout=/path/file |" | ||
| ) |
There was a problem hiding this comment.
read_output_from_process treats stream case-sensitively in the “not redirected to file” checks, but later lowercases it. Passing stream=STDOUT/STDERR/BOTH will skip these guards and can return empty output instead of raising the intended error. Normalize stream (e.g. .lower()) before the validation checks and use the normalized value consistently.
| logger.debug(f"Read std output from file:{result_obj.stdout_path}") | ||
| with open(result_obj.stdout_path, "rb") as f: | ||
| stdout = f.read() |
There was a problem hiding this comment.
read_output_from_process reads the entire stdout/stderr file into memory on every call and then splits all lines to trim. In interactive use where this keyword is called repeatedly, this becomes increasingly expensive as the files grow. Consider tracking per-handle read offsets (to return only new output) or tailing only the needed last N lines instead of reading the whole file each time.
| Run Python Process Interactive | ||
| [Arguments] ${command}=${EMPTY} ${alias}=${NONE} ${stdout}=${STDOUT} ${stderr}=${STDERR} ${stdin}=PIPE | ||
| ${result}= Start Process python -i ${command} | ||
| ... alias=${alias} stdout=${stdout} stderr=${stderr} stdin=${stdin} | ||
| RETURN ${result} |
There was a problem hiding this comment.
Keyword name Run Python Process Interactive is misleading because it uses Start Process and returns a process handle (like the existing Start Python Process). Consider renaming to Start Python Process Interactive (or changing it to use Run Process and return a result) so callers can infer what it returns.
| Run Python Process Interactive | |
| [Arguments] ${command}=${EMPTY} ${alias}=${NONE} ${stdout}=${STDOUT} ${stderr}=${STDERR} ${stdin}=PIPE | |
| ${result}= Start Process python -i ${command} | |
| ... alias=${alias} stdout=${stdout} stderr=${stderr} stdin=${stdin} | |
| RETURN ${result} | |
| Start Python Process Interactive | |
| [Arguments] ${command}=${EMPTY} ${alias}=${NONE} ${stdout}=${STDOUT} ${stderr}=${STDERR} ${stdin}=PIPE | |
| ${handle}= Start Process python -i ${command} | |
| ... alias=${alias} stdout=${stdout} stderr=${stderr} stdin=${stdin} | |
| RETURN ${handle} |
Add Following Methods
#5607 add methods Send Input To Process, Read Output From Process, Process Output Should Contain