Как запустить Perl `` ver`` TAP-жгут в небуферизованном режиме?
Как часть тестового набора, написанного на Python 3 [. 4-.6] в Linux, мне нужно запустить ряд сторонних тестов. Тесты сторонних разработчиков - это bash скрипты. Они предназначены для работы с Perl prove
TAP harness. Один bash script может содержать до нескольких тысяч отдельных тестов - и некоторые из них могут зависать бесконечно. После таймаута я хочу убить тест script и собрать некоторую информацию о том, где он застрял.
Поскольку сценарии bash создают собственные процессы, я пытаюсь изолировать все дерево процессов prove
в новой группе процессов, поэтому я могу в конечном итоге убить всю группу процессов в целом, если все пойдет не так. Поскольку тесты должны выполняться с привилегиями root, я использую sudo -b
для создания новой группы процессов с привилегиями root. Эта стратегия (в отличие от использования setsid
в той или иной форме) является результатом комментариев, полученных мной по этому вопросу в SE Unix & Linux
Проблема в том, что я теряю весь вывод из жгута prove
TAP, если я его преждевременно убиваю, когда запускается с помощью sudo -b
через Python subprocess.Popen
.
Я выделил его в простой тестовый пример. Ниже приведен тест bash script с именем job.t
:
#!/bin/bash
MAXCOUNT=20
echo "1..$MAXCOUNT"
for (( i=1; i<=$MAXCOUNT; i++ ))
do
echo "ok $i"
sleep 1
done
Просто для сравнения, я также написал Python script с именем 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 script job.py
, весь вывод этого script полностью до точки, когда он был убит, захватывается и печатается успешно. Когда run_demo
выполняется во второй раз с демонстрационным bash тестом script job.t
поверх prove
, вывод не записывается и печатаются только пустые строки.
[email protected]:~> 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:
[email protected]:~>
Что здесь происходит и как я могу это исправить?
т.е. как я могу прерывать/завершать тест bash script, работающий с prove
(и всей его группой процессов) таким образом, чтобы я мог записать его вывод?
EDIT: предложил в ответ, что наблюдаемое поведение происходит из-за того, что Perl выполняет буферизацию своего вывода. В пределах индивидуального Perl script это можно отключить. Однако нет очевидной опции, позволяющей отключить буферизацию для 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
, но я могу сгенерировать их самостоятельно.
Ответы
Ответ 1
По умолчанию STDOUT для многих программ (включая perl
) буферизируется по строке (сбрасывается в новой строке), когда STDOUT подключается к терминалу и блокируется буфером (сбрасывается при заполнении файлового буфера) в противном случае ( например, когда он подключен к трубе).
Вы можете обмануть такие программы, используя буферизацию строк, используя псевдо-tty (ptty) вместо канала. С этой целью unbuffer
является вашим другом. В Ubuntu это часть пакета expect
(sudo apt install expect
).
Из docs:
unbuffer отключает буферизацию вывода это происходит, когда выход программы перенаправляется от неинтерактивных программ. Например, предположим, что вы смотрите вывод из fifo, запустив его через od и затем больше.
od -c /tmp/fifo | more
Вы ничего не увидите до полной страницы вывода.
Вы можете отключить эту автоматическую буферизацию следующим образом:
unbuffer od -c /tmp/fifo | more
Я попробовал ваш пример кода и получил тот же результат, который вы описываете (благодаря вашему Минимальному, завершенному и проверяемому примеру!).
Затем я изменил
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
Ответ 2
Это начало ответа, у него больше информации, чем я мог бы вдавить в комментарий.
Проблема, которую вы выложили, на самом деле не связана с bash, она связана с Perl. В моей системе which prove
указывает на /usr/bin/prove
, который является perl script. Реальный вопрос здесь в основном о 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 script faile для вывода результата при его уничтожении.
$ 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
в unbuffer 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
$
Итак, возможно, если вы взломаете доказательство script и установите все, чтобы запустить небуферизованный, который будет делать то, что вы хотите. В любом случае, я думаю, ваш вопрос теперь "как я могу запустить prove -v
в небуферизованном режиме".
Надеюсь, это поможет.