Für eine Functional-Safety-Zertifizierung müssen Entwickler die Code-Coverage für den gesamten Quellcode inklusive aller Codevarianten nachweisen. Doch wie lässt sich nicht getesteter Code in den originalen C/C++-Quelldateien erkennen? Eine Lösung bietet hier die Ermittlung einer »Hyper-Coverage«.
Als »Code-Coverage« (Testabdeckung) bezeichnet man den Nachweis, dass alle Teile eines C/C++-Quellcodes möglichst vollständig getestet wurden. Die Normen empfehlen dabei, je nach Einstufung einer sicherheitskritischen Anwendung, passende Coverage-Maße wie beispielsweise Statement Coverage (C0) und Branch Coverage (C1) oder Modified Condition/Decision Coverage (MC/DC). Wurden alle verfügbaren Tests durchgeführt, lässt sich erkennen, ob und welche Teile des Codes nicht getestet wurden.
Die Coverage-Messung stützt sich dabei auf den Kontrollfluss der Funktionen/Methoden einer Quelldatei und prüft die erreichten Programmzweige bzw. Bedingungen. Dabei stellt sich die Frage, welcher Code tatsächlich im Kontrollfluss abgebildet wird. Die vielfachen Möglichkeiten des Präprozessors erlauben es, durch den Einsatz von Makros aus ein und derselben Quelldatei mehrere unterschiedliche Programme zu erstellen.
Aus dem Source-Code einer Quelldatei werden durch den Präprozessor zunächst alle Makros aufgelöst, sodass ein präprozessierter Code entsteht, der letztendlich vom Compiler übersetzt wird. In diesen Prozess fließen die folgenden Informationen ein:
Aus diesen Teilen entsteht der präprozessierte Code, der jeweils eine bestimmte Variante des originalen Source-Codes repräsentiert. Die Präprozessor-Makros werden dabei entweder durch #define/#ifdef-Anweisungen in den Header-Dateien oder durch Compiler-Switches aus der Konfiguration beim Aufruf des Compilers übergeben. Dieser präprozessierte Code stellt auch die technische Basis für Testwerkzeuge dar, auf dem Code wird die Analyse und Instrumentierung für die Coverage-Messung durchgeführt.
Beim Test von Varianten ist es entscheidend zu wissen, welche Codezeilen aus der Originalquelldatei in der jeweiligen Variante tatsächlich existieren. Und auch umgekehrt: Gibt es Code in der Originalquelldatei, der in keiner Variante enthalten ist?
Zunächst muss jede existierende Codezeile einer Variante auch genau in dieser Variante getestet werden. Für jede einzelne Variante muss eine möglichst vollständige Code-Coverage erreicht werden. Über Varianten von Testfällen lassen sich die meist relativ ähnlichen Codestrukturen der Varianten gut testen, sodass der Testaufwand reduziert werden kann.
In Bild 1 ist der Kontrollfluss von zwei Varianten mit der erreichten Code-Coverage dargestellt. Es zeigt, dass Coverage-Ergebnisse aufgrund der unterschiedlichen Kontrollflüsse nicht einfach übertragen werden können, aber die Testfälle aus der Basis-Implementierung genutzt werden könnten, um den in der Variante fehlenden Programmzweig auf der rechten Seite noch abzudecken. Eine Vererbung und Anpassung der Basistestfälle für die Variante spart Aufwand bei der Testerstellung und verhindert redundante Testfälle.
Beim Test mit vielen Varianten stellt sich die Frage, ob sich die Coverage-Ergebnisse zusammenfassen lassen, um den Testaufwand zu reduzieren. Diese Möglichkeit bietet sich zunächst nur für unterschiedliche Tests derselben Variante: Bei identischer Präprozessor-Datei ist die Codestruktur ebenfalls identisch. Damit können die Coverage-Ergebnisse unproblematisch über den Kontrollfluss zusammengeführt werden.
Beispielsweise lässt sich fehlende Coverage für Statements/Branches oder Bedingungskombinationen einer Funktion durch zusätzliche Unit- und Integrationstests ergänzen. In Bild 2 wird auf der linken Seite die erreichte Coverage aus dem Integrationstest dargestellt. Der rot unterlegte Programmzweig mit der Sonderbehandlung in dieser Funktion wird im Integrationstest nicht erreicht und daher durch einen weiteren Unit-Test ergänzt. In Summe ergibt sich damit eine vollständige Coverage für diese Funktion in dieser Codevariante (rechts im Bild 2).
Aus der Analyse des präprozessierten Source-Codes der Varianten ergibt sich eine Hierarchie von Quelldateien mit deren Präprozessor-Varianten und den jeweils darin enthaltenen Funktionen. Bild 3 zeigt eine Quelldatei mit zwei Varianten und den Coverage-Ergebnissen der enthaltenen Funktionen. In dieser Übersicht wurden die kombinierten Coverage-Ergebnisse aus verschiedenen Tests bereits pro Funktion zusammengefasst. Die Übersicht zeigt zwei noch offene Probleme: Obwohl die Funktionen aus derselben Quelldatei hervorgehen, können die Coverage-Ergebnisse aufgrund unterschiedlicher Kontrollflüsse nicht kombiniert werden. Zum anderen fehlt eine Coverage-Aussage auf der Ebene der Originalquelldatei.