Poglavlje 7: Tok podataka, statička analiza i mutacije

Priča za početak. Dok koristite internet, vaš računar i server stalno provjeravaju da li je onaj drugi još „živ”. U biblioteci OpenSSL, koja je 2014. čuvala veliki dio sigurnih (HTTPS) veza na svijetu, ta provjera se zvala heartbeat, „otkucaj srca”. Klijent pošalje kratku poruku, recimo riječ „PTICA”, i kaže: „Ako si tu, vrati mi ovu riječ; ima 5 slova.” Server vrati „PTICA”. Ali šta ako klijent slaže i kaže: „Vrati mi PTICA, ima 500 slova”? Server je vjerovao broju koji je dobio i nije provjeravao da li je riječ zaista toliko duga. Zato je vratio 5 slova riječi i još 495 bajtova onoga što je slučajno stajalo pored nje u memoriji. A tamo su mogle biti lozinke drugih korisnika, brojevi kartica, pa i tajni ključ samog servera. Napadač je mogao ponavljati zahtjev i svaki put dobiti do 64 KB memorije, bez ikakvog traga u zapisima servera. Grešku su nezavisno otkrili Neel Mehta iz Googlea i inženjeri finske firme Codenomicon, a objavljena je 7. aprila 2014. pod oznakom CVE-2014-0160 i imenom Heartbleed, „srce koje krvari”. Procjene su govorile da je u tom trenutku ranjivo bilo oko 17 % sigurnih web servera na internetu, oko pola miliona. Cijela greška je bila jedna provjera koja nedostaje: broj koji je došao spolja korišten je bez provjere. Kasnije se pokazalo i nešto važno za testere: poznati komercijalni alat za statičku analizu Coverity nije prijavio grešku, sve dok njegovi autori nisu dodali novo pravilo upravo za ovakve slučajeve. Ovo poglavlje govori o tri načina da se uhvate takve greške: praćenjem toka podataka, statičkom analizom i testiranjem mutacijama.

Slika 7.1 – Heartbleed: server je vjerovao dužini koju je poslao klijent

U poglavlju 6 gledali smo kod kao mapu puteva: graf toka kontrole, grane, pokrivenost. Ostala su dva pitanja. Prvo: šta se dešava s podacima koji putuju tim putevima? Promjenljiva može biti upotrijebljena prije nego što je dobila vrijednost, ili dobiti vrijednost koju niko nikad ne pročita. Drugo: kako znamo da su testovi dobri? Vidjeli smo da greška D-01 preživi 100 % pokrivenosti. Ovo poglavlje odgovara na oba pitanja. Prvo učimo tok podataka i njegove anomalije. Zatim statičku analizu: alate ruff i mypy koji čitaju kod bez pokretanja. Na kraju testiranje mutacijama: namjerno kvarimo kod da bismo vidjeli da li ga testovi čuvaju.

Literatura. Osnovna literatura predmeta:

  1. J. Popović: Priručnik za test inženjere, CET i Računarski fakultet, Beograd, 2023.
  2. Rakić-Skoković: Priručnik za testiranje softvera, Fakultet tehničkih nauka, Novi Sad, 2013.
  3. G. J. Myers: The Art of Software Testing, 2. izdanje, John Wiley & Sons, 2004.
  4. A. P. Mathur: Foundations of Software Testing, Addison-Wesley Professional, 2008.

Za ovo poglavlje: P. Ammann, J. Offutt: Introduction to Software Testing, 2nd ed., Cambridge University Press, 2016 (tok podataka i mutacije); dokumentacija alata ruff (docs.astral.sh/ruff) i mypy (mypy.readthedocs.io); heartbleed.com.

Riječi ovog poglavlja.

  • Tok podataka (data flow) – put vrijednosti kroz program: gdje je promjenljiva dobila vrijednost i gdje je ta vrijednost pročitana.
  • Definicija (d) – mjesto gdje promjenljiva dobija vrijednost, npr. dostava = 300.
  • Upotreba (u) – mjesto gdje se vrijednost promjenljive čita: u računu (ukupno = iznos + dostava) ili u odluci (if dostava > 0).
  • Par definicija–upotreba (def-use pair) – definicija i upotreba iste promjenljive između kojih postoji put bez nove definicije.
  • Anomalija toka podataka – sumnjiv redoslijed definicija i upotreba: upotreba bez definicije, dvije definicije zaredom, definicija bez upotrebe.
  • Statička analiza – provjera koda bez njegovog pokretanja, alatom koji „čita” kod.
  • Linter – alat za statičku analizu koji traži sumnjiva mjesta i loš stil (u Pythonu ruff, flake8, pylint).
  • Provjera tipova – statička analiza koja provjerava da li se vrijednosti koriste u skladu sa svojim tipom (u Pythonu mypy).
  • Lažna uzbuna (false positive) – alat prijavi problem kojeg nema; propust (false negative) – problem postoji, a alat ćuti.
  • Mutant – kopija programa s jednom malom namjernom izmjenom, npr. >= umjesto >.
  • Ubijen mutant – mutant zbog kojeg bar jedan test padne; preživjeli mutant – svi testovi i dalje prolaze.
  • Rezultat mutacija (mutation score) – procenat ubijenih mutanata; mjera snage testova.
  • Ekvivalentan mutant – izmjena koja ne mijenja ponašanje programa, pa je nijedan test ne može ubiti.

Ciljevi poglavlja. Nakon poglavlja možete: za svaki red funkcije označiti definicije i upotrebe promjenljivih; prepoznati tri anomalije toka podataka i objasniti zašto su opasne; nabrojati parove definicija–upotreba i napraviti testove koji ih pokrivaju; objasniti šta je statička analiza i šta može, a šta ne može naći; pokrenuti ruff i mypy i pročitati njihove poruke; razlikovati lažnu uzbunu i propust; objasniti ideju testiranja mutacijama; izračunati rezultat mutacija; za preživjelog mutanta napisati test koji ga ubija; objasniti zašto su mnogi preživjeli mutanti na granicama.

Plan poglavlja – Sedmica 7: tri časa teorije i tri časa praktične nastave

Čas Sadržaj Na času
Teorija 1 7.1–7.3 Definicija i upotreba; anomalije; kriteriji toka podataka Šta nije u redu s funkcijom racun?
Teorija 2 7.4–7.6 Statička analiza; ruff; mypy i tipovi Koji alat hvata koju anomaliju
Teorija 3 7.7–7.9 Mutacije: ideja, rezultat, ekvivalentni mutanti 100 % pokrivenosti, 58 % mutacija
Praksa 1–3 Popravka funkcije racun; ruff i mypy nad DeliveryApp; lov na preživjele mutante primjeri/tok_podataka/, primjeri/mutacije/

Dio 1 – Tok podataka

7.1 Život jedne promjenljive

Svaka promjenljiva ima mali „život”. Rodi se kad dobije vrijednost (definicija), koristi se kad se ta vrijednost pročita (upotreba), a nestaje kad funkcija završi. Slika iz života: paket na pošti. Neko ga pošalje (definicija), neko ga preuzme (upotreba). Problem je ako neko dođe po paket koji niko nije poslao, ako se pošalju dva paketa zaredom pa prvi završi u smeću, ili ako paket stoji na pošti i niko ga nikad ne preuzme.

U kodu to izgleda ovako:

dostava = 200 + 50 * dodatni_km     # d: dostava je definisana
if iznos >= 3000:                   # u: iznos je upotrijebljen u odluci
    dostava = 0                     # d: dostava je ponovo definisana
ukupno = iznos - p + dostava        # u: iznos, p i dostava upotrijebljeni u računu; d: ukupno
return ukupno                       # u: ukupno

Upotrebe se ponekad dijele na dvije vrste:

  • računska upotreba (c-use, od computation): vrijednost ulazi u račun, ukupno = iznos + dostava;
  • upotreba u odluci (p-use, od predicate): vrijednost odlučuje kojim putem ide program, if iznos >= 3000.

Zašto se testera tiče gdje je promjenljiva rođena i gdje je pročitana? Zato što je mnogo grešaka upravo u tom putovanju: vrijednost nije stigla tamo gdje treba, stigla je pogrešna vrijednost (prepisana u međuvremenu), ili je izračunata pa zaboravljena. Heartbleed je primjer prve vrste: broj „500” je stigao spolja i otišao pravo u kopiranje memorije, bez provjere usput.

Ukratko.

  • Definicija je mjesto gdje promjenljiva dobija vrijednost, a upotreba mjesto gdje se vrijednost čita.
  • Upotreba može biti računska (u izrazu) ili u odluci (u uslovu if ili while).
  • Mnogo grešaka nastaje na putu vrijednosti od definicije do upotrebe.
  • Heartbleed je upotrijebio vrijednost koja je stigla spolja bez provjere na tom putu.

7.2 Tri anomalije

Postoje tri sumnjiva redoslijeda, i svaki ima kratku oznaku od dva slova:

Anomalija Šta znači Zašto je opasna
u bez d (ur, upotreba prije definicije) vrijednost se čita, a nikad nije dodijeljena u Pythonu greška pri izvršavanju; u C-u slučajna vrijednost iz memorije
dd (dvije definicije zaredom) vrijednost se dodijeli, pa se prepiše prije nego što je iko pročita prva vrijednost je uzalud izračunata, a često to znači da je zaboravljen uslov
d bez u (du, definicija bez upotrebe) vrijednost se izračuna i nikad ne pročita zaboravljen dio računa: nešto je trebalo ući u rezultat, a nije

Pogledajmo funkciju iz primjeri/tok_podataka/racun.py. Kolega je trebao napisati račun s popustom i napojnicom, ali je pisao u žurbi:

import math
import os


def racun(iznos: int, km: float, kod: str = "", procenat: int = 0) -> int:
    """Ukupno za naplatu u centima: artikli − popust + dostava + napojnica."""
    if kod:
        p = min(iznos // 10, 500)
    dostava = 200 + 50 * math.ceil(max(0, km - 3))
    dostava = 0
    napojnica = iznos * procenat // 100
    ukupno = iznos - p + dostava
    return ukupno

Označimo definicije i upotrebe red po red (slika 7.2):

Slika 7.2 – Definicije i upotrebe u funkciji racun: tri anomalije

Tri anomalije, tri greške:

  1. u bez d za p. Ako kupac nema kod, red p = … se preskoči, a p se ipak koristi u računu. Python javlja grešku pri izvršavanju.
  2. dd za dostava. Prva vrijednost (cijena po kilometrima) odmah je prepisana nulom. Programer je vjerovatno htio „ako je iznos 30,00 € ili više, dostava je 0”, ali je zaboravio if. Rezultat: svi dobijaju besplatnu dostavu.
  3. d bez u za napojnicu. Napojnica je izračunata i nikad dodata u ukupno. Dostavljač ostaje bez napojnice.

Usput, i import os je definicija bez upotrebe: modul je uvezen, a nikad korišten. To nije opasno, ali je nered.

Šta kažu testovi iz primjeri/tok_podataka/test_racun.py? Svaki ima očekivani rezultat iz pravila:

Test Ulaz Očekivano Pravilo
test_n1001_bez_koda 21,50 €, 4,2 km 24,50 € PP-2
test_n1001_student10 21,50 €, 4,2 km, STUDENT10 22,35 € PP-2, PP-5
test_n1001_napojnica_10_posto 21,50 €, 4,2 km, 10 % 26,70 € (napojnica 2,20 €) PP-2, PP-8
test_n1002_besplatna_dostava 32,00 €, 2,5 km 32,00 € PP-4
pytest -q test_racun.py
FAILED test_racun.py::test_n1001_bez_koda - UnboundLocalError: cannot access ...
FAILED test_racun.py::test_n1001_student10 - AssertionError: assert 1935 == 2235
FAILED test_racun.py::test_n1001_napojnica_10_posto - UnboundLocalError: cann...
FAILED test_racun.py::test_n1002_besplatna_dostava - UnboundLocalError: canno...
4 failed in 0.04s

Sva četiri padaju. Puna poruka za p glasi: UnboundLocalError: cannot access local variable 'p' where it is not associated with a value, doslovno „ne mogu pročitati lokalnu promjenljivu p, jer nema vrijednost”. A jedini test s kodom pokazuje 19,35 € umjesto 22,35 €: nedostaju tačno 3,00 € dostave koje je prepisala anomalija dd.

Na času. Ako bismo popravili samo anomaliju za p (dodali p = 0 na početak), koji testovi bi i dalje padali i zašto? – Svi osim N-1002: dostava je i dalje uvijek 0 (dd), pa N-1001 daje 21,50 € umjesto 24,50 €, a napojnica se i dalje ne dodaje (d bez u). N-1002 bi slučajno prošao, jer je za 32,00 € dostava stvarno 0.

Ukratko.

  • Tri anomalije toka podataka su upotreba bez definicije, dvije definicije zaredom i definicija bez upotrebe.
  • Upotreba bez definicije u Pythonu daje UnboundLocalError, a u jeziku C slučajnu vrijednost iz memorije.
  • Dvije definicije zaredom često znače zaboravljen uslov, kao dostava = 0 bez if.
  • Definicija bez upotrebe često znači zaboravljen dio računa, kao napojnica koja nije dodata.

7.3 Kriteriji toka podataka

Kao što pokrivenost grana traži da svaka grana bude izvršena, kriteriji toka podataka traže da svako putovanje vrijednosti bude provjereno. Pogledajmo ispravljenu funkciju iz racun_ispravljen.py:

def racun(iznos: int, km: float, kod: str = "", procenat: int = 0) -> int:
    """Ukupno za naplatu u centima: artikli − popust + dostava + napojnica."""
    p = 0                                               # d1(p)
    if kod.strip().upper() == "STUDENT10":
        p = min(round(iznos * 10 / 100), 500)           # d2(p)
    dostava = 200 + 50 * math.ceil(max(0, km - 3))      # d1(dostava)
    if iznos >= 3000:
        dostava = 0                                     # d2(dostava)
    centi = -(-iznos * procenat // 100)
    napojnica = min(-(-centi // 10) * 10, 1000)
    return iznos - p + dostava + napojnica              # u(p), u(dostava), u(napojnica)

Sada nema anomalija: svaka promjenljiva je definisana prije upotrebe, i svaka definicija se negdje čita. Ali promjenljive p i dostava imaju po dvije definicije koje stižu do iste upotrebe u return. To daje parove definicija–upotreba:

Par Kada vrijednost stiže do return Test koji ga pokriva
d1(p) → u(p) bez važećeg koda: popust 0 test_n1001_bez_koda
d2(p) → u(p) s kodom STUDENT10 test_n1001_student10
d1(dostava) → u(dostava) iznos ispod 30,00 € test_n1001_bez_koda
d2(dostava) → u(dostava) iznos 30,00 € ili više test_n1002_besplatna_dostava
d(napojnica) → u(napojnica) uvijek; zanimljivo s procentom većim od 0 test_n1001_napojnica_10_posto

Tri najpoznatija kriterija, od slabijeg ka jačem:

  1. Sve definicije (all-defs): svaka definicija mora stići do bar jedne svoje upotrebe u nekom testu.
  2. Sve upotrebe (all-uses): svaki par definicija–upotreba mora biti izvršen bar jednom. Ovo je kriterij iz tabele iznad.
  3. Svi d–u putevi (all-du-paths): svaki put između definicije i upotrebe (kad ih ima više, npr. kroz petlju).

Naša četiri testa ispunjavaju kriterij sve upotrebe. Pokrenuti nad ispravljenom verzijom (RACUN=ispravljen, u PowerShellu $env:RACUN="ispravljen"), svi prolaze:

pytest -q test_racun.py
....                                                                     [100%]
4 passed in 0.01s

Po čemu se ovo razlikuje od pokrivenosti grana? Pokrivenost grana pita „da li je svaki put prođen”, a tok podataka pita „da li je svaka vrijednost stigla tamo gdje se koristi i da li je tamo provjerena”. Kod koji ima malo odluka, a mnogo računa (kao racun), ima malo grana, ali mnogo putovanja vrijednosti, i tu je tok podataka korisniji.

Za radoznale. Analiza toka podataka ne služi samo testerima. Kompajleri je koriste stalno: da otkriju promjenljive koje se ne koriste, da preskoče račun čiji se rezultat nikad ne čita, i da upozore na promjenljivu koja „možda nije inicijalizovana”. Upravo to rade i alati iz Dijela 2.

Ukratko.

  • Par definicija–upotreba je definicija i upotreba iste promjenljive između kojih nema nove definicije.
  • Kriterij sve definicije traži da svaka definicija stigne do bar jedne upotrebe, a kriterij sve upotrebe traži svaki par.
  • Četiri testa funkcije racun pokrivaju svih pet parova i prolaze nad ispravljenom verzijom.
  • Tok podataka je posebno koristan za kod s mnogo računa i malo odluka.

Provjeri se.

  1. Koja anomalija je x = 5 odmah iza x = 3, bez čitanja x između? – Dvije definicije zaredom (dd): prva vrijednost se nikad ne pročita.
  2. Šta znači poruka UnboundLocalError: cannot access local variable 'p'? – Promjenljiva p je upotrijebljena prije nego što je dobila vrijednost (u bez d).
  3. Koliko parova definicija–upotreba ima promjenljiva s dvije definicije i jednom upotrebom, ako obje definicije mogu stići do upotrebe? – Dva.

Dio 2 – Statička analiza

7.4 Čitanje bez pokretanja

Testovi iz Dijela 1 su našli sve tri anomalije, ali tek kad smo ih pokrenuli. Postoji i brži put: alat koji čita kod i javlja sumnjiva mjesta, bez pokretanja. To je statička analiza, ista ideja kao statičko testiranje iz poglavlja 2 (recenzija), samo što čita računar.

Slika iz života: provjera pravopisa u Wordu. Ona ne zna da li je vaš seminarski rad pametan, ali za sekundu podvuče svaku riječ koja ne postoji. Isto tako linter ne zna da li je račun tačan po pravilu PP-2, ali odmah vidi promjenljivu koja se nikad ne koristi.

Šta statička analiza može:

  • naći anomalije toka podataka (neke, ne sve);
  • naći pogrešne tipove (tekst gdje se očekuje broj);
  • izmjeriti složenost (V(G) iz poglavlja 6);
  • naći poznate opasne obrasce (npr. lozinku upisanu u kod, SQL upit sastavljen spajanjem teksta);
  • natjerati cijeli tim da piše u istom stilu.

Šta ne može:

  • znati šta je tačan rezultat: ne zna pravila DeliveryApp;
  • sigurno odgovoriti na svako pitanje o programu (matematika kaže da je to u opštem slučaju nemoguće), pa uvijek ima lažnih uzbuna i propusta.

Zato statička analiza ne zamjenjuje testove, nego ih dopunjuje. Najveća prednost joj je cijena: radi za sekundu, na svakom fajlu, prije nego što je iko napisao ijedan test.

Iz stvarnog svijeta. Nakon Heartbleeda firma Coverity, koja pravi poznat alat za statičku analizu, provjerila je da li bi njen alat našao grešku. Odgovor je bio „ne”, čak ni uz razna podešavanja. Tok podataka od mrežne poruke do kopiranja memorije bio je presložen za alat. Inženjeri su zatim dodali novo pravilo: vrijednost koja prolazi kroz zamjenu redoslijeda bajtova (što je tipično za podatke koji stižu s mreže) smatra se „nepouzdanom” dok se ne provjeri. S tim pravilom alat je našao Heartbleed. Pouka: statička analiza je moćna, ali nalazi ono što zna tražiti.

Ukratko.

  • Statička analiza provjerava kod bez pokretanja, kao provjera pravopisa u tekstu.
  • Može naći anomalije, pogrešne tipove, preveliku složenost i opasne obrasce.
  • Ne zna pravila poslovanja i ima lažne uzbune i propuste.
  • Ne zamjenjuje testove, nego ih dopunjuje, jer je brza i jeftina.

7.5 ruff: brzi linter

ruff je linter za Python, napisan u jeziku Rust, i zato vrlo brz: cijeli veliki projekat provjeri za manje od sekunde. Objedinjuje pravila starijih alata (pyflakes, pycodestyle, isort, mccabe i mnogih drugih). Instalacija i pokretanje:

pip install ruff
ruff check racun.py --output-format concise
racun.py:6:8: F401 [*] `os` imported but unused
racun.py:15:5: F841 Local variable `napojnica` is assigned to but never used
Found 2 errors.
[*] 1 fixable with `--fix` (1 unsafe fix available with `--fix --unsafe-fixes`).

Svaka poruka ima: fajl, red i kolonu (racun.py:15:5), šifru pravila (F841) i opis. Prevod:

  • F401: modul os je uvezen, a nije korišten (definicija bez upotrebe na nivou modula);
  • F841: lokalna promjenljiva napojnica je dobila vrijednost koja se nikad ne koristi (anomalija d bez u).

Zvjezdica [*] znači da ruff može sam ispraviti problem opcijom --fix. Ali oprez: za F841 ruff predlaže da obriše dodjelu. To bi ućutkalo upozorenje, ali greška bi ostala: napojnica i dalje ne bi ulazila u račun. Alat je pokazao simptom, a uzrok mora popraviti čovjek: napojnica treba ući u ukupno.

Šta ruff nije javio? Ni upotrebu p bez definicije, ni dostava prepisanu nulom. Za ruff je dostava = 0 sasvim legalan red. To su dva propusta.

ruff zna i izmjeriti složenost iz poglavlja 6, pravilom C901. Granica se zadaje u fajlu pyproject.toml ili na komandnoj liniji. Za vježbu postavimo je nisko, na 3:

ruff check --isolated --select C901 --config "lint.mccabe.max-complexity=3" ../pokrivenost/dostava.py --output-format concise
…\primjeri\pokrivenost\dostava.py:5:5: C901 `cijena_dostave` is too complex (4 > 3)
Found 1 error.

(Za fajl izvan trenutnog foldera ruff ispiše punu putanju.) Isti broj 4 koji smo u poglavlju 6 izračunali rukom. U pravom projektu granica je obično 10, u fajlu pyproject.toml:

[tool.ruff.lint]
select = ["E", "F", "B", "C90"]

[tool.ruff.lint.mccabe]
max-complexity = 10

(E su pravila stila, F greške kao F401 i F841, B česti „bagovi” iz alata flake8-bugbear, C90 složenost.)

Na času. Pokrenut nad cijelim folderom primjeri/deliveryapp sa podrazumijevanim pravilima (ruff check --isolated .), ruff javlja samo jedno: deliveryapp_v0.py:9:8: F401 'math' imported but unused. U ispravnoj verziji math se koristi za math.ceil (zaokruživanje kilometara naviše). Zašto bi prva verzija uvozila math, a ne koristila ga? Zapišite pretpostavku; provjerićemo je u poglavlju 8.

Ukratko.

  • ruff je brz linter za Python koji javlja sumnjiva mjesta sa šifrom pravila, npr. F401 i F841.
  • F841 je anomalija definicija bez upotrebe, a F401 neiskorišten uvoz.
  • Automatska ispravka može ućutkati upozorenje a ostaviti grešku, pa uzrok mora popraviti čovjek.
  • Pravilo C901 mjeri ciklomatsku složenost; u praksi je granica obično 10.

7.6 mypy: provjera tipova

Python ne traži da napišete tip promjenljive. Ali možete ga napisati, kao oznaku tipa (type hint): iznos: int, km: float, -> str. Python sam te oznake ne provjerava, ali alat mypy ih čita i javlja kad se ne slažu.

def eur(centi: int) -> str:
    return f"{centi // 100},{centi % 100:02d} €"


if __name__ == "__main__":
    print(eur(24.5))

Programer je zaboravio da su iznosi u centima (poglavlje 5) i poslao 24.5 umjesto 2450. Pokretanje programa pada tek kad dođe do tog reda:

python racun.py
ValueError: Unknown format code 'd' for object of type 'float'

mypy to vidi bez pokretanja:

pip install mypy
mypy --enable-error-code possibly-undefined racun.py
racun.py:16: error: Name "p" may be undefined  [possibly-undefined]
racun.py:25: error: Argument 1 to "eur" has incompatible type "float"; expected "int"  [arg-type]
Found 2 errors in 1 file (checked 1 source file)

Druga poruka je greška tipa: funkcija očekuje int, a dobija float. Prva poruka je naša anomalija u bez d: „ime p možda nije definisano”. Tu provjeru mypy radi samo kad je uključimo opcijom --enable-error-code possibly-undefined; bez nje bi javio samo grešku tipa.

Saberimo šta je ko našao u funkciji racun (slika 7.3):

Problem ruff mypy testovi
import os nekorišten da (F401) ne ne
napojnica izračunata, nekorištena (d bez u) da (F841) ne da (26,70 €)
p možda nedefinisan (u bez d) ne da (uz opciju) da (UnboundLocalError)
dostava prepisana nulom (dd) ne ne da (22,35 €)
eur(24.5) pogrešan tip ne da ne (kod nije u testovima)

Slika 7.3 – Svako sito hvata drugu vrstu greške

Nijedan alat nije našao sve. Anomaliju dd nije našao nijedan statički alat, a pogrešan tip nije našao nijedan test. Zato se u ozbiljnim projektima koriste sva sita zajedno, i to automatski, pri svakom push (poglavlje 14).

Česta zabluda. „Alat nije javio ništa, dakle kod je ispravan.” Ćutanje alata znači samo da alat nije našao ono što zna tražiti. Coverity je ćutao o Heartbleedu.

Ukratko.

  • Oznake tipa (iznos: int) Python ne provjerava, ali ih mypy provjerava bez pokretanja koda.
  • mypy je našao pogrešan tip 24.5 umjesto centi, prije nego što je program pao.
  • Uz opciju possibly-undefined mypy javlja i promjenljivu koja možda nije definisana.
  • Nijedan alat ni testovi nisu sami našli sve probleme; zajedno su našli svih pet.

Provjeri se.

  1. Šta je lažna uzbuna, a šta propust? – Lažna uzbuna: alat javi problem kojeg nema; propust: problem postoji, a alat ćuti.
  2. Šta znači ruff poruka F841? – Lokalna promjenljiva je dobila vrijednost koja se nikad ne koristi (definicija bez upotrebe).
  3. Koju anomaliju u funkciji racun nije našao ni ruff ni mypy? – Dvije definicije zaredom (dostava = 0), koju je našao tek test.

Dio 3 – Testiranje mutacijama

7.7 Ko čuva čuvare?

Testovi čuvaju kod. Ali ko čuva testove? U poglavlju 6 vidjeli smo da pet testova sa 100 % pokrivenosti ne primijeti > umjesto >=. Kako izmjeriti koliko su testovi stvarno jaki?

Ideja testiranja mutacijama je jednostavna i pomalo drska: namjerno pokvarimo kod i pogledamo da li testovi to primijete. Slika iz života: vatrogasna vježba. Ne čekamo pravi požar da vidimo radi li alarm; zapalimo malo dima i gledamo da li zazvoni. Ako ne zazvoni, alarm ne valja, iako je „uključen” (kao što je kod „pokriven”).

Postupak:

  1. Napravi se mutant: kopija koda s jednom malom izmjenom, kakvu bi programer mogao slučajno napraviti (>= u >, + u -, 200 u 250, or u and).
  2. Nad mutantom se pokrenu svi testovi.
  3. Ako bar jedan test padne, mutant je ubijen: testovi su primijetili kvar. Dobro.
  4. Ako svi testovi prođu, mutant je preživio: testovi nisu primijetili kvar. To je rupa u testovima.
  5. Ponovi se za mnogo mutanata, i izračuna se:

\[\text{rezultat mutacija} = \frac{\text{ubijeni mutanti}}{\text{svi mutanti} - \text{ekvivalentni}} \times 100\,\%\]

Izmjene se prave po operatorima mutacije, malim pravilima „šta zamijeniti čime”:

Operator Primjer u cijena_dostave Koju grešku glumi
zamjena relacionog operatora >= → >, < → <= granica pomjerena za jedan (D-01)
zamjena logičkog operatora or → and pogrešno spojeni uslovi
zamjena aritmetičkog operatora 50 * km → 50 + km pogrešna formula
zamjena konstante 200 → 250, - 3 → - 4 pogrešno prepisan broj iz pravila
brisanje poziva max(0, x) → x zaboravljen slučaj
zamjena funkcije math.ceil → round pogrešno zaokruživanje

Za radoznale. Testiranje mutacijama se oslanja na dvije pretpostavke. Prva: programeri uglavnom pišu skoro tačan kod, pa su prave greške male, kao mutanti. Druga, efekat sprezanja (coupling effect): testovi koji hvataju male greške obično hvataju i veće, složene greške. Obje su potvrđene u brojnim istraživanjima, iako ne važe uvijek.

Ukratko.

  • Testiranje mutacijama namjerno kvari kod da provjeri da li testovi primijete kvar.
  • Mutant je kopija koda s jednom malom izmjenom, napravljenom po operatoru mutacije.
  • Mutant koji obori bar jedan test je ubijen; mutant uz koji svi testovi prolaze je preživio.
  • Rezultat mutacija je procenat ubijenih mutanata i mjeri snagu testova, a ne kod.

7.8 Mutanti funkcije cijena_dostave

U folderu primjeri/mutacije je mala skripta mutanti.py. Ona uzme cijena_dostave iz primjeri/pokrivenost/dostava.py, napravi 12 mutanata (po jedan za svaki red tabele ispod), nad svakim pokrene zadate testove i ispiše rezultat. Pokrenimo je s testovima iz poglavlja 6 koji imaju 100 % pokrivenosti:

python mutanti.py ../pokrivenost/test_svi.py
M1   km < 0  ->  km <= 0                  PREŽIVIO
M2   km > 10  ->  km >= 10                PREŽIVIO
M3   km > 10  ->  km > 100                ubijen
M4   or  ->  and                          ubijen
M5   iznos < 500  ->  iznos <= 500        PREŽIVIO
M6   iznos >= 3000  ->  iznos > 3000      PREŽIVIO
M7   return 0  ->  return 1               ubijen
M8   bez max(0, …)                        PREŽIVIO
M9   km − 3  ->  km − 4                   ubijen
M10  ceil  ->  round                      ubijen
M11  200 +  ->  250 +                     ubijen
M12  50 *  ->  50 +                       ubijen
Ubijeno 7 od 12: rezultat mutacija 58 %

100 % pokrivenosti, a samo 58 % mutacija. Pet mutanata je preživjelo. Pogledajmo ih kao detektivi:

Mutant Šta mijenja Zašto je preživio Test koji ga ubija
M1 0 km se odbija nijedan test nema tačno 0 km 0 km, 10,00 € → 2,00 €
M2 10 km se odbija nijedan test nema tačno 10 km 10 km, 10,00 € → 5,50 €
M5 5,00 € se odbija nijedan test nema tačno 5,00 € 5 km, 5,00 € → 3,00 €
M6 30,00 € plaća dostavu (D-01) nijedan test nema tačno 30,00 € 5 km, 30,00 € → 0,00 €
M8 ispod 3 km dostava je jeftinija jedini test ispod 3 km ima besplatnu dostavu 1 km, 10,00 € → 2,00 €

Primijetite nešto važno: četiri od pet preživjelih su granice (0 km, 10 km, 5,00 €, 30,00 €), a peti je oblast „ispod 3 km” koja nije bila testirana s plaćenom dostavom. Testovi iz poglavlja 6 bili su napravljeni da obiđu puteve, a ne da stoje na granicama. Upravo zato je sljedeće poglavlje, tehnike crne kutije, prvo posvećeno granicama.

Dodamo li test_granica.py iz poglavlja 6 (tačno 30,00 € i 29,99 €), M6 je ubijen, a rezultat raste na 67 % (8 od 12). Dodamo li test_jaki.py s pet testova iz posljednje kolone tabele:

@pytest.mark.parametrize("km, iznos, ocekivano", [
    (0, 1000, 200),      # PP-3: 0 km je u zoni (ubija M1)
    (10, 1000, 550),     # PP-3: 10 km je u zoni (ubija M2)
    (5, 500, 300),       # PP-1: tačno 5,00 € se prihvata (ubija M5)
    (5, 3000, 0),        # PP-4: tačno 30,00 € je besplatno (ubija M6)
    (1, 1000, 200),      # PP-2: do 3 km je 2,00 € (ubija M8)
])
def test_granice(km, iznos, ocekivano):
    assert cijena_dostave(km, iznos) == ocekivano
python mutanti.py ../pokrivenost/test_svi.py test_jaki.py
...
Ubijeno 12 od 12: rezultat mutacija 100 %

Slika 7.4 – Pokrivenost je u sva tri slučaja 100 %, a snaga testova raste od 58 % do 100 %

Slika 7.4 sažima pouku poglavlja 6 i 7: pokrivenost kaže da je kod izvršen, mutacije kažu da je provjeren.

Na času. Smislite trinaesti mutant za cijena_dostave koji bi preživio sve testove iz test_svi.py i test_jaki.py. – Jedan primjer: if iznos >= 3000: → if iznos >= 3000 and udaljenost_km < 9: (besplatna dostava samo do 9 km). Svi testovi s 30,00 € ili više su na 2,5 km ili 5 km, pa mutant preživljava. Test koji ga ubija: 9,5 km i 32,00 € → 0,00 € (PP-4 ne spominje udaljenost).

Ukratko.

  • Testovi sa 100 % pokrivenosti ubili su samo 7 od 12 mutanata funkcije cijena_dostave (58 %).
  • Četiri od pet preživjelih mutanata su na granicama pravila: 0 km, 10 km, 5,00 € i 30,00 €.
  • Pet graničnih testova iz pravila podiže rezultat mutacija na 100 %.
  • Pokrivenost kaže da je kod izvršen, a rezultat mutacija da je provjeren.

7.9 Ekvivalentni mutanti i alati

Ponekad mutant ne može biti ubijen, jer izmjena ne mijenja ponašanje. Na primjer:

dodatni_km = math.ceil(max(0, udaljenost_km - 3))   # original
dodatni_km = max(0, math.ceil(udaljenost_km - 3))   # mutant

Za svaku udaljenost oba reda daju isti broj (zaokruživanje naviše i odsijecanje negativnih brojeva mogu zamijeniti mjesta). To je ekvivalentan mutant: nijedan test ga ne može ubiti, i ne treba ga ni pokušavati. Ekvivalentni mutanti se izuzimaju iz računa, ali ih je teško automatski prepoznati, pa čovjek mora pogledati svaki preživjeli mutant i odlučiti: „rupa u testovima” ili „ekvivalentan”.

Ručno pravljenje mutanata dobro je za učenje. Za pravi projekat postoje alati koji sami prave stotine mutanata:

Jezik Alat
Python mutmut, cosmic-ray
Java PIT (pitest)
JavaScript, TypeScript, C# Stryker

Alati imaju i cijenu: ako testovi traju 10 sekundi, a mutanata je 500, cijeli krug traje skoro 1,5 sat. Zato se mutacije obično ne puštaju pri svakom push, nego jednom dnevno ili sedmično, ili samo nad izmijenjenim dijelom koda. (mutmut na Windowsu radi kroz WSL, Linux unutar Windowsa; skripta mutanti.py radi svuda.)

Česta zabluda. „Treba težiti 100 % rezultatu mutacija u cijelom projektu.” Kao i kod pokrivenosti, slijepa trka za brojem je skupa. Mutacije su najkorisnije za najvažnije funkcije: novac (cijena, popust, napojnica), sigurnost i prava pristupa.

Ukratko.

  • Ekvivalentan mutant ne mijenja ponašanje programa, pa ga nijedan test ne može ubiti.
  • Svaki preživjeli mutant treba pogledati i odlučiti da li je rupa u testovima ili ekvivalentan.
  • Za Python postoje alati mutmut i cosmic-ray, za Javu PIT, za JavaScript i C# Stryker.
  • Mutacije su spore, pa se puštaju rjeđe i usmjeravaju na najvažnije funkcije.

Provjeri se.

  1. Šta znači da je mutant preživio? – Svi testovi prolaze i nad izmijenjenim kodom: testovi nisu primijetili kvar.
  2. Koliki je rezultat mutacija ako je od 20 mutanata ubijeno 15, a 2 su ekvivalentna? – 15 / (20 − 2) = 15 / 18 ≈ 83 %.
  3. Zašto su mnogi preživjeli mutanti funkcije cijena_dostave na granicama? – Testovi su pravljeni da obiđu puteve, a ne da stoje tačno na granicama pravila.

Praktična nastava – Tok podataka, alati i mutanti

Cilj je pronaći i popraviti anomalije toka podataka, pokrenuti ruff i mypy, i podići rezultat mutacija funkcije cijena_dostave na 100 %. Radi se u parovima.

Korak 1 – Anomalije rukom (30 minuta)

  1. Otvorite primjeri/tok_podataka/racun.py. Na papiru napravite tabelu: red, definicije, upotrebe (kao slika 7.2).
  2. Označite sve anomalije i za svaku napišite koju poslovnu grešku izaziva (koje pravilo PP krši).
  3. Pokrenite pytest -q test_racun.py. Uporedite poruke s vašim anomalijama: koja poruka odgovara kojoj anomaliji?

Korak 2 – ruff i mypy (30 minuta)

  1. Instalirajte alate: pip install ruff mypy.
  2. Pokrenite ruff check racun.py --output-format concise i mypy --enable-error-code possibly-undefined racun.py. Popunite tabelu iz 7.6 svojim rezultatima.
  3. Pokrenite mypy bez opcije --enable-error-code possibly-undefined. Koja poruka nestaje?
  4. Pokušajte ruff check --fix --unsafe-fixes racun.py na kopiji fajla. Šta je ruff uradio s napojnicom? Da li je greška ispravljena ili samo sakrivena?

Korak 3 – Popravka (30 minuta)

  1. Popravite racun.py (u svom repozitoriju, ne u primjeri/) tako da ruff ne javlja ništa, mypy ne javlja ništa i sva četiri testa prolaze. Ne gledajte racun_ispravljen.py dok ne završite.
  2. Napravite tabelu parova definicija–upotreba za svoju verziju i provjerite da li je svaki par pokriven nekim testom (kriterij sve upotrebe). Ako nije, dopišite test.
  3. Uporedite s racun_ispravljen.py. Pokrenite testove nad tom verzijom: $env:RACUN="ispravljen" (PowerShell) ili RACUN=ispravljen pytest (Linux, macOS).

Korak 4 – ruff nad DeliveryApp (15 minuta)

  1. U primjeri/deliveryapp pokrenite ruff check --isolated . --output-format concise. Zapišite nalaz i pretpostavku o tome šta on govori o prvoj verziji.
  2. Pokrenite ruff check --isolated --select C901 --config "lint.mccabe.max-complexity=3" . Koje funkcije imaju V(G) veće od 3? Uporedite s poglavljem 6.

Korak 5 – Lov na mutante (60 minuta)

  1. U primjeri/mutacije pokrenite python mutanti.py ../pokrivenost/test_svi.py. Zapišite rezultat.
  2. Za svakog preživjelog mutanta, prije gledanja u test_jaki.py, napišite u svom fajlu test_moji_mutanti.py test koji ga ubija. Očekivani rezultat računajte iz pravila PP-1 … PP-4.
  3. Pokrenite python mutanti.py ../pokrivenost/test_svi.py test_moji_mutanti.py dok rezultat ne bude 100 %.
  4. Dodajte u listu MUTANTI u svojoj kopiji skripte dva nova mutanta (npr. udaljenost_km - 3 u udaljenost_km - 2, ili 500 u 499). Da li vaši testovi ubijaju i njih? Ako ne, dopišite test.
  5. Napišite jedan ekvivalentan mutant i objasnite zašto ga nijedan test ne može ubiti.

Korak 6 – AI i mutacije (opcionalno)

Dajte AI asistentu funkciju napojnica iz primjeri/deliveryapp/napojnica.py i zatražite listu od 10 mutanata. Za svaki odlučite: da li ga 11 testova iz poglavlja 5 ubija? Provjerite tvrdnju tako što ćete mutant ručno unijeti u kopiju fajla i pokrenuti testove. Zapišite da li je AI predložio i neki ekvivalentan mutant, i da li je to sam primijetio.

Rezultat koji se predaje: fajl rezultati-7.md s tabelom definicija i upotreba funkcije racun i spiskom anomalija; tabela nalaza ruff, mypy i testova; ispravljen racun.py s tabelom parova definicija–upotreba; nalaz ruff nad DeliveryApp; test_moji_mutanti.py i ispis skripte sa 100 %; dva nova mutanta i jedan ekvivalentan s objašnjenjem.

Vježbe

Vježba za studenta 1 – definicije i upotrebe.

Označite definicije i upotrebe i nađite anomalije:

def vrijeme(restoran, km):
    if restoran == "R-1":
        priprema = 20
    if restoran == "R-2":
        priprema = 15
    voznja = 4 * math.ceil(km)
    voznja = 5
    return priprema + voznja

Rješenje. priprema: dvije definicije (u dva if), upotreba u return. Za restoran R-3 (ili nepoznat) nijedna definicija se ne izvrši, pa je to u bez d (UnboundLocalError). voznja: definisana računom, pa odmah prepisana brojem 5 bez čitanja: dd; vrijeme vožnje je uvijek 5 minuta, bez obzira na udaljenost. Po PP-9, R-1 na 4,2 km treba dati 20 + 4 × 5 = 40 minuta; ova funkcija daje 25.

Vježba za studenta 2 – parovi definicija–upotreba.

Za ispravljenu funkciju racun iz 7.3 napravite najmanji skup testova koji pokriva kriterij sve definicije (svaka definicija stiže do bar jedne upotrebe). Koliko testova treba?

Rješenje. Treba da se izvrše d1(p), d2(p), d1(dostava), d2(dostava) i d(napojnica), svaka do return. d1(p) i d1(dostava) se izvršavaju uvijek, ali „stižu” do return samo ako ih ne prepiše druga definicija. Dva testa su dovoljna: N-1001 bez koda (d1(p), d1(dostava) i napojnica stižu do return) i 32,00 € na 2,5 km sa STUDENT10 (d2(p) i d2(dostava)). Očekivano: 24,50 € i 32,00 € − 3,20 € + 0,00 € = 28,80 €.

Vježba za studenta 3 – poruke alata.

Šta znače poruke i šta treba uraditi?

deliveryapp.py:31:5: F841 Local variable `procenat` is assigned to but never used
narudzba.py:12: error: Argument 1 to "eur" has incompatible type "str"; expected "int"  [arg-type]

Rješenje. Prva: u redu 31 promjenljiva procenat je izračunata, a nikad korištena (d bez u); treba provjeriti da li je zaboravljena u računu popusta, a ne samo obrisati red. Druga: u redu 12 funkciji eur je poslan tekst (npr. „2450” iz forme), a očekuje cijeli broj centi; treba pretvoriti ulaz u broj (int(...)) i provjeriti šta se desi s pogrešnim unosom.

Vježba za studenta 4 – rezultat mutacija.

Alat je napravio 40 mutanata. Testovi su ubili 30, preživjelo je 10, a pregledom je utvrđeno da su 4 od preživjelih ekvivalentni. Koliki je rezultat mutacija? Šta treba uraditi s preostalih 6?

Rješenje. 30 / (40 − 4) = 30 / 36 ≈ 83 %. Preostalih 6 su rupe u testovima: za svakog napisati test koji ga ubija, s očekivanim rezultatom iz pravila.

Vježba za studenta 5 – mutant za popust.

Za funkciju popust iz deliveryapp.py (return min(round(iznos * procenat / 100), MAX_POPUST)) napravite mutant koji glumi grešku D-05 i test koji ga ubija.

Rješenje. Mutant: return round(iznos * procenat / 100) (obrisan min, nema gornje granice). Test: assert popust(8000, "STUDENT10") == 500. Na 80,00 € mutant daje 800 centi (8,00 €), a PP-5 kaže najviše 5,00 €. Test s 21,50 € ga ne bi ubio, jer je tamo popust 2,15 € i bez granice.

Sažetak u pet rečenica

  1. Heartbleed iz 2014. bio je jedna provjera koja nedostaje: server je vjerovao dužini poslanoj spolja i vraćao do 64 KB tuđe memorije.
  2. Tok podataka prati svaku promjenljivu od definicije do upotrebe i traži tri anomalije, upotrebu bez definicije, dvije definicije zaredom i definiciju bez upotrebe, koje su u funkciji racun značile pad bez koda, uvijek besplatnu dostavu i izgubljenu napojnicu.
  3. Statička analiza čita kod bez pokretanja: ruff je našao nekorištenu napojnicu i neiskorišten uvoz, a mypy pogrešan tip i promjenljivu koja možda nije definisana, ali nijedan nije našao dostava = 0.
  4. Testiranje mutacijama namjerno kvari kod malim izmjenama i mjeri koliko tih mutanata testovi ubiju, pa tako mjeri snagu testova, a ne samo da li je kod izvršen.
  5. Testovi funkcije cijena_dostave sa 100 % pokrivenosti ubili su samo 58 % mutanata, jer su preživjeli mutanti bili na granicama pravila, a pet graničnih testova podiglo je rezultat na 100 %.

Pitanja za ponavljanje

  1. Šta je bio Heartbleed i koja je vrsta greške u njegovom korijenu?
  2. Šta je definicija, a šta upotreba promjenljive? Koje dvije vrste upotrebe postoje?
  3. Nabrojte tri anomalije toka podataka i za svaku dajte primjer.
  4. Koje su tri anomalije u funkciji racun i koju poslovnu grešku izaziva svaka?
  5. Šta je par definicija–upotreba? Kako glase kriteriji sve definicije i sve upotrebe?
  6. Po čemu se tok podataka razlikuje od pokrivenosti grana?
  7. Šta je statička analiza? Šta može, a šta ne može naći?
  8. Šta je lažna uzbuna, a šta propust? Dajte primjer propusta iz ovog poglavlja.
  9. Šta javljaju pravila ruff F401, F841 i C901?
  10. Zašto automatska ispravka F841 može biti opasna?
  11. Šta radi mypy i šta je našao u fajlu racun.py?
  12. Objasnite ideju testiranja mutacijama. Šta je ubijen, a šta preživio mutant?
  13. Koliki je bio rezultat mutacija za test_svi.py i zašto je preživjelo pet mutanata?
  14. Šta je ekvivalentan mutant i kako se računa rezultat mutacija kad ih ima?

Odgovori na pitanja za ponavljanje.

  1. Heartbleed (CVE-2014-0160, objavljen 7. 4. 2014) bio je greška u OpenSSL-u: server je u poruci „heartbeat” vjerovao dužini koju je poslao klijent i vraćao do 64 KB memorije, s lozinkama i ključevima. U korijenu je nedostajuća provjera vrijednosti koja stiže spolja, greška toka podataka.
  2. Definicija je mjesto gdje promjenljiva dobija vrijednost (dostava = 300), a upotreba mjesto gdje se vrijednost čita. Upotreba je računska (u izrazu, ukupno = iznos + dostava) ili u odluci (u uslovu, if iznos >= 3000).
  3. Upotreba bez definicije (return p kad p nije dobio vrijednost), dvije definicije zaredom (x = 3 pa x = 5 bez čitanja) i definicija bez upotrebe (napojnica = … koja se nikad ne pročita).
  4. p bez definicije kad nema koda: funkcija pada s UnboundLocalError. dostava prepisana nulom: svi dobijaju besplatnu dostavu (krši PP-2 i PP-4). napojnica izračunata, nekorištena: napojnica se ne naplaćuje (krši PP-8).
  5. Par definicija–upotreba je definicija i upotreba iste promjenljive između kojih postoji put bez nove definicije. Sve definicije: svaka definicija mora stići do bar jedne upotrebe. Sve upotrebe: svaki par mora biti izvršen bar jednom.
  6. Pokrivenost grana pita da li je svaki put kroz kod prođen; tok podataka pita da li je svaka vrijednost stigla od definicije do upotrebe i tamo bila provjerena. Za kod s mnogo računa i malo odluka tok podataka je korisniji.
  7. Statička analiza je provjera koda bez pokretanja. Može naći anomalije, pogrešne tipove, preveliku složenost, opasne obrasce i neujednačen stil. Ne zna poslovna pravila, pa ne zna tačan rezultat, i ima lažne uzbune i propuste.
  8. Lažna uzbuna: alat javi problem kojeg nema. Propust: problem postoji, a alat ćuti. Primjer propusta: ni ruff ni mypy nisu javili dostava = 0; i Coverity u početku nije javio Heartbleed.
  9. F401: modul uvezen, a nije korišten. F841: lokalna promjenljiva dobila vrijednost koja se nikad ne koristi. C901: funkcija ima ciklomatsku složenost veću od zadate granice.
  10. Jer ruff predlaže brisanje dodjele, što ućutka upozorenje, a greška ostaje: npr. napojnica i dalje ne ulazi u račun. Alat pokazuje simptom, a uzrok mora popraviti čovjek.
  11. mypy provjerava oznake tipa bez pokretanja koda. U racun.py je našao da se funkciji eur šalje float (24.5) umjesto int, i uz opciju possibly-undefined da p možda nije definisan.
  12. Kod se namjerno pokvari jednom malom izmjenom (mutant), pa se pokrenu testovi. Ako bar jedan test padne, mutant je ubijen i testovi su primijetili kvar; ako svi prođu, mutant je preživio i u testovima postoji rupa.
  13. 58 % (7 od 12), iako je pokrivenost bila 100 %. Preživjeli su mutanti na granicama 0 km, 10 km, 5,00 € i 30,00 € i mutant bez max(0, …), jer nijedan test nije stajao tačno na granicama niti testirao plaćenu dostavu ispod 3 km.
  14. Ekvivalentan mutant je izmjena koja ne mijenja ponašanje programa (npr. zamijenjen redoslijed ceil i max), pa ga nijedan test ne može ubiti. Izuzima se iz računa: rezultat = ubijeni / (svi − ekvivalentni) × 100 %.