Технологии разработки программного обеспечения

Требования к выполнению работ

В курсе предусмотрены лабораторные работы и курсовой проект.

Цель лабораторных работ — изучение инструментов и практик, применяющихся при разработке программного обеспечения:

  • Система контроля версий (git)

  • Системы сборки на примере make

  • Разработка модульных тестов

  • Непрерывная интеграция

Цель курсовой работы — разработка законченного программного продукта. При этом важно отличать программу от программного продукта.

Программа пригодна для запуска своим автором на системе, на которой была разработана.

Программный продукт — программа, которую любой человек может запускать, тестировать, исправлять и развивать 1. Такая программа должна быть написана в обобщенном стиле. В частности, диапазон и вид входных данных должны быть настолько обобщенными, насколько это допускается базовым алгоритмом 2. Затем программа должна быть тщательно протестирована. Это означает, что должно быть подготовлено достаточное количество тестовых данных. Тесты должны быть зафиксированы, а их запуск автоматизирован. Наконец, развитие программы в программный продукт требует создания подробной документации.

1

Ф. Брукс — Мифический человеко-месяц

2

Пример кода, непригодного для передачи другим пользователям: FILE *dict = fopen("/home/v.pupkin/myproject/dict.txt", "r");

Общие требования

  1. Приложения в рамках лабораторных работ разрабатываются на языке C в операционной системе GNU/Linux.

  2. Исходный код оформляется в соответствии с принятыми в курсе соглашениями: Code Style

    Допускается использование других распространенных соглашений:

    Независимо от выбранных соглашений необходимо соблюдать консистентность в рамках проекта.

  3. Вся работа с изучаемым в курсе инструментарием выполняется в командной строке.

Источники

В курсе изучаются широко применяемые в повседневной практике инструменты. Как следствие, по каждому из них существует подробная официальная документация. На страницах методических указаний вы найдете ссылки на документацию или ее разделы. Все указанные материалы обязательны к изучению для выполнения работ.

Основные источники, используемые в курсе:

  1. Pro Git https://git-scm.com/book/en/v2

  2. Git Reference https://git-scm.com/docs

  3. A simple git branching model https://gist.github.com/jbenet/ee6c9ac48068889b0912

  4. GNU Make Manual https://www.gnu.org/software/make/manual/

  5. Lee Copeland — A Practitioner’s Guide to Software Test Design

Проект на лабораторные работы

В рамках лабораторных работ вам предлагается реализовать законченный проект, включающий в себя:

  1. Приложение, разработанное в соответствии с заданием.

  2. Систему сборки.

  3. Unit-тесты.

  4. Настроенные автоматические сборки (CI).

Работа над проектом разбивается на короткие итерации. Процесс разработки должен быть зафиксирован в системе контроля версий (git). Проект должен быть опубликован на github.

На выбор вам предлагается два проекта:

Геометрия

Нужно реализовать операции с геометрическими фигурами: ввод и вывод в одном или нескольких текстовых форматах, вычисление площади, периметра и факта пересечения двух выбранных фигур. Сложность проекта регулируется за счет добавления дополнительных типов фигур.

Шахматы

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

В каждом из проектов возможно несколько вариантов считывания входных данных.

  1. Считывание со стандартного потока ввода. Наиболее простой в реализации, но наименее удобный вариант. Ввод больших массивов данных слишком трудоемок, поэтому удобнее хранить их в файле, а для ввода использовать перенаправление.

  2. Считывание данных из файла. В этом случае синтаксис запуска приложения выглядит следующим образом:

    chessviz <FILE>
    

    При невозможности открыть файл приложение должно выводить человекопонятное сообщение об ошибке и завершаться с ненулевым кодом.

  3. Гибридный вариант. Если в командной строке указан путь к файлу, то ходы считываются из него. Иначе приложение считывает данные из stdin. Аналогичное поведение можно наблюдать у многих стандартных утилит, например, cat

    Синтаксис запуска в этом случае:

    chessviz [FILE]
    

Ниже приводятся полные описания проектов. Отдельно взятая лабораторная работа предполагает реализацию небольшой части выбранного проекта.

Геометрия

Приложение принимает на вход геометрические фигуры различных типов в WKT-подобном формате.

Для каждой фигуры приложение определяет:

  1. Периметр.

  2. Площадь.

  3. С какими фигурами пересекается текущая.

Поддерживаемые фигуры в зависимости от уровня сложности:

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 *)

Дополнительные замечания:

  1. Типы фигур нечувствительны к регистру (case insensitive).

  2. Между токенами может быть произвольное количество пробельных символов.

  3. Для простоты можно считать, что одна фигура занимает ровно одну строку. В строке не может быть нескольких фигур.

Пример входных данных:

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 для визуализации расположения фигур на доске по записи партии в алгебраической нотации. Запись ходов может содержать ошибки, ваше приложение должно сообщать о них пользователю.

Формат входных данных

На вход приложение принимает список ходов, разделенных символом перевода строки. Запись хода состоит из таких компонентов:

  1. Тип фигуры, выполняющей ход (король, конь и т. д.).

  2. Поле, с которого сделан ход.

  3. Для тихого хода — дефис (-). Для взятия — буква x.

  4. Поле, на которое сделан ход.

  5. Если пешка совершила превращение, после целевого поля указывается фигура, которой она стала.

  6. Для взятия на проходе — знак e.p. (фр. en passant).

  7. Для шаха — знак плюс (+). Для мата — решётка (#).

Грамматика (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

В дополнение к проверкам из предыдущих пунктов:

  1. Обозначения фигур должны соответствовать фактическим (нельзя взять коня с поля, на котором стоял слон).

  2. Тип хода должен соответствовать фактическому. Например, если во входных данных записано взятие, а в конечной ячейке нет фигуры соперника, то сообщить об ошибке. Взятие на проходе не рассматриваем.

Hard

В дополнение к проверкам из предыдущих пунктов:

  1. Проверки соблюдения правил движения фигур (слон движется только по диагонали, только конь может перешагивать через фигуры и т. д.).

  2. Проверка очередности ходов.

Nightmare

В дополнение к проверкам из предыдущих пунктов:

  1. Проверки допустимости рокировки, см. Рокировка (wiki)

  2. Нарушения формата входных данных: отсутствие любого обязательного компонента записи хода или наличие лишних элементов, некорректные символы (например, недопустимые обозначения фигур) и т. д.

Формат выходных данных

После запуска приложение выводит в 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>

Отображение в браузере:

chessviz
1. e2-e4

Пример работы приложения

Входные данные:

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;
}
  1. Приходится экранировать символы, имеющие специальное значение (в данном случае обратный слеш и кавычки).

  2. Используется множество вызовов 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 баллов без возможности пересдачи.

Цель работы

  1. Установить необходимое для работы ПО: git, текстовый редактор, clang-format.

  2. Выполнить базовую настройку рабочего окружения.

  3. Создать репозиторий с приложением «Hello, World», выполнить несколько коммитов, загрузить результат на GitHub.

Подготовка к работе

Для выполнения работы необходимо изучить следующий материал из книги Pro Git:

  1. Getting Started

  2. Git Basics

Вам понадобятся следующие команды:

  1. git clone — клонирование удаленного репозитория.

  2. git config — установка и получение глобальных или локальных параметров.

  3. git status — просмотр состояния файлов в локальном репозитории.

  4. git log — просмотр истории изменений.

  5. git add — индексация файлов.

  6. git commit — создание коммита.

  7. git push — обновление ссылок в удаленном репозитории.

Все работы выполняются в командной строке, в ОС Gnu/Linux. Основы работы в командной строке см. в Лабораторная работа 0. Знакомство с ОС GNU/Linux.

Полезные утилиты:

  1. man(1) — просмотр документации.

    Пример:

    man ls
    
  2. mkdir(1) — создание каталога.

    Пример:

    # Создание каталога myproject
    mkdir myproject
    
    # Создание каталога d и всех родительских каталогов
    # при необходимости
    mkdir -p a/b/c/d
    
  3. cd — изменение рабочего каталога. Описание команды расположено в man-странице bash(1).

    Пример:

    # Переход в каталог myproject
    cd myproject
    
    # Переход в домашний каталог
    cd
    
  4. ls(1) — просмотр списка файлов.

  5. touch(1) — создание файла.

Руководство

  1. Для создания репозитория пройдите по ссылке ЛР0: hello и выберите себя в списке. Если вас нет в списке, обратитесь к преподавателю практики.

  2. Для вас будет создан репозиторий с именем вида hello-<username>. Склонируйте его с помощью команды git clone и перейдите в каталог с репозиторием:

    $ git clone https://github.com/trpo2021/hello-<username>
    $ cd hello-<username>
    
  3. Для фиксации изменений в репозитории нужно указать свои имя и 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». Выполните работу в несколько коммитов.

  1. В склонированном репозитории создайте пустой файл main.c. В каком состоянии он находится? Для просмотра состояния используйте команду git status.

  2. Проиндексируйте пустой файл main.c. Используйте команду git add. В каком состоянии находится файл?

  3. Зафиксируйте изменения с помощью команды git commit. В заголовке коммита укажите: «Create empty main.c». После создания коммита выполните команду git status, объясните вывод. Выполните команду git log, объясните вывод. Обратите внимание, что локальная ветка main теперь опережает удаленную origin/main на один коммит.

  4. Напишите функцию main:

    int main()
    {
        return 0;
    }
    

    Проверьте состояние файла main.c.

  5. Проиндексируйте файл. Создайте еще один коммит. Посмотрите историю репозитория.

  6. По аналогии добавьте вывод сообщения «Hello, World». Приложение должно компилироваться с опцией -Wall без ошибок и предупреждений.

  7. В лабораторных работах и курсовом проекте требуется соблюдение code style. Для автоматического форматирования исходников можно использовать утилиту clang-format. Скачайте конфиг .clang-format в корень репозитория hello-<username>. Обратите внимание, что имя файла должно начинаться с точки. Для скачивания конфига воспользуйтесь командой:

    wget https://csc-software-development.readthedocs.io/ru/2021/_static/.clang-format
    

    Установите в систему clang-format. Настройте свой текстовый редактор так, чтобы форматирование исходников выполнялось при сохранении файла. Убедитесь, что автоматическое форматирование работает.

  8. Теперь файл .clang-format можно закоммитить:

    $ git add .clang-format
    $ git commit -m "Import .clang-format config"
    
  9. Загрузите изменения в удаленный репозиторий с помощью команды git push. Посмотрите историю изменений. Убедитесь, что локальная ветка main и удаленная origin/main указывают на один и тот же коммит.

  10. Передайте выполненное задание на ревью преподавателю практики. Для этого:

    1. Откройте в браузере страницу репозитория.

    2. На вкладке Pull requests перейдите в Feedback. Этот Pull request создан автоматически. Не закрывайте и не вливайте его. В лабораторных и курсовых он должен оставаться открытым.

    3. В поле Reviewers выберите своего преподавателя.

    _images/review-box.png
    1. Если ревью было пройдено со статусом Changes requested, исправьте замечения и запросите повторное ревью.

    _images/review-request.png

Полезные ссылки для настройки текстового редактора:

  1. Официальная документация ClangFormat содержит информацию по настройке vim и emacs.

  2. Vim: плагин rhysd/vim-clang-format (не проверялся)

  3. Visual Studio Code: Code Formatting — официальная документация.

  4. Atom — clang-format — плагин для редактора Atom. Может потребовать дополнительной настройки.

  5. SublimeClangFormat — плагин для Sublime Text.

  6. 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.

  1. Включить плагин External Tools: Menu → Preferences → Plugins, отметить плагин External Tools.

  2. Menu → Manage External Tools … → Add a new tool (+).

  3. Заполнить поля (см. скриншот):

_images/clang-format-gedit.png

Известная проблема: при форматировании документа редактор выполняет прокрутку к началу файла.

Контрольные вопросы

  1. Где хранятся настройки git?

  2. Как просмотреть состояния файлов в репозитории?

Заключение

Работа считается выполненной, если:

  1. У вас установлено и настроено необходимое ПО.

  2. Создан репозиторий hello-<username>, в котором находится исходный код программы. Приложение компилируется с опциями -Wall -Werror. Ветка main запушена в удаленный репозиторий.

  3. В репозитории есть конфиг .clang-format. В вашем текстовом редакторе настроено автоматическое форматирование исходного кода с использованием этого конфига. Исходный код приложения отформатирован.

  4. Работа выполнена в несколько коммитов, все действия по индексации файлов, созданию коммитов и загрузке в удаленный репозиторий выполнялись в командной строке.

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 и анализировать их вывод.

  1. Выберите вариант задания. В работе вам нужно реализовать часть функциональности приложения.

    Геометрия

    Ввод и вывод окружностей в формате WKT, проверка корректности входных данных.

    Шахматы

    Вывод исходного состояния шахматной доски в текстовом формате и/или в формате html.

    Получение задания:

  2. Добавьте в репозиторий файл .clang-format из предыдущей работы:

    wget https://csc-software-development.readthedocs.io/ru/2021/_static/.clang-format
    
  3. Реализуйте часть приложения в соответствии с выбранным вариантом.

  4. Напишите простейший Makefile для сборки приложения. Приложение следует компилировать с опциями -Wall -Werror. Компиляция должна проходить без ошибок.

  5. Добавьте в репозиторий файл .gitignore, настройте игнорирование файлов.

  6. Загрузите изменения на GitHub. После завершения работы назначте ее на ревью своему преподавателю практики.

Локально отработайте перемещение по истории репозитория. Переместитесь по истории к первому коммиту. Посмотрите содержимое файлов. Вернитесь к последнему коммиту.

Пример содержания коммитов:

  • Создание приложения «Hello, World».

  • Реализация функции вывода доски в stdout.

  • Добавление мейкфайла

  • Добавление .gitignore.

Контрольные вопросы

  1. Что такое коммит?

  2. Этапы создания коммита.

  3. В каких состояниях может находиться файл в репозитории? Как происходит изменение состояния файла?

  4. Зачем нужен файл .gitignore?

2. Ветвление в git

Цель работы

Отработать базовые операции с ветками: создание, переключение между ветками, слияние (merge), перебазирование (rebase). Для выполнения вам предоставляется ряд подготовленных репозиториев. В них нужно выполнить слияние различными способами: формируя линейную или псевдолинейную историю, а также выполнить операции merge и rebase, устранив возникшие в процессе конфликты.

Работа не оценивается. В руководстве предоставлена информация для самопроверки. Отработанные приемы необходимы для выполнения следующих лабораторных и курсового.

После выполнения работы ознакомьтесь с A simple git branching model.

Материал для подготовки к работе

  1. Branches in a Nutshell

  2. Git Branching — Rebasing

Команды:

Настройка отображения конфликтов

Git позволяет настроить более подробное отображение конфликтов:

git config --global merge.conflictstyle diff3

По умолчанию конфликты отображаются в виде:

<<<<<<< HEAD
Левая ветка
=======
Правая ветка
>>>>>>> c2392943

В формате diff3 отображается также состояние общего предка сливаемых веток:

<<<<<<< HEAD
Левая ветка
||||||| merged common ancestors
Общий предок
=======
Правая ветка
>>>>>>> c2392943

Руководство

Часть 1: создание веток

  1. Создать новый репозиторий branches-basics.

  2. В основной ветке master создать несколько коммитов.

  3. Создать новую ветку develop. Выполнить несколько коммитов.

  4. Вернуться на ветку master. Создать коммит.

  5. Внести изменения в рабочую копию репозитория, не коммитить. Переключиться на ветку develop

Часть 2: merge и rebase

В этой части нужно выполнить слияние веток одним из предложенных способов. Предварительно необходимо изучить историю каждой ветки репозитория и понять, какие изменения были сделаны. В результате слияния вы должны получить работоспособный исходный код, объединяющий в себе изменения, выполненные в ветках main и develop.

Представьте, что вы работали в ветке develop, а другой разработчик опередил вас и влил свои изменения в main раньше. В его и в вашей ветках рабочий код. После слияния код должен остаться корректным, и работа, выполненная в main, не должна быть удалена, как и работа, выполненная в develop.

Формального алгоритма разрешения конфликтов нет, нужно анализировать изменения и принимать решения, руководствуясь здравым смыслом.

Дополнительные коммиты с исправлениями после мержа или ребейза нежелательны, поскольку замусоривают историю.

В случае ошибки вы можете откатить ветку к ее исходному состоянию с помощью команды git reset с опцией --hard.

  1. Склонируйте репозитории:

    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) коммиты присутствуют только в правой ветке.

Историю такого вида будем называть псевдолинейной.

Преимущества псевдолинейной истории:

  1. Если каждая задача решается в отдельной ветке, то при псевдолинейной истории очевидно, какая группа коммитов относится к задаче.

  2. Появляется потенциальная возможность откатить задачу целиком. Для этого достаточно выполнить revert мерж-коммита.

  3. Мерж-коммит не содержит разрешения конфликтов, как это может быть при нелинейной истории.

  4. Поскольку история остается линейной, легко искать проблемный коммит методом бинарного поиска (git bisect).

Недостатки:

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

  2. Если требуется выполнить вливание нескольких веток подряд, то разработчикам приходится договариваться об очередности вливания, иначе приходится многократно выполнять ребейз фиче-ветки на 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

Мы получили историю, в которой:

  • У коммита может быть более одного предка.

  • Между началом ветки и мерж-коммитом как в левой, так и в правой ветке могут присутствовать другие коммиты.

Историю такого вида будем называть нелинейной.

Преимущества нелинейной истории:

  1. Сравнительно легко формировать.

Недостатки:

  1. Топология истории значительно усложняется.

  2. Нарушается принцип «один коммит — одно изменение», поскольку мерж-коммит может содержать:

    • Изменения из левой ветки

    • Изменения из правой ветки

    • Разрешения конфликтов

    • Случайные изменения, внесенные в процессе разрешения конфликтов

В лабораторных работах и курсовом не следует формировать нелинейную историю.

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

В этой ситуации нужно:

  1. Переключиться на ветку develop.

  2. Выполнить ее rebase на main.

  3. Переключиться на ветку main.

  4. Выполнить 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
  1. Прочитайте и осознайте изменения в каждом коммите.

  2. Прочитайте и осознайте изменения в каждом коммите. Вас предупредили дважды.

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

Исходная история:

* 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

Заключение

  1. В рамках нашего курса следует формировать псевдолинейную историю. Оптимизируйте результат своей работы для удобства читателя.

  2. На практике возможны процессы, в которых трудозатраты на операции rebase+merge слишком высоки, поэтому на уровне соглашений принято создавать нелинейную историю.

  3. Подробнее о соглашениях по работе с репозиторием можно почитать в A simple git branching model.

Контрольные вопросы

  1. Что такое ветка?

  2. Что такое HEAD?

  3. Способы создания веток.

  4. Как узнать текущую ветку?

  5. Как переключаться между ветками?

  6. Что такое merge? Что такое fast-forward merge?

  7. Что такое rebase? Как он работает?

  8. Как можно перемещать метки?

3. Сборка приложения

Цель работы

Разделить приложение, разработанное в лабораторной работе 1, на два модуля: статическую библиотеку и приложение. Доработать Makefile. Настроить автоматические сборки в сервисе GitHub Actions.

Геометрия

Реализовать вычисление периметра и площади для выбранных фигур.

Easy

Окружность

Normal

Окружность, треугольник.

Hard

Окружность, треугольник, полигон.

Шахматы

Реализовать движение пешек в приложении. Для этого будет достаточно поддержать входные данные в формате вида:

1. e2-e4 e7-e5
…

Движения фигур на этом этапе реализовывать не нужно.

Материалы для подготовки к работе

  1. Документация GNU Make

    Обязательный минимум:

    1. Overview of make

    2. An Introduction to Makefiles

    3. Writing Makefiles

    4. Writing Rules

    5. Writing Recipes in Rules

    6. How to Use Variables:

      1. Basics of Variable References

      2. The Two Flavors of Variables

  2. Граф зависимостей

  3. Static library

  4. GitHub Actions:

    1. About continuous integration

    2. Quickstart for GitHub Actions

Процесс сборки

Этапы компиляции

В простейшем случае компилятор принимает на вход файлы с исходным кодом и создает исполняемый файл:

_images/gcc-blackbox2.svg

Процесс сборки приложения состоит из ряда этапов, на каждом из которых ее можно прервать:

_images/compilation-stages-2.svg

Для сборки многофайловых приложений сначала из файлов с исходным кодом получают объектные файлы:

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 $@ $<

Приведенный мейкфайл намеренно избыточен для простоты понимания. Ему соответствует следующий граф зафисимостей:

digraph G {
  bgcolor = "#ffffff00";
  rankdir = LR;
  "hello" -> "main.o";
  "hello" -> "hello.o";
  "main.o" -> "main.c";
  "hello.o" -> "hello.c";
}

Легко понять последовательность команд в случае изменения одного из исходников. Так, при изменении файла main.c:

\begin{eqnarray} mtime(main.c) &\gt& mtime(main.o) &\Rightarrow rebuild(main.o) \\ mtime(main.o) &\gt& mtime(hello) &\Rightarrow rebuild(hello) \end{eqnarray}

Где \(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

Статические библиотеки

Для удобства сборки, тестирования и распространения приложения разделяют на модули. Один из типов таких модулей — статическая библиотека.

Для сборки статической библиотеки нужно:

  1. Скомпилировать исходники в объектные файлы.

  2. Создать архив объектных файлов.

Для нашего примера мейкфайл может выглядеть так:

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

Тогда граф зависимостей примет вид:

digraph G {
  bgcolor = "#ffffff00";
  rankdir = LR;
  "hello" -> "main.o";
  "hello" -> "libhello.a";
  "libhello.a" -> "hello.o";
  "main.o" -> "main.c";
  "hello.o" -> "hello.c";
  "main.c" -> "hello.h";
  "hello.c" -> "hello.h";
}

Нет строгого алгоритма для определения, какой код следует разместить в библиотеке, а какой в приложении. Руководствуйтесь здравым смыслом и размещайте в библиотеке код, который потенциально может быть переиспользован в другом приложении.

Структура проекта

Структурировать проект на файловой системе следовало бы в первую очередь, но мы отложили этот этап для сокращения примеров выше. Для старта предлагается следующая структура:

.
|-- 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.

Если мейкфайл в примере по ссылке кажется вам слишком сложным или непонятным, вы можете пойти по одному из двух путей:

  1. Писать свой вариант проще. Пусть для каждого исходника или объектного файла будет явно прописанное правило. В этом случае будьте готовы к большому работу однообразной работы и необходимости вручную вписывать каждый новый исходник в мейкфайл. Если вы пойдете этим путем, то хотя бы вынесите флаги компиляции в переменную. Это сэкономит время, если потребуется изменить набор опций для отладочной сборки. В рамках курса мы осознанно не рассматриваем как реализовывать отдельные конфигурации Debug/Release с помощью мейкфайла.

  2. Прочитайте документацию и разберитесь в происходящем.

Код возврата (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.

  1. Для каждой задачи/новой функциональности/лабораторной работы создается новая ветка.

  2. После завершения работы ветка сливается в 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>

Требования к работе:

  1. Компилировать приложение следует с опциями -Wall -Werror

  2. Для сборки приложения должно быть достаточно команд git clone <remote url> && cd chessviz && make.

  3. При изменении одного файла с исходным кодом должен перекомпилироваться только он и зависящие от него артефакты. При изменении заголовочного файла должны перекомпилироваться все файлы, в которые он подключен.

  4. Если в исходниках не было изменений, то при повторном запуске make не должно выполняться никаких сборочных операций.

  5. Makefile должен содержать цель clean для удаления артефактов сборки. Цель должна выполняться, даже если в корне репозитория создан файл с именем clean.

  6. В заголовочных файлах должны быть #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

поскольку вам может понадобиться адаптировать ее для своего проекта.

  1. 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
    
  2. Конструкция вида a | b перенаправляет stdout команды a в stdin команды b.

  3. xargs(1):

    1. Принимает список аргументов из командной строки.

    2. Читает список строк из stdin и добавляет их аргументами к формируемой команде.

    3. Выполняет собранную команду.

    В нашем примере получим команду:

    clang-format -i --verbose src/hello/main.c src/libhello/hello.c src/libhello/hello.h
    

    После ее выполнения исходники в репозитории будут отформатированы в соответствии с приложенным конфигом.

  4. git diff --exit-code выведет в stdout изменения. Если исходники после автоматического форматирования отличаются от закоммиченных, то благодаря опции --exit-code команда завершится с ненулевым кодом и сборка не пройдет.

Реализация функциональности

Теперь, когда структура проекта подготовлена, можно реализовать функциональность приложения в соответствии со своим вариантом. После завершения работы ветку lab-3 можно смержить в main с опцией --no-ff и передать на ревью.

Контрольные вопросы

  1. В чем преимущества правильно написанного Makefile перед простым скриптом сборки вида gcc -Wall -Wextra -Werror *.c -o app?

  2. Как make определяет необходимость выполнения команд?

  3. Что такое и зачем используется CI?

4. Модульное тестирование

Цель работы

Дорисовать остальную сову. Доработать и покрыть тестами приложение.

Проверить, что в случае непрохождения тестов автоматическая сборка завершается со статусом failed.

Геометрия

Реализовать определение факта пересечения фигур.

Easy

Окружность.

Normal

Окружность, треугольник.

Hard

Окружность, треугольник, полигон.

Шахматы

Реализовать движение всех фигур.

Материалы для подготовки к работе

Классы эквивалентности, граничные значения

Общие сведения

Для покрытия тестами приложение декомпозируют на минимальные самодостаточные единицы. В процедурных языках программирования такая единица — функция.

Для покрытия функции тестами необходимо вызывать ее с заранее подготовленными входными параметрами и сравнить фактический результат ее работы с ожидаемым.

Код тестов должен быть написан отдельно от кода приложения. Поэтому для запуска тестов пишется отдельное приложение. Тестовое приложение обычно включает в себя автоматическую регистрацию новых тестов и формирование отчета. Для автоматизации процесса используют специальные библиотеки.

Сборка приложения и тестов

В предыдущей лабораторной работе мы разделили проект на консольное приложение и статическую библиотеку. Артефакт сборки тестов — еще один исполняемый файл, который линкуется со статической библиотекой библиотекой. Таким образом, граф зависимостей для примера hello принимает вид (диаграмма сокращена):

digraph G {
  bgcolor = "#ffffff00";
  rankdir = LR;
  "hello" -> "obj/app/main.o";
  "hello" -> "obj/libhello/libhello.a";
  "hello-test" -> "obj/test/main.o";
  "hello-test" -> "obj/libhello/libhello.a";
}

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

При сборке нужно предусмотреть возможность конфликтов имен объектных файлов. Например, при компиляции файлов 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);
}

Здесь:

  1. CTEST(<suite_name>, <test_name>) — макрос для создания и регистрации тестовой функции. Все определенные таким образом тесты запускаются автоматически при вызове ctest_main.

  2. 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]. Все аргументы типа int

  • ASSERT_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 приводит к появлению предупреждений на этапе компиляции.

Этапы работы

Каждый этап — отдельный коммит.

  1. Добавить в репозиторий библиотеку ctest (заголовочный файл).

  2. Добавить точку входа для запуска тестов — test/main.c. Настроить сборку и запуск тестов (доработать Makefile). Обычно цель по умолчанию используется только для компиляции приложения. Для компиляции и запуска тестов создают отдельную цель test. Таким образом, для полной сборки приложения и запуска тестов нужно выполнить команды:

    make
    make test
    
  3. Написать любой простейший тест, проверить его работоспособность.

  4. Реализовать функциональность в соответствии со своим вариантом.

  5. Покрыть приложение тестами. Каждую группу тестов можно оформить в отдельный коммит.

  6. Настроить запуск тестов в 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;

Wiki: Magic number

Не пишите user-specific код

Подобный код не является переносимым:

FILE* dict = fopen("/home/v.pupkin/myproject/dict.txt", "r");

Лучше:

// Получение пути к файлу из внешнего источника:
// из старнадрного потока ввода, аргументов командной строки,
// конфигурационного файла, и т. д.
const char* dict_file_path = ... ;

FILE* dict = fopen(dict_file_path, "r");

Не используйте глобальные переменные

В подавляющем большинстве случаев глобальные переменные усложняют поддержку кода.

Некоторые последствия использования глобальных переменных:

  1. Нарушение локальности. Чем меньше область видимости отдельных элементов, тем проще отлаживать код.

  2. Неявные зависимости. Глобальные переменные могут неявно связывать любые части части исходного кода.

  3. Усложнение тестирования. Каждый тест должен быть независим. При наличии глобальных переменных каждый тест вынужден явно присваивать всем переменным некоторые значения для настройки стартового окружения. Наличие разделяемого между тестами состояния может привести к тому, что результат их выполнения зависит от порядка запуска.

Использование глобальных констант допустимо.

C2 Wiki: Global Variables Are Bad

Использование 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

Пишите короткие функции

Функции должны быть короткими и решать ровно одну задачу, очевидную из имени. По возможности старайтесь писать чистые функции.

Можно ориентироваться на следующие эвристики:

  1. Функция должна умещаться на экране.

  2. В функции должно быть не более 5−10 локальных переменных.

  3. В функции должно быть не более 3 уровней вложенности.

Linux kernel coding style: Functions

Указывайте имена параметров в объявлениях функций

Имена параметров в объявлениях функций не являются обязательными для компилятора, но значительно улучшают читаемость.

// Кто кого захватывает?
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++;
    }
}

LLVM Coding Standards: Don’t use else after a return

Выносите предикаты в функции

Часто в коде встречаются циклы, вычисляющие единственное булево значение. Пример:

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:

Порядок заголовочных файлов

Предпочительный порядок заголовочных файлов:

  1. Главный заголовочный файл модуля.

  2. Локальные/приватные заголовочные файлы.

  3. Заголовочные файлы сторонних библиотек.

  4. Системные заголовочные файлы.

Блоки следует отделять пустой строкой. Файлы в каждом блоке должны быть отсортированы лексикографически.

Абстрактный пример для circle.c:

#include "circle.h"

#include "wkt_reader.h"

#include <json.h>
#include <svg.h>

#include <math.h>
#include <stdio.h>
#include <stdlib.h>

LLVM Coding Standards: #include Style

Подключайте как можно меньше

Избыточное подключение заголовочных файлов замедляет компиляцию, особенно при подключении в другие заголовочные файлы.

Распространенная ошибка: в заголовочный файл 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);

LLVM Coding Standards: #include as Little as Possible

Скрывайте детали реализации

Не выносите приватные объявления в заголовочный файл. Каждое публичное определение становится частью контракта с пользователем модуля. В долгосрочной перспективе это осложняет поддержку и рефакторинг кода.

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 человек. Основное требование к проекту: это должен быть законченный программный продукт. Проект при сдаче будет оцениваться по следующим критериям:

  1. Соответствие реализованной функциональности требуемой в проекте.

  2. Поэтапность разработки через системы контроля версий.

  3. Покрытие тестами, включая unit тестирование.

  4. Стиль программирования (Code Style).

  5. Практика использования CI.

  6. Полнота документации.

  7. Итоговая презентация.

Команда может предложить свою тему в дополнение к перечисленным ниже.

С 8 по 21 февраля — выбор и согласование темы.

С 22 февраля по 7 марта – разработка первой версии ТЗ.

С 8 по 28 марта — разработка и согласование ТЗ и плана работ.

С 29 марта — начало кодирования.

С 25 апреля — отладка, документация, интеграционное тестирование, готовность показать beta-версию программы.

С 9 мая – быть готовыми представлять законченный проект.

К защите допускаются команды, не имеющие долгов по лабораторным занятиям, а также предоставившим beta-версию программы в срок.

Выбор темы и формирование команды

  1. В установленный срок выберите тему и согласуйте ее с преподавателем практики. В одной группе темы не могут повторяться.

  2. Для создания команды и репозитория пройдите по ссылке: GitHub Classroom: trpo2021/cw

  3. Если команда еще не создана, создайте ее. Укажите название команды по шаблону: <группа>_<тема>. Например: iv-021_search-engine. Если в команде участвуют студенты из разных групп, то укажите все: ip-011_ip-012_fuzzy-matching. Hint: договоритесь между собой, кто из участников создает команду.

    Если команда создана, найдите ее в списке и нажмите Join.

  4. В репозитории создайте файл README.md. В заголовке укажите свою тему.

  1. В разделе wiki созданного репозитория напишите ТЗ, в разделе issues заведите задачи на разработку.

  2. По готовности ТЗ и плана работ заполните форму: ТРПО 2021: ТЗ и план работ.

Содержание ТЗ

Техническое задание (ТЗ) — документ, содержащий набор требований к проекту. По итогу составления ТЗ у заказчика и исполнителя должно сформироваться общее видение проекта. Приемочное тестирование продукта будет выполняться с учетом заявленной в ТЗ функциональности.

В зависимости от специфики компании и проекта ТЗ может принимать различную форму. По мере развития проекта ТЗ может уточняться в силу изменчивости требований.

На первой итерации ТЗ должно включать как минимум следующие пункты.

  1. Функциональность проекта. Описание с точки зрения пользователя: какие задачи решает продукт, какие покрывает сценарии использования.

  2. Формат входных данных.

  3. Интерфейс приложения. В каком режиме работает приложение (интерактивный или нет, фоновый процесс, сетевой сервис и т. д.). Какие элементы интерфейса предусмотрены, их поведение.

  4. Если приложение принимает аргументы командной строки, то их формат и описание.

  5. Если предполагается использовать чтение исходных данных извне программы: конфигурационного файла, базы данных, источников в Интернет и т.д., то необходимо описание формата / протокола взаимодействия.

У репозиториев на GitHub предусмотрен раздел wiki. Согласованное ТЗ нужно разместить на wiki-странице с соблюдением структуры и форматирования (заголовки различных уровней, абзацы и т. д.). Верстка страниц выполняется на языке разметки markdown.

Составление плана работ

После составления ТЗ необходимо декомпозировать проект на ряд задач. Каждая задача должна быть достаточно конкретизирована, чтобы участники команды понимали ее содержание, DoD (definition of done) и могли оценить сроки ее выполнения. Необходимо также предоставить план тестирования созданного продукта.

Задачи следует создавать в разделе issues проекта.

Составленный план работ не является окончательным и может быть скорректирован в процессе разработки, но является обязательным начальным этапом и должен быть максимально полным исходя из ТЗ.

Защита проекта

Каждый участник команды представляет письменный отчет о проделанной работе. Отчет состоит из следующих обязательных частей:

  1. ТЗ проекта и итоговый план работ на команду.

  2. Описание командной работы и полученного результата.

  3. Описание личного вклада в результат работы команды.

Для законченных проектов будет организован процесс защиты перед заказчиками, успешное прохождение которого может существенно облегчить процесс итоговой аттестации в виде экзамена. Для защиты проекта нужно будет предоставить в наглядном виде:

  1. ТЗ и итоговый план работ.

  2. Сделанный продукт, для детализации см. критерии оценки в начале.

  3. Распечатанный отчет.

Для защиты команда готовит презентацию. Регламент защиты:

  1. Тимлид начинает презентацию проекта. Озвучиваются технические требования к проекту (ТЗ), декомпозицию задачи (план работ) и распределение ролей в команде.

  2. Далее каждый участник команды рассказывает о своей части работы. В процессе необходимо продемонстрировать рабочую функциональность и фрагменты кода.

На доклад отводится 5−7 минут.

Оформление отчета

Шаблон: trpo_report_template.odt.

Документ подготовлен в LibreOffice Writer и может некорректно отображаться в Microsoft Word.

Оценка за курсовой проект может быть выставлена только при наличии отчета. У каждого участника команды должен быть свой личный отчет.

Для оформления следует использовать настроенные стили:

  1. Heading 1 и Heading 2 для заголовков и подзаголовков.

  2. Text body для основного текста.

  3. Table Heading и Table Contents для заголовков таблиц и содержимого ячеек.

  4. 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 будет выпущена, если останется время.

1

About GitHub Wikis

2

Mastering issues

3

Bootstrap

Дополнение списка задач

Первоначальный список задач не обязан оставаться неизменным. В процессе работы над проектом могут появляться новые задачи. Примеры ситуаций:

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

  2. При работе над проектом была обнаружена ошибка, исправление которой либо выходит за рамки текущей задачи, либо требует ощутимых затрат по времени, либо может быть исправлена позже, и т. д. В этом случае также создается новая задача.

Подготовка инфраструктуры

В рамках лабораторных работ вы двигались от простого к сложному, постепенно добавляя новые инструменты в рабочий процесс.

На практике перед началом разработки следует подготовить инфраструктуру:

  1. Создать репозиторий.

  2. Подготовить систему сборки (написать Makefile).

  3. Настроить автоматические сборки в CI.

Написание Unit-тестов не является завершающим этапом разработки проекта. Тесты пишутся в процессе выполнения каждой задачи. Наличие тестов — негласный Definition of Done.

Рабочий процесс

Резюмируя вышесказанное, к моменту начала кодирования у вас должны быть:

  1. ТЗ в wiki

  2. Список задач в Issues

  3. Репозиторий с каркасом проекта, где подготовлены система сборки, настроен CI и все готово для добавления модульных тестов.

Дальнейший процесс состоит из следующих шагов:

  1. Выбрать задачу из issues, на которой нет исполнителя. Назначить ее на себя.

  2. Создать в репозитории ветку для реализации задачи. Желательно договориться об именовании веток. Например, ветки можно называть по номерам задач: issue-1, issue-2, issue-42. Допускается дополнять названия веток краткими метками: issue-42-user-score.

  3. Реализовать задачу. При оформлении коммитов руководствоваться соглашениями 4.

  4. Написать тесты, покрывающие разработанную функциональность.

  5. Если ветка master ушла вперед, то выполнить rebase своей ветки на master 5. Убедиться, что после ребейза сборка успешно проходит в CI.

  6. Смержить ветку в мастер с ключом --no-ff.

  7. Закрыть задачу. При необходимости оставить комментарии с пояснениями о выполнении.

Порядок может меняться. Например, если задача требует исправления ошибки, то сначала пишется тест для воспроизведения проблемы (тест должен падать), а затем реализуется исправление ошибки.

4

How to Write a Git Commit Message

5

A simple git branching model

Распространенные ошибки

ТЗ в последний момент

Как правило, ТЗ требует согласования и редко остается в своем первоначальном виде. Подготовка в последний момент не позволяет предотвратить наиболее фатальные ошибки.

Делегирование разработки юнит-тестов

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

Подгон под ответ

Сначала реализовать всю функциональность, а затем по памяти написать список по факту выполненных задач, и тут же закрыть их. Бессмысленная трата сил.

Избыточная детализация задач

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

Подмена основных активностей побочными

Предполагается, что все участники пишут код. Любая побочная деятельность при всей своей полезности не рассматривается как требующая специально выделенной роли в команде. Примеры: управление проектом, дизайн, подготовка документации и презентации и т. д.

Некоторые признаки некачественного кода

В процессе проверки курсовых проектов помимо названных выше критериев мы обращаем внимание на качество кода. Ниже приведены некоторые наиболее распространенные ошибки. Рекомендуется в конце выполнения каждой задачи просматривать код на наличие следующих «запахов» (code smell).

  1. Дублирование кода.

    Повторяющийся код — один из признаков нарушения принципа DRY (Don’t repeat yourself).

    См. Duplicate Code, Comments.

  2. Длинная функция.

    Функция должна решать одну задачу. Такие функции проще читать, поддерживать и тестировать. Длинная функция может быть признаком нарушения принципов DRY, SRP (Single Responsibility Principle) и/или SLA (Single Level of Abstraction)

    См. Long Method

  3. Длинный список параметров.

    Пример:

    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.

  4. Магические константы.

    См. Magic Number

  5. User-specific code.

    Пример:

    FILE* f = fopen("/home/v.pupkin/myproject.dict.txt", "r");
    

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

  6. Использование глобальных переменных.

    См. Global Variables Are Bad.

Выбор инструментов

Лабораторные работы выполняются на языке C. Для разработки курсового проекта вы можете выбрать язык на свое усмотрение, но необходимо, чтобы все участники команды были знакомы с выбранным языком на достаточном уровне.

Запрещается использовать слишком высокоуровные инструменты: Unity, Unreal Engine и др. Мы ожидаем, что большая часть вашей работы будет заключаться в ручном написании кода и работе с инструментами, а не в сборке конструктора из готовых блоков. Кроме того, у вас могут возникнуть сложности с настройкой CI, и работа не будет выполнена в полном объеме.

Запрещается использовать устаревшие инструменты. Например, библиотеку BGI, известную некоторым из вас по заголовочному файлу graphics.h. Последняя среда разработки с поддержкой этой библиотеки была выпущена в 1997 году, более двадцати лет назад.

Поиск актуальных полезных библиотек можно начать отсюда: getAwersomeness()

Темы

Для каждой темы указан рекомендованный размер команды. Если вы планируете собрать команду больше рекомендуемой, то следует более детально продумать план работ. Часто возникает ситуация, когда в небольшом проекте сложно найти задачи для всей команды.

Некоторые проекты являются упрощенными клонами существующих систем. Для составления ТЗ рекомендуется ознакомиться с указанными и другими существующими реализациями.

2 человека:

  1. Hangman. Виселица.

  2. Игра «Жизнь»

  3. Пятнашки.

  4. Игра «100 спичек»

  5. Игра «Ним» (три кучки спичек)

  6. Сортировка входных данных (аналог sort)

  7. Программа символы: проверка сбалансированности скобок в программе на С

  8. Интерпретатор языка программирования Машины Тьюринга

  9. Система сборки (аналог make)

  10. Вывод фрагментов файлов (аналог head и tail, с обязательной реализацией режима --follow).

2−3 человека:

  1. QuizRunner. Система проведения тестирования.

  2. KeyboardNinja. Клавиатурный тренажер.

  3. FileProcessor. Приложение для массового переименования файлов в указанном каталоге по заданным шаблонам.

  4. Formatter. Приложение для форматирования текста: выравнивание, разбивка на абзацы, разбивка на страницы с колонтитулами и т. д.

  5. Приложение для рекомендации оптимального тарифа на основе затрат

  6. Задачи на графах: кратчайший путь между городами, количество разных маршрутов между городами, длиннейший путь между городами

  7. Движок SQL-запросов к данным в формате CSV

  8. Генератор документации по комментариям в исходном коде (аналог doxygen)

  9. Конвертер файлов из Markdown в HTML (аналог pandoc)

  10. Todo лист

  11. Анализ использования диска (аналог ncdu)

  12. Unit Converter (аналог units)

  13. Password Generator (аналог pwgen)

  14. Генератор статичных сайтов (аналог jekyll)

2−3 человека, узкоспециализированные темы, рекомендуется консультация преподавателя:

  1. SpellChecker. Приложение, исправляющее опечатки во введенных словах. (аналог hunspell)

  2. Система полнотекстового поиска. Ожидается реализация с построением обратного индекса.

Журнал успеваемости

gantt title Календарный план dateFormat YYYY-MM-DD axisFormat %d.%m section ЛР ЛР0 : 2021-02-01, 2w ЛР1 : 2021-02-08, 2w ЛР2, ветки : done, 2021-02-15, 2w ЛР3, Make, CI : 2021-02-22, 3w ЛР4, Unit Tests : 2021-03-08, 3w section КР Согласование темы : 2021-02-08, 2w ТЗ, План работ : 2021-02-08, 7w Разработка беты : 4w Доработка до релиза : 2w Защита: 3w