1. Что такое CI и почему он похож на «вежливого робота»
Представьте, что у вас есть друг, который каждый раз после вашего коммита молча делает три вещи: конфигурирует проект, собирает, запускает тесты — и говорит «ок» или «сломалось». Это и есть CI в бытовом смысле. Не магия и не «ещё один язык», а просто дисциплина: запускать проверку одинаково, повторяемо и без ручных танцев.
CI (Continuous Integration) — это подход, когда проверка проекта запускается автоматически при изменениях в репозитории. Обычно это происходит на отдельной машине (сервере), но нам сейчас важнее не «где», а «что именно». CI почти всегда сводится к пайплайну из нескольких команд, которые можно выполнить и локально. Если вы можете сделать это у себя в терминале одной последовательностью команд — вы уже понимаете основу CI, даже если ни разу не видели GitHub Actions или Jenkins.
Главный язык CI — не YAML, а код завершения процесса
Пока вы учитесь программировать, у вас есть привычка оценивать успех так: «Запустил — вроде вывело то, что хотел». Для человека это нормальная эвристика. Для машины — абсолютно бесполезная. Машине нужно короткое и недвусмысленное правило: успех или провал.
Таким правилом в мире командной строки является exit code (код возврата процесса). Если программа завершилась с кодом 0, это обычно означает «всё хорошо». Если завершилась с кодом не 0, это означает «ошибка». И обратите внимание: это относится не только к вашим программам, но и к утилитам сборки, и к тест‑раннерам, и к CTest.
Вот минимальный «ручной тест‑раннер», который отлично объясняет идею CI:
#include <iostream>
bool test_add() {
return (2 + 3) == 5;
}
int main() {
if (!test_add()) {
std::cerr << "test_add FAILED\n";
return 1; // не-0 = провал
}
return 0; // 0 = успех
}
Если такую программу запустит CI, ему не нужно «читать глазами» FAILED. Ему достаточно кода 1.
2. Пайплайн configure → build → test: смысл трёх шагов
Слова configure, build, test звучат как заклинание. Но на практике это три разные проверки, каждая ловит свой класс проблем. Важно понимать их смысл, потому что новичок часто ждёт от одного шага того, что должен делать другой.
Ниже — удобная таблица, которую стоит держать в голове:
| Шаг пайплайна | Пример команды (концептуально) | Что подтверждаем | Типичный провал |
|---|---|---|---|
| configure | |
проект вообще можно настроить (нашли компилятор, зависимости, CMakeLists корректен) | «CMake не нашёл…», «ошибка синтаксиса CMake», «не та версия стандарта» |
| build | |
код компилируется и линкуется | «ошибка компиляции», «undefined reference», «не тот include» |
| test | |
поведение не сломано, тесты проходят | «упал тест», «регрессия», «неверный результат» |
Чтобы визуально закрепить, вот простая схема:
flowchart LR
A[configure] -->|ok| B[build]
A -->|fail| X[PIPELINE FAILED]
B -->|ok| C[test]
B -->|fail| X
C -->|ok| Y[PIPELINE PASSED]
C -->|fail| X
Важная деталь: каждый шаг — это отдельная команда, и каждая команда тоже заканчивается кодом 0/не‑0. Поэтому пайплайн — это не «сложная система», а просто цепочка: «если предыдущий шаг успешен, запускаем следующий».
Configure: почему этот шаг полезно “роняет” проект раньше компиляции
Новички часто недооценивают configure‑шаг. Кажется: «Ну CMake же просто что-то там генерирует». На деле configure — это проверка того, что проект в принципе описан корректно и его можно собрать в данном окружении.
Например, если вы случайно написали в CMakeLists.txt опечатку, вы можете получить ошибку ещё до компиляции C++. И это прекрасно, потому что вы ловите проблему раньше.
Минимальный CMakeLists.txt для идеи может выглядеть так:
cmake_minimum_required(VERSION 3.20)
project(TaskBook LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
add_executable(taskbook src/main.cpp)
Если configure не проходит, это значит: на этом компьютере или в этой конфигурации проект пока нельзя собрать. В CI это особенно важно, потому что CI‑машина обычно «чистая»: у неё нет ваших локальных костылей.
Build: почему сборка — это ещё и про линковку
Шаг build многие воспринимают как «ну компилятор же скажет, если я ошибся». Да, но ошибки бывают не только синтаксические.
Есть ещё ошибки линковки: вы объявили функцию, но забыли добавить файл в сборку; вы переименовали .cpp, но не поправили CMake; вы случайно сделали две одинаковые реализации одной функции в разных местах. Для новичка эти ошибки часто выглядят как «магия, ничего не понимаю», но CI как раз хорош тем, что он стабильно воспроизводит проблему.
Показательный пример «новичковой» ситуации: вы добавили новый файл src/parse.cpp, написали там parse_priority, локально в IDE оно как-то собралось (например, IDE сама подхватила файл), а в CMake вы забыли его добавить. На вашем компьютере всё может быть «зелёным» из-за кеша или особенностей IDE, а в CI сборка будет падать честно и сразу.
Test: почему тесты — обязательный третий шаг
Тест‑шаг — это то, что превращает «оно компилируется» в «оно работает так, как мы договорились». И важнейшее слово здесь — «договорились». Тесты фиксируют контракт поведения.
Если вы используете CTest, то общая идея такая: у вас есть отдельный исполняемый файл с тестами, и CTest его запускает.
Концептуально это выглядит так:
enable_testing()
add_executable(taskbook_tests tests/test_main.cpp)
add_test(NAME taskbook_core COMMAND taskbook_tests)
После этого команда ctest запускает зарегистрированные тесты и суммирует результат. Если хоть один тест упал, ctest завершится с кодом не 0, а значит «пайплайн красный».
И вот тут CI становится максимально понятным даже новичку: если тесты упали, значит, вы сломали поведение. Не «возможно» и не «кажется», а сломали конкретное правило, и тест‑раннер обычно покажет где.
3. Зачем CI новичку
У новичка обычно две большие боли: он часто ломает проект случайно и не понимает, где именно сломал; и он не уверен, что «всё работает», потому что проверял только один сценарий «на глаз». CI решает обе проблемы без философии — чисто технически.
Во‑первых, CI учит вас думать о проекте как о штуке, которая должна воспроизводимо собираться не только «на моём компьютере». И это не про «серверы и корпорации», а про простую вещь: вы сами через неделю — это уже другой человек с другим настроением, и он не помнит, какие кнопки в IDE нужно было нажать, чтобы оно заработало.
Во‑вторых, CI заставляет вас разделить «код компилируется» и «код ведёт себя правильно». Новички часто путают эти два понятия и удивляются: «Но оно же собралось!» Да, собралось. Это означает только то, что компилятор понял ваш текст. Это никак не гарантирует, что логика верна.
В‑третьих, CI помогает ловить регрессии. Регрессия — это когда вы «улучшили» код, а старая фича тихо умерла. И самое неприятное: умерла не там, где вы меняли, а где‑то рядом (или вообще в другом файле). Автоматические тесты плюс автоматический запуск — это ваша сигнализация от таких сюрпризов.
Что значит «CI‑дружелюбные тесты»
У тестов есть неприятное свойство: их легко написать так, что они «иногда проходят, иногда нет». А новичок и так живёт в мире, где «иногда оно работает». Нам не нужно добавлять ещё один источник мистики.
CI‑дружелюбный тест — это тест, который даёт одинаковый результат при одинаковых входах. То есть он детерминированный и независим от внешнего мира. На практике новички часто случайно делают тесты «живыми»: читают std::cin, смотрят на текущее время, используют случайные числа без фиксированного seed, зависят от порядка выполнения тестов.
Очень практичное правило: если тест для проверки логики требует от вас «ввести что‑то в консоль», это не unit‑тест, а спектакль «я притворился пользователем». Для CI спектакли не подходят: CI не умеет вводить текст и не должен.
Чуть менее очевидный пример: тест, который сравнивает строки вывода целиком, включая лишние пробелы и переносы, может быть слишком хрупким. Иногда это нужно, но чаще вы хотите тестировать смысл (структуру данных, коды ошибок), а не «красоту ASCII‑таблички».
Почему CI полезен даже если вы один
Есть соблазн думать: «CI нужен в команде, а я один — зачем мне это». На практике даже одиночный проект выигрывает от CI, потому что у вас появляется внешний «объективный судья». Он не устал, не торопится, не забывает запустить тесты, не путает Debug и Release и не говорит «ну вроде норм».
Кроме того, CI помогает выработать привычку маленьких, безопасных изменений. Если вы делаете одну правку и сразу прогоняете пайплайн, вы точно знаете, что сломали (если сломали). Если вы сделали 15 правок, а потом решили «ну давай-ка я запущу тесты», вы уже не знаете, какая именно правка виновата. Это не вопрос таланта, это чистая математика количества изменений.
И наконец, CI очень помогает психологически. Когда пайплайн зелёный, можно спокойнее рефакторить. Вы перестаёте относиться к своему коду как к хрустальной вазе, которую страшно трогать.
4. Практический пример: TaskBook и функция, которую удобно тестировать
Чтобы это не было абстракцией, продолжим условный учебный проект: маленькое консольное приложение TaskBook, которое хранит список задач в памяти и умеет добавлять задачу, валидировать данные и печатать список. Важная часть для тестов — вынести «мозги» в функции и/или в небольшой модуль, который можно вызывать без std::cin.
Пусть у нас есть функция в библиотечной части, которую удобно тестировать:
#include <string>
#include <optional>
std::optional<int> parse_priority(const std::string& s) {
if (s.empty()) return std::nullopt;
int x = 0;
for (char c : s) {
if (c < '0' || c > '9') return std::nullopt;
x = x * 10 + (c - '0');
}
if (x < 1 || x > 5) return std::nullopt;
return x;
}
С точки зрения CI:
- шаг build подтверждает, что функция компилируется вместе со всем проектом;
- шаг test подтверждает, что она ведёт себя правильно на нормальных и «плохих» входах.
5. Локальный мини‑пайплайн: «CI без CI»
Очень полезная мысль для новичка: CI — это не обязательно «облачная штука». CI начинается с того момента, когда у вас есть одна повторяемая команда или скрипт, которая делает configure/build/test и возвращает правильный код.
Самая типичная локальная последовательность для CMake‑проекта выглядит так (пример для out‑of‑source сборки в папку build):
cmake -S . -B build
cmake --build build
ctest --test-dir build
Это уже «мини‑CI», потому что:
- команды запускаются одинаково каждый раз;
- результат определяется exit code каждой команды;
- вы не зависите от «настроек IDE, которые где-то там в проекте спрятаны».
Если хочется сделать из этого одну кнопку, вы можете создать, например, скрипт ci_local.sh:
#!/usr/bin/env bash
set -e
cmake -S . -B build
cmake --build build
ctest --test-dir build
Здесь set -e означает: «если любая команда упала — сразу завершай скрипт». Это поведение очень похоже на то, как работает настоящий CI‑пайплайн: любой провал = всё красное.
6. Типичные ошибки
Ошибка №1: считать, что “build = всё работает”.
Сборка подтверждает только то, что ваш проект компилируется и линкуется. Ошибка в формуле, неверная обработка пустой строки, перепутанное условие валидации — всё это легко компилируется. Если вы не запускаете тесты автоматически, вы очень быстро начнёте «ловить баги глазами», а это хобби на любителя.
Ошибка №2: тесты зависят от ввода, времени или случайности.
Когда тест читает std::cin, он становится интерактивным и неприменимым для автоматического запуска. Когда тест зависит от текущей даты/времени, он может начать падать «по расписанию», и это особенно весело, если падение происходит раз в сутки. Когда тест использует случайные числа без фиксированного seed, вы получаете “иногда красное” — самый токсичный вид ошибки.
Ошибка №3: “ну локально же прошло” и игнорирование чистой сборки.
Локальная среда часто содержит кеши, собранные файлы и случайно оставшиеся артефакты, которые маскируют проблему. Поэтому out‑of‑source сборка в отдельную папку и повторяемый пайплайн важны: они приближают вашу проверку к тому, что увидит CI на чистой машине.
Ошибка №4: один огромный тест‑сценарий вместо набора понятных тестов.
Новички иногда пишут один гигантский тест «на всё»: добавили задачу, потом ещё, потом распечатали, потом сравнили весь вывод. Если такой тест падает, вы не знаете, что именно сломалось: парсинг, сортировка, формат. Гораздо устойчивее иметь несколько коротких тестов, каждый из которых проверяет одно правило, и запускать их все в CI.
Ошибка №5: тесты падают, но пайплайн зелёный из-за неверных кодов возврата.
Если ваш тестовый бинарник всегда возвращает 0, даже когда что‑то пошло не так, CI будет считать, что всё хорошо. Тест‑фреймворки и CTest как раз полезны тем, что они дисциплинируют это место: провал проверки превращается в не‑нулевой exit code автоматически.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ