Komplexe SoCs managen Code-Coverage im automatisierten Test

Die SoC-Komplexität bedeutet in Bezug auf Softwarequalität und -sicherheit eine Vielzahl neuer Herausforderungen. So ist für einen effektiven Softwaretest nicht nur die automatische Generierung der Testergebnisse unabdingbar. Es gilt auch, das erreichte Code-Coverage zu messen und zu dokumentieren.

Bei der Entwicklung komplexer Applikationen mit etlichen MByte Quellcode lassen sich Softwarefehler nie 100prozentig vermeiden. Umso wichtiger ist es insbesondere bei sicherheitskritischen Anwendungen, diese Bugs durch Anwendung moderner Methoden so früh wie möglich zu erkennen und zu eliminieren. Einer der wichtigsten Bestandteile dieses Prozesses ist der Test. Nicht ohne Grund wird diesem in einschlägigen Normen wie der ISO 26262 für den Automobilsektor, der EN/IEC 62061 für die Sicherheit von Maschinen oder der für die Luftfahrt relevanten DO-178 so viel Aufmerksamkeit geschenkt.

Ob eine Softwarelösung endgültig für den Einsatz beim Kunden freigegeben wird, oder die Entwicklungsabteilung noch einmal Nacharbeit leisten muss, hängt allerdings nicht nur vom erfolgreichen Testabschluss ab. Auch die Güte des durchgeführten Tests muss stimmen. Maßgeblich hierfür ist die Testabdeckung. Wie hoch ist der Anteil der Applikation, des Modules oder einzelner Funktionen, die der jeweilige Test gestresst hat gegenüber dem Anteil, der durch eine ungünstige Wahl von Testfällen gar nicht gestresst werden konnte? Um dieser für die Softwarequalität und -sicherheit enorm wichtigen Frage auf den Grund zu gehen, wird im Bereich der Softwareentwicklung heutzutage in der Regel zur Bewertung der Testgüte das »Code-Coverage« herangezogen. Da die eingangs genannten Normen ohnehin nicht nur die Dokumentation der Testergebnisse, sondern auch der erreichten Abdeckungsgrade verlangen, ist es natürlich sinnvoll, parallel zur Ausführung der jeweiligen Testfälle auch gleich noch das Code-Coverage für die zu testende Funktion oder das zu testende Modul zu bestimmen.

Code-Coverage ermitteln

Um das Code-Coverage beispielsweise einer Funktion zu berechnen, benötigt man Informationen, die sich nur durch die Ausführung ermitteln lassen.

Im einfachsten Fall muss lediglich aufgezeichnet werden, welche Codeteile tatsächlich ausgeführt wurden und welche nicht. Letzteres ist implizit gegeben, wenn die Quellen oder zumindest das Binary zur Verfügung stehen. Aus diesen Informationen lässt sich wiederum direkt das Statement-Coverage ableiten, also wieviel Code durch die Testfälle im Verhältnis zum Gesamtcode geprüft wird. Mit diesem vergleichsweise einfachen Verfahren kann zwar nicht ausgeführter Programmcode (»dead code«) aufgespürt werden, die Anforderungen der ISO 26262, EN IEC 62061 oder DO-178 an die Testqualität werden aber meist nicht erfüllt.

Schon deutlich aussagefähiger hinsichtlich der Testqualität ist das Branch-Coverage. Dafür werden die Ausführungen aller möglichen Programmverzweigungen herangezogen. Im Falle einer einfachen IF-Anweisung muss also die Bedingung einmal den Wahrheitswert WAHR und einmal den Wert FALSCH annehmen. Da hier implizit auch alle Anweisungen erreicht werden müssen, ist das Statement-Coverage bereits aus dem Ergebnis des Branch-Coverage ableitbar.

Noch einen Schritt weiter bei der Betrachtung von Verzweigungen geht Modified Condition/Decision Coverage (MC/DC). Für das MC/DC müssen für jede zusammengesetzte Bedingung alle darin enthaltenen Einzelbedingungen jeweils beide Wahrheitswerte annehmen, um als getestet zu gelten. Dies stellt sicher, dass jede Einzelbedingung auch unabhängig von den anderen beteiligten Einzelbedingungen das Gesamtergebnis bestimmt. In der praktischen Anwendung erweist sich dieses Verfahren allerdings als äußerst aufwendig, da insbesondere bei Schleifen, die bedingte Ausführungssequenzen enthalten, extrem viele Pfade zu betrachten sind.

In der Praxis gibt es nun mehrere Möglichkeiten, das Code-Coverage zu ermitteln:

  • Simulation des ausführbaren Codes auf einer virtuellen Plattform und Ermittlung der notwendigen Information zur Berechnung.

Naturgemäß lassen sich aus Simulationen recht einfach Daten zum Laufzeitverhalten bestimmen, darum ist diese Methode auch bei vielen Test-Tools sehr verbreitet. Nachteilig ist, dass der eigentliche Test nicht auf dem realen Embedded-System durchgeführt wird, das tatsächliche Zeitverhalten also keine Berücksichtigung findet.

  • Instrumentierung des zu testenden Codes und Ausführung auf realer Hardware.

Dabei wird in die Software zusätzlicher Testcode eingefügt, der für die Berechnung des Code-Coverage notwendige Informationen sammelt und diese im Speicher des Embedded-Systems ablegt. Die Konsequenz daraus ist aber, dass der eingefügte Testcode nicht nur das Laufzeitverhalten und die Codegröße, sondern gegebenenfalls sogar das Speicherlayout beeinflusst. Bei sicherheitskritischen Anwendungen oder Systemen mit hohen Echtzeitanforderungen ist dies durchaus ein kritischer Punkt.

  • Ausführung des unveränderten Codes auf der realen Hardware mit gleichzeitigem Programmtrace.

Dafür muss das Zielsystem eine geeignete Trace-Hardware inklusive einer Trace-Schnittstelle zum Coverage-Tool bereitstellen. Diese Methode kommt dann gänzlich ohne Instrumentierung aus, und auch das Zeitverhalten der zu testeten Applikation bleibt unverändert.

Entscheidend für die erfolgreiche Testdurchführung und die Ermittlung des Code-Coverage auf der realen Zielhardware durch Instrumentierung oder Trace sind allerdings entsprechend leistungsfähige Debugger-Lösungen wie die Universal Debug Engine (UDE) von PLS. Denn im instrumentierten Fall muss zumindest der Targetspeicher, der die Daten für die Coverage-Berechnung enthält, durch den Debugger ausgelesen werden. Für die Trace-basierte Lösung bedient der Debugger sogar die gesamte Trace-Infrastruktur auf dem Chip.

Automatischer Test

Für Tests auf realer Hardware von essentieller Bedeutung ist die enge Kopplung zwischen dem Debugger, der ja den eigentlichen Zugang zum Zielsystem ermöglicht, und dem Testwerkzeug, welches die Verwaltung von Testfällen und die Testdokumentation übernimmt. In den seltensten Fällen bieten Debugger nämlich auch eine komplette Projektverwaltung für den Test an. Testtools wiederum fehlt es oft an geeigneten Möglichkeiten, direkt mit dem Zielsystem über die Debug-Schnittstelle zu kommunizieren. Moderne High-End-Debugger wie die UDE stellen deshalb für die Tool-Kopplung eine Automatisierungsschnittstelle bereit, über die sich die Testwerkzeuge die Debugger-Funktionen zum Steuern des Zielsystems und zum Manipulieren sowie Auslesen der Target-Zustände zu Nutze machen können.

Im Falle der UDE handelt es sich bei dieser Automatisierungsschnittstelle um ein auf dem Microsoft Common-Object-Model (COM) basierendes Application-Programming-Interface (API). COM hat sich über einen langen Zeitraum hinweg als De-facto-Standard in der Windows-Welt etabliert. Auch Microsoft selbst bietet einen großen Teil seiner neu hinzukommenden Windows-Funktionen über COM-Schnittstellen an. Das Objektmodell der UDE umfasst nahezu alle Funktionen des Debuggers wie FLASH-Programmierung, Ablaufsteuerung, Lesen und Schreiben von Speicherinhalten des Zielsystems, Trace-Daten-Erfassung/-Analyse und auch das Code-Coverage (Bild 1). Über besagte Schnittstelle ist es also möglich, mit der UDE eine enge Kopplung zwischen Testsystemen verschiedener Hersteller herzustellen und somit eine komplette Tool-Kette für den Test auf realer Hardware aufzubauen. COM bietet außerdem den großen Vorteil, dass es von einer sehr großen Anzahl unterschiedlichster Sprachen nutzbar ist. C, C++, C# und andere .NET-Sprachen zählen hier genauso dazu wie die Skriptsprachen JavaScript, Python, Perl und VB Script oder die windowseigene ¬PowerShell. Die UDE lässt sich also ohne Zuhilfenahme eines Testwerkzeugs auch sehr leicht über eigene Skripte automatisieren und fernsteuern.

Nachdem aus bereits bekannten Gründen zur Bestimmung des Code-Coverage in sicherheitskritischen Anwendungen oder Systemen mit hohen Echtzeitanforderungen eine Instrumentierung des ausgeführten Codes nicht wirklich ratsam ist, kommt in diesen Fällen meist zwangsläufig die bereits erwähnte Trace-basierte Lösung zum Zuge. Auch hierfür bietet die Automatisierungsschnittstelle der UDE entsprechende Funktionen. So kann bei der Steuerung der UDE durch ein Testsystem meist schon bei der Konfiguration der Testaufgaben die zusätzliche Aufzeichnung von Trace für das Code-Coverage aktiviert werden. Die weiteren Einstellungen für das Code-Coverage werden anschließend im Debugger selbst vorgenommen:

  • Verwendete Coverage-Stufe.

Hier kann zwischen Statement- oder Branch-Coverage gewählt werden. Für beide Stufen lässt sich das Coverage alleine durch Programm-Trace und durch die dem Debugger vorliegenden Debug-Informationen der Applikation ermitteln.

  • Akkumulation nacheinander durchgeführter Coverage-Messungen.

Wenn für das Testen aller Funktionen einer Applikation mehrere Ausführungsläufe notwendig sind, weil beispielsweise der Trace-Speicher für das Coverage nicht ausreichend groß ist, ist eine akkumulierte Berechnung des Code-Coverages für alle Ausführungsläufe sinnvoll. Die Ergebnisse der Einzelläufe werden in diesem Fall zu einem Gesamtergebnis zusammengefasst.

  • Einstellungen für die Berichtgenerierung.

Je nach Erfordernis können getrennte Berichte für alle getesteten Funktionen oder ein Gesamtbericht erzeugt werden (Bild 2). Ebenso lassen sich grundlegende Dinge wie Dateinamenskonversionen oder Speicherorte für die jeweiligen Berichte festlegen.

Alle Einstellungen können durch den Nutzer direkt über die Bedienoberfläche des Debuggers angepasst werden. Zudem sind sie aber auch über Funktionen der COM-basierten Automatisierungsschnittstelle zugreifbar. Das Code-Coverage kann somit auch durch Werkzeuge von Drittanbietern oder in eigenen Skripten genutzt werden.

Fazit

Wie der Beitrag verdeutlicht, ist es mittels Code-Coverage prinzipiell gar nicht so schwierig, parallel zum Software- und Systemtest auf realer Hardware gleich auch noch die Testqualität zu ermitteln. Unabdingbar für korrekte und belastbare Ergebnisse ist in jedem Fall ein durchgängiger, lückenloser Workflow für den Software- und Systemtest, der logischerweise offene Schnittstellen zur effizienten Tool-Kopplung voraussetzt.

Eine immer wichtigere Schlüsselfunktion in diesem engen Zusammenspiel verschiedenster Komponenten und Tools spielen moderne High-End-Debugger wie die UDE von PLS, mit denen sich dank einer Vielzahl COM-basierter interner Funktionen faktisch sämtliche Einstellungen wahlweise über Werkzeuge von Drittanbietern oder über die eigenen Skripte vornehmen lassen. Ist dieses Kriterium erfüllt, steht einem vollständig automatisierten Test mit dokumentierter Testqualität nichts mehr im Wege.

Über den Autor:

Dipl.-Inf. Jens Braunes ist Entwicklungsingenieur bei PLS.