1. Czym jest CI i dlaczego przypomina „uprzejmego robota”
Wyobraź sobie, że masz znajomego, który za każdym razem po twoim commicie bez słowa wykonuje trzy rzeczy: konfiguruje projekt, buduje go, uruchamia testy — i mówi "OK" albo "zawiodło". To jest właśnie CI w sensie praktycznym. To nie magia i nie "kolejny język", a dyscyplina: uruchamiać sprawdzenie w sposób jednakowy, powtarzalny i bez ręcznych tańców.
CI (Continuous Integration) to podejście, w którym weryfikacja projektu uruchamia się automatycznie przy zmianach w repozytorium. Zwykle dzieje się to na oddzielnej maszynie (serwerze), ale dla nas ważniejsze jest nie „gdzie”, lecz „co dokładnie”. CI niemal zawsze sprowadza się do pipeline'u z kilku poleceń, które można wykonać także lokalnie. Jeśli możesz to zrobić w swoim terminalu jedną sekwencją poleceń — rozumiesz podstawę CI, nawet jeśli nigdy nie widziałeś GitHub Actions lub Jenkins.
Główny język CI — nie YAML, lecz kod zakończenia procesu
Gdy uczysz się programować, masz nawyk oceniać sukces tak: „Uruchomiłem — wydaje się, że wypisało to, czego chciałem”. Dla człowieka to sensowna heurystyka. Dla maszyny — kompletnie bezużyteczna. Maszyna potrzebuje krótkiej i jednoznacznej reguły: sukces lub porażka.
Taką regułą w świecie wiersza poleceń jest exit code (kod zwrotu procesu). Jeśli program zakończył się kodem 0, zwykle oznacza to „wszystko w porządku”. Jeśli zakończył się kodem nie 0, oznacza to „błąd”. I zwróć uwagę: to dotyczy nie tylko twoich programów, lecz także narzędzi budujących, runnerów testów i CTest.
Poniżej minimalny "ręczny test‑runner", który świetnie ilustruje ideę CI:
#include <iostream>
bool test_add() {
return (2 + 3) == 5;
}
int main() {
if (!test_add()) {
std::cerr << "test_add FAILED\n";
return 1; // nie-0 = niepowodzenie
}
return 0; // 0 = sukces
}
Jeśli taki program uruchomi CI, nie musi on "czytać oczami" słowa FAILED. Wystarczy mu kod 1.
2. Pipeline configure → build → test: sens trzech kroków
Słowa configure, build, test brzmią jak zaklęcie. W praktyce to trzy różne weryfikacje, z których każda łapie inny rodzaj problemów. Ważne jest zrozumienie ich sensu, bo początkujący często oczekuje od jednego kroku tego, co powinien robić inny.
Poniżej — pomocna tabela, którą warto mieć w głowie:
| Krok pipeline'u | Przykładowe polecenie (koncepcyjnie) | Co potwierdzamy | Typowy błąd |
|---|---|---|---|
| configure | |
projekt w ogóle da się skonfigurować (znaleziono kompilator, zależności, CMakeLists jest poprawny) | "CMake nie znalazł ...", "błąd składni CMake", "nieodpowiednia wersja standardu" |
| build | |
kod się kompiluje i linkuje | "błąd kompilacji", "undefined reference", "zły include" |
| test | |
zachowanie nie jest złamane, testy przechodzą | "test spadł", "regresja", "niepoprawny wynik" |
Aby to wizualnie utrwalić, oto prosta schematyczna ilustracja:
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
Ważny szczegół: każdy krok to osobne polecenie i każde polecenie kończy się kodem 0/nie‑0. Dlatego pipeline to nie "skomplikowany system", a po prostu łańcuch: "jeśli poprzedni krok powiódł się, uruchamiamy następny".
Configure: dlaczego ten krok użytecznie "upuszcza" projekt przed kompilacją
Początkujący często nie doceniają kroku configure. Może się wydawać: "Przecież CMake tylko coś tam generuje". W rzeczywistości configure to weryfikacja, że projekt w ogóle jest opisany poprawnie i da się go zbudować w danym środowisku.
Na przykład, jeśli przypadkowo zrobiłeś literówkę w CMakeLists.txt, możesz dostać błąd jeszcze przed kompilacją C++. To dobrze, bo łapiesz problem wcześniej.
Minimalny CMakeLists.txt dla demonstracji może wyglądać tak:
cmake_minimum_required(VERSION 3.20)
project(TaskBook LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
add_executable(taskbook src/main.cpp)
Jeśli configure nie przechodzi, to oznacza: na tej maszynie albo w tej konfiguracji projekt jeszcze nie da się zbudować. W CI to szczególnie ważne, bo maszyna CI jest zwykle "czysta": nie ma twoich lokalnych sztuczek.
Build: dlaczego budowanie to także kwestia linkowania
Krok build wielu osobom kojarzy się tylko z "kompilatorem, który powie, jeśli coś jest nie tak". Tak, ale błędy to nie tylko składnia.
Są też błędy linkowania: zadeklarowałeś funkcję, ale zapomniałeś dodać pliku do kompilacji; zmieniłeś nazwę .cpp, a nie zaktualizowałeś CMake; przypadkowo masz dwie identyczne implementacje tej samej funkcji w różnych miejscach. Dla początkującego takie błędy wyglądają jak "magia, nic nie rozumiem", ale CI jest właśnie dobre, bo stabilnie reprodukuje problem.
Przykładowa sytuacja "dla początkujących": dodałeś nowy plik src/parse.cpp, napisałeś w nim funkcję parse_priority, lokalnie w IDE jakoś się to zbudowało (IDE samo dodało plik), a w CMake zapomniałeś go dodać. Na twoim komputerze może być "zielono" z powodu cache'a lub zachowania IDE, a w CI budowanie uczciwie spadnie.
Test: dlaczego testy to obowiązkowy trzeci krok
Krok test to to, co zmienia "to się kompiluje" w "to działa tak, jak uzgodniliśmy". Najważniejsze słowo to "uzgodniliśmy". Testy utrwalają kontrakt zachowania.
Jeśli używasz CTest, ogólna idea jest taka: masz osobny plik wykonywalny z testami, a CTest go uruchamia.
Koncepcyjnie wygląda to tak:
enable_testing()
add_executable(taskbook_tests tests/test_main.cpp)
add_test(NAME taskbook_core COMMAND taskbook_tests)
Po tym polecenie ctest uruchamia zarejestrowane testy i sumuje wynik. Jeśli choć jeden test się nie powiedzie, ctest zakończy się kodem nie 0, a to znaczy "pipeline na czerwono".
I tutaj CI staje się maksymalnie zrozumiałe nawet dla początkującego: jeśli testy nie przechodzą, znaczy, złamałeś zachowanie. Nie "może" i nie "wydaje się", tylko złamałeś konkretne założenie; runner testów zwykle pokaże, gdzie.
3. Po co CI początkującemu
Początkujący zwykle mają dwa duże problemy: często przypadkowo łamią projekt i nie wiedzą, gdzie dokładnie; oraz nie są pewni, że "wszystko działa", bo sprawdzali tylko jeden scenariusz "na oko". CI rozwiązuje oba problemy bez filozofii — czysto technicznie.
Po pierwsze, CI uczy myśleć o projekcie jako o rzeczy, którą trzeba potrafić powtarzalnie zbudować nie tylko "na moim komputerze". I to nie o "serwery i korporacje", lecz o prostą rzecz: ty sam za tydzień to już inna osoba z innym nastawieniem, i nie pamiętasz, jakie przyciski w IDE trzeba było nacisnąć, żeby to ruszyło.
Po drugie, CI zmusza do rozdzielenia "kod się kompiluje" i "kod zachowuje się poprawnie". Początkujący często mylą te dwa pojęcia i dziwią się: "Ale skompilowało się!" Tak, skompilowało się. To jedynie znaczy, że kompilator zrozumiał twój tekst. Nie gwarantuje, że logika jest poprawna.
Po trzecie, CI pomaga łapać regresje. Regresja to sytuacja, gdy "ulepszyłeś" kod, a stara funkcjonalność cicho zniknęła. Najgorsze: zniknęła nie tam, gdzie zmieniałeś, lecz gdzieś obok (albo w innym pliku). Automatyczne testy plus automatyczne uruchamianie to twoja sygnalizacja przed takimi niespodziankami.
Co to są "CI‑przyjazne testy"
Testy mają nieprzyjemną własność: łatwo je napisać tak, że "czasem przechodzą, czasem nie". A początkujący już żyje w świecie, gdzie "czasem działa". Nie potrzebujemy kolejnego źródła magii.
CI‑przyjazny test to test, który daje ten sam wynik przy tych samych wejściach. Czyli jest deterministyczny i niezależny od świata zewnętrznego. W praktyce początkujący często przypadkowo tworzą testy "żywe": czytają std::cin, sprawdzają bieżący czas, używają losów bez ustalonego seed, zależą od kolejności uruchomienia testów.
Bardzo praktyczne zasady: jeśli test do weryfikacji logiki wymaga od ciebie "wpisać coś w konsolę", to nie jest unit‑test, lecz spektakl "udaję użytkownika". Do CI spektakle nie pasują: CI nie umie wpisywać tekstu i nie powinien.
Mniej oczywisty przykład: test, który porównuje cały tekstowy output, łącznie z nadmiarowymi spacjami i podziałami linii, może być zbyt kruchy. Czasami to potrzebne, ale częściej chcesz testować sens (strukturę danych, kody błędów), a nie "estetykę ASCII‑tabelki".
Dlaczego CI jest przydatny nawet jeśli pracujesz sam
Kusi myśl: "CI jest dla zespołu, a ja sam — po co mi to". W praktyce nawet pojedynczy projekt zyskuje dzięki CI, bo dostajesz zewnętrznego "obiektywnego sędziego". On nie jest zmęczony, nie śpieszy się, nie zapomina uruchomić testów, nie miesza Debug z Release i nie mówi "wydaje się ok".
Poza tym CI pomaga wyrobić nawyk drobnych, bezpiecznych zmian. Jeśli robisz jedną poprawkę i od razu odpalasz pipeline, wiesz dokładnie, co zepsułeś (jeśli coś zepsułeś). Jeśli wykonałeś 15 poprawek, a potem "no dobra, uruchomię testy", to już nie wiesz, która zmiana jest winna. To nie kwestia talentu, a czysta matematyka liczby zmian.
I wreszcie CI bardzo pomaga psychicznie. Gdy pipeline jest zielony, możesz spokojniej refaktoryzować. Przestajesz traktować swój kod jak szklaną wazę, której boisz się dotknąć.
4. Praktyczny przykład: TaskBook i funkcja wygodna do testowania
Aby nie być abstrakcyjnym, kontynuujemy na przykładzie szkolnego projektu: mała aplikacja konsolowa TaskBook, która przechowuje listę zadań w pamięci, potrafi dodać zadanie, walidować dane i wypisać listę. Ważna część dla testów to wydzielenie "mózgu" do funkcji albo małego modułu, który można wywołać bez std::cin.
Niech mamy funkcję w bibliotecznej części, którą łatwo testować:
#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;
}
Z punktu widzenia CI:
- krok build potwierdza, że funkcja kompiluje się razem z resztą projektu;
- krok test potwierdza, że zachowuje się poprawnie dla poprawnych i "złych" wejść.
5. Lokalny mini‑pipeline: "CI bez CI"
Bardzo użyteczna myśl dla początkującego: CI to niekoniecznie "chmurowa rzecz". CI zaczyna się w momencie, gdy masz jedno powtarzalne polecenie lub skrypt, które wykonuje configure/build/test i zwraca poprawny kod.
Najczęstsza lokalna sekwencja dla projektu CMake wygląda tak (przykład out‑of‑source do katalogu build):
cmake -S . -B build
cmake --build build
ctest --test-dir build
To już "mini‑CI", ponieważ:
- polecenia uruchamiane są tak samo za każdym razem;
- wynik określony jest przez exit code każdego polecenia;
- nie zależysz od "ustawień IDE, które gdzieś w projekcie są ukryte".
Jeśli chcesz mieć to pod jednym przyciskiem, możesz stworzyć np. skrypt ci_local.sh:
#!/usr/bin/env bash
set -e
cmake -S . -B build
cmake --build build
ctest --test-dir build
Tutaj set -e oznacza: "jeśli dowolne polecenie upadło — natychmiast zakończ skrypt". To zachowanie bardzo przypomina działanie prawdziwego pipeline'u CI: każdy błąd = wszystko na czerwono.
6. Typowe błędy
Błąd nr 1: myśleć, że "build = wszystko działa".
Budowanie potwierdza tylko, że projekt kompiluje się i linkuje. Błąd w formule, niepoprawne traktowanie pustego stringa, pomylone warunki walidacji — to wszystko łatwo się kompiluje. Jeśli nie uruchamiasz testów automatycznie, szybko zaczniesz "łapać bugi wzrokiem", a to hobby dla wytrwałych.
Błąd nr 2: testy zależą od wejścia, czasu lub losowości.
Gdy test czyta std::cin, staje się interaktywny i nie nadaje się do automatycznego uruchamiania. Gdy test zależy od bieżącej daty/czasu, może zacząć upadać "zgodnie z harmonogramem", co jest zabawne, jeśli upadek następuje raz na dobę. Gdy test używa losowych liczb bez stałego seed, masz "czasem czerwone" — najbardziej toksyczny rodzaj błędu.
Błąd nr 3: "lokalnie przeszło" i ignorowanie czystej kompilacji.
Lokalne środowisko często zawiera cache, zbudowane pliki i pozostałe artefakty, które maskują problem. Dlatego out‑of‑source build w oddzielnym katalogu i powtarzalny pipeline są ważne: przybliżają twoją weryfikację do tego, co zobaczy CI na czystej maszynie.
Błąd nr 4: jeden ogromny scenariusz testowy zamiast zestawu czytelnych testów.
Początkujący czasami piszą jeden gigantyczny test "na wszystko": dodałem zadanie, potem kolejne, potem wypisałem, potem porównałem cały output. Jeśli taki test upadnie, nie wiesz, co konkretnie się zepsuło: parsowanie, sortowanie, format. Znacznie bardziej odporne jest mieć kilka krótkich testów, każdy sprawdzający jedno założenie, i uruchamiać je wszystkie w CI.
Błąd nr 5: testy upadają, ale pipeline jest zielony z powodu niewłaściwych kodów zwrotu.
Jeśli twój binarny plik testowy zawsze zwraca 0, nawet gdy coś poszło nie tak, CI uzna, że wszystko jest w porządku. Frameworki testowe i CTest pomagają w tym miejscu: porażka testu automatycznie zamienia się w nie‑zerowy exit code.
GO TO FULL VERSION