通过 Popen 测试外部脚本 运行 的调用,该脚本期望使用 pytest 的标准输入中没有可用数据

Test calling of an external script run via Popen that expects no available data in stdin using pytest

我有一个带有命令行界面的外部工具,它使用 sys.stdin in select.select([sys.stdin], [], [], 0)[0] 检查是否通过标准输入提供了任何数据,并相应地调整预期参数。我通过 subprocess 调用此外部工具,并依赖于没有通过标准输入提供输入的用例。

现在我想 运行 通过 pytest 对此功能进行自动集成测试。但是如果不提供 pytest 命令行选项 --capture=sys 禁用所有测试的文件描述符级别的捕获,我就无法让它工作。在任何其他情况下(同样在测试中使用 capfd.disabled()capsys.disabled() 调用时,因为它们仅禁用 stdout 和 stderr 而不是 stdin 捕获),外部工具检测到将通过 stdin 提供输入,触发关于其他参数的错误结论,从而导致我的测试失败。

为了举例,我基本上有如下文件:

external_script.py:

import select
import sys

print(sys.stdin in select.select([sys.stdin], [], [], 0)[0])
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
    print(sys.stdin.readline().strip())
print(sys.argv)

internal_part.py:

import subprocess

def call_external():
    popen = subprocess.Popen(["/usr/bin/python3", "external_script.py", "1"],
                             stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    print(popen.communicate()[:2])

if __name__ == "__main__":
    call_external()

test.py:

import internal_part

def test(capsys):
    with capsys.disabled():  # or capfd.disabled()
        internal_part.call_external()

当我现在通过 python 运行 internal_part.py 时,我得到了预期的输出,对 select.select 的调用不会返回 sys.stdin 并且因此标准输入中没有可用的输入。但是当我通过 pytest 运行 test.py 而没有 --capture=sys 时,我得到的输出表明数据是通过标准输入提供的。从 sys.stdin 读取然后给出一个空字符串。

如果 select.select 在内部部分调用 where ,我至少会得到一个错误,这是伪文件作为重定向标准输入的不受支持的操作。对于外部工具,运行ning 在它自己的解释器中,除了错误地检测到标准输入以提供输入之外,我没有得到任何出错的迹象。

是否有任何选项可以让这个测试场景工作,例如通过禁用此特定测试的标准输入捕获,而不禁用该测试套件中我所有测试的文件描述符级别的捕获?

如果您愿意生成并显式传递一个尚未准备好在生产代码中读取的文件描述符1,您可以通过[=11生成一个新的命名管道=] 并将其读取端作为标准输入传递。显然,对于奇怪的接口来说,生成一个新管道的唯一目的是空的,这在某种程度上是一种变通方法。但是只要您不向管道中写入任何内容,管道就不应该准备好读取,因此 select.select 调用不应该接收它。内部部分可能看起来有点像下面的代码:

import contextlib
import os
import subprocess

@contextlib.contextmanager
def not_ready_to_read():
    try:
        read_end, write_end = os.pipe()
        yield read_end
    finally:
        os.close(read_end)
        os.close(write_end)

def call_external():
    with not_ready_to_read() as not_ready_stdin:
        popen = subprocess.Popen(["/usr/bin/python3", "external_script.py", "1"],
                                 stdin=not_ready_stdin, stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
        print(popen.communicate()[:2])

if __name__ == "__main__":
    call_external()

1这也可以帮助您处理通过 select.select 进行的测试错误地表明某些数据是通过标准输入传递的其他情况,例如如果您 运行 脚本通过 slurm 作业调度程序。因此,它不是以启用测试为唯一目的的生产代码改编。