Ebben a leckében a programozással kapcsolatos, az eddigi lépéseknél kevésbé kreatív, ámde nem nélkülözhető gyakorlati ismeretek közül a hibakereséssel ismerkedünk meg.
Az előző fejezetekben a hibafelderítéssel foglalkoztunk, ennek a fejezetnek a tárgya a hiba behatárolása, a hiba kijavítása.
Első lépésként tekintsük át, hogy milyen hibajelenségekkel találkozhatunk, és észlelésükkor mi a teendő!
Csak értelmezés (interpretálás) esetén fordul elő, hiszen a fordítóprogramok korábban még a fordítási fázisban kijelzik, s nem is készül el a futtatható változat.
Ennél a hibánál nincs különösebb teendőnk, hiszen a hibajelenség a hibás utasításnál (vagy annak környékén) jelentkezik, csak ki kell javítanunk.
Futás során a fordítóprogram által beépített hibafigyelő eljárás által adott hiba, például nullával osztás, túlcsordulás stb. Az ilyen hibajelenségek közös jellemzője, hogy a leállás sorában szerepel a – rossz értéke miatt – hibát okozó változó, és meg kell keresni, hogy értéke hol romlott el.
Abból indultunk ki, hogy olyan programfejlesztői környezetben keressük a hibát, amelyben kiderül, hogy a forrásprogram mely utasítássoránál következett be a hiba. (Ellenkező esetben csak egy nem túl sokat mondó memóriacím és egy hibakód kettőse áll rendelkezésünkre.) Ez a feltevésünk természetesen igaz a továbbiakban is.
Végtelen ciklusra utaló jelenség, a program láthatólag semmit nem csinál, mégsem fejeződik be. Rosszabb, ha valamit eközben tesz is (képernyőre ír, fájlba ír stb.)
Ekkor a program külső leállításakor általában meg lehet tudni az aktuálisan végrehajtott utasítást. Innen kiindulva meg kell keresni azt a ciklust, amelyik nem fejeződik be, majd meg kell találnunk e nem befejeződés okát (azaz a ciklusfeltétel változatlanságának okát).
Sajnos nem biztos, hogy a jelenség hiba következménye, lehet ugyanis, hogy csak túl lassú a programunk, és nem győztük kivárni a befejeződését. (Erre a hatékonysággal összefüggő kérdésre egy későbbi fejezetben még visszatérünk.)
A program futása leáll, és nem látunk semmilyen eredményt, vagy a várt eredmények közül csak egyeseket látunk.
Ebben az esetben vagy elfelejtettünk valamilyen kiíró utasítást tenni a programba, vagy rossz helyre írjuk ki az eredményt (pl. nem abba a fájlba, mint ahova kellene), vagy pedig a kiírások ugyan szerepelnek a programban, de egy olyan ágon, amelyet nem mindig vagy nem a kellő pillanatban hajtunk végre.
Bármelyik esetről is van szó, teendőnk a kiíró utasítások végigbogarászása, kijavítása vagy jó helyre tétele.
Hasonló a helyzet a futási hibák esetéhez, itt is ugyanaz a teendő, mint amit ott már részleteztünk.
A hibakeresés akkor kezdődik, ha egy hibajelenséget tapasztaltunk, és célja a hiba helyének megtalálása. Először módszereket fogunk áttekinteni, majd megnézzük, hogy ehhez milyen eszközök állnak rendelkezésünkre.
Ha a hibakeresés véget ért, akkor kezdődik a hibajavítás.
E két résztevékenységre vonatkozóan a tesztelés alapelveihez hasonlóan megfogalmazhatunk néhány alapelvet.
A hibakeresési módszerek kétféle kérdésre adhatnak választ:
Ehhez jön a hibajavítás:
Először a hibaosztályt azonosítjuk, azaz arra a kérdésre keressük a választ, hogy milyen adatfélékre működik helytelenül a programunk.
Indukció [Új Magyar Lexikon]: Abból a tényből, hogy nagyszámú tárgynak meghatározott tulajdonsága van, és közös nemhez tartozik, arra következtetünk, hogy az adott nemhez tartozó összes tárgynak megvan ez az ismertetőjegye.
A hibakeresést a következőképpen végezzük: kiindulunk a rendelkezésre álló teszteset-eredményekből, majd megpróbáljuk azokat rendezni. Célszerű megvizsgálni az olyan teszteseteket is , amelyek nem idézik elő az adott hibát. A rendezett adatokból megpróbálunk valamilyen feltevést tenni a hiba okára vonatkozóan.
A legelső feltevés, hogy a program csak azokra az esetekre hibás, amelyeket már kipróbáltunk, és rájuk rossz eredményt kaptunk. Ha ezt a feltevést igazolni tudjuk, akkor következhet a hiba lokalizálása, majd kijavítása, egyébként a hibás bemenő adatok körét próbáljuk meg – fokozatosan – bővíteni újabb teszteléssel! Igazoljuk az egyre bővülő adathalmazra a program hibás működését mindaddig, amíg csak lehet.
Végül eljutunk a bemenő adatoknak ahhoz a halmazához, amelyre a program hibás, és egy másikhoz, amelyre pedig helyes. Ezután meg kell állapítanunk, hogy a programon átvezető tesztutak mely ágai azok, amelyek a hibás tesztpredikátumnak megfelelnek, és melyek azok, amelyek nem. Azokon a programágakon kell keresni a hibát, amelyek csak a hibás teszt-predikátumhoz tartoznak.
X=0, 1, 5, 100 – a program jól működik
X=-1, -7, -50 – rosszul működik
→ feltevés: a program a negatív számokra működik hibásan.
Dedukció [Új Magyar Lexikon]: Abból az ítéletből, hogy az adott nemhez tartozó összes tárgy meghatározott ismertetőjeggyel rendelkezik, arra következtetünk, hogy bizonyos, az adott nemhez tartozó tárgyak szintén rendelkeznek a szóban forgó ismertetőjeggyel.
A módszer lényege az, hogy egyre szűkíti a hiba lehetséges okainak körét. A meglévő teszteset-eredményekből adódó mindenféle lehetséges okot fel kell tételezni az első lépésben, majd ezek közül ki kell küszöbölni azokat, amelyek a részletesebb vizsgálat során nem állják meg a helyüket. Ha egy feltevést teszünk, ugyanúgy igazolnunk kell, mint az előző módszer esetén. Ha nem sikerül, akkor újabb információkat kell gyűjtenünk a hibakereséshez a hiba-jelenségről.
Ha elérkeztünk a hibás adatok köréhez, akkor már ugyanaz a teendőnk, mint az előbbi esetben volt.
feltevés: a program mindig rosszul működik
teszt: X=3, 5, 7 – rossz eredmény
X=12, 20 – jó eredmény
→ feltevések:
• X= prímszám → rossz
• X= páratlan → rossz
• X<10 → rossz
• 4 nem osztója X-nek → rossz
Ha egy feltevés igaz, akkor az általa kijelölt halmaz (ekvivalenciaosztály) minden egyes tagjára rosszul fog működni a program.
Ha a hiba természetét kiderítettük, akkor következhet a lokalizálás, a hibahelykeresés.
A legismertebb hibakeresési módszer, amelyet úgy végzünk el, hogy kiindulunk a hiba előfordulásának helyéről, és a programot visszafelé hajtjuk végre mindaddig, míg a végrehajtás eredményét hibásnak találjuk. Ha elérkeztünk egy olyan ponthoz a programban, ahol a hibás eredmények után helyes eredményeket kapunk, akkor valószínűleg megtaláltuk a hiba forrását.
A visszalépéses technikát sokszor segítik hibakeresési eszközök, melyek segítségével a visszalépést nem kézzel kell elvégezni, hanem a program futása során automatikusan megtörténik.
A teszteseteket megkülönböztethetjük aszerint, hogy hibát akarunk felfedezni, vagy pedig egy ismert hibát akarunk előidézni a programban (a hiba okát keresve). Az utóbbi típusú tesztesetek szolgálnak hibakeresésre (emlékeztetünk itt a tesztesetek megismételhetőségére).
Ezeknek a teszteseteknek jellegzetessége, hogy csak egyetlen feltételt fednek le. Az első két módszer alapján még esetleg több programágon is lehet a hiba; teszteseteket úgy kell választani, hogy mindegyik tesztesethez más-más programág tartozzon!
Ezt a módszert általában nem önállóan, hanem az előző három módszer segítésére használjuk.
A hibakeresési eszközök a programozási környezet olyan elemei, amelyek a hiba okának megállapítását, a hiba helyének megkeresését teszik könnyebbé azzal, hogy futás közbeni információt szolgáltatnak a programról.
Egyes eszközök alkalmazásához a programszövegbe tesztelő utasításokat kell elhelyezni, az eszközök alkalmazása után pedig eltávolítani. A gyakori hibakeresésnél azonban sokszor éppen azt kell visszaírni, amit előzőleg eltávolítottunk. Emiatt a jó fordítóprogramok működnek ún. tesztelő üzemmódban, amelyben a tesztelő utasításokat is bele kell fordítani a célprogramba, normál üzemmódban azonban nem. Ilyen esetben a tesztelő utasításokat nem kell kitörölni a programszövegből.
Az animáció bemutatja a teszteset készítés alapjait és ennek a hibakeresésben játszott szerepét:
A leggyakrabban használt eszköz alkalmas adatkiírások elhelyezése. Kétféle fajtája lehet.
Az egyikben a programszöveg bizonyos helyeire helyezünk el tesztkiírásokat. Ha a futás során arra a pontra érünk, akkor a benne szereplő változókat a program kiírja, majd várakozik a továbbindításra.
A másikban a kiírandó változókat rögzítjük, és értékük a futás során mindig látszik/megnézhető a képen. Ez utóbbi gyakran más eszközökkel (pl. töréspont) kombináltan jelenik meg.
Egy primitívebb fajtája az assembly programoknál használatos regiszter, illetve memóriakiírás.
A nyomkövetés lényege a végrehajtott utasítások követése a programban. Itt tehát futás során az eredményképernyő mellett a programszöveget is látnunk kell. A programszövegből vagy az éppen végrehajtott utasítást látjuk, vagy a teljes programszövegben mutatja egy mutató az aktuális utasítást, vagy pedig algoritmikus struktúrák végrehajtását figyelhetjük meg.
A nyomkövetés kiterjedhet a program egészére, de megjelölhetünk programrészeket, és ilyenkor csak ezek végrehajtását kell követni. A programrész lehet a teljes program; egy adott eljárás és mindazok, akiket hív; vagy egy adott eljárás az általa hívott eljárások nélkül. Egyes esetekben pedig mi helyezhetünk el a programszövegben tetszőleges helyre nyomkövetést bekapcsoló, illetve kikapcsoló utasításokat.
A nyomkövetés általában sokféle információt adhat a program futásáról: a végrehajtott utasítás mellett kiírhatjuk a képernyőre annak hatását (értékadásnál a változóba elhelyezett értéket, elágazás- vagy ciklusfeltétel kiértékelésénél annak igaz vagy hamis értékét).
Egy szűkebb információt adó változatban csak bizonyos típusú utasításokat nyomkövetünk, például az eljárás- és függvényhívásokat.
A nyomkövetés egy speciális változatában nem az utasításokat vizsgáljuk, hanem a változókat. Ebben az esetben akkor kapunk a képernyőn üzenetet, ha a kijelölt változó(ka)t valaki használja, illetve módosítja.
Az adatok nyomkövetését könnyen megvalósíthatjuk az általunk készített típusok moduljaiban: csupán annyi a teendőnk, hogy a típust kezelő eljárásokat ellássuk megfelelő kiíró utasításokkal.
Ez egy olyan nyomkövetési eljárás, amely akkor lép életbe, ha a program futása hibával megszakadt (valamint ha a lépésenkénti végrehajtás során mi magunk kezdeményezzük ezt). Ekkor – a végrehajtásról gyűjtött adatok alapján – elindulunk a programban visszafelé, és látjuk mindazt, amit a normális nyomkövetésnél láttunk.
A töréspontok a program olyan utasításai, amelyeknél a végrehajtásnak meg kell állnia, a felhasználó információt szerezhet a program állapotáról, majd folytatódhat a végrehajtás.
Egy speciális fajtája az a – végtelen ciklusok felfedésére szolgáló – változat, amikor a töréspontnak kijelölt utasításnak csak adott darabszámszori végrehajtása után kell a futást felfüggeszteni.
Leálláskor a felhasználó dönthet a futtatás abbahagyásáról, illetve folytatásáról. Megnézheti változók értékeit, nyomkövetést be- és kikapcsolhat, töréspontokat megszüntethet, illetve újakat definiálhat stb. Sőt némely környezet azt a – nem veszélytelen – lehetőséget is biztosítja, hogy egy-egy változó értékét módosítsuk, és újabb elindítás nélkül lokalizálhassunk esetleges más hibákat is. (Veszélyes, mert elvonhatja a figyelmünket az épp megtalált hibáról, illetve ha nem minden szükséges változót állítottunk megfelelő értékűre, akkor álhibák keletkezhetnek, vagy meglévő hibák kendőződhetnek el.)
A töréspontok a program olyan utasításai, amelyeknél a végrehajtásnak meg kell állnia, a felhasználó információt szerezhet a program állapotáról, majd folytatódhat a végrehajtás.
Egy speciális fajtája az a – végtelen ciklusok felfedésére szolgáló – változat, amikor a töréspontnak kijelölt utasításnak csak adott darabszámszori végrehajtása után kell a futást felfüggeszteni.
Leálláskor a felhasználó dönthet a futtatás abbahagyásáról, illetve folytatásáról. Megnézheti változók értékeit, nyomkövetést be- és kikapcsolhat, töréspontokat megszüntethet, illetve újakat definiálhat stb. Sőt némely környezet azt a – nem veszélytelen – lehetőséget is biztosítja, hogy egy-egy változó értékét módosítsuk, és újabb elindítás nélkül lokalizálhassunk esetleges más hibákat is. (Veszélyes, mert elvonhatja a figyelmünket az épp megtalált hibáról, illetve ha nem minden szükséges változót állítottunk megfelelő értékűre, akkor álhibák keletkezhetnek, vagy meglévő hibák kendőződhetnek el.)
Ez tulajdonképpen olyan eszköz, amely a program minden utasítására egy töréspontot definiál. A program minden utasításának végrehajtása után lehetőség van a töréspontoknál ismertetett beavatkozásokra.
Fordítóprogramoknak általában vannak olyan lehetőségei, hogy a futás közbeni ellenőrzéseket beépítsék a program kódjába, illetve kihagyják belőle. A kihagyás a már biztosan helyes programnál futási időt csökkentő tényező lehet, emiatt érdemes a kész programokat így lefordítani.
A befordított ellenőrzések (pl. túlcsordulás, indextúllépés) viszont elősegítik a hibajelenségek minél korábbi felismerését.
Itt kell megjegyeznünk, hogy az ilyen hibaüzenetek nem a program majdani felhasználójának szólnak, hanem a programfejlesztőnek. Programfelhasználó a nyelvi környezet hibajelzéseivel nem találkozhat! Arról is tudnunk kell, hogy minden hibafigyelő mechanizmus lassítja a programot, növeli annak méretét, tehát hatékonyság szempontjából kedvezőtlen.
Ha a programot valamely formális tervezési eszközzel készítettük (levezetés, helyességbizonyítás), akkor rendelkezésünkre állnak a program állapotára vonatkozó állítások. Ilyen állításokat egyébként tetszőleges programhoz is készíthetünk. Az automatikus ellenőrző rendszer feladata annak ellenőrzése, hogy a program megfelelő pontjain ezek az állítások valóban teljesülnek-e vagy sem.
Ez az eszköz hasonlít az előzőre, de nemcsak az ún. fatális hibákat jelzi, hanem a futás közbeni teljes állapotot ellenőrzi.
Az állapotellenőrzés alkalmas lehet feltételes töréspontok, feltételes kiírásának elhelyezésére a programban.
A hibakeresést nagyban megkönnyíti az, ha tudjuk, hogy mire figyeljünk. Szép számmal vannak olyan hibák, amelyeket sokan, sokszor követnek el. A hibakeresés első lépése lehet e tipikus hibák megvizsgálása. Ebben a fejezetben ezeket soroljuk fel vázlatszerűen.