I want to test a Python function that executes a shell command.
According to testfixtures there are two approaches:
My function is called run_in_shell
. Although the subprocess module is the obvious way to implement it, my function doesn't explicitly depend on it, so I'm trying to do the "real" test.
import subprocess
def run_in_shell(command, shell_name=None):
"""Run command in default shell. Use named shell instead if given."""
subprocess.run(command, shell=True, executable=shell_name)
This test shows that the function can execute a command with the default shell.
import pytest
def test_with_command_string(capfd):
run_in_shell("echo 'hello'")
cap = capfd.readouterr()
assert cap.out.strip() == "hello"
assert cap.err.strip() == ""
I also want to show that it can execute in the user's chosen shell such as /bin/bash
.
The invocation is simple enough. This prints hello
to the terminal.
run_in_shell("echo 'hello'", shell_name="/bin/bash")
Without mocking, how do I show that it executed /bin/bash
to do so?
I tried ptracer to trace the system calls, but the output disappointed me.
def callback(syscall):
name = syscall.name
args = ",".join([str(a.value) for a in syscall.args])
print(f"{name}({args})")
with ptracer.context(callback):
run_in_shell("echo 'hello'")
with ptracer.context(callback):
run_in_shell("echo 'hello'", shell_name="/bin/bash")
I was hoping to see a clone
or fork
call with the name of the shell, but there is nothing so clear. I don't understand the strings in the read
calls, and I don't see any write
calls.
hello
pipe2((23, 24),524288)
clone(18874385,0,None,261939,0)
close(24)
read(23,bytearray(b'U\r\r\n'),50000)
close(23)
wait4(262130,0,0,None)
hello
pipe2((24, 25),524288)
clone(18874385,0,None,261939,0)
futex(2,129,1,0,9693984,9694016)
futex(0,129,1,0,None,9694016)
close(25)
read(24,bytearray(b'\x1d'),50000)
close(24)
wait4(262308,0,0,None)
At this point I'm out of my depth. I've surely misunderstood what the system calls are really doing. What am I missing?
Is it possible and practical to test this using Python and PyTest? If not, I'll redefine my function to depend explicitly on one of the subprocess functions, and then I can use a mock to test that it sends the right messages to the function.
As Teejay Bruno shows, you can use the command to introspect the shell. The shell already stores its name in the $0
variable, so you can just echo that and check the standard output.
def test_executes_in_specified_shell(stack, capfd):
run_in_shell("echo $0", shell_name="/bin/bash")
cap = capfd.readouterr()
assert cap.out.strip() == "/bin/bash"
assert cap.err.strip() == ""
I think $0
should work in any POSIX-compliant shell, although I can't find clear documentation for that. It certainly works in Bash and dash.