Метапрограммирование
Что авторы курса подразумевают под “метапрограммированием”? Оказалось, что это лучший из предложенных терминов для описания процесса разработки в общем, чем написания кода и эффективной работы. В этой лекции мы рассмотрим инструменты для создания и тестирования кода и управления зависимостями. Может показаться, что эти вещи играют совсем не ключевую роль в повседневной жизни студента. Но в стажировке и будущей работе полученные знания пригодятся. Обращаем внимание, что у метапрограммирования есть и другое общепринятое значение “вид программирования, связанный с созданием программ, которые порождают другие программы как результат своей работы”, которое отличается от того, что мы имеем в виду в лекции.
Системы сборки
Для большинства проектов, содержат они код или нет (например, LaTeX), существует процесс сборки. Это некоторый пошаговый набор действий обязательный для получения финального результата. Этот процесс часто состоит из множества этапов. Для получения окончательного результата нужно сначала запустить одно, потом сделать другое, переделать черновик, улучшить/изменить. Весь процесс довольно долгий и раздражающий, но, как и для многих вещей в этом курсе, уже созданы инструменты, облегчающие работу.
Они многочисленны и обычно называются системами сборки. Какую выбрать, зависит от поставленной задачи, языка программирования, размера проекта. Хотя в принципе все системы похожи между собой. Вы определяете набор зависимостей (dependencies), количество этапов/таргетов (targets, это названия/имена того, что надо собирать) и правил (rules). Вы сообщаете системе сборки конкретный таргет, а ее задача - найти все транзитивные (непрямые) зависимости, а затем применить правила для создания промежуточных таргетов, пока не будет исполнен окончательный таргет. В идеале системы сборки делают это без лишнего применения правил для таргегов, зависимости которых не изменились и чей результат доступен из предыдущего билда.
make
одна из самых распространенных систем сборки, установленная на большинстве UNIX машин. У нее есть свои недостатки, но она хорошо
работает для простых, средних и сложных проектов. Когда вы запускаете make
, она обращается к файлу Makefile
в текущей директории. Все таргеты, их зависимости и правила определены в этом файле:
paper.pdf: paper.tex plot-data.png
pdflatex paper.tex
plot-%.png: %.dat plot.py
./plot.py -i $*.dat -o $@
Каждая директива в этом файле - это правило по созданию левой части с помощью правой. Иначе говоря, то, что справа - это зависимости, а
слева - таргеты. Блок с отступом - это серия программ, которые производят таргеты из этих зависимостей.
В make
первая директива определяет цель по умолчанию. Если make
запустить без аргументов, то будут собираться все файлы сразу.
Поэтому, если нужно собрать конкретный подпроект, требуется уточнение, например, make plot-data.png
.
Символ %
содержится в паттернах, он показывает любую подстроку для сбора таргета. Например, если таргет plot-foo.png
, то make
будет искать зависимости foo.dat
и plot.py
.
Посмотрим, что будет, если запустить make
с пустой исходной директорией:
$ make
make: *** No rule to make target 'paper.tex', needed by 'paper.pdf'. Stop.
make
говорит нам о том, что для сборки paper.pdf
нужен paper.tex
. Но как создать этот требуемый файл, не указано.
Давайте попробуем его создать:
$ touch paper.tex
$ make
make: *** No rule to make target 'plot-data.png', needed by 'paper.pdf'. Stop.
Так, интересно. Есть правило для создания plot-data.png
, но это правило паттерна. До тех пор, пока файла (foo.dat
) не существует,
make
не может его создать. Тогда все файлы создадим мы:
$ cat paper.tex
\documentclass{article}
\usepackage{graphicx}
\begin{document}
\includegraphics[scale=0.65]{plot-data.png}
\end{document}
$ cat plot.py
#!/usr/bin/env python
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import argparse
parser = argparse.ArgumentParser()
parser.add_argument('-i', type=argparse.FileType('r'))
parser.add_argument('-o')
args = parser.parse_args()
data = np.loadtxt(args.i)
plt.plot(data[:, 0], data[:, 1])
plt.savefig(args.o)
$ cat data.dat
1 1
2 2
3 3
4 4
5 8
Теперь что будет, если мы запустим make
?
$ make
./plot.py -i data.dat -o plot-data.png
pdflatex paper.tex
... lots of output ...
PDF создан!
А если еще раз запустить make
?
$ make
make: 'paper.pdf' is up to date.
Ничего не произошло! Но системе сборки ничего и не нужно было делать. Она просто проверила все ранее созданные билды и зависимости.
Ошибок не обнаружено. Мы можем немного изменить paper.tex
и перезапустить make
:
$ vim paper.tex
$ make
pdflatex paper.tex
...
Заметьте, что make
не перезапустил plot.py
, потому что это не нужно, так как ни одна из зависимостей plot-data.png
не изменилась.
Система зависимостей
На более высоком макро уровне, выши проекты скорее всего будут иметь зависимости, которые сами по себе будет проектами. Это может
зависеть от установленных программ (например, python
), системных пакетов (openssl
) или библиотек языка программирования
(matplotlib
). Сегодня большинство зависимостей доступны через репозиторий, который содержит большое количество таких зависимостей в
одном месте и предоставляет удобный механизм для их установки. Это, например, RubyGems для библиотек Ruby, PyPi для библиотек Python
или репозиторий Arch для Arch Linux.
Поскольку точный механизм взаимодействия с разными репозиториями сильно различается, мы не будем сейчас вдаваться в детали какого-либо
конкретного репозитория. Рассмотрим некоторые из наиболее распространенных терминов, которые все они используют. Первый из них - это
версии. Большинство проектов содержит номер версии, обновляемый с каждым новым релизом. Обычно он выглядит как-то так: 8.1.3 или
64.1.20192004. Обычно, но не всегда, версиям присваиваются номера.
Номера нужны по разным причинам, основная - обеспечение работы программ. Представьте, например, что выпущена новая версия библиотеки, в
которой переименована одна функция. Если кто-то попытается собрать программу, которая зависит от этой библиотеки, после того, как было
выпущено обновление, сборка может завершиться ошибкой, потому что вызываемая функция больше не существует!
Версии пытаются решить эту проблему, потому что указывают, что проект зависит от конкретной версии. Таким образом, даже если библиотека
изменится, программное обеспечение, зависящее от нее, продолжит сборку с использованием старой версии библиотеки.
Это тоже не идеальное решение! Что делать, если сделать обновление безопасности, которое не меняет публичный интерфейс этой описанной выше библиотеки (ее API). И новую версию с обновленными параметрами безопасности теперь необходимо использовать во всех программах, которые зависели от старой версии. Здесь нужно смотреть на разные группы чисел в версиях. Точное значение каждого из них зависит от проекта, но один относительно общий стандарт - это семантическое версионирование semantic versioning. С таким подходом, номер каждой версии выглядит как: мажорная.минорная.патч версия (major.minor.patch). Учитывая номер версии МАЖОРНАЯ.МИНОРНАЯ.ПАТЧ, следует увеличивать:
- МАЖОРНУЮ версию, когда сделаны обратно несовместимые изменения API.
- МИНОРНУЮ версию, когда вы добавляете новую функциональность, не нарушая обратной совместимости.
- ПАТЧ-версию, когда вы делаете обратно совместимые исправления. Дополнительные обозначения для предрелизных и билд-метаданных возможны как дополнения к МАЖОРНАЯ.МИНОРНАЯ.ПАТЧ формату.
Такая нумерация дает преимущества. Если мой проект использовал при сборке чужую библиотеку с версией 1.3.7
, я все еще могу
использовать версии с 1.3.8
, 1.6.1
или даже 1.3.0
. Версия 2.2.4
, вероятно, уже не подходит, поскольку мажорная версия
увеличилась. Пример семантического версионирования - версии Python. Многие из вас могли заметить, что версии Python 2 and Python 3
не согласуются между собой, как раз потому что изменилась мажорная версия. А код, написанные на Python 3.5 нормально запускается и на
Python 3.7.
Работая с системами версий, вы могли заметить lock файлы. Это файлы, кторые содержат в себе полную информацию обо всех установленных зависимостях, включая их точные версии. Обычно необходимо вручную запустить обновление программы, чтобы обновить зависимости до более новых версий. На это есть много причин, таких как избежание перекомпиляции, детерминированной (воспроизводимой) компиляции или невозможность автоматического обновления до последней версии (которая может быть повреждена).
Continuous integration systems (CI) или тестирующие системы
С ростом проекта будет возникать больше дополнительных задач, которые нужно выполнять всякий раз при внесении изменений. Возможно, потребуется обновить новую версию документации, загрузить куда-нибудь скомпилированную версию, опубликовать код в pypi, выполнить тестирование и многое другое. Может быть, каждый раз, когда кто-то отправляет пуллреквест в GitHub, вы хотите проверить его код? На помощь к автоматизации таких процессов приходят тестирующие системы (CI).
CI (continuous integration) системы обозначают “запускаем что-то при любом изменении кода”. Есть много разных, обычно опен-сорсных, и бесплатных CI. Наиболее крупные и известные: Travis CI, Azure Pipelines и GitHub Actions. Все они работают по похожему сценарию: при любом изменении кода запускаются тесты.
Коротко о тестировании
Большинство крупных программных проектов поставляются с «набором тестов». Вы уже знакомы с общей концепцией тестирования, но ниже перечислены некоторые подходы к тестированию и терминология, которые вы можете встретить в работе:
- Тестовый набор (Test Suite): набор тест кейсов, которые объединены тем, что относятся к одному тестируемому модулю, функциональности, приоритету или одному типу тестирования.
- Юнит тест, модельный тест (Unit Test): “микротест”, который изолированно тестирует определенную функцию.
- Интеграционный тест: «макротест», который запускает большую часть программы, чтобы проверить, что различные функции или компоненты работают вместе.
- Регрессионный тест: тест, реализующий определенный шаблон, который ранее вызывал ошибку, чтобы гарантировать, что ошибка не появится снова.
- Мокинг (Mocking): проверяет, что какой-то код выполнился определённым образом. Это может быть вызов функции, HTTP-запрос и т.д. Задача мока убедиться в том, что это произошло, и в том, как конкретно это произошло, например, что в функцию были переданы конкретные данные.
Упражнения
- Большинство makefiles предоставляют таргет (цель)
clean
. Он не предназначен для создания файлаclean
, а вместо этого удаляет файлы, которые могут быть пересобраныmake
. Считайте, что это способ “отменить” все шаги сборки. Используйте таргетclean
для сборкиpaper.pdf
изMakefile
выше. Создайте таргетclean
для приведенного выше файлаpaper.pdf
. Вам необходимо сделать таргет phony. Может пригодиться подкомандаgit ls-files
. Некоторые другие, широко используемые таргеты, перечислены здесь. - Обратите внимание на разные способы определить требуемую версию зависимостей в системе сборки Rust. Большинство репозиториев поддерживают похожий синтаксис. Для каждого из вариантов (caret, tilde, wildcard, comparison, and multiple) постарайтесь придумать сценарий использования, в котором такое указание версии зависимости будет иметь смысл.
- Git может сам по себе выступать в качестве простой системы CI. В
.git/hooks
внутри любого git репозитория, вы найдете (неактивные в данный момент) файлы, которые выполняются как скрипты, когда происходит определенное действие. Напишитеpre-commit
хук, который запускаетmake paper.pdf
и отклоняет коммит, еслиmake
не может быть выполнен. Этот скрипт должен предотвратить появление в любом коммите несобираемой версииpaper
. - Настройте простую автопубликуемую страницу с помощью GitHub Pages. Добавьте GitHub Action, чтобы запустить
shellcheck
на любых shell-файлах в этом репозитории (вот один из способов сделать это). Проверьте, что все работает! - [Создайте свой собственный] (https://help.github.com/en/actions/automating-your-workflow-with-github-actions/building-actions) GitHub Action для запуска
proselint
илиwrite-good
для всех файлов.md
в репозитории. Включите его в своем репозитории и проверьте, что он работает, создав пул-реквест (pull request) с опечаткой в нем.
Лицензия CC BY-NC-SA.