Технологии разработки программного обеспечения¶
Требования к выполнению работ¶
В курсе предусмотрены лабораторные работы и курсовой проект.
Цель лабораторных работ — изучение инструментов и практик, применяющихся при разработке программного обеспечения:
Система контроля версий (git)
Системы сборки на примере make
Разработка модульных тестов
Непрерывная интеграция
Цель курсовой работы — разработка законченного программного продукта. При этом важно отличать программу от программного продукта.
Программа пригодна для запуска своим автором на системе, на которой была разработана.
Программный продукт — программа, которую любой человек может запускать, тестировать, исправлять и развивать 1. Такая программа должна быть написана в обобщенном стиле. В частности, диапазон и вид входных данных должны быть настолько обобщенными, насколько это допускается базовым алгоритмом 2. Затем программа должна быть тщательно протестирована. Это означает, что должно быть подготовлено достаточное количество тестовых данных. Тесты должны быть зафиксированы, а их запуск автоматизирован. Наконец, развитие программы в программный продукт требует создания подробной документации.
- 1
Ф. Брукс — Мифический человеко-месяц
- 2
Пример кода, непригодного для передачи другим пользователям:
FILE *dict = fopen("/home/v.pupkin/myproject/dict.txt", "r");
Общие требования¶
Приложения в рамках лабораторных работ разрабатываются на языке C в операционной системе GNU/Linux.
Исходный код оформляется в соответствии с принятыми в курсе соглашениями: Code Style
Допускается использование других распространенных соглашений:
Независимо от выбранных соглашений необходимо соблюдать консистентность в рамках проекта.
Вся работа с изучаемым в курсе инструментарием выполняется в командной строке.
Источники¶
В курсе изучаются широко применяемые в повседневной практике инструменты. Как следствие, по каждому из них существует подробная официальная документация. На страницах методических указаний вы найдете ссылки на документацию или ее разделы. Все указанные материалы обязательны к изучению для выполнения работ.
Основные источники, используемые в курсе:
Pro Git https://git-scm.com/book/en/v2
Git Reference https://git-scm.com/docs
A simple git branching model https://gist.github.com/jbenet/ee6c9ac48068889b0912
GNU Make Manual https://www.gnu.org/software/make/manual/
Lee Copeland — A Practitioner’s Guide to Software Test Design
Проект на лабораторные работы¶
В рамках лабораторных работ вам предлагается реализовать законченный проект, включающий в себя:
Приложение, разработанное в соответствии с заданием.
Систему сборки.
Unit-тесты.
Настроенные автоматические сборки (CI).
Работа над проектом разбивается на короткие итерации. Процесс разработки должен быть зафиксирован в системе контроля версий (git). Проект должен быть опубликован на github.
На выбор вам предлагается два проекта:
- Геометрия
Нужно реализовать операции с геометрическими фигурами: ввод и вывод в одном или нескольких текстовых форматах, вычисление площади, периметра и факта пересечения двух выбранных фигур. Сложность проекта регулируется за счет добавления дополнительных типов фигур.
- Шахматы
Приложение принимает на вход запись партии в шахматной нотации и выводит последовательность шахматных досок с расположением фигур после каждого хода. Сложность проекта регулируется за счет реализации проверок корректности входных данных и соблюдения правил игры. Так, в простейшем случае достаточно проверять, что фигуры перемещаются в пределах доски и не покидают ее пределы. Более полная реализация должна проверять корректность ходов согласно правилам игры, например, допустимость рокировки.
В каждом из проектов возможно несколько вариантов считывания входных данных.
Считывание со стандартного потока ввода. Наиболее простой в реализации, но наименее удобный вариант. Ввод больших массивов данных слишком трудоемок, поэтому удобнее хранить их в файле, а для ввода использовать перенаправление.
Считывание данных из файла. В этом случае синтаксис запуска приложения выглядит следующим образом:
chessviz <FILE>
При невозможности открыть файл приложение должно выводить человекопонятное сообщение об ошибке и завершаться с ненулевым кодом.
Гибридный вариант. Если в командной строке указан путь к файлу, то ходы считываются из него. Иначе приложение считывает данные из
stdin
. Аналогичное поведение можно наблюдать у многих стандартных утилит, например, catСинтаксис запуска в этом случае:
chessviz [FILE]
Ниже приводятся полные описания проектов. Отдельно взятая лабораторная работа предполагает реализацию небольшой части выбранного проекта.
Геометрия¶
Приложение принимает на вход геометрические фигуры различных типов в WKT-подобном формате.
Для каждой фигуры приложение определяет:
Периметр.
Площадь.
С какими фигурами пересекается текущая.
Поддерживаемые фигуры в зависимости от уровня сложности:
- Easy
Окружность.
- Normal
Окружность, треугольник.
- Hard
Окружность, треугольник, полигон.
Грамматика (EBNF):
Object = 'circle' '(' Point ',' Number ')'
| 'triangle' '(' '(' Point ',' Point ',' Point ',' Point ')' ')'
| 'polygon' '(' '(' Point ',' Point ',' Point {',' Point } ')' ')'
Point = Number Number
Number = (* Floating-point number *)
Дополнительные замечания:
Типы фигур нечувствительны к регистру (case insensitive).
Между токенами может быть произвольное количество пробельных символов.
Для простоты можно считать, что одна фигура занимает ровно одну строку. В строке не может быть нескольких фигур.
Пример входных данных:
triangle((-3.0 -2, -1 0.0, -3.0 2.0, -3 -2))
circle(0 0, 1.5)
triangle((3 -2.0, 3.0 2, 1.0 0, 3.0 -2))
Иллюстрация:
Вывод:
1. triangle((-3 -2, -1 0, -3 2, -3 -2))
perimeter = 9.657
area = 4
intersects:
2. circle
2. circle(0 0, 1.5)
perimeter = 9.4247
area = 7.0686
intersects:
1. triangle
3. triangle
3. triangle((3 -2, 3 2, 1 0, 3 -2))
perimeter = 9.657
area = 4
intersects:
2. circle
Приложение должно обрабатывать некорректные входные данные и выводить сообщения об ошибках. Примеры:
circlee(1.0 2.0, 3)
^
Error at column 0: expected 'circle', 'triangle' or 'polygon'
circle(x1 2, 3.0)
^
Error at column 7: expected '<double>'
circle(1 2, 3.1(
^
Error at column 15: expected ')'
circle(1.0 2.1, 3) 123
^
Error at column 19: unexpected token
Шахматы¶
Функциональность приложения¶
Существует ряд шахматных нотаций
для записи партии. Однако текстовая запись может быть неудобна для анализа.
Вам необходимо разработать приложение chessviz
для визуализации
расположения фигур на доске по записи партии в алгебраической нотации.
Запись ходов может содержать ошибки, ваше приложение должно сообщать о них
пользователю.
Формат входных данных¶
На вход приложение принимает список ходов, разделенных символом перевода строки. Запись хода состоит из таких компонентов:
Тип фигуры, выполняющей ход (король, конь и т. д.).
Поле, с которого сделан ход.
Для тихого хода — дефис (-). Для взятия — буква x.
Поле, на которое сделан ход.
Если пешка совершила превращение, после целевого поля указывается фигура, которой она стала.
Для взятия на проходе — знак e.p. (фр. en passant).
Для шаха — знак плюс (+). Для мата — решётка (#).
Грамматика (pseudo-EBNF):
СписокХодов = {ЗаписьХода ПереводСтроки}
ЗаписьХода = НомерХода. Ход Ход
Ход = [ТипФигуры] Поле ТипХода Поле [ТипФигуры | 'e.p.' | '+' | '#']
| Рокировка
ТипФигуры = 'K' | 'Q' | 'R' | 'B' | 'N'
Поле = /[a-h][1-8]/
ТипХода = '-' | 'x'
Рокировка = '0-0-0' | '0-0'
Пример:
1. e2-e4 e7-e5
2. Bf1-c4 Nb8-c6
3. Qd1-h5 Ng8-f6
4. Qh5xf7#
Проверки корректности входных данных¶
При обнаружении ошибки во входных данных приложение прекращает свою работу, выводит человекопонятное сообщение, достаточное для диагностики ошибки, и завершается с ненулевым кодом.
- Easy
Проверки диапазонов: все поля (начало и конец хода) должны быть в пределах доски.
- Normal
В дополнение к проверкам из предыдущих пунктов:
Обозначения фигур должны соответствовать фактическим (нельзя взять коня с поля, на котором стоял слон).
Тип хода должен соответствовать фактическому. Например, если во входных данных записано взятие, а в конечной ячейке нет фигуры соперника, то сообщить об ошибке. Взятие на проходе не рассматриваем.
- Hard
В дополнение к проверкам из предыдущих пунктов:
Проверки соблюдения правил движения фигур (слон движется только по диагонали, только конь может перешагивать через фигуры и т. д.).
Проверка очередности ходов.
- Nightmare
В дополнение к проверкам из предыдущих пунктов:
Проверки допустимости рокировки, см. Рокировка (wiki)
Нарушения формата входных данных: отсутствие любого обязательного компонента записи хода или наличие лишних элементов, некорректные символы (например, недопустимые обозначения фигур) и т. д.
Формат выходных данных¶
После запуска приложение выводит в stdout
номер хода, ход и состояние
доски после него. Вывод может быть в текстовом формате и/или html. В случае
реализации двух форматов вывода пользователь может выбрать формат с помощью
опции командной строки. Возможные варианты синтаксиса:
chessviz [--text|--html]
chessviz [--output=<text|html>]
Текстовый формат¶
Белые фигуры обозначаются символами в верхнем регистре, черные — символами в нижнем регистре.
Обозначение |
Фигура |
---|---|
K |
Король (king) |
Q |
Ферзь (queen) |
R |
Ладья (rook) |
N |
Конь (kNight) |
B |
Слон (bishop) |
P |
Пешка (pawn) |
Пример:
1. e2-e4
8 r n b q k b n r
7 p p p p p p p p
6
5
4 P
3
2 P P P P P P P
1 R N B Q K B N R
a b c d e f g h
HTML¶
Возможно реализовать вывод в формате html. Доска представлена таблицей,
фигуры — элементами span
с классами:
<black|white> <king|queen|rook|knight|bishop|pawn>
Шаблон документа (показать):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>chessviz</title>
<style type="text/css">
table.chessboard {
border: 5px solid #333;
border-collapse: collapse;
height: 320px;
margin: 20px;
width: 320px;
}
table.chessboard caption {
text-align: left;
}
table.chessboard td {
background-color: #fff;
font-size: 25px;
height: 40px;
text-align: center;
vertical-align: middle;
width: 40px;
}
table.chessboard tr:nth-child(odd) td:nth-child(even),
table.chessboard tr:nth-child(even) td:nth-child(odd) {
background-color: #999;
}
table.chessboard .white.king:before { content: "\2654"; }
table.chessboard .white.queen:before { content: "\2655"; }
table.chessboard .white.rook:before { content: "\2656"; }
table.chessboard .white.bishop:before { content: "\2657"; }
table.chessboard .white.knight:before { content: "\2658"; }
table.chessboard .white.pawn:before { content: "\2659"; }
table.chessboard .black.king:before { content: "\265A"; }
table.chessboard .black.queen:before { content: "\265B"; }
table.chessboard .black.rook:before { content: "\265C"; }
table.chessboard .black.bishop:before { content: "\265D"; }
table.chessboard .black.knight:before { content: "\265E"; }
table.chessboard .black.pawn:before { content: "\265F"; }
</style>
</head>
<body>
<table class="chessboard">
<caption>1. e2-e4</caption>
<tr>
<td><span class="black rook"></span></td>
<td><span class="black knight"></span></td>
<td><span class="black bishop"></span></td>
<td><span class="black queen"></span></td>
<td><span class="black king"></span></td>
<td><span class="black bishop"></span></td>
<td><span class="black knight"></span></td>
<td><span class="black rook"></span></td>
</tr>
<tr>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
<td><span class="black pawn"></span></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td><span class="white pawn"></span></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td><span class="white pawn"></span></td>
<td><span class="white pawn"></span></td>
<td><span class="white pawn"></span></td>
<td><span class="white pawn"></span></td>
<td></td>
<td><span class="white pawn"></span></td>
<td><span class="white pawn"></span></td>
<td><span class="white pawn"></span></td>
</tr>
<tr>
<td><span class="white rook"></span></td>
<td><span class="white knight"></span></td>
<td><span class="white bishop"></span></td>
<td><span class="white queen"></span></td>
<td><span class="white king"></span></td>
<td><span class="white bishop"></span></td>
<td><span class="white knight"></span></td>
<td><span class="white rook"></span></td>
</tr>
</table>
</body>
</html>
Отображение в браузере:
Пример работы приложения¶
Входные данные:
1. e2-e4 e7-e5
2. Bf1-c4 Nb8-c6
3. Qd1-h5 Ng8-f6
4. Qh5xf7#
Пример работы приложения (показать):
$ ./chessviz scholars_mate.txt
8 r n b q k b n r
7 p p p p p p p p
6
5
4
3
2 P P P P P P P P
1 R N B Q K B N R
a b c d e f g h
1. e2-e4
8 r n b q k b n r
7 p p p p p p p p
6
5
4 P
3
2 P P P P P P P
1 R N B Q K B N R
a b c d e f g h
1. e7-e5
8 r n b q k b n r
7 p p p p p p p
6
5 p
4 P
3
2 P P P P P P P
1 R N B Q K B N R
a b c d e f g h
2. Bf1-c4
8 r n b q k b n r
7 p p p p p p p
6
5 p
4 B P
3
2 P P P P P P P
1 R N B Q K N R
a b c d e f g h
2. Nb8-c6
8 r b q k b n r
7 p p p p p p p
6 n
5 p
4 B P
3
2 P P P P P P P
1 R N B Q K N R
a b c d e f g h
3. Qd1-h5
8 r b q k b n r
7 p p p p p p p
6 n
5 p Q
4 B P
3
2 P P P P P P P
1 R N B K N R
a b c d e f g h
3. Ng8-f6
8 r b q k b r
7 p p p p p p p
6 n n
5 p Q
4 B P
3
2 P P P P P P P
1 R N B K N R
a b c d e f g h
4. Qh5xf7#
8 r b q k b r
7 p p p p Q p p
6 n n
5 p
4 B P
3
2 P P P P P P P
1 R N B K N R
a b c d e f g h
Замечания по реализации¶
Вывод HTML¶
Генерация HTML верстки без использования предназначенных для этого шаблонизаторов может быть неудобна.
#include <stdio.h>
int main()
{
printf(" <meta charset=\"utf-8\">\n");
printf(" <style type=\"text/css\">\n");
printf(" .white.king:before { content: \"\\2654\"; }\n");
printf(" </style>\n");
return 0;
}
Приходится экранировать символы, имеющие специальное значение (в данном случае обратный слеш и кавычки).
Используется множество вызовов
printf
.
От части вызовов printf
можно избавиться, используя многострочные литералы:
#include <stdio.h>
int main()
{
printf(
" <meta charset=\"utf-8\">\n"
" <style type=\"text/css\">\n"
" .white.king:before { content: \"\\2654\"; }\n"
" </style>\n");
return 0;
}
Современные версии gcc
в режиме компиляции кода на языке C поддерживают
некоторые возможности языка C++, в частности
raw string literal.
Это позволяет избавиться от экранирования специальных символов и от
использования символа переноса строки \n
. Для использования этого расширения
может потребоваться опция компилятора -std=gnu99
или -std=gnu11
.
#include <stdio.h>
int main()
{
printf(
R"( <meta charset="utf-8">
<style type="text/css">
.white.king:before { content: "\2654"; }
</style>)");
return 0;
}
0. Введение в Git¶
Предупреждение
Максимальная оценка за работу — 1 балл. Срок выполнения — 1 неделя. Невыполненная или выполненная частично работа оценивается в 0 баллов без возможности пересдачи.
Цель работы¶
Установить необходимое для работы ПО: git, текстовый редактор, clang-format.
Выполнить базовую настройку рабочего окружения.
Создать репозиторий с приложением «Hello, World», выполнить несколько коммитов, загрузить результат на GitHub.
Подготовка к работе¶
Для выполнения работы необходимо изучить следующий материал из книги Pro Git:
Вам понадобятся следующие команды:
git clone — клонирование удаленного репозитория.
git config — установка и получение глобальных или локальных параметров.
git status — просмотр состояния файлов в локальном репозитории.
git log — просмотр истории изменений.
git add — индексация файлов.
git commit — создание коммита.
git push — обновление ссылок в удаленном репозитории.
Все работы выполняются в командной строке, в ОС Gnu/Linux. Основы работы в командной строке см. в Лабораторная работа 0. Знакомство с ОС GNU/Linux.
Полезные утилиты:
man(1) — просмотр документации.
Пример:
man ls
mkdir(1) — создание каталога.
Пример:
# Создание каталога myproject mkdir myproject # Создание каталога d и всех родительских каталогов # при необходимости mkdir -p a/b/c/d
cd — изменение рабочего каталога. Описание команды расположено в man-странице bash(1).
Пример:
# Переход в каталог myproject cd myproject # Переход в домашний каталог cd
ls(1) — просмотр списка файлов.
touch(1) — создание файла.
Руководство¶
Для создания репозитория пройдите по ссылке ЛР0: hello и выберите себя в списке. Если вас нет в списке, обратитесь к преподавателю практики.
Для вас будет создан репозиторий с именем вида
hello-<username>
. Склонируйте его с помощью команды git clone и перейдите в каталог с репозиторием:$ git clone https://github.com/trpo2021/hello-<username> $ cd hello-<username>
Для фиксации изменений в репозитории нужно указать свои имя и email. Подробности см. в 1.6 Getting Started - First-Time Git Setup. Обратите внимание, в каком файле хранятся параметры после их установки. Если вы не хотите публиковать свой email, ознакомьтесь с руководством: Setting your commit email address.
Возможный вид команд:
$ git config user.email <username>@users.noreply.github.com $ git config user.name "Ivan Ivanov"
Разработайте приложение «Hello, World». Выполните работу в несколько коммитов.
В склонированном репозитории создайте пустой файл
main.c
. В каком состоянии он находится? Для просмотра состояния используйте команду git status.Проиндексируйте пустой файл
main.c
. Используйте команду git add. В каком состоянии находится файл?Зафиксируйте изменения с помощью команды git commit. В заголовке коммита укажите: «Create empty main.c». После создания коммита выполните команду git status, объясните вывод. Выполните команду git log, объясните вывод. Обратите внимание, что локальная ветка
main
теперь опережает удаленнуюorigin/main
на один коммит.Напишите функцию
main
:int main() { return 0; }
Проверьте состояние файла
main.c
.Проиндексируйте файл. Создайте еще один коммит. Посмотрите историю репозитория.
По аналогии добавьте вывод сообщения «Hello, World». Приложение должно компилироваться с опцией
-Wall
без ошибок и предупреждений.В лабораторных работах и курсовом проекте требуется соблюдение code style. Для автоматического форматирования исходников можно использовать утилиту clang-format. Скачайте конфиг
.clang-format
в корень репозиторияhello-<username>
. Обратите внимание, что имя файла должно начинаться с точки. Для скачивания конфига воспользуйтесь командой:wget https://csc-software-development.readthedocs.io/ru/2021/_static/.clang-format
Установите в систему clang-format. Настройте свой текстовый редактор так, чтобы форматирование исходников выполнялось при сохранении файла. Убедитесь, что автоматическое форматирование работает.
Теперь файл
.clang-format
можно закоммитить:$ git add .clang-format $ git commit -m "Import .clang-format config"
Загрузите изменения в удаленный репозиторий с помощью команды git push. Посмотрите историю изменений. Убедитесь, что локальная ветка
main
и удаленнаяorigin/main
указывают на один и тот же коммит.Передайте выполненное задание на ревью преподавателю практики. Для этого:
Откройте в браузере страницу репозитория.
На вкладке Pull requests перейдите в Feedback. Этот Pull request создан автоматически. Не закрывайте и не вливайте его. В лабораторных и курсовых он должен оставаться открытым.
В поле Reviewers выберите своего преподавателя.
Если ревью было пройдено со статусом Changes requested, исправьте замечения и запросите повторное ревью.
Полезные ссылки для настройки текстового редактора:
Официальная документация ClangFormat содержит информацию по настройке vim и emacs.
Vim: плагин rhysd/vim-clang-format (не проверялся)
Visual Studio Code: Code Formatting — официальная документация.
Atom — clang-format — плагин для редактора Atom. Может потребовать дополнительной настройки.
SublimeClangFormat — плагин для Sublime Text.
gedit-plugin-clang-format (не проверялся)
Настройка clang-format на ПК в аудиториях¶
Один из способов настроить автоматическое форматирование исходного кода — создание git hook. Скрипт и подробная инструкция доступны здесь: https://github.com/andrewseidl/githook-clang-format. Способ не рекомендуется самим автором.
В аудиториях вам доступны редакторы: vim, emacs, gedit. Из них наиболее низкий порог входа у gedit.
Gedit не поддерживает возможность автоматического форматирования файла при сохранении. Плагин из списка выше форматирует файл только при нажатии сочетания Alt+Shift+F или любого другого выбранного вами.
Если добиться работоспособности плагина не удалось, есть альтернативный способ подключения clang-format к gedit.
Включить плагин External Tools: Menu → Preferences → Plugins, отметить плагин External Tools.
Menu → Manage External Tools … → Add a new tool (+).
Заполнить поля (см. скриншот):

Известная проблема: при форматировании документа редактор выполняет прокрутку к началу файла.
Контрольные вопросы¶
Где хранятся настройки git?
Как просмотреть состояния файлов в репозитории?
Заключение¶
Работа считается выполненной, если:
У вас установлено и настроено необходимое ПО.
Создан репозиторий
hello-<username>
, в котором находится исходный код программы. Приложение компилируется с опциями-Wall -Werror
. Веткаmain
запушена в удаленный репозиторий.В репозитории есть конфиг
.clang-format
. В вашем текстовом редакторе настроено автоматическое форматирование исходного кода с использованием этого конфига. Исходный код приложения отформатирован.Работа выполнена в несколько коммитов, все действия по индексации файлов, созданию коммитов и загрузке в удаленный репозиторий выполнялись в командной строке.
1. Основы git, основы make¶
Цель работы¶
Создать репозиторий для проекта на лабораторные работы. Репозиторий должен
содержать исходный код, конфигурационный файл .clang-format
, файл
.gitignore
и простейший Makefile
. Отработать этапы создания коммита и
просмотр истории репозитория.
Подготовка к работе¶
Формат вывода команды git log по умолчанию не очень удобен для анализа.
Пример:
$ git log
commit 7198ea228bf5d2f4aea7824f0f0688ddab6a8e25
Merge: 6e71962 4877c36
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:44:21 2016 +0600
Merge branch 'solver'
commit 4877c368dc49037df8decad065d374d567e7e1d9
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:44:06 2016 +0600
Add unit tests
commit 398bdc86f674e3f0410d6a6bdc74b3953625c4df
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:43:40 2016 +0600
Implement quadratic equation solver
commit b5ef5db8508e310f3a41adaea06459edda114568
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:41:51 2016 +0600
Validate user input
commit d4bbead33b6d1f66169c46c189196f6c83b75e0a
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:41:18 2016 +0600
Implement input-output functions
commit 6e71962d03e17154d72b7f3cb95ea4ba2a3d2d1c
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:40:52 2016 +0600
Implement helloworld program
commit de2f75641cfb773f35cd434e5f7861e7195445b3
Author: Evgeny Pimenov <evgeny-p@users.noreply.github.com>
Date: Tue Feb 16 14:39:51 2016 +0600
Initial commit
В качестве альтернативы можно запускать git log
с несколькими
параметрами:
$ git log --oneline --decorate --graph --all
* 7198ea2 (HEAD, main) Merge branch 'solver'
|\
| * 4877c36 (solver) Add unit tests
| * 398bdc8 Implement quadratic equation solver
| * b5ef5db Validate user input
| * d4bbead Implement input-output functions
|/
* 6e71962 Implement helloworld program
* de2f756 Initial commit
Описание используемых параметров доступно в мануале команды git log.
Для упрощения ввода команды, можно создать алиас (синоним). Для этого нужно выполнить в терминале:
git config --global alias.hist "log --oneline --decorate --graph --all"
После чего станет доступна команда git hist
:
$ git hist
* 7198ea2 (HEAD, main) Merge branch 'solver'
|\
| * 4877c36 (solver) Add unit tests
| * 398bdc8 Implement quadratic equation solver
| * b5ef5db Validate user input
| * d4bbead Implement input-output functions
|/
* 6e71962 Implement helloworld program
* de2f756 Initial commit
Подробнее об алиасах: Git Aliases.
Make¶
Make — утилита для преобразования файлов из одной формы в другую. Чаще всего применяется для сборки приложений.
Мейкфайл состоит из правил вида:
<цель> …: <зависимость> …
<команда>
…
Здесь:
<цель>
— имя файла, который должен быть создан или обновлен в результате выполнения правила. В простейшем случае однофайлового приложения это имя исполняемого файла.
<зависимость>
— имя входного файла, необходимого для получения целевого файла. В простейшем случае — имя файла с исходным кодом.
<команды>
— инструкции командной оболочки. В простейшем случае — запуск компилятора.
В качестве отступа перед командами используется один символ табуляции.
Официальная документация: GNU Make.
.gitignore¶
В процессе работы над проектом в рабочем каталоге могут появляться файлы, которые не должны попадать в репозиторий. Например:
Артефакты сборки: исполняемые файлы, объектные файлы
*.o
.Бекап-файлы используемого текстового редактора (
.main.c.swp
,main.c~
,#main.c#
и т. д.).Специфичные для пользователя настройки среды разработки или специфичные для платформы проектные файлы.
Для того, чтобы такие файлы не были закоммичены случайно, а также не
отображались в выводе команды git status, используется файл .gitignore
.
В него записываются имена и маски файлов, которые git должен игнорировать.
Официальная документация: gitignore.
Содержание коммитов¶
Основной принцип: один коммит включает в себя только одно изменение. Изменения в каждом коммите самодостаточны и независимы.
Нарушение этого принципа легко заметить при написании заголовка коммита. Если заголовок выглядит как «Format source code AND implement user input validation AND fix crash», значит один коммит включает в себя слишком много изменений. В данном примере изменения следует разделить на три коммита.
Это не означает, что в каждом коммите может быть изменена только одна строка кода или затронут только один файл. Например, если требуется переименовать функцию, которая используется в сотне файлов, то выполнить переименование во всех точках вызова логично в одном коммите.
Оформление коммитов¶
Заголовок коммита пишется на английском языке, в императиве. Начинается с заглавной буквы, точка в конце не ставится.
Неправильно: implemented request handling.
Правильно: Implement request handling
Подробнее: How to Write a Git Commit Message
Перемещение по истории коммитов¶
Для перемещения по истории коммитов необходимо, чтобы все изменения были
закоммичены. Т.е. не должно быть файлов в состоянии modified
или
staged
.
Один из способов перемещения по истории — использование команды git checkout.
Руководство¶
Предупреждение
Не следует коммитить артефакты сборки (исполняемые и объектные файлы).
Во время выполнения работы до и после команд git add, git commit следует выполнять git status и git hist и анализировать их вывод.
Выберите вариант задания. В работе вам нужно реализовать часть функциональности приложения.
- Геометрия
Ввод и вывод окружностей в формате WKT, проверка корректности входных данных.
- Шахматы
Вывод исходного состояния шахматной доски в текстовом формате и/или в формате html.
Получение задания:
Добавьте в репозиторий файл
.clang-format
из предыдущей работы:wget https://csc-software-development.readthedocs.io/ru/2021/_static/.clang-format
Реализуйте часть приложения в соответствии с выбранным вариантом.
Напишите простейший
Makefile
для сборки приложения. Приложение следует компилировать с опциями-Wall -Werror
. Компиляция должна проходить без ошибок.Добавьте в репозиторий файл
.gitignore
, настройте игнорирование файлов.Загрузите изменения на GitHub. После завершения работы назначте ее на ревью своему преподавателю практики.
Локально отработайте перемещение по истории репозитория. Переместитесь по истории к первому коммиту. Посмотрите содержимое файлов. Вернитесь к последнему коммиту.
Пример содержания коммитов:
Создание приложения «Hello, World».
Реализация функции вывода доски в
stdout
.Добавление мейкфайла
Добавление
.gitignore
.
Контрольные вопросы¶
Что такое коммит?
Этапы создания коммита.
В каких состояниях может находиться файл в репозитории? Как происходит изменение состояния файла?
Зачем нужен файл
.gitignore
?
2. Ветвление в git¶
Цель работы¶
Отработать базовые операции с ветками: создание, переключение между ветками, слияние (merge), перебазирование (rebase). Для выполнения вам предоставляется ряд подготовленных репозиториев. В них нужно выполнить слияние различными способами: формируя линейную или псевдолинейную историю, а также выполнить операции merge и rebase, устранив возникшие в процессе конфликты.
Работа не оценивается. В руководстве предоставлена информация для самопроверки. Отработанные приемы необходимы для выполнения следующих лабораторных и курсового.
После выполнения работы ознакомьтесь с A simple git branching model.
Материал для подготовки к работе¶
Команды:
Настройка отображения конфликтов¶
Git позволяет настроить более подробное отображение конфликтов:
git config --global merge.conflictstyle diff3
По умолчанию конфликты отображаются в виде:
<<<<<<< HEAD
Левая ветка
=======
Правая ветка
>>>>>>> c2392943
В формате diff3 отображается также состояние общего предка сливаемых веток:
<<<<<<< HEAD
Левая ветка
||||||| merged common ancestors
Общий предок
=======
Правая ветка
>>>>>>> c2392943
Руководство¶
Часть 1: создание веток¶
Создать новый репозиторий
branches-basics
.В основной ветке
master
создать несколько коммитов.Создать новую ветку
develop
. Выполнить несколько коммитов.Вернуться на ветку
master
. Создать коммит.Внести изменения в рабочую копию репозитория, не коммитить. Переключиться на ветку
develop
Часть 2: merge и rebase¶
В этой части нужно выполнить слияние веток одним из предложенных способов.
Предварительно необходимо изучить историю каждой ветки репозитория и понять,
какие изменения были сделаны. В результате слияния вы должны получить
работоспособный исходный код, объединяющий в себе изменения, выполненные
в ветках main
и develop
.
Представьте, что вы работали в ветке develop
, а другой разработчик опередил
вас и влил свои изменения в main
раньше. В его и в вашей ветках рабочий
код. После слияния код должен остаться корректным, и работа, выполненная
в main
, не должна быть удалена, как и работа, выполненная в develop
.
Формального алгоритма разрешения конфликтов нет, нужно анализировать изменения и принимать решения, руководствуясь здравым смыслом.
Дополнительные коммиты с исправлениями после мержа или ребейза нежелательны, поскольку замусоривают историю.
В случае ошибки вы можете откатить ветку к ее исходному состоянию с помощью
команды git reset с опцией --hard
.
Склонируйте репозитории:
git clone https://github.com/branches-sandbox/merge-simple.git 1-merge-ff git clone https://github.com/branches-sandbox/merge-simple.git 2-merge-no-ff git clone https://github.com/branches-sandbox/merge-dirty.git 3-merge-dirty git clone https://github.com/branches-sandbox/no-conflict.git 4-rebase-no-conflict git clone https://github.com/branches-sandbox/conflict-easy.git 5-merge-conflict-easy git clone https://github.com/branches-sandbox/conflict-easy.git 6-rebase-conflict-easy git clone https://github.com/branches-sandbox/conflict-hard.git 7-merge-conflict-hard git clone https://github.com/branches-sandbox/conflict-hard.git 8-rebase-conflict-hard
В результате вы должны получить такой список каталогов:
$ ls -1 1-merge-ff 2-merge-no-ff 3-merge-dirty 4-rebase-no-conflict 5-merge-conflict-easy 6-rebase-conflict-easy 7-merge-conflict-hard 8-rebase-conflict-hard
1-merge-ff¶
Нужно выполнить простейшее слияние. История исходного репозитория:
* de873d1 (origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (HEAD -> main, origin/main) Implement min_element function
* 04e3251 Initial commit
История после слияния:
* de873d1 (HEAD -> main, origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (origin/main, origin/HEAD) Implement min_element function
* 04e3251 Initial commit
Мы получили историю, в которой у каждого коммита не более одного предка. Историю такого вида будем называть линейной.
2-merge-no-ff¶
Исходный репозиторий такой же, как и в задании 1-merge-ff
:
* de873d1 (origin/develop, develop) Extract printing array to function
* 03b93ef Count array items with macro
* 5cbd705 (HEAD -> main, origin/main) Implement min_element function
* 04e3251 Initial commit
Нужно выполнить слияние веток с опцией --no-ff
. Результат:
* a7f1782 (HEAD -> main) Merge branch 'develop' into main
|\
| * de873d1 (origin/develop, develop) Extract printing array to function
| * 03b93ef Count array items with macro
|/
* 5cbd705 (origin/main) Implement min_element function
* 04e3251 Initial commit
Мы получили историю, в которой:
У каждого коммита не более двух предков
Между началом ветки (
5cbd705
) и мерж-коммитом (a7f1782
) коммиты присутствуют только в правой ветке.
Историю такого вида будем называть псевдолинейной.
Преимущества псевдолинейной истории:
Если каждая задача решается в отдельной ветке, то при псевдолинейной истории очевидно, какая группа коммитов относится к задаче.
Появляется потенциальная возможность откатить задачу целиком. Для этого достаточно выполнить
revert
мерж-коммита.Мерж-коммит не содержит разрешения конфликтов, как это может быть при нелинейной истории.
Поскольку история остается линейной, легко искать проблемный коммит методом бинарного поиска (git bisect).
Недостатки:
Некоторые рабочие процессы крайне усложняют формирование псевдолинейной истории. Например, если параллельно разрабатывается несколько версий продукта или существуют долгоживущие ветки из десятков сотен коммитов. В этих случаях принимают другие соглашения по работе с репозиторием.
Если требуется выполнить вливание нескольких веток подряд, то разработчикам приходится договариваться об очередности вливания, иначе приходится многократно выполнять ребейз фиче-ветки на
main
.
По соглашениям курса в лабораторных работах и курсовом проекте следует формировать псевдолинейную историю.
3-merge-dirty¶
Исходная история:
* 899302d (develop, origin/develop) Extract printing array to function
* 9513c27 Count array items with macro
| * fe2320b (HEAD -> main, origin/main, origin/HEAD) Expand abbreviation 'min' in message
|/
* 5855f29 Implement min_element function
* 155841b Initial commit
Поскольку коммит fe2320b (main)
не является предком коммита
899302d (develop)
, в этой ситуации fast-forward merge невозможен.
Изучите историю изменений. Для этого воспользуйтесь коммантами git hist -p
или gitk --all
. Посмотрите изменения в каждом коммите и подумайте,
какое поведение должно быть у приложения после слияния веток.
Выполните слияние. В процессе у вас возникнет несложный конфликт. Если вы внимательно изучили историю, то вы заметите, что для разрешения конфликта недостаточно выбрать одну из веток, нужно объединять изменения вручную.
После слияния приложение должно выводить:
Array: 3 1 4 1 5 9 2
Minimum element: 1
Если у вас вывод отличается, выполните откат и повторите слияние.
Результат:
* 8edb092 (HEAD -> main) Merge branch 'develop' into main
|\
| * 899302d (origin/develop, develop) Extract printing array to function
| * 9513c27 Count array items with macro
* | fe2320b (origin/main, origin/HEAD) Expand abbreviation 'min' in message
|/
* 5855f29 Implement min_element function
* 155841b Initial commit
Мы получили историю, в которой:
У коммита может быть более одного предка.
Между началом ветки и мерж-коммитом как в левой, так и в правой ветке могут присутствовать другие коммиты.
Историю такого вида будем называть нелинейной.
Преимущества нелинейной истории:
Сравнительно легко формировать.
Недостатки:
Топология истории значительно усложняется.
Нарушается принцип «один коммит — одно изменение», поскольку мерж-коммит может содержать:
Изменения из левой ветки
Изменения из правой ветки
Разрешения конфликтов
Случайные изменения, внесенные в процессе разрешения конфликтов
В лабораторных работах и курсовом не следует формировать нелинейную историю.
4-rebase-no-conflict¶
В исходной истории fast-forward merge невозможен:
* f5275c7 (origin/develop, develop) Extract printing array to function
* 3314b69 Count array items with macro
| * 4e235e6 (HEAD -> main, origin/main, origin/HEAD) Remove unused header
|/
* 41653c8 Implement min_element function
* 7e36880 Initial commit
В этой ситуации нужно:
Переключиться на ветку
develop
.Выполнить ее rebase на
main
.Переключиться на ветку
main
.Выполнить merge.
В результате получим псевдолинейную историю:
* d3a9b2f (HEAD -> main) Merge branch 'develop' into main
|\
| * b3f6746 (develop) Extract printing array to function
| * 155a81a Count array items with macro
|/
* 4e235e6 (origin/main, origin/HEAD) Remove unused header
| * f5275c7 (origin/develop) Extract printing array to function
| * 3314b69 Count array items with macro
|/
* 41653c8 Implement min_element function
* 7e36880 Initial commit
В склонированном репозитории у вас нет прав на push. При работе со своими репозиториями ветку фиче-ветку можно удалять после мержа, тогда история примет вид:
* d3a9b2f (HEAD -> main, origin/main, origin/HEAD) Merge branch 'develop' into main
|\
| * b3f6746 Extract printing array to function
| * 155a81a Count array items with macro
|/
* 4e235e6 Remove unused header
* 41653c8 Implement min_element function
* 7e36880 Initial commit
Такой подход рекомендуется при выполнении лабораторных работ и курсового.
5-merge-conflict-easy¶
Нужно выполнить мерж и разрешить конфликты.
Исходная история:
* 330f3ad (origin/develop, develop) Separate elements with comma
* d9796db Count array items with macro
| * 7c1c684 (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit
Результат (нелинейная история):
* 642abc8 (HEAD -> main) Merge branch 'develop' into main
|\
| * 330f3ad (origin/develop, develop) Separate elements with comma
| * d9796db Count array items with macro
* | 7c1c684 (origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit
Разультат работы приложения после мержа:
Array: 3, 1, 4, 1, 5, 9, 2, 6, 5, 3
Min element: 1
6-rebase-conflict-easy¶
Те же исходные данные, что и в 5-merge-conflict-easy
, но нужно сформировать
псевдолинейную историю.
Исходная история:
* 330f3ad (origin/develop, develop) Separate elements with comma
* d9796db Count array items with macro
| * 7c1c684 (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit
Результат (псевдолинейная история):
* 41a27f4 (HEAD -> main) Merge branch 'develop' into main
|\
| * 495d413 (develop) Separate elements with comma
| * 4890b37 Count array items with macro
|/
* 7c1c684 (origin/main, origin/HEAD) Add more elements to array
| * 330f3ad (origin/develop) Separate elements with comma
| * d9796db Count array items with macro
|/
* 586e5bd Implement min_element function
* 9e8374a Initial commit
Повторим: после ребейза в истории не сохраняются разрешения конфликтов, это упрощает ее чтение.
7-merge-conflict-hard¶
Прочитайте и осознайте изменения в каждом коммите.
Прочитайте и осознайте изменения в каждом коммите. Вас предупредили дважды.
До сих пор конфликты разрешались относительно легко. В этом примере приходится вручную собирать код из конфликрующих фрагментов, полностью понимая мотивацию изменений.
Исходная история:
* 2a5b4e7 (origin/develop, develop) Extract printing array to function
* 9284a54 Count array items with macro
| * 85c6c1b (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit
Результат (нелинейная история):
* b9f54e9 (HEAD -> main) Merge branch 'develop' into main
|\
| * 2a5b4e7 (origin/develop, develop) Extract printing array to function
| * 9284a54 Count array items with macro
* | 85c6c1b (origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit
Результат работы программы:
Array: 3 1 4 1 5 9 2 6 5 3
Max element: 9
8-rebase-conflict-hard¶
Исходные данные те же, что и в 7-merge-conflict-hard
, но теперь разрешать
конфликты придется дважды: при применении каждого коммита из ветки develop
.
Это также пример ситуации, в которой формирование псевдолинейной истории может
быть более трудоемким, чем простой мерж.
Исходная история:
* 2a5b4e7 (origin/develop, develop) Extract printing array to function
* 9284a54 Count array items with macro
| * 85c6c1b (HEAD -> main, origin/main, origin/HEAD) Add more elements to array
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit
Результат (псевдолинейная история):
* 74c74d1 (HEAD -> main) Merge branch 'develop' into main
|\
| * 150787b (develop) Extract printing array to function
| * 2a05362 Count array items with macro
|/
* 85c6c1b (origin/main, origin/HEAD) Add more elements to array
| * 2a5b4e7 (origin/develop) Extract printing array to function
| * 9284a54 Count array items with macro
|/
* 96568b0 Implement max_element function
* c61eb1d Initial commit
Результат работы программы:
Array: 3 1 4 1 5 9 2 6 5 3
Max element: 9
Заключение¶
В рамках нашего курса следует формировать псевдолинейную историю. Оптимизируйте результат своей работы для удобства читателя.
На практике возможны процессы, в которых трудозатраты на операции rebase+merge слишком высоки, поэтому на уровне соглашений принято создавать нелинейную историю.
Подробнее о соглашениях по работе с репозиторием можно почитать в A simple git branching model.
Контрольные вопросы¶
Что такое ветка?
Что такое
HEAD
?Способы создания веток.
Как узнать текущую ветку?
Как переключаться между ветками?
Что такое merge? Что такое fast-forward merge?
Что такое rebase? Как он работает?
Как можно перемещать метки?
3. Сборка приложения¶
Цель работы¶
Разделить приложение, разработанное в
лабораторной работе 1, на два модуля: статическую
библиотеку и приложение. Доработать Makefile
. Настроить автоматические
сборки в сервисе GitHub Actions.
- Геометрия
Реализовать вычисление периметра и площади для выбранных фигур.
- Easy
Окружность
- Normal
Окружность, треугольник.
- Hard
Окружность, треугольник, полигон.
- Шахматы
Реализовать движение пешек в приложении. Для этого будет достаточно поддержать входные данные в формате вида:
1. e2-e4 e7-e5 …
Движения фигур на этом этапе реализовывать не нужно.
Материалы для подготовки к работе¶
Документация GNU Make
Обязательный минимум:
Overview of make
An Introduction to Makefiles
Writing Makefiles
Writing Rules
Writing Recipes in Rules
How to Use Variables:
Basics of Variable References
The Two Flavors of Variables
GitHub Actions:
Процесс сборки¶
Этапы компиляции¶
В простейшем случае компилятор принимает на вход файлы с исходным кодом и создает исполняемый файл:
Процесс сборки приложения состоит из ряда этапов, на каждом из которых ее можно прервать:
Для сборки многофайловых приложений сначала из файлов с исходным кодом получают объектные файлы:
gcc -Wall -Werror -c main.c
gcc -Wall -Werror -c chess.c
Затем из объектных файлов собирают исполняемый файл:
gcc main.o chess.o -o chessviz
Зависимости при сборке приложения на языке С¶
Рассмотрим примитивное приложение на языке С, состоящее из трех файлов.
hello.h:
#pragma once
void say_hello();
hello.c:
#include "hello.h"
#include <stdio.h>
void say_hello()
{
printf("Hello\n");
}
main.c:
#include "hello.h"
int main()
{
say_hello();
return 0;
}
Для сборки этого приложения можно использовать мейкфайл:
CFLAGS = -Wall -Wextra -Werror
hello: main.o hello.o
$(CC) $(CFLAGS) -o $@ $^
main.o: main.c
$(CC) -c $(CFLAGS) -o $@ $<
hello.o: hello.c
$(CC) -c $(CFLAGS) -o $@ $<
Приведенный мейкфайл намеренно избыточен для простоты понимания. Ему соответствует следующий граф зафисимостей:

Легко понять последовательность команд в случае изменения одного из исходников.
Так, при изменении файла main.c
:
Где \(mtime\) — Modification time, время изменения файла, см. stat(2).
Проблема этого мейкфайла в том, что он не учитывает зависимость исходников от
заголовочного файла. Так, при изменении файла hello.h
должны быть
перекомпилированы все подключающие его исходники, но в данном случае этого не
произойдет. На практике крайне сложно вручную отслеживать подключения всех
заголовочных файлов. Для решения этой проблемы компилятор может сгенерировать
файлы с зависимостями с помощью опции -MMD
. Эти файлы нужно подключить с
помощью директивы -include
. После этого мейкфайл примет вид:
CFLAGS = -Wall -Wextra -Werror
CPPFLAGS = -MMD
hello: main.o hello.o
$(CC) $(CFLAGS) -o $@ $^
main.o: main.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
hello.o: hello.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
-include main.d hello.d
Статические библиотеки¶
Для удобства сборки, тестирования и распространения приложения разделяют на модули. Один из типов таких модулей — статическая библиотека.
Для сборки статической библиотеки нужно:
Скомпилировать исходники в объектные файлы.
Создать архив объектных файлов.
Для нашего примера мейкфайл может выглядеть так:
CFLAGS = -Wall -Wextra -Werror
CPPFLAGS = -MMD
hello: main.o libhello.a
$(CC) $(CFLAGS) -o $@ $^
main.o: main.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
libhello.a: hello.o
ar rcs $@ $^
hello.o: hello.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) -o $@ $<
-include main.d hello.d
Тогда граф зависимостей примет вид:

Нет строгого алгоритма для определения, какой код следует разместить в библиотеке, а какой в приложении. Руководствуйтесь здравым смыслом и размещайте в библиотеке код, который потенциально может быть переиспользован в другом приложении.
Структура проекта¶
Структурировать проект на файловой системе следовало бы в первую очередь, но мы отложили этот этап для сокращения примеров выше. Для старта предлагается следующая структура:
.
|-- bin
| `-- .keep
|-- .clang-format
|-- .gitignore
|-- Makefile
|-- obj
| |-- .keep
| `-- src
| |-- hello
| | `-- .keep
| `-- libhello
| `-- .keep
|-- README.md
`-- src
|-- hello
| `-- main.c
`-- libhello
|-- hello.c
`-- hello.h
Здесь:
bin
— каталог для исполняемых файлов.obj
— каталог для промежуточных артефактов сборки (объектные файлы, файлы зависимостей, статические библиотеки).*/.keep
— пустой файл для сохранения структуры каталогов проекта.
Пример структуры доступен по ссылке: project-skeleton.
Если мейкфайл в примере по ссылке кажется вам слишком сложным или непонятным, вы можете пойти по одному из двух путей:
Писать свой вариант проще. Пусть для каждого исходника или объектного файла будет явно прописанное правило. В этом случае будьте готовы к большому работу однообразной работы и необходимости вручную вписывать каждый новый исходник в мейкфайл. Если вы пойдете этим путем, то хотя бы вынесите флаги компиляции в переменную. Это сэкономит время, если потребуется изменить набор опций для отладочной сборки. В рамках курса мы осознанно не рассматриваем как реализовывать отдельные конфигурации Debug/Release с помощью мейкфайла.
Прочитайте документацию и разберитесь в происходящем.
Код возврата (exit status)¶
Код возврата — это целочисленное значение, которое дочерний процесс возвращает родительскому процессу в момент завершения.
При запуске программ из командной строки родительским процессом выступает командная оболочка (зачастую bash), а дочерним процессом — запускаемая утилита.
До сих пор написанные вами приложения возвращали 0. Возвращаемое значение можно
посмотреть в переменной окружения $?
. Например:
int main()
{
return 42;
}
$ gcc -o answer main.c
$ ./answer
$ echo $?
42
Принято соглашение: в случае успешного завершения приложение должно возвращать
ноль, в случае ошибки — ненулевой код. Этому соглашению следует большинство
утилит, в том числе gcc
и make
:
$ gcc
gcc: fatal error: no input files
compilation terminated.
$ echo $?
1
Руководство¶
Соглашения о рабочем процессе¶
Эта и следующая лабораторные работы выполняются в соответствии с A simple git branching model.
Для каждой задачи/новой функциональности/лабораторной работы создается новая ветка.
После завершения работы ветка сливается в
main
. При необходимости выполняетсяrebase
.
Подробнее — см. A simple git branching model.
Аналогичные соглашения требуется соблюдать и в курсовом проекте.
Доработка структуры проекта¶
Прежде чем реализовывать новую функциональность, подготовье структуру проекта по
аналогии с примером выше. Создайте ветку lab-3
и приведите проект к
требуемой структуре.
Возможная структура проекта:
.
|-- bin
| `-- .keep
|-- .clang-format
|-- .gitignore
|-- Makefile
|-- obj
| |-- .keep
| `-- src
| |-- chessviz
| | `-- .keep
| `-- libchessviz
| `-- .keep
|-- README.md
`-- src
|-- chessviz
| |-- board_read.c
| |-- board_read.h
| `-- main.c
`-- libchessviz
|-- board.c
|-- board.h
|-- move.c
|-- move.h
|-- board_print_html.c
|-- board_print_html.h
|-- board_print_plain.c
`-- board_print_plain.h
Ваш набор файлов может отличаться.
Добавьте каталог src
в include path (опция -I
компилятора). Не
используйте относительные пути для подключения заголовочных файлов.
Для примера рассмотрим файл chessviz/board_read.c
.
Неправильно:
#include "../libchessviz/board.h"
При использовании относительных путей их придется менять при перемещении
клиентского файла в другой каталог. Например, при перемещении
chessviz/board_read.c
-> chessviz/io/board_read.c
относительный путь к
файлу изменится:
#include "../../libchessviz/board.h"
Заголовочные файлы должны подключаться единообразно независимо от физического расположения всего дерева исходников.
Правильно:
#include <libchessviz/board.h>
Требования к работе:
Компилировать приложение следует с опциями
-Wall -Werror
Для сборки приложения должно быть достаточно команд
git clone <remote url> && cd chessviz && make
.При изменении одного файла с исходным кодом должен перекомпилироваться только он и зависящие от него артефакты. При изменении заголовочного файла должны перекомпилироваться все файлы, в которые он подключен.
Если в исходниках не было изменений, то при повторном запуске
make
не должно выполняться никаких сборочных операций.Makefile
должен содержать цельclean
для удаления артефактов сборки. Цель должна выполняться, даже если в корне репозитория создан файл с именемclean
.В заголовочных файлах должны быть #include guard или #pragma once.
Настройка автоматических сборок¶
Для настройки автоматических сборок можно использовать следующий шаблон.
.github/workflows/build.yml
:
name: Build
on: push
jobs:
build:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Check codestyle
run: git ls-files *.{c,h} | xargs clang-format -i --verbose && git diff --exit-code
- name: Build application
run: make
Подробности см. в документации GitHub Actions.
Для проверки создайте отдельную ветку lab-3-fail
и попробуйте закоммитить в
нее неотформатированный или некомпилируемый код. Убедитесь, что сборка не
проходит.
Сборка не проходит, если хотя бы одна команда завершилась с ненулевым кодом.
Разберем подробнее команду:
git ls-files *.{c,h} | xargs clang-format -i --verbose && git diff --exit-code
поскольку вам может понадобиться адаптировать ее для своего проекта.
git ls-files *.{c,h}
раскрывается оболочкой вgit ls-files *.c *.h
, см Brace Expansion в bash(1). Выводит вstdout
список файлов, соответствующих маскам. Пример:src/hello/main.c src/libhello/hello.c src/libhello/hello.h
Конструкция вида
a | b
перенаправляетstdout
командыa
вstdin
командыb
.-
Принимает список аргументов из командной строки.
Читает список строк из
stdin
и добавляет их аргументами к формируемой команде.Выполняет собранную команду.
В нашем примере получим команду:
clang-format -i --verbose src/hello/main.c src/libhello/hello.c src/libhello/hello.h
После ее выполнения исходники в репозитории будут отформатированы в соответствии с приложенным конфигом.
git diff --exit-code
выведет вstdout
изменения. Если исходники после автоматического форматирования отличаются от закоммиченных, то благодаря опции--exit-code
команда завершится с ненулевым кодом и сборка не пройдет.
Реализация функциональности¶
Теперь, когда структура проекта подготовлена, можно реализовать функциональность
приложения в соответствии со своим вариантом. После завершения работы ветку
lab-3
можно смержить в main
с опцией --no-ff
и передать на ревью.
Контрольные вопросы¶
В чем преимущества правильно написанного
Makefile
перед простым скриптом сборки видаgcc -Wall -Wextra -Werror *.c -o app
?Как make определяет необходимость выполнения команд?
Что такое и зачем используется CI?
4. Модульное тестирование¶
Цель работы¶
Проверить, что в случае непрохождения тестов автоматическая сборка завершается
со статусом failed
.
- Геометрия
Реализовать определение факта пересечения фигур.
- Easy
Окружность.
- Normal
Окружность, треугольник.
- Hard
Окружность, треугольник, полигон.
- Шахматы
Реализовать движение всех фигур.
Материалы для подготовки к работе¶
Общие сведения¶
Для покрытия тестами приложение декомпозируют на минимальные самодостаточные единицы. В процедурных языках программирования такая единица — функция.
Для покрытия функции тестами необходимо вызывать ее с заранее подготовленными входными параметрами и сравнить фактический результат ее работы с ожидаемым.
Код тестов должен быть написан отдельно от кода приложения. Поэтому для запуска тестов пишется отдельное приложение. Тестовое приложение обычно включает в себя автоматическую регистрацию новых тестов и формирование отчета. Для автоматизации процесса используют специальные библиотеки.
Сборка приложения и тестов¶
В предыдущей лабораторной работе мы разделили проект на консольное приложение и
статическую библиотеку. Артефакт сборки тестов — еще один исполняемый файл,
который линкуется со статической библиотекой библиотекой. Таким образом,
граф зависимостей для примера hello
принимает вид (диаграмма сокращена):

Из графа зависимостей мы видим, что один и тот же код как используется в приложении, так и покрыт тестами.
При сборке нужно предусмотреть возможность конфликтов имен объектных файлов.
Например, при компиляции файлов src/main.c
и test/main.c
не должен
создаваться один и тот же объектный файл obj/main.o
. Один из способов
предовращения таких конфликтов — дублирование структуры репозитория в каталоге
obj
:
obj
|-- src
| |-- board.o
| `-- main.o
`-- test
|-- board_test.o
`-- main.o
Библиотека ctest¶
Примеры доступны по адресу: https://github.com/bvdberg/ctest.
Файл test/main.c
:
#define CTEST_MAIN
#include <ctest.h>
int main(int argc, const char** argv)
{
return ctest_main(argc, argv);
}
Пример определения теста:
#include <sum.h>
#include <ctest.h>
CTEST(arithmetic_suite, simple_sum)
{
// Given
const int a = 1;
const int b = 2;
// When
const int result = sum(a, b);
// Then
const int expected = 3;
ASSERT_EQUAL(expected, result);
}
Здесь:
CTEST(<suite_name>, <test_name>)
— макрос для создания и регистрации тестовой функции. Все определенные таким образом тесты запускаются автоматически при вызовеctest_main
.ASSERT_EQUAL(<expected>, <real>)
— макрос для сравнения ожидаемого результата с фактическим.
Библиотека ctest
предоставляет следующие макросы:
CTEST(sname, tname)
— макрос для определения теста.sname
— имя набора тестов,tname
— имя теста.CTEST_LOG(const char* fmt, ...)
— запись в лог. Используется синтаксис аналогичныйprintf
.ASSERT_STR(exp, real)
— макрос для сравнения строк (``char* ``).ASSERT_EQUAL(exp, real)
— сравнение переменных типаint
.ASSERT_EQUAL_U(exp, real)
— сравнение переменных типаunsigned
.ASSERT_NOT_EQUAL(exp, real)
— проверка на неравенство переменных типаint
.ASSERT_NOT_EQUAL_U(exp, real)
— проверка на неравенство переменных типаunsigned
.ASSERT_DATA(exp, expsize, real, realsize)
— сравнение массивов.ASSERT_INTERVAL(exp1, exp2, real)
— проверка принадлежности числаreal
интервалу[exp1, exp2]
. Все аргументы типа intASSERT_DBL_NEAR_TOL(exp, real, tol)
— сравнение переменных типаdouble
с заданным допустимым отклонениемtol
.ASSERT_DBL_NEAR(exp, real)
— сравнение переменных типаdouble
сtol = 1e-4
ASSERT_NULL(real)
ASSERT_NOT_NULL(real)
ASSERT_TRUE(real)
ASSERT_FALSE(real)
Руководство¶
В структуру проекта, сформированную в предыдущей лабораторной работе, следует
добавить каталоги test
и thirdparty
. В результате получим:
.
|-- bin
| |-- chessviz
| `-- chessviz-test
|-- .clang-format
|-- .gitignore
|-- Makefile
|-- obj
|-- README.md
|-- src
|-- test
| |-- board_test.c
| `-- main.c
`-- thirdparty
|-- .clang-format
`-- ctest.h
Для сторонних библиотек создан отдельный конфиг .clang-format
. Его
содержимое:
---
DisableFormat: true
SortIncludes: false
...
Форматирование исходного кода сторонних библиотек усложняет их поддержку и
вендоринг новых версий, а в случае с ctest
приводит к появлению
предупреждений на этапе компиляции.
Этапы работы¶
Каждый этап — отдельный коммит.
Добавить в репозиторий библиотеку ctest (заголовочный файл).
Добавить точку входа для запуска тестов —
test/main.c
. Настроить сборку и запуск тестов (доработатьMakefile
). Обычно цель по умолчанию используется только для компиляции приложения. Для компиляции и запуска тестов создают отдельную цель test. Таким образом, для полной сборки приложения и запуска тестов нужно выполнить команды:make make test
Написать любой простейший тест, проверить его работоспособность.
Реализовать функциональность в соответствии со своим вариантом.
Покрыть приложение тестами. Каждую группу тестов можно оформить в отдельный коммит.
Настроить запуск тестов в CI.
Code Style¶
В руководство включены рекомендации из различных источников, рекомендуется ознакомиться с оригиналами. Здесь собраны правила, нарушения которых наиболее часто встречаются в коде студентов.
Вертикальные отступы¶
Рекомендуется использовать пустые строки вокруг определений функций, типов и логических блоков кода. Допускается использование не более одной пустой строки подряд.
double calculate_distance(...)
{
...
}
void circle_print(...)
{
...
}
void triangle_print(...)
{
...
}
double calculate_distance(...)
{
...
}
void circle_print(...)
{
...
}
void triangle_print(...)
{
...
}
Имена идентификаторов¶
lower_case_with_underscores
: переменные, функции;UpperCamelCase
: структуры, объединения, перечисления;UPPER_CASE_WITH_UNDERSCORES
: макросы.
Следует давать переменным содержательные имена. Различные типы объявлений подчиняются следующим правилам:
Имена типов и переменных должны быть существительными.
Имена функций должны содержать глаголы.
Неудачные имена:
// Слишком общее имя.
bool flag = false;
// Непонятно назначение буфера.
char buf[BUFSIZE];
// Герундий в имени функции.
int* finding_element(const int* begin, const int* end);
// Что конкретно проверяет эта функция?
bool check(const Triangle* triangle);
Лучше:
// Название флага отражает смысл.
bool found = false;
// Понятно, какие данные хранятся в буфере.
char error_message[MAX_MESSAGE_LENGTH];
// Ок - глагол.
int* find_element(const int* begin, const int* end);
// Семантика функции понятна без документации.
// Для предиката используется префикс is_, в точке вызова
// будет очевиден тип возвращаемого значения.
bool is_equilateral(const Triangle* triangle);
Для структур, объединений и перечислений рекомендуется создавать синонимы.
Правильно:
typedef struct {
double x;
double y;
} Point;
Point point;
Исключение — рекурсивные структуры:
typedef struct List {
void* data;
struct List* next;
} List;
List* list;
Неправильно:
struct Point {
double x;
double y;
};
struct Point point;
Для элементов перечислений используется префикс имени перечисления:
typedef enum {
HttpStatusOk = 200,
HttpStatusBadRequest = 400,
HttpStatusNotFound = 404,
...
} HttpStatus;
Не используйте магические константы¶
Неправильно:
for (int i = 0; i < 12; ++i) {
...
}
Правильно:
enum { MONTHS_IN_YEAR = 12 };
for (int i = 0; i < MONTHS_IN_YEAR; ++i) {
...
}
Иногда арифметическое выражение понятнее, чем значение этого выражения.
const int seconds_in_day = 86400;
Такую запись проще проверить на корректность:
const int seconds_in_day = 24 * 60 * 60;
Не пишите user-specific код¶
Подобный код не является переносимым:
FILE* dict = fopen("/home/v.pupkin/myproject/dict.txt", "r");
Лучше:
// Получение пути к файлу из внешнего источника:
// из старнадрного потока ввода, аргументов командной строки,
// конфигурационного файла, и т. д.
const char* dict_file_path = ... ;
FILE* dict = fopen(dict_file_path, "r");
Не используйте глобальные переменные¶
В подавляющем большинстве случаев глобальные переменные усложняют поддержку кода.
Некоторые последствия использования глобальных переменных:
Нарушение локальности. Чем меньше область видимости отдельных элементов, тем проще отлаживать код.
Неявные зависимости. Глобальные переменные могут неявно связывать любые части части исходного кода.
Усложнение тестирования. Каждый тест должен быть независим. При наличии глобальных переменных каждый тест вынужден явно присваивать всем переменным некоторые значения для настройки стартового окружения. Наличие разделяемого между тестами состояния может привести к тому, что результат их выполнения зависит от порядка запуска.
Использование глобальных констант допустимо.
Использование const¶
Используйте const
везде, где это имеет смысл. Неизменяемые объекты упрощают
понимание программы. Рассматривайте const
в сигнатурах функций как часть
контракта.
Правильно:
// Сигнатура гарантирует, что переданный объект не будет изменен.
void print_circle(const Circle* circle);
// Вычисленное расстояние не меняется в последующем коде.
const double distance = calculate_distance(...);
Избыточно:
// Результат может быть сохранен в изменяемую переменную.
const double calculate_distance(...);
// Невозможно форсировать контракт.
double distance = calculate_distance(...);
// Параметры копируются и гарантированно не будут изменены в вызывающем коде
// даже без использования const.
double calculate_distance(const Point point_a, const Point point_b);
Tip of the Week #109: Meaningful const in Function Declarations
Пишите короткие функции¶
Функции должны быть короткими и решать ровно одну задачу, очевидную из имени. По возможности старайтесь писать чистые функции.
Можно ориентироваться на следующие эвристики:
Функция должна умещаться на экране.
В функции должно быть не более 5−10 локальных переменных.
В функции должно быть не более 3 уровней вложенности.
Указывайте имена параметров в объявлениях функций¶
Имена параметров в объявлениях функций не являются обязательными для компилятора, но значительно улучшают читаемость.
// Кто кого захватывает?
bool can_capture(Piece, Piece);
bool can_capture(Piece attacker, Piece victim);
Избегайте избыточных ветвлений (if true return true)¶
Избыточно:
bool can_capture(Piece attacker, Piece victim)
{
if (attacker.color != victim.color) {
return true;
} else {
return false;
}
}
Лаконично:
bool can_capture(Piece attacker, Piece victim)
{
return attacker.color != victim.color;
}
Используйте ранние возвраты¶
if (ok) {
call_a()
if (a_ok) {
call_b()
call_c()
if (b_ok && c_ok) {
call_d()
call_e()
call_f()
call_g()
} else {
throw_exception("Method A failed")
} else {
throw_exception("Method B or C failed")
} else {
throw_exception()
}
call_a()
if (a_error) {
throw_exception()
}
call_b()
call_c()
if (c_error || c_error) {
throw_exception()
}
call_d()
call_e()
call_f()
call_g()
LLVM Coding Standards: Use Early Exits and continue to Simplify Code C2 Wiki: If Ok
Не используйте else после return, break, continue¶
Не используйте else
или else if
после ключевых слов, прерывающих поток
выполнения команд.
void foo(int value)
{
int local = 0;
for (int i = 0; i < 42; i++) {
if (value == 1) {
return;
} else {
local++;
}
if (value == 2) {
continue;
} else {
local++;
}
}
}
void foo(int value) {
int local = 0;
for (int i = 0; i < 42; i++) {
if (value == 1) {
return;
}
local++;
if (value == 2) {
continue;
}
local++;
}
}
Выносите предикаты в функции¶
Часто в коде встречаются циклы, вычисляющие единственное булево значение. Пример:
bool found_foo = false;
for (size_t i = 0; i n; ++i) {
if (is_foo(bar_list[i])) {
found_foo = true;
break;
}
}
if (found_foo) {
...
}
Вместо таких циклов рекомендуется писать функции с использованием ранних выходов:
static bool contains_foo(Bar* bars, size_t n)
{
for (size_t i = 0; i <n; ++i) {
if (is_foo(bars[i])) {
return true;
}
}
return false;
}
...
if (contains_foo(bars, n)) {
...
}
Такой подход уменьшает уровнь вложенности и помогает вынести общий код, который может быть переиспользован при проверке такого же предиката.
Гораздо важнее, что в этом случае вы вынуждены выбрать имя для предиката. Польза не так очевидна на синтетическом примере, но если условие становится более сложным, то именованный предикат позволяет понять смысл условия, не погружаясь в детали реализации.
LLVM Coding Standards: Turn Predicate Loops into Predicate Functions:
Порядок заголовочных файлов¶
Предпочительный порядок заголовочных файлов:
Главный заголовочный файл модуля.
Локальные/приватные заголовочные файлы.
Заголовочные файлы сторонних библиотек.
Системные заголовочные файлы.
Блоки следует отделять пустой строкой. Файлы в каждом блоке должны быть отсортированы лексикографически.
Абстрактный пример для circle.c:
#include "circle.h"
#include "wkt_reader.h"
#include <json.h>
#include <svg.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
Подключайте как можно меньше¶
Избыточное подключение заголовочных файлов замедляет компиляцию, особенно при подключении в другие заголовочные файлы.
Распространенная ошибка: в заголовочный файл circle.h
подключается
stdio.h
, при этом в circle.h
не используются определения из stdio.h
:
circle.h:
#pragma once
#include <stdio.h>
typedef struct {
...
} Circle;
void circle_print(const Circle* circle);
circle.c:
#include "circle.h"
void circle_print(const Circle* circle)
{
printf(...);
}
В этом случае stdio.h
следует подключать в circle.c
:
circle.h:
#pragma once
typedef struct {
...
} Circle;
void circle_print(const Circle* circle);
circle.c:
#include "circle.h"
#include <stdio.h>
void circle_print(const Circle* circle)
{
printf(...);
}
В этом примере в circle.h
нужно подключить stdio.h
, поскольку
используется тип FILE
:
circle.h:
#pragma once
#include <stdio.h>
typedef struct {
...
} Circle;
void circle_print(const Circle* circle, FILE* stream);
Скрывайте детали реализации¶
Не выносите приватные объявления в заголовочный файл. Каждое публичное определение становится частью контракта с пользователем модуля. В долгосрочной перспективе это осложняет поддержку и рефакторинг кода.
circle.h:
#pragma once
typedef struct {
...
} Circle;
// Функция calculate_distance - деталь реализации функции circle_intersects,
// пользователь модуля не будет вызывать ее напрямую.
double calculate_distance(const Point* point_a, const Point* point_b);
bool circle_intersects(const Circle* circle_a, const Circle* circle_b);
circle.c:
#include "circle.h"
// Функцию следует определить только в файле реализации и добавить
// спецификатор static.
static double calculate_distance(const Point* point_a, const Point* point_b)
{
...
}
Кодировка файлов¶
Файлы с исходным кодом должны быть в кодировке UTF-8. Преобразование из
кодировки Windows-1251 можно выполнить с помощью утилиты iconv
:
iconv -f cp1251 -t utf8 input.c > output.c
Символ перевода строки — Line Feed (LF). Для преобразования файла из формата
DOS в формат Unix используется утилита dos2unix
:
dos2unix input.c
.clang-format¶
Код следует форматировать утилитой clang-format с конфигурационным файлом .clang-format:
---
Language: Cpp
AccessModifierOffset: -4
AlignAfterOpenBracket: AlwaysBreak
AlignConsecutiveAssignments: false
AlignConsecutiveDeclarations: false
AlignEscapedNewlines: Left
AlignOperands: false
AlignTrailingComments: true
AllowAllParametersOfDeclarationOnNextLine: true
AllowShortBlocksOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: None
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AlwaysBreakAfterReturnType: None
AlwaysBreakBeforeMultilineStrings: true
AlwaysBreakTemplateDeclarations: true
BinPackArguments: false
BinPackParameters: false
BraceWrapping:
AfterClass: false
AfterControlStatement: false
AfterEnum: false
AfterFunction: true
AfterNamespace: false
AfterStruct: false
AfterUnion: false
AfterExternBlock: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: true
SplitEmptyRecord: true
SplitEmptyNamespace: true
BreakBeforeBinaryOperators: All
BreakBeforeBraces: Custom
BreakBeforeInheritanceComma: false
BreakBeforeTernaryOperators: true
BreakConstructorInitializersBeforeComma: false
BreakConstructorInitializers: BeforeColon
BreakStringLiterals: true
ColumnLimit: 80
CompactNamespaces: true
ConstructorInitializerAllOnOneLineOrOnePerLine: true
ConstructorInitializerIndentWidth: 4
ContinuationIndentWidth: 8
Cpp11BracedListStyle: true
DerivePointerAlignment: false
ExperimentalAutoDetectBinPacking: false
FixNamespaceComments: false
IncludeBlocks: Preserve
IndentCaseLabels: false
IndentPPDirectives: None
IndentWidth: 4
IndentWrappedFunctionNames: false
KeepEmptyLinesAtTheStartOfBlocks: false
MaxEmptyLinesToKeep: 1
NamespaceIndentation: None
PenaltyBreakAssignment: 2
PenaltyBreakBeforeFirstCallParameter: 19
PenaltyBreakComment: 300
PenaltyBreakFirstLessLess: 120
PenaltyBreakString: 1000
PenaltyExcessCharacter: 1000000
PenaltyReturnTypeOnItsOwnLine: 60
PointerAlignment: Left
ReflowComments: true
SortIncludes: true
SortUsingDeclarations: true
SpaceAfterCStyleCast: false
SpaceAfterTemplateKeyword: true
SpaceBeforeAssignmentOperators: true
SpaceBeforeParens: ControlStatements
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 1
SpacesInAngles: false
SpacesInContainerLiterals: false
SpacesInCStyleCastParentheses: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
Standard: Cpp11
TabWidth: 8
UseTab: Never
...
Процессы, сроки, темы¶
Общая информация и порядок работы¶
Командный проект предлагается на команду из 2-3 человек. Основное требование к проекту: это должен быть законченный программный продукт. Проект при сдаче будет оцениваться по следующим критериям:
Соответствие реализованной функциональности требуемой в проекте.
Поэтапность разработки через системы контроля версий.
Покрытие тестами, включая unit тестирование.
Стиль программирования (Code Style).
Практика использования CI.
Полнота документации.
Итоговая презентация.
Команда может предложить свою тему в дополнение к перечисленным ниже.
С 8 по 21 февраля — выбор и согласование темы.
С 22 февраля по 7 марта – разработка первой версии ТЗ.
С 8 по 28 марта — разработка и согласование ТЗ и плана работ.
С 29 марта — начало кодирования.
С 25 апреля — отладка, документация, интеграционное тестирование, готовность показать beta-версию программы.
С 9 мая – быть готовыми представлять законченный проект.
К защите допускаются команды, не имеющие долгов по лабораторным занятиям, а также предоставившим beta-версию программы в срок.
Выбор темы и формирование команды¶
В установленный срок выберите тему и согласуйте ее с преподавателем практики. В одной группе темы не могут повторяться.
Для создания команды и репозитория пройдите по ссылке: GitHub Classroom: trpo2021/cw
Если команда еще не создана, создайте ее. Укажите название команды по шаблону:
<группа>_<тема>
. Например:iv-021_search-engine
. Если в команде участвуют студенты из разных групп, то укажите все:ip-011_ip-012_fuzzy-matching
. Hint: договоритесь между собой, кто из участников создает команду.Если команда создана, найдите ее в списке и нажмите Join.
В репозитории создайте файл
README.md
. В заголовке укажите свою тему.
В разделе wiki созданного репозитория напишите ТЗ, в разделе issues заведите задачи на разработку.
По готовности ТЗ и плана работ заполните форму: ТРПО 2021: ТЗ и план работ.
Содержание ТЗ¶
Техническое задание (ТЗ) — документ, содержащий набор требований к проекту. По итогу составления ТЗ у заказчика и исполнителя должно сформироваться общее видение проекта. Приемочное тестирование продукта будет выполняться с учетом заявленной в ТЗ функциональности.
В зависимости от специфики компании и проекта ТЗ может принимать различную форму. По мере развития проекта ТЗ может уточняться в силу изменчивости требований.
На первой итерации ТЗ должно включать как минимум следующие пункты.
Функциональность проекта. Описание с точки зрения пользователя: какие задачи решает продукт, какие покрывает сценарии использования.
Формат входных данных.
Интерфейс приложения. В каком режиме работает приложение (интерактивный или нет, фоновый процесс, сетевой сервис и т. д.). Какие элементы интерфейса предусмотрены, их поведение.
Если приложение принимает аргументы командной строки, то их формат и описание.
Если предполагается использовать чтение исходных данных извне программы: конфигурационного файла, базы данных, источников в Интернет и т.д., то необходимо описание формата / протокола взаимодействия.
У репозиториев на GitHub предусмотрен раздел wiki. Согласованное ТЗ нужно разместить на wiki-странице с соблюдением структуры и форматирования (заголовки различных уровней, абзацы и т. д.). Верстка страниц выполняется на языке разметки markdown.
Составление плана работ¶
После составления ТЗ необходимо декомпозировать проект на ряд задач. Каждая задача должна быть достаточно конкретизирована, чтобы участники команды понимали ее содержание, DoD (definition of done) и могли оценить сроки ее выполнения. Необходимо также предоставить план тестирования созданного продукта.
Задачи следует создавать в разделе issues проекта.
Составленный план работ не является окончательным и может быть скорректирован в процессе разработки, но является обязательным начальным этапом и должен быть максимально полным исходя из ТЗ.
Защита проекта¶
Каждый участник команды представляет письменный отчет о проделанной работе. Отчет состоит из следующих обязательных частей:
ТЗ проекта и итоговый план работ на команду.
Описание командной работы и полученного результата.
Описание личного вклада в результат работы команды.
Для законченных проектов будет организован процесс защиты перед заказчиками, успешное прохождение которого может существенно облегчить процесс итоговой аттестации в виде экзамена. Для защиты проекта нужно будет предоставить в наглядном виде:
ТЗ и итоговый план работ.
Сделанный продукт, для детализации см. критерии оценки в начале.
Распечатанный отчет.
Для защиты команда готовит презентацию. Регламент защиты:
Тимлид начинает презентацию проекта. Озвучиваются технические требования к проекту (ТЗ), декомпозицию задачи (план работ) и распределение ролей в команде.
Далее каждый участник команды рассказывает о своей части работы. В процессе необходимо продемонстрировать рабочую функциональность и фрагменты кода.
На доклад отводится 5−7 минут.
Оформление отчета¶
Шаблон: trpo_report_template.odt.
Документ подготовлен в LibreOffice Writer и может некорректно отображаться в Microsoft Word.
Оценка за курсовой проект может быть выставлена только при наличии отчета. У каждого участника команды должен быть свой личный отчет.
Для оформления следует использовать настроенные стили:
Heading 1 и Heading 2 для заголовков и подзаголовков.
Text body для основного текста.
Table Heading и Table Contents для заголовков таблиц и содержимого ячеек.
Code для исходного кода.
Процесс работы над проектом¶
Текст носит рекомендательный характер. На практике организация процесса разработки может отличаться. Если вы придерживаетесь других соглашений, их следует задокументировать на отдельной странице в wiki проекта, выбор должен быть аргументирован.
ТЗ и план работ¶
До начала реализации проекта необходимо подготовить техническое задание и план работ.
ТЗ позволяет сформировать общее видение проекта у разработчиков и заказчиков. Документ должен быть размещен в wiki 1 проекта. Распространение ТЗ в виде файлов может привести к расхождению версий документа у всех участников.
Задачи создаются в разделе issues 2. Список задач помогает оценивать фронт работ, отслеживать статус задач, выбирать задачи к реализации. Задача состоит из заголовка и описания. Заголовок должен быть коротким и ясным, отражать суть задачи и отвечать на вопрос «Что нужно сделать?». В описании задачи следует размещать дополнительные детали: замечания о реализации, ссылки на фрагменты ТЗ, при необходимости допускается размещение ссылок на другие задачи (например, «Брать в работу после выполнения задачи #42»).
При необходимости указывать DoD задачи (Definition of Done, критерии готовности).
Пример исследовательской задачи:
Выбрать библиотеку для выполнения HTTP-запросов
Собрать список популярных библиотек, доступных для языков С и С++, с каждой из них реализовать простой пример получения веб-страницы.
DoD: в wiki написана статья со сравнением библиотек и примерами кода, проведено обсуждение с разработчиками, результат обсуждения записан в комментариях к задаче.
Также у задач есть поля: Assignee (исполнитель), Tags, Milestones.
Поле Assignee заполняет либо сам исполнитель, либо другой разработчик, если исполнитель известен заранее. Если поле Assignee пустое, значит, задача свободна и ее можно брать в работу. Игнорирование этого поля может привести к тому, что одну и ту же функциональность независимо реализуют несколько разработчиков.
Tags — дополнительные метки задачи. Могут быть использованы как фильтры для поиска или признаки для классификации задач. Использование остается на усмотрение команды. Пример использования можно посмотреть в репозитории bootstrap 3.
Milestones — вехи развития проекта. Задачи можно сгруппировать по майлстоунам. Например, можно завести майлстоуны Beta и Release, в каждый из которых включить некоторое подмножество задач и установить договоренность: в Beta попадает необходимый для защиты проекта минимум задач, а версия Release будет выпущена, если останется время.
Дополнение списка задач¶
Первоначальный список задач не обязан оставаться неизменным. В процессе работы над проектом могут появляться новые задачи. Примеры ситуаций:
Существующая задача оказывается слишком крупной и должна быть декомпозирована на несколько. В этом случае задача закрывается, создаются более мелкие.
При работе над проектом была обнаружена ошибка, исправление которой либо выходит за рамки текущей задачи, либо требует ощутимых затрат по времени, либо может быть исправлена позже, и т. д. В этом случае также создается новая задача.
Подготовка инфраструктуры¶
В рамках лабораторных работ вы двигались от простого к сложному, постепенно добавляя новые инструменты в рабочий процесс.
На практике перед началом разработки следует подготовить инфраструктуру:
Создать репозиторий.
Подготовить систему сборки (написать Makefile).
Настроить автоматические сборки в CI.
Написание Unit-тестов не является завершающим этапом разработки проекта. Тесты пишутся в процессе выполнения каждой задачи. Наличие тестов — негласный Definition of Done.
Рабочий процесс¶
Резюмируя вышесказанное, к моменту начала кодирования у вас должны быть:
ТЗ в wiki
Список задач в Issues
Репозиторий с каркасом проекта, где подготовлены система сборки, настроен CI и все готово для добавления модульных тестов.
Дальнейший процесс состоит из следующих шагов:
Выбрать задачу из issues, на которой нет исполнителя. Назначить ее на себя.
Создать в репозитории ветку для реализации задачи. Желательно договориться об именовании веток. Например, ветки можно называть по номерам задач:
issue-1
,issue-2
,issue-42
. Допускается дополнять названия веток краткими метками:issue-42-user-score
.Реализовать задачу. При оформлении коммитов руководствоваться соглашениями 4.
Написать тесты, покрывающие разработанную функциональность.
Если ветка
master
ушла вперед, то выполнитьrebase
своей ветки наmaster
5. Убедиться, что после ребейза сборка успешно проходит в CI.Смержить ветку в мастер с ключом
--no-ff
.Закрыть задачу. При необходимости оставить комментарии с пояснениями о выполнении.
Порядок может меняться. Например, если задача требует исправления ошибки, то сначала пишется тест для воспроизведения проблемы (тест должен падать), а затем реализуется исправление ошибки.
Распространенные ошибки¶
ТЗ в последний момент¶
Как правило, ТЗ требует согласования и редко остается в своем первоначальном виде. Подготовка в последний момент не позволяет предотвратить наиболее фатальные ошибки.
Делегирование разработки юнит-тестов¶
Наличие юнит-тестов — часть DoD задачи. Без тестов задача не считается выполненной. Тесты пишет тот же разработчик, что и реализовывал функциональность. На практике это не всегда так, но осознанно полностью делегировать юнит-тесты для покрытия роли тестировщика в команде не следует.
Подгон под ответ¶
Сначала реализовать всю функциональность, а затем по памяти написать список по факту выполненных задач, и тут же закрыть их. Бессмысленная трата сил.
Избыточная детализация задач¶
Если в формулировка задачи расписана с точностью до используемых переменных и последовательности вызова функций, то, скорее всего, эта задача уже была решена, а заведение ее в список — фикция.
Подмена основных активностей побочными¶
Предполагается, что все участники пишут код. Любая побочная деятельность при всей своей полезности не рассматривается как требующая специально выделенной роли в команде. Примеры: управление проектом, дизайн, подготовка документации и презентации и т. д.
Некоторые признаки некачественного кода¶
В процессе проверки курсовых проектов помимо названных выше критериев мы обращаем внимание на качество кода. Ниже приведены некоторые наиболее распространенные ошибки. Рекомендуется в конце выполнения каждой задачи просматривать код на наличие следующих «запахов» (code smell).
Дублирование кода.
Повторяющийся код — один из признаков нарушения принципа DRY (Don’t repeat yourself).
См. Duplicate Code, Comments.
Длинная функция.
Функция должна решать одну задачу. Такие функции проще читать, поддерживать и тестировать. Длинная функция может быть признаком нарушения принципов DRY, SRP (Single Responsibility Principle) и/или SLA (Single Level of Abstraction)
См. Long Method
Длинный список параметров.
Пример:
bool triangle_intersects_triangle( double t1x1, double t1y1, double t1x2, double t1y2, double t1x3, double t1y3, double t2x1, double t2y1, double t2x2, double t2y2, double t2x3, double t2y3 );
Должно быть:
bool triangle_intersects_tirangle(Triangle lhs, Triangle rhs);
См. Long Parameter List.
Магические константы.
См. Magic Number
User-specific code.
Пример:
FILE* f = fopen("/home/v.pupkin/myproject.dict.txt", "r");
Если в коде используется абсолютный путь, содержащий имя пользователя, то этот код не может быть переносимым. Такие константы следует выносить в конфиг или параметры командной строки.
Использование глобальных переменных.
Выбор инструментов¶
Лабораторные работы выполняются на языке C. Для разработки курсового проекта вы можете выбрать язык на свое усмотрение, но необходимо, чтобы все участники команды были знакомы с выбранным языком на достаточном уровне.
Запрещается использовать слишком высокоуровные инструменты: Unity, Unreal Engine и др. Мы ожидаем, что большая часть вашей работы будет заключаться в ручном написании кода и работе с инструментами, а не в сборке конструктора из готовых блоков. Кроме того, у вас могут возникнуть сложности с настройкой CI, и работа не будет выполнена в полном объеме.
Запрещается использовать устаревшие инструменты. Например, библиотеку
BGI,
известную некоторым из вас по заголовочному файлу graphics.h
. Последняя
среда разработки с поддержкой этой библиотеки была выпущена в 1997 году,
более двадцати лет назад.
Поиск актуальных полезных библиотек можно начать отсюда: getAwersomeness()
Темы¶
Для каждой темы указан рекомендованный размер команды. Если вы планируете собрать команду больше рекомендуемой, то следует более детально продумать план работ. Часто возникает ситуация, когда в небольшом проекте сложно найти задачи для всей команды.
Некоторые проекты являются упрощенными клонами существующих систем. Для составления ТЗ рекомендуется ознакомиться с указанными и другими существующими реализациями.
2 человека:
Hangman. Виселица.
Игра «Жизнь»
Пятнашки.
Игра «100 спичек»
Игра «Ним» (три кучки спичек)
Сортировка входных данных (аналог sort)
Программа символы: проверка сбалансированности скобок в программе на С
Интерпретатор языка программирования Машины Тьюринга
Система сборки (аналог make)
Вывод фрагментов файлов (аналог head и tail, с обязательной реализацией режима
--follow
).
2−3 человека:
QuizRunner. Система проведения тестирования.
KeyboardNinja. Клавиатурный тренажер.
FileProcessor. Приложение для массового переименования файлов в указанном каталоге по заданным шаблонам.
Formatter. Приложение для форматирования текста: выравнивание, разбивка на абзацы, разбивка на страницы с колонтитулами и т. д.
Приложение для рекомендации оптимального тарифа на основе затрат
Задачи на графах: кратчайший путь между городами, количество разных маршрутов между городами, длиннейший путь между городами
Движок SQL-запросов к данным в формате CSV
Генератор документации по комментариям в исходном коде (аналог doxygen)
Конвертер файлов из Markdown в HTML (аналог pandoc)
Todo лист
Анализ использования диска (аналог ncdu)
Unit Converter (аналог units)
Password Generator (аналог pwgen)
Генератор статичных сайтов (аналог jekyll)
2−3 человека, узкоспециализированные темы, рекомендуется консультация преподавателя:
SpellChecker. Приложение, исправляющее опечатки во введенных словах. (аналог hunspell)
Система полнотекстового поиска. Ожидается реализация с построением обратного индекса.