J'utilise subprocess.run()
pour certains tests automatisés. Surtout pour automatiser le faire:
dummy.exe < file.txt > foo.txt
diff file.txt foo.txt
Si vous exécutez la redirection ci-dessus dans un shell, les deux fichiers sont toujours identiques. Mais chaque fois que file.txt
est trop long, le code ci-dessous Python ne renvoie pas le résultat correct.
Voici le code 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:])
Voici le code C++ dans dummy.cc
:
#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
peut ressembler à ceci:
1 100000
0 417
0 842
0 919
...
Le deuxième entier sur la première ligne est le nombre de lignes suivantes, donc ici file.txt
comportera 100 001 lignes.
Question: Suis-je en train d'utiliser abusivement subprocess.run ()?
Modifier
Mon code exact Python après commentaire (newlines, rb) est pris en compte:
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:])
Voici le premier diff:
Voici le fichier d'entrée: https://drive.google.com/open?id=0B--mU_EsNUGTR3VKaktvQVNtLTQ
Pour reproduire, la commande Shell:
subprocess.run("dummy.exe < file.txt > foo.txt", Shell=True, check=True)
sans le Shell en 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)
Il fonctionne avec des fichiers volumineux arbitraires.
Vous pouvez utiliser subprocess.check_call()
dans ce cas (disponible depuis Python 2), au lieu de subprocess.run()
qui n'est disponible que dans Python 3,5+.
Fonctionne très bien merci. Mais alors pourquoi l'original a-t-il échoué? Taille du tampon de tuyau comme dans Kevin Answer?
Cela n'a rien à voir avec les tampons de tuyaux OS. L'avertissement des documents du sous-processus que @Kevin J. Chase cite n'est pas lié à subprocess.run()
. Vous ne devez vous soucier des tampons de tuyaux du système d'exploitation que si vous utilisez process = Popen()
et manuellement read ()/write () via plusieurs flux de tuyaux ( process.stdin/.stdout/.stderr
).
Il s'avère que le comportement observé est dû à bogue Windows dans le CRT universel . Voici le même problème qui est reproduit sans Python: Pourquoi la redirection fonctionnerait-elle en cas d'échec de la tuyauterie?
Comme indiqué dans la description du bogue , pour le contourner:
ReadFile()
directement au lieu de std::cin
g++
sous WindowsLe bogue affecte uniquement les canaux de texte, c'est-à-dire que le code qui utilise <>
Devrait être correct (stdin=input_file, stdout=output_file
Devrait toujours fonctionner ou c'est un autre bogue).
Je vais commencer par un avertissement: je n'ai pas Python 3.5 (donc je ne peux pas utiliser la fonction run
), et je n'ai pas pu reproduire votre problème sous Windows (Python 3.4.4) ou Linux (3.1.6). Cela dit ...
subprocess.PIPE
Et la familleLes documents subprocess.run
disent que ce n'est qu'un frontal pour l'ancienne technique subprocess.Popen
- et -communicate()
. Les documents subprocess.Popen.communicate
préviennent que:
Les données lues sont mises en mémoire tampon, donc n'utilisez pas cette méthode si la taille des données est grande ou illimitée.
Cela ressemble à votre problème. Malheureusement, les documents ne disent pas combien de données sont "volumineuses", ni quoi ne se produira après la lecture de "trop" de données. Juste "ne fais pas ça, alors".
Les documents pour subprocess.call
vont dans un peu plus de détails (soulignement le mien) ...
N'utilisez pas
stdout=PIPE
Oustderr=PIPE
Avec cette fonction. Le processus enfant bloquera s'il génère suffisamment de sortie vers un canal pour remplir le tampon de canal du système d'exploitation car les canaux ne sont pas lus.
... tout comme les documents pour subprocess.Popen.wait
:
Cela se bloquera lors de l'utilisation de
stdout=PIPE
Oustderr=PIPE
Et le processus enfant génère suffisamment de sortie vers un canal de telle sorte qu'il bloque l'attente du tampon de canal du système d'exploitation pour accepter plus de données. UtilisezPopen.communicate()
lorsque vous utilisez des tuyaux pour éviter cela.
Cela semble sûr que Popen.communicate
Est la solution à ce problème, mais les propres documents de communicate
disent "n'utilisez pas cette méthode si la taille des données est grande" --- exactement la situation où le wait
les documents vous disent à utilisez communicate
. (Peut-être qu'il "évite (nt) cela" en déposant silencieusement des données sur le sol?)
De façon frustrante, je ne vois aucun moyen d'utiliser un subprocess.PIPE
en toute sécurité, à moins que vous ne soyez sûr de pouvoir le lire plus rapidement que ce que votre processus enfant y écrit.
Sur cette note ...
tempfile.TemporaryFile
Vous tenez tous vos données en mémoire ... deux fois, en fait. Cela ne peut pas être efficace, surtout s'il est déjà dans un fichier.
Si vous êtes autorisé à utiliser un fichier temporaire, vous pouvez comparer les deux fichiers très facilement, une ligne à la fois. Cela évite tout le gâchis subprocess.PIPE
, Et c'est beaucoup plus rapide, car il n'utilise qu'un peu de RAM à la fois. (Le IO = de votre sous-processus peut également être plus rapide, selon la façon dont votre système d'exploitation gère la redirection de sortie.)
Encore une fois, je ne peux pas tester run
, voici donc une solution Popen
- et -communicate
légèrement plus ancienne (moins main
et le reste de votre configuration) :
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
Malheureusement, comme je ne peux pas reproduire votre problème, même avec une entrée d'un million de lignes, je ne peux pas dire si cela fonctionne. Si rien d'autre, cela devrait vous donner des réponses erronées plus rapidement.
Si vous souhaitez conserver la sortie de votre test dans foo.txt
(À partir de votre exemple de ligne de commande), vous dirigerez la sortie de votre sous-processus vers un fichier normal au lieu d'un TemporaryFile
. C'est la solution recommandée dans réponse de J.F. Sebastian .
Je ne peux pas dire à partir de votre question si vous voulufoo.txt
, Ou si ce n'était qu'un effet secondaire du test en deux étapes-puis -diff
--- votre exemple de ligne de commande enregistre la sortie de test dans un fichier, tandis que votre script Python ne le fait pas. L'enregistrement de la sortie serait pratique si vous souhaitez enquêter sur un échec de test, mais il nécessite de trouver un nom de fichier unique pour chaque test que vous exécutez, afin qu'ils ne se remplacent pas mutuellement.