Как запустить жгут Perl ``proof`` TAP в небуферизованном режиме?

Как часть набора тестов, написанного на Python 3 [.4-.6] для Linux, я должен выполнить ряд сторонних тестов. Сторонние тесты - это bash-скрипты. Они предназначены для работы с Perl prove Жгут проводов. Один скрипт bash может содержать до нескольких тысяч отдельных тестов, и некоторые из них могут зависать бесконечно долго. После тайм-аута я хочу убить тестовый скрипт и собрать некоторую информацию о том, где он застрял.

Поскольку скрипты bash создают свои собственные процессы, я стараюсь изолировать весь prove дерево процессов в новую группу процессов, поэтому я могу в конечном итоге убить всю группу процессов в целом, если что-то пойдет не так. Поскольку тесты должны выполняться с правами root, я использую sudo -b для создания новой группы процессов с привилегиями root. Эта стратегия (в отличие от использования setsid так или иначе) является результатом комментариев, которые я получил по этому вопросу в SE Unix&Linux

Проблема в том, что я теряю весь вывод из prove Если использовать "преждевременно" при запуске с нажатием sudo -b через питона subprocess.Popen,

Я выделил это в простой контрольный пример. Ниже приведен скрипт тестирования bash с именем job.t:

#!/bin/bash

MAXCOUNT=20
echo "1..$MAXCOUNT"
for (( i=1; i<=$MAXCOUNT; i++ ))
do
   echo "ok $i"
   sleep 1
done

Просто для сравнения, я также написал скрипт на Python с именем job.py производит более или менее одинаковый результат и демонстрирует одинаковое поведение:

import sys
import time
if __name__ == '__main__':
    maxcount = 20
    print('1..%d' % maxcount)
    for i in range(1, maxcount + 1):
        sys.stdout.write('ok %d\n' % i)
        time.sleep(1)

И последнее, но не менее важное: ниже приведена моя сокращенная "инфраструктура тестирования Python" demo.py:

import psutil # get it with "pip install psutil"
import os
import signal
import subprocess

def run_demo(cmd, timeout_after_seconds, signal_code):
    print('DEMO: %s' % ' '.join(cmd))
    proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
    try:
        outs, errs = proc.communicate(timeout = timeout_after_seconds)
    except subprocess.TimeoutExpired:
        print('KILLED!')
        kill_pid = _get_pid(cmd)
        subprocess.Popen(['sudo', 'kill', '-%d' % signal_code, '--', '-%d' % os.getpgid(kill_pid)]).wait()
        outs, errs = proc.communicate()
    print('Got our/err:', outs.decode('utf-8'), errs.decode('utf-8'))

def _get_pid(cmd_line_list):
    for pid in psutil.pids():
        proc = psutil.Process(pid)
        if cmd_line_list == proc.cmdline():
            return proc.pid
    raise # TODO some error ...

if __name__ == '__main__':
    timeout_sec = 5
    # Works, output is captured and eventually printed
    run_demo(['sudo', '-b', 'python', 'job.py'], timeout_sec, signal.SIGINT)
    # Failes, output is NOT captured (i.e. printed) and therefore lost
    run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

когда demo.py запускается, запускается рутина run_demo дважды - с разными конфигурациями. Оба раза запускается новая группа процессов с привилегиями root. Оба раза "тестовое задание" печатает новую строку (ok [line number]) раз в секунду - теоретически на 20 секунд / 20 строк. Однако для обоих сценариев есть тайм-аут 5 секунд, и вся группа процессов уничтожается после этого тайм-аута.

когда run_demo запускается впервые с моим маленьким скриптом Python job.py все выходные данные этого сценария вплоть до момента его уничтожения фиксируются и печатаются успешно. когда run_demo запускается во второй раз с тестовым скриптом demo bash job.t на вершине prove, выходные данные не записываются, и печатаются только пустые строки.

user@computer:~> python demo.py 
DEMO: sudo -b python job.py
KILLED!
Got our/err: 1..20
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
 Traceback (most recent call last):
  File "job.py", line 11, in <module>
    time.sleep(1)
KeyboardInterrupt

DEMO: sudo -b prove -v /full/path/to/job.t
KILLED!
Got our/err:  
user@computer:~>

Что здесь происходит и как я могу это исправить?

Т.е. как я могу прервать / прекратить выполнение сценария bash с помощью prove (и вся его группа процессов) таким образом, чтобы я мог захватить его вывод?

РЕДАКТИРОВАТЬ: В ответ было предложено, что наблюдаемое поведение происходит из-за буферизации Perl его выходных данных. В отдельном скрипте Perl это можно отключить. Тем не менее, нет очевидного варианта, позволяющего отключить буферизацию для prove [-v]. Как мне этого добиться?


Я могу обойти эту проблему, запустив мою тестовую работу с bash непосредственно. Следующая команда должна быть изменена с

run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

в

run_demo(['sudo', '-b', 'bash', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

Таким образом, я не получаю статистику теста, напечатанную prove, но я могу генерировать их сам.

2 ответа

Решение

По умолчанию STDOUT многих программ (в том числе perl) буферизуется строкой (сбрасывается на новой строке), когда STDOUT подключен к терминалу, и буферизуется блоком (сбрасывается, когда буфер файла заполнен) в противном случае (например, когда он подключен к каналу).

Вы можете обмануть такие программы, используя буферизацию строки, используя псевдо-tty (ptty) вместо канала. С этой целью, unbuffer твой друг. На Ubuntu это часть expect пакет (sudo apt install expect).

Из документов:

unbuffer отключает буферизацию вывода, которая происходит, когда вывод программы перенаправляется из неинтерактивных программ. Например, предположим, что вы просматриваете вывод с fifo, пропустив его через od, а затем more.

od -c /tmp/fifo | more

Вы ничего не увидите, пока не будет произведена полная страница вывода.

Вы можете отключить эту автоматическую буферизацию следующим образом:

unbuffer od -c /tmp/fifo | more

Я попробовал ваш пример кода и получил тот же результат, что вы описали (благодаря вашему Minimal, Complete и Verifiable пример!).

Я тогда поменял

run_demo(['sudo', '-b', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

в

run_demo(['sudo', '-b', 'unbuffer', 'prove', '-v', os.path.join(os.getcwd(), 'job.t')], timeout_sec, signal.SIGINT)

То есть: я просто добавил unbuffer к prove команда. Выход был тогда:

DEMO: sudo -b python job.py
KILLED!
Got our/err: 1..20
ok 1
ok 2
ok 3
ok 4
ok 5
ok 6
 Traceback (most recent call last):
  File "job.py", line 8, in <module>
    time.sleep(1)
KeyboardInterrupt

DEMO: sudo -b unbuffer prove -v /home/dirk/w/sam/p/job.t
KILLED!
Got our/err: /home/dirk/w/sam/p/job.t .. 
1..20
ok 1
ok 2
ok 3
ok 4
ok 5

Это начало ответа, в нем больше информации, чем я могу втиснуть в комментарий.

Выложенная вами проблема на самом деле не связана с bash, она связана с Perl. В моей системе which prove указывает на /usr/bin/prove, который является сценарием Perl. Реальный вопрос здесь, как правило, о сценариях Perl, даже не относится к prove, Я скопировал ваши файлы выше и проверил, что могу воспроизвести то, что вы видите, затем я создал третий тест:

$ cat job.pl
#!/usr/bin/perl
foreach (1..20){
  print "$_\n";   
  sleep 1;
}

Круто, тогда я добавил это в демонстрационную программу:

(После импорта shlex как хорошо`):

cmdargs = shlex.split('sudo -b '+os.path.join(os.getcwd(), 'job.pl'))
run_demo(cmdargs, timeout_sec, signal.SIGINT)

И, конечно же, этот простой Perl-скрипт не может выдавать результат при уничтожении.

$ python3 demo.py
...(output as you wrote above followed by)... 
DEMO: sudo -b /home/jawguychooser/job.pl
KILLED!
Got our/err:  
$

Таким образом, это означает, что ваша проблема на самом деле является конкретным примером того, как захватить вывод из убитой perl-программы, работающей в фоновом режиме, управляемой Python-программой.

В качестве следующего шага я установил job.pl снять буфер с stdout:

$ cat job.pl
#!/usr/bin/perl
$| = 1;
foreach (1..20){
  print "$_\n"; 
  sleep 1;
}

А потом я снова запускаю demo.py и вуаля!

$ python3 demo.py 
DEMO: sudo -b /home/jawguychooser/job.pl
KILLED!
Got our/err: 1
2
3
4
5
6
$ 

Так что, возможно, если вы взломаете сценарий доказательства и настроите его для работы без буферизации, это будет делать то, что вы хотите. В любом случае, я думаю, что ваш вопрос сейчас "как я могу бежать prove -v в небуферизованном режиме ".

Надеюсь, это поможет.

Другие вопросы по тегам