自動テストにsubprocess.run()
を使用しています。主に実行を自動化する:
dummy.exe < file.txt > foo.txt
diff file.txt foo.txt
シェルで上記のリダイレクトを実行すると、2つのファイルは常に同じになります。ただし、file.txt
が長すぎる場合は、以下のPythonコードは正しい結果を返しません。
これはPythonコードです:
import subprocess
import sys
def main(argv):
exe_path = r'dummy.exe'
file_path = r'file.txt'
with open(file_path, 'r') as test_file:
stdin = test_file.read().strip()
p = subprocess.run([exe_path], input=stdin, stdout=subprocess.PIPE, universal_newlines=True)
out = p.stdout.strip()
err = p.stderr
if stdin == out:
print('OK')
else:
print('failed: ' + out)
if __name__ == "__main__":
main(sys.argv[1:])
dummy.cc
のC++コードは次のとおりです。
#include <iostream>
int main()
{
int size, count, a, b;
std::cin >> size;
std::cin >> count;
std::cout << size << " " << count << std::endl;
for (int i = 0; i < count; ++i)
{
std::cin >> a >> b;
std::cout << a << " " << b << std::endl;
}
}
file.txt
は次のようになります。
1 100000
0 417
0 842
0 919
...
最初の行の2番目の整数は後続の行数です。したがって、ここでfile.txt
は100,001行になります。
質問:subprocess.run()を誤用していますか?
編集
私の正確なPythonコメント後のコード(改行、rb)が考慮されます:
import subprocess
import sys
import os
def main(argv):
base_dir = os.path.dirname(__file__)
exe_path = os.path.join(base_dir, 'dummy.exe')
file_path = os.path.join(base_dir, 'infile.txt')
out_path = os.path.join(base_dir, 'outfile.txt')
with open(file_path, 'rb') as test_file:
stdin = test_file.read().strip()
p = subprocess.run([exe_path], input=stdin, stdout=subprocess.PIPE)
out = p.stdout.strip()
if stdin == out:
print('OK')
else:
with open(out_path, "wb") as text_file:
text_file.write(out)
if __name__ == "__main__":
main(sys.argv[1:])
これが最初の差分です:
これが入力ファイルです: https://drive.google.com/open?id=0B--mU_EsNUGTR3VKaktvQVNtLTQ
再現するには、シェルコマンド:
_subprocess.run("dummy.exe < file.txt > foo.txt", Shell=True, check=True)
_
pythonのシェルなし:
_with open('file.txt', 'rb', 0) as input_file, \
open('foo.txt', 'wb', 0) as output_file:
subprocess.run(["dummy.exe"], stdin=input_file, stdout=output_file, check=True)
_
任意の大きなファイルで動作します。
Python 3.5+でのみ利用可能なsubprocess.check_call()
の代わりに、この場合はsubprocess.run()
(Python 2以降で利用可能))を使用できます。
ありがとうございます。しかし、なぜ元のものが失敗したのですか?ケビンアンサーのようなパイプバッファーサイズ?
OSパイプバッファとは関係ありません。 @Kevin J. Chaseが引用するサブプロセスドキュメントからの警告は、subprocess.run()
とは無関係です。複数のパイプストリーム(_process.stdin/.stdout/.stderr
_)を介してprocess = Popen()
およびmanuallyread()/ write()を使用する場合にのみ、OSパイプバッファーに注意する必要があります。
観察された動作は niversal CRTのWindowsバグ によるものであることがわかります。 Pythonなしで再現された同じ問題を次に示します。 パイプラインが失敗した場合にリダイレクトが機能する理由
バグの説明 で述べたように、それを回避するには:
ReadFile()
を使用_std::cin
_の代わりに直接g++
_を使用しても問題ありませんこのバグはテキストパイプにのみ影響します。つまり、_<>
_を使用するコードは問題ありません(_stdin=input_file, stdout=output_file
_は引き続き機能するか、他のバグです)。
免責事項から始めます。Python 3.5がないため(run
関数を使用できません)、Windows(Pythonで問題を再現できませんでした) 3.4.4)またはLinux(3.1.6)。
subprocess.PIPE
_およびファミリの問題_subprocess.run
_ のドキュメントでは、これは古い_subprocess.Popen
_- and -communicate()
手法のフロントエンドにすぎないとしています。 _subprocess.Popen.communicate
_ のドキュメントは次のことを警告しています:
読み込まれたデータはメモリにバッファリングされるため、データサイズが大きい場合や無制限の場合は、このメソッドを使用しないでください。
これは確かにあなたの問題のように聞こえます。残念ながら、ドキュメントには「大きい」データの量は記載されていません。また、「多すぎる」データが読み取られた後にwhatが発生することもありません。 「それをしないでください」。
_subprocess.call
_ のドキュメントをもう少し詳しく説明します(強調は私のものです)...
この関数で_
stdout=PIPE
_または_stderr=PIPE
_を使用しないでください。パイプが読み取られていないため、子プロセスはOSパイプバッファーをいっぱいにするのに十分な出力をパイプに生成する場合をブロックします。
... _subprocess.Popen.wait
_ のドキュメントと同様に:
これは、_
stdout=PIPE
_または_stderr=PIPE
_および子プロセスOSパイプバッファーの待機をブロックするような十分な出力を生成しますがより多くのデータを受け入れるために使用すると、デッドロックします。それを避けるためにパイプを使用するときはPopen.communicate()
を使用してください。
確かに_Popen.communicate
_がこの問題の解決策のように聞こえますが、communicate
の独自のドキュメントは、「データサイズが大きい場合はこのメソッドを使用しないでください」--- wait
のドキュメントがtocommunicate
を使用します。 (おそらく、データを静かに床に落とすことで「回避」するのでしょうか?)
イライラして、私は _subprocess.PIPE
_ を安全に使用する方法を考えていません。ただし、子プロセスが書き込むよりも速く読み取ることができると確信している場合を除きます。
そのメモについて...
tempfile.TemporaryFile
_あなたはallデータをメモリに保持しています...実際には2回特にファイルに既にある場合は、効率的ではありません。
一時ファイルの使用が許可されている場合は、2つのファイルを1行ずつ簡単に比較できます。これは、すべての_subprocess.PIPE
_混乱を回避し、一度に少しだけRAMを使用するだけなので、はるかに高速です。(IOオペレーティングシステムが出力リダイレクトを処理する方法によっては、サブプロセスからの処理も高速になる場合があります。
繰り返しますが、run
をテストすることはできません。そのため、少し古いPopen
- and -communicate
ソリューションを使用します(main
と残りの設定を差し引いたもの):
_import io
import subprocess
import tempfile
def are_text_files_equal(file0, file1):
'''
Both files must be opened in "update" mode ('+' character), so
they can be rewound to their beginnings. Both files will be read
until just past the first differing line, or to the end of the
files if no differences were encountered.
'''
file0.seek(io.SEEK_SET)
file1.seek(io.SEEK_SET)
for line0, line1 in Zip(file0, file1):
if line0 != line1:
return False
# Both files were identical to this point. See if either file
# has more data.
next0 = next(file0, '')
next1 = next(file1, '')
if next0 or next1:
return False
return True
def compare_subprocess_output(exe_path, input_path):
with tempfile.TemporaryFile(mode='w+t', encoding='utf8') as temp_file:
with open(input_path, 'r+t') as input_file:
p = subprocess.Popen(
[exe_path],
stdin=input_file,
stdout=temp_file, # No more PIPE.
stderr=subprocess.PIPE, # <sigh>
universal_newlines=True,
)
err = p.communicate()[1] # No need to store output.
# Compare input and output files... This must be inside
# the `with` block, or the TemporaryFile will close before
# we can use it.
if are_text_files_equal(temp_file, input_file):
print('OK')
else:
print('Failed: ' + str(err))
return
_
残念ながら、100万行を入力しても問題を再現できないため、これがworksであるかどうかはわかりません。他に何もなければ、それはあなたに間違った答えを速く与えるべきです。
テスト実行の出力を_foo.txt
_で(コマンドラインの例から)保持したい場合は、サブプロセスの出力をTemporaryFile
ではなく通常のファイルに送信します。これは J.F。セバスチャンの答え で推奨されている解決策です。
wanted_foo.txt
_であるか、それが2段階テストの副作用であるか、diff
であるかどうかは、質問からはわかりません- -コマンドラインの例では、テスト出力をファイルに保存しますが、Pythonスクリプトでは保存しません。テストの失敗を調査したい場合は、出力を保存すると便利ですが、実行するテストごとに一意のファイル名を考え出すので、お互いの出力を上書きしません。