TSM - O strategie de testare pentru aplicații C embedded în timp real

Alexandru Bolboacă - Agile Coach and Trainer, with a focus on technical practices

În ultimele șase luni, am lucrat cu un client care dezvoltă dispozitive hardware. Provocarea cu care s-a confruntat a constat în plângerile clienților, din cauza bugurilor apărute după câteva luni de la instalare la unul dintre produsele lor. Bineînțeles, cauza respectivelor buguri necesită o investigare minuțioasă.

Contextul

Dispozitivul hardware rulează un sistem de operare în timp real, care este în momentul de față în tranziție de la un sistem proprietar la FreeRTOS. Majoritatea codului este scris în C și utilizează apeluri de sistem pentru crearea și sincronizarea firelor de execuție. Codul este vechi, deoarece a rulat aproape 10 ani.

Evaluarea

Primul pas a fost efectuarea unei evaluări tehnice a codului. Aceasta a fost realizată printr-o combinație de: inspecție vizuală, rularea unor instrumente de analiză și intervievarea membrilor echipei. Ca urmare a evaluării, s-a stabilit că a fost bine scris codul, singura problemă fiind implementarea inconsistentă a comunicării dintre procese. Fiecare membru al echipei a recunoscut lipsa testării și faptul că ar fi fost foarte utilă. Așadar, a venit timpul pentru pasul următor.

Strategia de testare

Atunci când definim strategia de testare, începem de obicei de la piramida testelor. Din cauza structurii neobișnuite a programului, strategia a fost mai puțin evidentă decât de obicei.

OS în timp real au o caracteristică: în loc de a scrie programe, dezvoltatorii scriu așa-numitele "taskuri" care rulează în funcție de diverse politici. Taskurile discută utilizând mecanisme IPC (comunicare inter-proces). Am decis să încercăm să scriem teste unitare pentru un task și să vedem dacă putem identifica modele pe care le putem utiliza ulterior în întreaga aplicație.

Dar chiar dacă toate taskurile sunt corecte, pot apărea probleme legate de concurență. Probleme de tip deadlock, starvation etc. nu sunt prevenite de testarea taskurilor. Avem nevoie de alt tip de teste pentru a găsi aceste tipuri de probleme.

O soluție este scrierea unor teste de stres. Ele ar trimite din ce în ce mai multe solicitări, sau ar accelera ceasul sistemului, până în punctul în care ceva cedează. Vom avea apoi șansa de a le investiga. Mă gândesc la tehnica aceasta ca "accelerând timpul", astfel încât să putem găsi problemele care apar în mod normal după șase luni, într-o săptămână sau mai puțin.

Am implementat cu succes testele taskurilor, după cum veți vedea în continuare. Testele de stres sunt deocamdată o idee de lucru, care se poate schimba în timpul implementării.

Testarea taskurilor

Am colaborat cu unul dintre dezvoltatorii clientului, patru ore pe săptămână, în două sesiuni de câte două ore. Am început prin configurarea suitei de teste, utilizând CppUTest, un framework de testare unitară, care poate testa codul C/C++ embedded. Avantajul său constă în consumul foarte scăzut de memorie. Alternativa este GoogleTest și este frameworkul pe care echipa a decis să îl adopte în final. Fiecare dintre ele funcționează bine în contextul dat.

Noi am utilizat abordarea clasică pentru scrierea testelor de caracterizare. Mai întâi, scriem un test care inițializează taskul și vedem ce nu funcționează. Apoi, apelăm o funcție din același test și vedem ce nu funcționează etc. . Ori de câte ori testul nu a reușit, găsim metode de a reduce dependențele, astfel încât testul să fie executat cu succes.

Din fericire, dependențele de OS au fost extrase deja în macro-uri. Diminuarea acestor dependențe a însemnat doar crearea unui fișier .h, includerea lui în test și înlocuirea macro-ului cu orice am avut nevoie pentru test. Acesta a fost un caz fericit; se consumă mult mai mult timp atunci când apelurile de funcții din OS sunt răspândite în cod.

Un alt tip de dependență pe care am întâlnit-o a fost legat de pornirea, sincronizarea și oprirea threadurilor. O opțiune a fost înlocuirea metodelor de pornire, oprire și sincronizare cu funcții fără implementare (execuția rămânând astfel în același thread) sau cu o implementare dummy. Problema a fost că sincronizarea threadurilor este o parte importantă a sistemului.

În final, am decis să separăm codul executat de un thread de codul care sincronizează prin extragere de funcții. Apoi, am înlocuit pentru teste toate funcțiile primitive cu funcții din librăria pthread. Astfel, am putut testa separat coregrafia de implementare.

În linii mari, scrierea testelor pentru taskuri s-a dovedit a fi destul de simplă. Am identificat, de asemenea, modele pe care le putem utiliza pentru a testa toate celelalte taskuri din sistem. Mă aștept încă la câteva probleme particulare, dar le vom rezolva pe fiecare în parte.

Beneficii raportate

După testarea taskului, colaboratorul meu a prezentat tehnica și rezultatele celorlalți membri ai echipei. Aceștia și-au dat seama că există câteva avantaje:

Ei au decis, de asemenea, să utilizeze Google Test în locul CppUTest, deoarece acesta simplifică anumite taskuri și dispozitivul are suficiente resurse pentru a-l executa.

Ce urmează

Execuția testelor pe calculatorul personal este deosebit de importantă pentru feedback, dar nu este suficientă. Acest lucru este adevărat, în deosebi când se utilizează diferite librării pentru testare față de cel de producție (pthread pe computerul local, funcțiile primitive OS pe dispozitiv). Astfel, pasul următor va fi găsirea unei metode de deployment automat al software-ului pe un dispozitiv și execuția testelor taskurilor pe acesta. După ce vom face acest lucru, este destul de ușor să avem un sistem de integrare continuă care compilează, face deployment și execută testele pe dispozitiv. Acest sistem va permite dezvoltatorilor care nu au acces la dispozitiv să își testeze codul în câteva ore, ceea ce este un lucru grozav.

Pentru a finaliza strategia de testare, trebuie să scriem și testele de stres și să efectuăm un orar de execuție. Și, pentru a îmbunătăți conceptul, va trebui să refactorizăm IPC, astfel încât să fie consistent în tot codul.

Concluzii

Scrierea de teste automate pentru aplicații embedded C în timp real este nu doar posibilă, ci și foarte utilă. Tehnicile obișnuite se aplică: piramida testelor, teste de caracterizare și tehnici pentru ruperea dependențelor. O complexitate suplimentară este dată de nevoia de avea două configurații de testare: una pentru calculatorul dezvoltatorului și una pentru dispozitiv, dar este ușor să rezolvăm această problemă cu un sistem bun de compilare.

Cel mai important factor pentru adoptarea testelor este designul existent. Un layer de integrare cu dependențele externe și un mod ușor de injectare a dublelor de testare duc la o diferență mare de eficiența în scrierea primelor teste. C/C++ are un avantaj în acest context: macro-urile reprezintă o metodă de rupere și injectare a dependențelor indisponibilă în alte limbaje. Experiența mea arată tendința C/C++ de a conduce la o separare mai clară a dependentelor comparat cu Java, PHP sau C#, datorită complexității inerente a limbajului, simplificând astfel scrierea testelor pe cod existent.