labgrid
labgrid copied to clipboard
Add support for running process asynchronously
Description Adds support for running processes asynchronously with the main test code. Attempts to incorporate the feed back from #552
Checklist
- [ ] Documentation for the feature
- [x] Tests for the feature
- [ ] The arguments and description in doc/configuration.rst have been updated
- [ ] Add a section on how to use the feature to doc/usage.rst
- [ ] Add a section on how to use the feature to doc/development.rst
- [ ] CHANGES.rst has been updated
- [x] PR has been tested
- [ ] Man pages have been regenerated
Codecov Report
Merging #835 (58d847d) into master (bd0ede7) will increase coverage by
0.5%. The diff coverage is73.3%.
@@ Coverage Diff @@
## master #835 +/- ##
========================================
+ Coverage 56.9% 57.4% +0.5%
========================================
Files 145 146 +1
Lines 10859 11102 +243
========================================
+ Hits 6180 6374 +194
- Misses 4679 4728 +49
| Impacted Files | Coverage Δ | |
|---|---|---|
| labgrid/util/timeout.py | 100.0% <ø> (ø) |
|
| labgrid/util/expect.py | 73.9% <28.5%> (-19.9%) |
:arrow_down: |
| labgrid/driver/bareboxdriver.py | 60.3% <32.0%> (-2.1%) |
:arrow_down: |
| labgrid/driver/ubootdriver.py | 60.7% <36.0%> (-0.3%) |
:arrow_down: |
| labgrid/driver/shelldriver.py | 29.1% <37.5%> (+0.8%) |
:arrow_up: |
| labgrid/util/marker.py | 83.3% <82.6%> (-16.7%) |
:arrow_down: |
| labgrid/driver/sshdriver.py | 63.5% <90.3%> (+5.2%) |
:arrow_up: |
| labgrid/exceptions.py | 100.0% <100.0%> (ø) |
|
| labgrid/protocol/__init__.py | 100.0% <100.0%> (ø) |
|
| labgrid/protocol/commandprotocol.py | 100.0% <100.0%> (ø) |
|
| ... and 4 more |
Continue to review full report at Codecov.
Legend - Click here to learn more
Δ = absolute <relative> (impact),ø = not affected,? = missing dataPowered by Codecov. Last update bd0ede7...58d847d. Read the comment docs.
Overall this design looks like what we had in mind in #552. Thank you very much for the implementation, looking forward to testing this on my lab at home.
It is unfortunately a little hard to write automated tests for most of this since it requires actual hardware interaction, but I'll take a stab at it. I was able to test on actual hardware (Raspberry Pi 4 w/ u-boot) with the following test suite:
import socket
import os
import time
def test_uboot_async(uboot_command):
with uboot_command.popen("echo hello") as p:
assert p.read(4).decode("utf-8").splitlines() == ["hell"]
assert p.read().decode("utf-8").splitlines() == ["o"]
with uboot_command.popen("echo bzap hello bzap") as p:
index, before, match, after = p.expect("hello")
assert index == 0
assert match.group(0).decode("utf-8") == "hello"
assert before.decode("utf-8") == "bzap "
assert after.decode("utf-8") == "hello"
assert p.read().decode("utf-8").splitlines() == [" bzap"]
def test_ssh_async(ssh_command):
with ssh_command.popen("echo hello") as p:
assert p.read(4).decode("utf-8").splitlines() == ["hell"]
assert p.read().decode("utf-8").splitlines() == ["o"]
with ssh_command.popen("echo bzap hello bzap") as p:
index, before, match, after = p.expect("hello")
assert index == 0
assert match.group(0).decode("utf-8") == "hello"
assert before.decode("utf-8") == "bzap "
assert after.decode("utf-8") == "hello"
assert p.read().decode("utf-8").splitlines() == [" bzap"]
def test_shell_nc(shell_command, ssh_command):
with shell_command.popen("nc -l -p 5555") as p:
# Wait for netcat to start listening
time.sleep(2)
with ssh_command.forward_local_port(5555) as local_port:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("localhost", local_port))
s.sendall(b"Hello World")
p.expect(b"Hello World")
time.sleep(2)
def test_ssh_nc(ssh_command):
with ssh_command.popen("nc -l -p 5555 127.0.0.1") as p:
# Wait for netcat to start listening
time.sleep(2)
with ssh_command.forward_local_port(5555) as local_port:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("localhost", local_port))
s.sendall(b"Hello World")
p.expect(b"Hello World")
time.sleep(2)
def test_shell_async(shell_command):
with shell_command.popen("echo hello") as p:
assert p.read(4).decode("utf-8").splitlines() == ["hell"]
assert p.read().decode("utf-8").splitlines() == ["o"]
with shell_command.popen("echo bzap hello bzap") as p:
index, before, match, after = p.expect("hello")
assert index == 0
assert match.group(0).decode("utf-8") == "hello"
assert before.decode("utf-8") == "bzap "
assert after.decode("utf-8") == "hello"
assert p.read().decode("utf-8").splitlines() == [" bzap"]
def test_shell_interrupt(shell_command):
with shell_command.popen("yes") as p:
l = p.read(1024).decode("utf-8").splitlines()
assert l == ["y"] * len(l)
shell_command.run_check("true")
Note that I wasn't able to test the barebox driver, so manual testing there would be appreciated
I added some tests, which revealed a few bugs that needed fixed. I think it's all good to go now though (although I still have no way to test barebox)
Great work! I found some trivial things. I'll have to dive deeper into this later.
This should be mentioned in
CHANGES.rstand probably needs some documentation elsewhere?
I added a little to CHANGES.rst; I can add more documentation but I'm not really sure where it should go.
I've tested this briefly now.
Issues I stumbled upon:
* `BareboxDriver` now times out on `run()`
I don't have any way of testing barebox, can you provide more information so I can fix it?
* running multiple processes on `BareboxDriver`/`ShellDriver` is not forbidden, but runs in timeouts
Do we want to explicitly disallow that? I'm fine with that if that's what everyone wants to do.
* calling `stop()` on `ConsoleMarkerProcess` returned by `ShellDriver.popen()` leads to a timeout (e.g. stopping a "sleep 100") - the implementation looks incomplete, I don't see the prompt being detected on `stop()`Open questions:
* why is `SSHDriver.run()` not using `popen()`?
It would be nice, but I can't do that without breaking the SSH driver because of the stderr_merge option, which is not possible to implement with popen()
* I do not really understand the difference between `read_nonblocking()`/`read()`
read_nonblocking allows a CommandProcessProcotol to be directly used as the backend for a popen.spawn object, since the latter requires the the read_nonblocking function to be implemented. read is the API for normal folks :)
Styling:
* we use f-strings where appropriate since #838, please do the same
I tried to use f-strings everywhere.... IHMO it's confusing to developers to require different string formatting for different uses (unless there is some really compelling performance reason to do otherwise).
Note: this is no comprehensive review yet, just a collection of things I saw while testing with a QEMU and a physical target.
BareboxDrivernow times out onrun()I don't have any way of testing barebox, can you provide more information so I can fix it?
Here is my demo script:
#!/usr/bin/env python3
import labgrid
labgrid.StepReporter()
env = labgrid.Environment("beaglebone.yaml")
target = env.get_target()
barebox = target.get_driver("BareboxDriver")
target.activate(barebox)
barebox.run("echo hi")
This results in: labgrid-barebox-run.log
I have not looked into it yet.
I think I've addressed most of the comment, is there anything else needed to merge this?
@JoshuaWatt Thanks, re-requesting review or a short notice like yours is always welcome, so we know when to look at PRs again. I need to look at/test the current state again. I'll come back to you.
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy):
strategy.transition("shell")
with strategy.shell.popen("sleep 40") as p:
pass
This ends up with a TIMEOUT exception.
Another thing: how would you read the output of a long-running command (e.g. find /) asynchronously? Could you provide an example? I tried something with while p.poll() is None, but the poll() method blocks for a second if the process is still alive, so that feels wrong.
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: passThis ends up with a
TIMEOUTexception.
Ah, this has nothing to do with the long-running command. The context manager hits CTRL-c on close, which prevents the second marker from being echoed (on the shell, not in barebox though). That's why we run into the timeout there.
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: passThis ends up with a
TIMEOUTexception.Another thing: how would you read the output of a long-running command (e.g.
find /) asynchronously? Could you provide an example? I tried something withwhile p.poll() is None, but thepoll()method blocks for a second if the process is still alive, so that feels wrong.
poll is for checking if the process is still alive; to read output use p.read() or p.expect()
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: passThis ends up with a
TIMEOUTexception.Ah, this has nothing to do with the long-running command. The context manager hits CTRL-c on close, which prevents the second marker from being echoed (on the shell, not in barebox though). That's why we run into the timeout there.
Does something need to change? It seems like it's doing what you are asking for (launching the process, then immediately terminating it). If you want to wait for it to exit normally, use p.poll() in a loop, or p.wait()
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: passThis ends up with a
TIMEOUTexception. Another thing: how would you read the output of a long-running command (e.g.find /) asynchronously? Could you provide an example? I tried something withwhile p.poll() is None, but thepoll()method blocks for a second if the process is still alive, so that feels wrong.
pollis for checking if the process is still alive; to read output usep.read()orp.expect()
Okay, will try that.
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: pass This ends up with a `TIMEOUT` exception.Ah, this has nothing to do with the long-running command. The context manager hits CTRL-c on close, which prevents the second marker from being echoed (on the shell, not in barebox though). That's why we run into the timeout there.
Does something need to change? It seems like it's doing what you are asking for (launching the process, then immediately terminating it). If you want to wait for it to exit normally, use
p.poll()in a loop, orp.wait()
I think the behavior between barebox and shell should be consistent. I would not expect to get a TIMEOUT exception if I do not wait for the process to finish. Do you agree?
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: passThis ends up with a
TIMEOUTexception. Another thing: how would you read the output of a long-running command (e.g.find /) asynchronously? Could you provide an example? I tried something withwhile p.poll() is None, but thepoll()method blocks for a second if the process is still alive, so that feels wrong.
pollis for checking if the process is still alive; to read output usep.read()orp.expect()Okay, will try that.
How would you run a command that takes longer than 30 seconds?
def test_async_output(strategy): strategy.transition("shell") with strategy.shell.popen("sleep 40") as p: pass This ends up with a `TIMEOUT` exception.Ah, this has nothing to do with the long-running command. The context manager hits CTRL-c on close, which prevents the second marker from being echoed (on the shell, not in barebox though). That's why we run into the timeout there.
Does something need to change? It seems like it's doing what you are asking for (launching the process, then immediately terminating it). If you want to wait for it to exit normally, use
p.poll()in a loop, orp.wait()I think the behavior between barebox and shell should be consistent. I would not expect to get a
TIMEOUTexception if I do not wait for the process to finish. Do you agree?
Ya, that makes sense; let me see what I can do
@Bastian-Krause Ok, I tracked this down to a behavior difference is bash (which I was using) vs. busybox. I've corrected it and now it should work the same for both shells
For those interested, the difference is:
bash:
raspberrypi4-64:~# echo -n 'ABC'; sleep 10; echo 'ABC' $?;
ABC^C
ABC 130
raspberrypi4-64:~#
busybox:
raspberrypi4-64:~# echo -n 'ABC'; sleep 10; echo 'ABC' $?;
ABC^C
raspberrypi4-64:~#
Note that interrupting the command on busybox prevents all subsequent commands (the echo with the exit code) from running, where as they still run under bash.
Ping
Ping
Ping
Ping
Hi,
I deployed this internally and let it run over our internal test suites and didn't find an error related to this PR. Thats great, thanks a lot for the work! The issue I see at the moment is that the verbose pytest output becomes unreadable:
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'y'>, b'y')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b't'>, b't')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'e'>, b'e')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'm'>, b'm')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'/'>, b'/')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'y'>, b'y')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b't'>, b't')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'e'>, b'e')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'm'>, b'm')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'd'>, b'd')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'-'>, b'-')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'a'>, b'a')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'u'>, b'u')
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}
Since we match on a single byte a lot of the steps don't contain any meaningful information. I am not sure how to solve this, one way would be to create a helper which does not contain the step decorator to supress this output. Note that expect() is still called by drivers and I wouldn't want to get rid of this as is.
Hi,
I deployed this internally and let it run over our internal test suites and didn't find an error related to this PR. Thats great, thanks a lot for the work! The issue I see at the moment is that the verbose pytest output becomes unreadable:
10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'y'>, b'y') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b't'>, b't') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'e'>, b'e') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'm'>, b'm') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'/'>, b'/') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'y'>, b'y') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b's'>, b's') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b't'>, b't') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'e'>, b'e') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'm'>, b'm') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'd'>, b'd') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'-'>, b'-') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'a'>, b'a') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]} 10:13:34 SerialDriver.expect state='stop' result=(0, b'', <re.Match object; span=(0, 1), match=b'u'>, b'u') 10:13:34 SerialDriver.expect state='start' args={'pattern': [re.compile(b'^(?!WASPAEMYUO)(?!WASPAEMYU$)(?!WASPAEMY$)(?!WASPAEM$)(?!WASPAE$)(?!WASPA$)(?!WASP$)(?!WAS$)(?!WA$)(?!W$).', re.DOTALL), re.compile(b'^WASPAEMYUO\\s+(\\d+)\\s+.*root@[^:]+:[^ ]+'), <class 'pexpect.exceptions.TIMEOUT'>]}Since we match on a single byte a lot of the steps don't contain any meaningful information. I am not sure how to solve this, one way would be to create a helper which does not contain the step decorator to supress this output. Note that
expect()is still called by drivers and I wouldn't want to get rid of this as is.
Yes, I noticed that; do you have any suggestions for how to make it more reasonable?
Yes, I noticed that; do you have any suggestions for how to make it more reasonable?
Hah, you did give a suggestion! My fault for not reading thoroughly. I'll look into doing that.