git bisect run: a 125-ös exit kód megment egy rossz vádlottól

A git bisect run automatizálja a regressziókeresést, de egy buildhiba könnyen hamis bűnöst gyárt. Így használd helyesen a 125-ös kilépési kódot.

A regresszió természetesen péntek délután kerül elő, és természetesen „valamikor az elmúlt három hónapban” még működött. Ilyenkor a commitlistát felülről lefelé olvasni nem hibakeresés, hanem vezeklés. A git bisect felezi a gyanús történetet, a git bisect run pedig a tesztelést is átveszi. Van azonban egy apró szerződés, amelyen könnyű elrontani az egész nyomozást: a buildhiba nem jelent rossz commitot. Erre való a 125-ös kilépési kód.

Git commitgráf feleződő keresési tartományokkal, egy kihagyott és egy hibás committal

Ezer commitból nagyjából tíz kérdés

A Git hivatalos leírása szerint két biztos pont kell: egy régi commit, ahol a vizsgált tulajdonság még „jó”, és egy új, ahol már „rossz”. A Git választ egy köztes commitot, mi tesztelünk, majd válaszolunk. Lineáris, ezer commitos történetben ez nagyjából tíz kör, nem ezer.

git bisect start
git bisect bad HEAD
git bisect good v2.4.0

# tesztelés után:
git bisect good   # vagy: git bisect bad

git bisect reset

A „jó” és „rossz” itt nem erkölcsi ítélet a kollégáról, csak két állapot. Fixet, teljesítményromlást vagy akár egy fájlnév-váltást is kereshetünk; ehhez a Git enged saját kifejezéseket, például --term-old fast --term-new slow. A részletes algoritmus nem pusztán dátum szerint felez: a commitgráf elérhetőségét súlyozza, amit a projekt bisect-design jegyzete bont ki.

A 125 nem kudarc, hanem „ezt nem tudom megítélni”

Ha van géppel ellenőrizhető reprodukciónk, a kézi körök helyett jöhet a run:

git bisect start HEAD v2.4.0 --
git bisect run /tmp/bisect-check.sh
git bisect reset

A futtatott program kilépési kódja a válasz. A 0 jó állapotot jelent; az 1–127 közötti érték rosszat, egyetlen kivétellel. A 125 azt mondja: ez a commit nem tesztelhető, ugord át. A 126 és 127 a POSIX shellben külön hibáknak van fenntartva, ezért esett a választás 125-re. Na, ezt nem tudtam, amíg először automatizáltam egy bisectet.

#!/bin/sh

npm run build >/dev/null 2>&1 || exit 125
npm test

A különbség nem akadémiai. Ha egy régi commit egy teljesen más okból nem fordul, és a script simán továbbadja az npm run build 1-es kódját, a Git azt a keresett regresszió bizonyítékaként kezeli. Udvariasan, gyorsan és teljesen tévesen megvádol egy ártatlan commitot.

A fenti scriptet egy 16 commitos mesterséges történeten futtattam. A 7. commit szintaktikailag hibás volt, a valódi regresszió a 11.-ben jelent meg. A run a hetediket skip állapotúnak jelölte, majd helyesen a 11.-et adta vissza első rossz commitként. A Git dokumentációja azt is javasolja, hogy a tesztscript a repositoryn kívül legyen: különben a checkout közben saját maga is eltűnhet vagy megváltozhat. Ez az a fajta geg, amelyet egyszer nevetve mesélünk, előtte viszont húsz percig nem nevetünk.

A kihagyás nem felmentés

A 125 nem varázspor. Ha a kihagyott commit közvetlenül a jó és a rossz határán áll, a Git nem tudja bizonyítani, hogy ő vagy a következő commit hozta a hibát. Ilyenkor több lehetséges első rossz commitot sorol fel. A Git felhasználói kézikönyve ezért a git bisect visualize lehetőségét is ajánlja: választhatunk egy közeli, tesztelhető commitot a vak kihagyás helyett.

Három feltételt érdemes komolyan venni:

  • A teszt legyen determinisztikus. Egy flakey E2E-teszt véletlent felez, nem történetet.
  • A vizsgált állapot legyen nagyjából monoton. Ha a hiba eltűnt, majd később visszajött, a bisect találhat egy határt, de nem garantálja a legelső valaha rossz commitot.
  • A történelmi környezet legyen reprodukálható. Régi lockfile, eltűnt csomag, megváltozott adatbázisséma vagy külső API könnyen gyárt hamis 125-öket.

Szűkíteni lehet, de csak őszintén

Nagy monorepóban megadhatunk pathspecet, így csak az adott útvonalakat érintő commitok maradnak jelöltek:

git bisect start HEAD v2.4.0 -- \
  src/payments package.json package-lock.json

Ez gyors, amíg biztosak vagyunk benne, hogy egy közös logger, buildkonfiguráció vagy infrastruktúrafájl nem okozhatta a hibát. A --first-parent másképp szűkít: merge esetén csak az első szülő láncát követi. Release-ágon jól megmondja, melyik merge hozta be a regressziót, de a feature branch konkrét commitját szándékosan nem keresi meg.

Használnám-e production hibánál?

Igen, ha a hibából tudok olcsó, determinisztikus tesztet készíteni, és van ismert jó határpontom. Ilyenkor a git bisect run az egyik leghasznosabb unalmas robot: éjszaka végigkattintja helyettem a történetet, és reggelre hoz egy commitot.

Nem használnám önmagában időzítésfüggő teljesítményhibára, külső szolgáltatástól függő reprodukcióra vagy olyan migrációs hibára, amelyhez minden commitnál más adatbázisállapot kell. Ott előbb hermetikus tesztkörnyezetet építenék. A bisect ugyanis nem érti a programot; csak vakon megbízik a kilépési kódunkban. Legalább most már tudjuk, hogyan mondjuk neki azt is, hogy „passz”.

Források

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük