Tacsiazuma
Tacsiazuma A letscode.hu alapitója, több, mint egy évtized fejlesztői tapasztalattal. Neovim függő hobbi pilóta.

Pushbreaker – Az élet CI szerver nélkül

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

comments powered by Disqus