Рано или поздно ошибки случаются. Как мы уже видели, Python предоставляет интерфейсы доступа к различным системным службам, а также инструменты для добавления новых интерфейсов. В примере 6.9 показаны некоторые часто используемые системные инструменты в действии. В нем реализована простая система регрессивного тестирования сценариев на языке Python. Она запускает каждый сценарий Python в указанном каталоге с заданным набором входных файлов и аргументов командной строки и сравнивает вывод каждого прогона с предыдущими результатами. Этот сценарий можно было бы использовать в качестве автоматизированной системы тестирования, чтобы отлавливать ошибки, появляющиеся в результате изменений в исходных файлах программы, — в большой системе нельзя быть уверенным в том, что исправление не является в действительности скрытой ошибкой.
Пример 6.9. PP4E\System\Tester\tester.py
############################################################################## Тестирует сценарии Python в каталоге, передает им аргументы командной строки, выполняет перенаправление stdin, перехватывает stdout, stderr и код завершения, чтобы определить наличие ошибок и отклонений от предыдущих результатов выполнения. Запуск сценариев и управление потоками ввода-вывода производится с помощью переносимого модуля subprocess (как это делает функция os.popen3 в Python 2.X). Потоки ввода-вывода всегда интерпретируются модулем subprocess как двоичные. Стандартный ввод, аргументы, стандартный вывод и стандартный вывод ошибок отображаются в файлы, находящиеся в подкаталогах.
Этот сценарий командной строки позволяет указать имя тестируемого каталога и флаг принудительной генерации выходного файла. Этот программный код можно было бы упаковать в функцию, однако то обстоятельство, что результатами сценария являются сообщения и выходные файлы, снижает практическую пользу модели вызов/ возвращаемое значение.
Дополнительные возможные расширения: можно было бы реализовать по несколько наборов аргументов командной строки и/или входных файлов для каждого тестируемого сценария и запускать их по несколько раз (использовать функцию glob для выборки нескольких файлов “.in*” в каталоге Inputs).
Возможно, было бы проще хранить все файлы, необходимые для проведения тестов, в одном и том же каталоге, но с различными расширениями, однако с течением времени их объем мог бы оказаться слишком большим.
В случае ошибок можно было бы сохранять содержимое потоков вывода stderr и stdout в подкаталоге Errors, но я предпочитаю иметь ожидаемый/фактический вывод в подкаталоге Outputs.
##############################################################################
import os, sys, glob, time
from subprocess import Popen, PIPE
# конфигурационные аргументы
testdir = sys.argv[1] if len(sys.argv) > 1 else os.curdir
forcegen = len(sys.argv) > 2
print(‘Start tester:’, time.asctime())
print(‘in’, os.path.abspath(testdir))
def verbose(*args):
print(‘-’*80)
for arg in args: print(arg)
def quiet(*args): pass
trace = quiet
# отбор сценариев для тестирования
testpatt = os.path.join(testdir, ‘Scripts’, ‘*.py’)
testfiles = glob.glob(testpatt)
testfiles.sort()
trace(os.getcwd(), *testfiles)
numfail = 0
for testpath in testfiles: # протестировать все сценарии
testname = os.path.basename(testpath) # отбросить путь к файлу
# получить входной файл и аргументы для тестируемого сценария
infile = testname.replace(‘.py’, ‘.in’)
inpath = os.path.join(testdir, ‘Inputs’, infile)
indata = open(inpath, ‘rb’).read() if os.path.exists(inpath) else b’’
argfile = testname.replace(‘.py’, ‘.args’)
argpath = os.path.join(testdir, ‘Args’, argfile)
argdata = open(argpath).read() if os.path.exists(argpath) else ‘’
# местоположение файлов для сохранения stdout и stderr,
# очистить предыдущие результаты
outfile = testname.replace(‘.py’, ‘.out’)
outpath = os.path.join(testdir, ‘Outputs’, outfile)
outpathbad = outpath + ‘.bad’
if os.path.exists(outpathbad): os.remove(outpathbad)
errfile = testname.replace(‘.py’, ‘.err’)
errpath = os.path.join(testdir, ‘Errors’, errfile)
if os.path.exists(errpath): os.remove(errpath)
# запустить тестируемый сценарий, перенаправив потоки ввода-вывода
pypath = sys.executable
command = ‘%s %s %s’ % (pypath, testpath, argdata)
trace(command, indata)
process = Popen(command, shell=True, stdin=PIPE, stdout=PIPE, stderr=PIPE) process.stdin.write(indata) process.stdin.close()
outdata = process.stdout.read()
errdata = process.stderr.read() # при работе с двоичными файлами exitstatus = process.wait() # данные имеют тип bytes
trace(outdata, errdata, exitstatus)
# проанализировать результаты if exitstatus != 0:
print(‘ERROR status:’, testname, exitstatus) # код заверш.
if errdata: # и/или stderr
print(‘ERROR stream:’, testname, errpath) # сохр. текст ошибки open(errpath, ‘wb’).write(errdata)
if exitstatus or errdata: # оба признака ошибки
numfail += 1 # можно получить код завершения + код ошибки
open(outpathbad, ‘wb’).write(outdata) # сохранить вывод
elif not os.path.exists(outpath) or forcegen: print(‘generating:’, outpath) # создать файл, если
open(outpath, ‘wb’).write(outdata) # необходимо
else:
priorout = open(outpath, ‘rb’).read() # или сравнить с прежними
# результатами
if priorout == outdata: print(‘passed:’, testname) else:
numfail += 1
print(‘FAILED output:’, testname, outpathbad) open(outpathbad, ‘wb’).write(outdata)
print(‘Finished:’, time.asctime())
print(‘%s tests were run, %s tests failed.’ % (len(testfiles), numfail))
Мы уже познакомились с инструментами, используемыми этим сценарием, выше в этой части книги — с модулем subprocess, с функциями os.path, glob, с файлами и другими. Этот пример в значительной степени просто объединяет эти инструменты для решения поставленной задачи. Основной операцией в сценарии является сравнение нового и старого вывода с целью обнаружить изменения («регрессии»). Попутно он также манипулирует аргументами командной строки, сообщениями об ошибках, кодами завершения и файлами.
Кроме того, этот сценарий не только самый большой из тех, что встречались нам до сих пор; это также самый практичный и представительный инструмент системного администрирования (он был разработан на основе похожего инструмента, который я использовал в прошлом для выявления изменений в компиляторе). Самый лучший способ понять, как он действует, вероятно, состоит в том, чтобы продемонстрировать его на конкретных примерах. В следующем разделе демонстрируется сеанс тестирования и попутно даются пояснения по реализации сценария.
Использованная литература:
Марк Лутц — Программирование на Python, 4-е издание, I том, 2011