Verificarea automată a calității codului reprezintă unul dintre primii pași în garantarea corectitudinii codului. Așa cum ne putem imagina, un program formatat inconsistent și cu greșeli frecvente de structură e predispus la a avea probleme ulterioare, chit că inițial el e funcțional. De asemenea, mai ales în proiectele open-source, ne dorim ca programele noastre să fie cât mai prezentabile pentru a atrage contribuitori și utilizatori, cât și pentru a face contribuțiile mai consistente. Dacă fiecare contribuitor ar avea un stil de formatare al codului diferit, s-ar ajunge garantat la neînțelegeri.
Aceste idei sunt întărite când este vorba de limbaje interpretate precum Python. După cum știm, în Python tipul variabilelor este determinat la rulare, deci nu putem ști cu siguranță că avem un cod funcțional, până când nu rulăm cu succes pe toate căile posibile de execuție. Totodată, codul Python este bazat pe indentare și o formatare cât de cât corectă, deci fără să respectăm un minim de formatare, este posibil ca interpretorul de Python să refuze sa ne ruleze programul cu totul. Deoarece Python este mai lax în scriere, trebuie să folosim folosim aplicații terțe pentru a ne formata codul, comparat cu limbaje mult mai stricte ca Rust.
Dacă combinăm toate aceste idei cu o formatare automată și folosirea sistemelor de CI/CD, precum în laboratoarele precedente, putem să ne asigurăm că în proiectul nostru nu pătrunde cod de o calitate inferioară celei dorite.
Linterele, sau uneltele de verificare a calității codului, reprezintă prima metodă de verificare a codului. De multe ori acestea sunt integrate în editorul folosit de developeri, în așa fel încât codul este evidențiat, sau chiar formatat automat la fiecare salvare a fișierului.
Deoarece procesul de linting poate semnala multe probleme, este recomandat să fie integrat în dezvoltarea unui program cât mai devreme, pentru a evita schimbări fundamentale ale codului ulterior. Dificultatea principală în procesul de linting vine din stabilirea unui standard de formatare a codului care să corespundă cu așteptările tuturor contribuitorilor. În cazul nostru, configurările de bază sunt mai mult decât suficiente.
Static checkerele, sau unelte de verificare statică a codului, reprezintă o verificare mai amănunțită a codului. Acestea parcurg codul și încearcă să îl înțeleagă în amănunt pentru a deduce folosiri eronate ale anumitor funcții și/sau cod. Un bun exemplu de astfel de verificări sunt cele de securitate. Prin anumite static checkere putem descoperi automat parole, tokene, și alte date de autentificare uitate în cod, înainte de a face public codul și a ne compromite. Totodată, vom vedea că există și unelte mai amănunțite, ce realizează o detecție automată a tipurilor fiecărei variabile, pentru a ne garanta că nu există erori de runtime generate de acestea.
Static checkerele reprezintă, pe scurt, următorul pas al verificării codului de către developer, înainte de rulare și de publicarea efectivă a codului.
Diferentța majoră între acest checking și cel anterior este timpul în care se fac verificările. Pentru Static Checking, uneltele principale de verificare sunt rulate înainte de a rula codul sursă. În cazul Dynamic Checking, sunt rulate odată cu codul sursă. De aceea, și din laboratoarele trecute, putem identifica câteva forme de Dynamic Checking, precum: Testarea Unitară, Testarea de Integrare, Fuzz Testing, etc..
Verificările dinamice rulează efectiv codul și observă comportamentul la execuție. Ele surprind erori de logică, condiții de margine și probleme de integrare care scapă analizei statice.
Caracteristică | Static Checking | Dynamic Testing |
---|---|---|
Momentul analizei | Înainte de execuție | În timpul rulării testelor sau în mediul de dev |
Detectează | Erori de stil, tipuri, vulnerabilități de securitate | Erori de logică, edge cases, probleme de integrare |
Exemplu de problemă | String concatenat cu int, importuri inutile | Funcție care aruncă excepție pentru input neprevăzut |
Viteză | Rapid (nu rulează codul) | Mai lent (rulează efectiv cod) |
Acoperire | Nu poate evalua toate căile de execuție | Poate acoperi doar căile pentru care există teste |
Cost de scriere dat de | Configurare inițială și tunare a regulilor | Scriere de teste și date de testare |
În exercițiile din laborator vom explora cinci unelte de linting și static checking. Pentru fiecare va trebui să o rulați și să rezolvați (pe cât se poate) problemele semnalate. Vom folosi un repository open source pentru a rula exercițiile. Din acesta va trebui să alegeți 1-2 mini-proiecte pe care să rulați uneltele și să rezolvați problemele.
Dacă proiectul ales nu mai are destule probleme pentru a fi util, sau ați rezolvat deja toate problemele, puteți încerca altul.
1. Instalăm pip dacă lipsește: https://pip.pypa.io/en/stable/installation
$ python3 -m ensurepip --upgrade
2. Pentru început va trebui să instalăm tool-urile ce le vom folosi:
$ pip3 install pylint ruff black bandit pytype
$ pip3 install pytest typeguard coverage hypothesis atheris
3. Apoi clonăm repository-ul peste care o sa rulăm uneltele:
$ git clone https://github.com/Ingineria-Calculatoarelor-ACS-UPB/python-mini-project.git
Pylint este unul dintre cele mai cunoscute unelte de linting. Acesta oferă informații simple, cât și avansate în legătură cu codul pe care îl analizează, și apoi generează un raport cu toate erorile și acordă o notă acestuia. Acesta are și integrare cu VSCode, precum și alte editoare, ce ușurează folosirea lui.
Observăm că fiecare linie din raport menționează un cod de verificare, prin care e identificată fiecare problemă. Putem avea probleme de tip error, warning, comment, etc.
1. Rulați Pylint și observați problemele semnalate de acesta. 2. Rezolvați problemele semnalate pentru proiectul ales.
De cele mai multe ori, un singur linter nu poate prinde toate erorile ce le avem în cod, de aceea suntem nevoiți să folosim mai multe. Ruff este o suită de verificatoare de cod ce este des folosită în proiectele mari. Aceasta ne poate ajuta să descoperim probleme ce au fost ratate de Pylint. De asemenea, înglobează verificări de la diferite alte unelte.
1. Rulați Ruff și observați problemele semnalate de acesta. 2. Rezolvați problemele semnalate pentru proiectul ales.
Am observat că este destul de obositor să facem atatea modificări și timpul consumat pe formatarea codului ar fi putut să fie investit în adăugarea de funcționalități noi. Black vine cu o paradigmă nouă, cea de cedare a controlului asupra linting-ului către linter. Pe lângă semnalarea problemelor, acesta le și rezolvă după un standard propus de dezvoltatori. Desigur, noi putem să rulăm fără a aplica modificările.
1. Rulați Black în mod de diff color precizând argumentele --diff și --color la rulare și observați problemele. 2. Rulați Black fără argumente adiționale pentru a aplica schimbările. 3. Rezolvați problemele rămase semnalate pentru proiectul ales (dacă mai există).
Bandit este o unealtă de static checking concentrată pe partea de securitate. Aceasta se asigură că programul nostru nu poate fi exploatat de alte persoane și că este sigur în timpul folosirii. Pe lângă verificarea existenței unor parole/tokene uitate, acesta analizează și cod ce ar putea fi exploatat în anumite circumstanțe.
În cazul de față, avem o singură problemă, aceea că descărcăm o pagină fără a menționa un timp de oprire. Un utilizator malițios poate să apeleze funcția cu un url invalid și să blocheze execuția programului.
1. Rulați Bandit și observați problemele. 2. Rezolvați problemele semnalate pentru proiectul ales.
Pytype este un tool de static checking mai avansat față de cele precedente. Se folosește de un engine de analiză statică pentru a garanta corectitudinea tipurilor variabilelor din codul nostru. Acesta ideal este folosit în timpul dezvoltării programului, sau atunci când avem bug-uri. Este foarte probabil ca acesta să nu întoarcă nimic, în cazul proiectelor analizate în cadrul laboratorului, deoarece acestea sunt deja funcționale.
Fără a rula codul, acesta a analizat operațiile realizate și a detectat adunarea unui întreg cu un șir de caractere. În Python astfel de problemă ar fi apărut doar la rulare, și dacă nu am testat acel caz posibil să treacă nedetectată și să ajungă în mediu de producție.
1. Rulați Pytype și observați problemele. 2. Rezolvați problemele semnalate pentru proiectul ales (dacă există).
Coverage este un tool testare al acoperirii testelor unitare și de integrare în cadrul codului vostru. De aceea, pentru a rezolva acest exercițiu, recomandăm reamintirea conceptelor de Teste Unitare. Printr-un raport ușor de descifrat, vă arată destul de clar unde sunt lipsuri și ce puteți îmbunătăți.
Rulând comanda:
$ coverage run -m pytest
$ coverage report
pe orice proiect în Pyhton care deține o suită de teste unitare sau de integrare, veți putea obține raportul care poate fi deschis și accesând fișierele noi generate.
1. Rulați Coverage și observați problemele. 2. Încercați să creșteți test coverage-ul prin mai multe teste unitare.
Typeguard Assertions este un tip de verificare al tipurilor odată cu rularea codului sursă. Este un tip de dynamic checking ce ajută la observarea problemelor de tip ce nu pot fi identificate fără a interpreta codul.
În fișierul vostru Python, importați și aplicați decoratorul @typechecked pe funcțiile critice:
from typeguard import typechecked @typechecked def process_data(data: dict, limit: int) -> list: assert limit > 0, "Limit trebuie să fie pozitiv" # … restul implementării … return result_list
Apoi rulați:
$ pytest --typeguard-packages=.
1. Rulați Typeguard și observați problemele. 2. Rezolvați problemele semnalate pentru proiectul ales (dacă există).
Creați un fișier python-checks.yaml în calea .github/workflows și completați conform specificației următoare:
Aveți grijă să rulați uneltele în mod neinteractiv (dacă este disponibil), și să verificați codurile de eroare întoarse de acestea.