web-dev-qa-db-fra.com

Pourquoi la sortie subprocess.run est-elle différente de la sortie Shell de la même commande?

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:

enter image description here

Voici le fichier d'entrée: https://drive.google.com/open?id=0B--mU_EsNUGTR3VKaktvQVNtLTQ

9
user2346536

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:

  • "utilisez un tube binaire et faites le mode texte CRLF => LF traduction manuellement côté lecteur" ou utilisez ReadFile() directement au lieu de std::cin
  • ou attendez la mise à jour de Windows 10 cet été (où le bug devrait être corrigé)
  • ou utilisez un compilateur C++ différent, par exemple, il y a pas de problème si vous utilisez g++ sous Windows

Le 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).

6
jfs

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 ...

Problèmes avec subprocess.PIPE Et la famille

Les 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 Ou stderr=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 Ou stderr=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. Utilisez Popen.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 ...

Alternative: 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.

Variante: fichier ordinaire

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.

1
Kevin J. Chase