Pushbreaker – Az élet CI szerver nélkül
Ha körbenézünk, hogy milyen/mekkora projekteken dolgozunk nap, mint nap (és itt főleg a kisebb projektekre gondolok), akkor számunkra is világos lesz, hogy bizony nem minden projekt érdemli meg azt, hogy pl. Jenkins job-ot rendeljünk hozzá és a .gitlab.yml fájl is hiánycikk, netán nem is bevált gyakorlat a CI szerver a cégnél, ahol dolgozunk, mert csak KKV-knek szórjuk ki az apróbb oldalakat.
Ami viszont természetesen továbbra is fontos, az az hogy verziókövetve legyenek ezek a kódbázisok is, betartsunk bizonyos konvenciókat, ha írtunk teszteket, azokat ne törjük össze az egyes commitok során és lehetőleg a PHP mess detector se akadjon ki fájljaink láttán. Mindezt azért, hogy a kódunk megbízható legyen, mások által átlátható és az esetleges utódunk se fakadjon sírva, ha megnyitja a projektet (ez utóbbit főleg akkor értékeljük majd, ha átvettünk egy rendesen karbantartott kódot a sok legacy borzalom után).
A cikkünkben a git kliensoldali hookjait fogjuk igénybe venni és megnézzük, hogy is tudunk bizonyos teszteket és ellenőrzéseket automatizálni a gépünkön, hogy csak olyan kódot engedjünk ki a kezünk közül, amihez jó esetben bátran adjuk a nevünket is a commit authorban.
Korábban már volt szó arról, hogy pontosan mik is ezek a git hook-ok, így akinek ez nem tiszta, az itt utánajárhat. Mi most az ún. pre-push hookra fogunk koncentrálni, ami - nevéből is kiderül -, a push folyamat előtt fog futni és meg tudja azt akadályozni. PHP példákkal fogok dolgozni, de igazából bármilyen nyelvre rá lehet ezeket illeszteni, ahol lehetőségünk van command line meghívni a szükséges teszteket/checkstyle-t, stb.
Azt már tudjuk, hogy ezek az ún. .git/hooks
mappában tanyáznak, amivel a legfőbb problémánk, hogy a Git mappája nincs verziókövetve, tehát ezeket a fájlokat nekünk kell kézzel bemásolni oda. Bevált szokás, hogy a projektben létrehozunk egy ún. support mappát, amibe pakoljuk a doksikat, hooks fájlokat, konfigokat, stb. Így mi is így teszünk majd.
Először is hozzunk létre egy üres repository-t valahol:
1
git init
Ezután hozzuk létre az alábbi szerkezetet:
1
2
3
4
-- src
-- test
-- support
-- git
A support/git
mappában hozzunk létre egy pre-push
nevű fájlt, egyelőre az alábbi tartalommal:
1
2
3
4
5
6
7
8
#! /bin/bash
function header {
echo "Letscode.hu combo-breaker"
}
header
exit 0
A hook egyelőre csak kiír egy sort és utána továbbengedi a futást, de mi csak azt akarjuk egyelőre látni, hogy működik-e.
Viszont a gond az az, hogy ez most nem jó helyen van, hiszen nekünk a .git/hooks
mappába kellene ezt helyezni. Mivel a PHP projektek 99%-a használ composert, ezért mi ott is beiktatunk egy hookot, mégpedig az install után fogjuk megtenni mindezt. Hozzunk hát létre egy composer.json
-t:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "letscodehu/combobreaker-dummy",
"description": "Letscode.hu Combobreaker dummy",
"type": "project",
"homepage": "http://letscode.hu",
"require-dev": {
"phpunit/phpunit": "^4.8",
"squizlabs/php_codesniffer": "^2.3",
"phpmd/phpmd": "@stable"
},
"autoload": {
"psr-4": {
"App\\": "src/App/"
}
},
"autoload-dev": {
"psr-4": {
"AppTest\\": "test/AppTest/"
}
},
"scripts": {
"post-install-cmd" : "cp support/git/pre-push .git/hooks/pre-push && chmod +x .git/hooks/pre-push"
}
}
Akkor nézzük csak meg mi történik itt? Először is behúzzuk a lényeges függőségeket, phpunitot, codesniffert, mess detectort. Az autoloadert bekonfigoljuk, valamint a lényeg: amikor a composer install
-t futtatjuk, utána bemásolja a support mappából a pre-push
hookot a helyére és futtathatóvá teszi azt. Ezután, ha futtatunk egy composer install
-t, láthatjuk is, hogy lefutott a parancs, tehát a fájl a helyére került, nem kell amiatt aggódni, hogy elfelejtjük odamásolni.
Az első lépés megvan, most magát a pushbreakert kellene tesztelni. Adjunk hozzá egy remote-ot saját szájízünk szerint:
1
git remote add origin <repository-url>
Ezután pedig próbáljunk egyet pusholni oda:
1
git push -u origin master
Láthatjuk, hogy lefutott, mikor kiírja:
1
Letscode.hu combo-breaker
Viszont ezzel még sokra nem mentünk, jöjjenek a konkrét lépések, először is vezessünk be egy tesztet. Ahhoz, hogy ez menjen, először is kell egy phpunit.xml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="App\\Tests">
<directory>./test</directory>
</testsuite>
</testsuites>
<filter>
<whitelist>
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>
Roppant egyszerű, a composer autoloaderét haszáljuk és a test mappából futtatjuk a teszteket. Ha ez megvolt, akkor az eddigi kódot kiegészítjük a pre-push
fájlban:
1
2
3
4
5
6
7
8
9
10
11
#! /bin/bash
function header {
echo "Letscode.hu combo-breaker"
}
function test {
echo -e 'Running tests...\c'
vendor/phpunit/phpunit/phpunit > /dev/null
check $?
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function check {
if [ $1 == 0 ]; then
pass
else
failed
fi
}
function pass {
echo -e "\e[32mpassed!\e[0m\n"
}
function failed {
echo -e "\e[31mfailed!\e[0m\n"
exit 1
}
header
test
exit 0
Na de már megint mi történik itt? Először is meghívjuk a header functiont, utána pedig a test-et. A testben kiírunk egy sort, amit nem zárunk le, elindítjuk a phpunit-ot, annak kimenetére nem vagyunk kiváncsiak ( > /dev/null
), csak az exit code-jára ($?
), amit átadunk a check functionnek (check $?
). A check megkapja ezt a paramétert ($1
) és onnan állapítjuk meg, hogy hibára futottak-e a tesztek, hogy az exit code 0-tól eltérő-e (if [ $1 == 0 ]
). Ha hibára futott, akkor a failed function lesz meghívva, ami miután pirossal (\e[31m
) kiírta, hogy failed, kilép mégpedig 1-es exit code-al (exit 1
), ami megállítja a push folyamatát. Ha 0-val végződtek a tesztek, akkor a pass
function lesz meghívva, ami csak befejezi a “running tests…” sort és utána továbbengedi a program futását.
Ahhoz, hogy ezt tudjuk tesztelni, vegyünk fel egy-egy egyszerű tesztet és tesztelendő osztályt:
src/App/Dummy.php
:
1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace App;
class Dummy {
public function comboBreaker() {
return "c-c-c-combo breaker";
}
}
A hozzá tartozó teszt pedig:
test/AppTest/DummyTest.php
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<?php
namespace AppTest;
use App\Dummy;
class DummyTest extends \PHPUnit_Framework_TestCase {
/**
* @var Dummy
*/
private $underTest;
public function setUp() {
$this->underTest = new Dummy();
}
/**
* @test
*/
public function it_returns_string() {
// GIVEN
// WHEN
$actual = $this->underTest->comboBreaker();
// THEN
$this->assertEquals("c-c-c-combo breaker", $actual);
}
}
A kód egyébként megtalálható itt. Akkor most próbáljunk pusholni egyet!
1
2
3
4
5
6
7
Letscode.hu combo-breaker
Running tests...passed!
Counting objects: 15, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (15/15), 8.01 KiB | 0 bytes/s, done.
Siker! Akkor jöjjenek a következő lépések. Nem csak az a lényeg, hogy a tesztjeink lefutottak-e, hiszen a kódminőség soktényezős, ezért nézzük, hogy betartottuk-e a szabályokat, kódunk konzisztens-e és tiszta.
Toldjuk meg a pushbreakert egy plusz metódussal, ez lesz a checkstyle:
1
2
3
4
5
6
7
8
9
10
function checkstyle {
echo -e 'Running codesniffer...\c'
vendor/squizlabs/php_codesniffer/scripts/phpcs src --standard=PSR2 > /dev/null
check $?
}
header
checkstyle
test
exit 0
A művelet hasonló, mint a tesztek esetén, ráengedjük az src mappára és várjuk, hogy volt-e valami hiba. A CodeSniffernek itt járhattok utána, mert mindenkinek más és más beállítások kellenek, rengeteg szabály van, amik közül válogathatunk, mi jelenleg a PSR2 által megszabott coding standard szerint vizsgáljuk a kódot.
A következő lépés a PHP mess detector lesz:
1
2
3
4
5
function mess_detector {
echo -e 'Running mess detector...\c'
vendor/phpmd/phpmd/src/bin/phpmd src text cleancode,naming,controversial,design --suffixes php,phtml > /dev/null
check $?
}
1
2
3
4
5
header
checkstyle
mess_detector
test
exit 0
A mess detectornál egy kivételével az összes előre definiált ruelesetet ráengedjük, az src
mappában levő .php
és .phtml
kiterjesztésű fájlokra, és a korábbiakhoz hasonlóan a kimenettől függően szakítjuk meg a push folyamatát.
Na mostmár elvileg azt hihetnénk, hogy minden szép és jó, viszont egy probléma még hátravan, aminek az oka a git működésében keresendő.
Tegyük fel, hogy a pushbreakerünk megfog egy hibát, majd azt gyors kijavítjuk, de nem commitoljuk be, hanem egyből újrapusholjuk azt. Bizony, a pushbreakerünk ezt szó nélkül jóváhagyja, ugyanis ő annyit lát, hogy a working directoryban levő kódban ki lett javítva, nem azt figyeli, hogy a pushal milyen kód is megy fel. Kétféle megoldás létezik arra, hogy a valóban repository-ba becheckolt kódot vizsgáljuk:
Az egyik, hogy kicheckoutoljuk egy build directory-ba, amit minden pre-push előtt/után kitakarítunk. Ezzel a gond az, hogy egy nagyobb projekt esetén ez némileg lassítja a dolgot, valamint lehet csak a script sokadik lépése során derül ki, hogy a hibát mégsem javítottuk ki és akkor vissza a kódhoz, gyors commit, utána újra.
A másik módszer az lesz, hogy megvizsgáljuk, hogy valóban mindent becommitoltunk/elstasheltünk-e és a working directory megegyezik azzal, ami felkerül. Ez utóbbit fogjuk most alkalmazni a példában:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function git_checker {
echo -e 'Checking working directory...\c'
if [ -n "$(git status --porcelain)" ]; then
echo -e "\e[31mplease commit/stash your changes first!\e[0m\n"
exit 1
else
echo -e "\e[32mclean!\e[0m\n"
fi
}
header
git_checker
checkstyle
mess_detector
test
exit 0
A tesztek lefutása előtt megvizsgáljuk a git status
paranccsal, hogy van-e olyan változás a working directory-ban, ami nincs becommitolva és ha van, akkor megállítjuk a folyamatot, ha nincs, akkor minden mehet tovább. Ha esetleg szeretnénk még megalázóbbá tenni a hibákat, akkor feltelepíthetjük az mplayer
csomagot:
1
sudo apt-get install mplayer
Ezután kiegészíthetjük a következő sorral a fail function-t:
1
mplayer support/git/sad-trompone.mp3 > /dev/null 2>&1
Természetesen ehhez szükség lesz a sad-trompone.mp3-ra is, ami itt található. Ezután bármikor hibára fut a scriptünk, azt hanggal is a tudtunkra hozza, kollégáink legnagyobb örömére.
Nos ennyire futotta most, láthattuk, hogy is lehet viszonylag könnyedén ellenőrízni automatizáltan a saját gépünkön. Ez jól jöhet akár van CI szerverünk, akár nem, ugyanis minél előbb vesszük észre a hibákat, annál hamarabb tudunk rájuk reagálni. Persze nehéz bevezetni az ilyesmit, akár egymagunk vagyunk, akár százan dolgozunk a projekten, lesz ellenállás az ilyenekkel szemben, de még mindig jobb ha így derül ki, amolyan titokban, mintsem körbekürtölje a jenkins, hogy biza Te voltál az, aki eltörte a buildet. Értelemszerűen limitáltak a lehetőségeink egy ilyen futtatókörnyezetben, lévén nem akarunk egy órát várni, míg felmegy a push, na meg a komolyabb integrációs, selenium teszteket nem itt fogjuk futtatni, viszont a gyorsabban végrehajtható ellenőrzések számára egy igen korai pont lehet mindez.
A példában szereplő példaprojektet »itt« találjátok.
Update: Mivel kommentben jelezték, hogy a lint kimaradt, ezért azt is hozzáadnám itt.
Ez azért lehet fontos, mert nem minden fájlt tesztelünk és a checkstyle sem tökéletes ezen a téren, mert habár a legtöbb ilyen lehetséges parse errort észreveszi, de mégsem a PHP értelmezőt futtatjuk a fájllal szemben, hanem tokenekre bontva vizsgáljuk azt.
1
2
3
4
5
6
7
8
9
10
11
12
function lint {
echo -e 'Checking for syntax errors...\c'
for file in `find src test -type f -iname "*.php" -o -iname "*.phtml"`
do
php -l $file > /dev/null 2>&1
if [ $? != 0 ]
then
failed
fi
done
pass
}
A fenti kódrészletben a find-al lekérjük rekurzívan az src és test mappában található fájlokat és kiválogatjuk közülük a php és phtml kiterjesztésűeket, majd azok ellen futtatunk egy syntax ellenőrzést és ha hibát találunk, akkor megállítjuk a push-t, ahogy a korábbiakban is tettük. Ezt a git_checker és a checkstyle közé tettem:
1
2
3
4
5
6
7
header
git_checker
lint
checkstyle
mess_detector
test
exit 0