Universität Hannover Fakultät für Elektrotechnik und Informatik Institut für Praktische Informatik Fachgebiet Software Engineering Entwicklung eines Tools zur Messung von Testabdeckungen Masterarbeit im Studiengang Angewandte Informatik von Przemyslaw Dul Prüfer: Prof. Dr. Kurt Schneider Zweitprüfer: Dipl.-Math. Christof Graß-De Iuliis Betreuer: Dipl.-Wirt.-Inform. Daniel Lübke Hannover, 18. Oktober 2005 Kurzfassung Die Messung von Testabdeckungen ist ein wichtiges Mittel zur Qualitätskontrolle von Tests. In dieser Arbeit werden zwei völlig verschiedene Verfahren vorgestellt, implementiert und untersucht, die als Basis für ein Werkzeug zur Messung von Tes­ tabdeckungen dienen können. Im Zentrum des Interesses befindet sich der Vergleich der beiden Verfahren. Anhand der Entwicklung und Integration zu einem Werkzeug für die Messung von Testabdeckungen wird eine Vielzahl von Eigenschaften der beiden Verfahren untersucht und miteinander verglichen. Die entwickelten Verfahren haben das Potential in diversen Werkzeugen eingesetzt zu werden, die sowohl Informationen über den Programmablauf in Echtzeit, als auch über die statische Programmstruktur selbst benötigen. Als Grundlage für die Implementierung der beiden Verfahren wird die Eclipse Plat­ form verwendet. Eclipse ist eine erweiterbare Platform zur Erstellung integrierter Software. Die Erweiterbarkeit wird durch sogenannte Plugins ermöglicht. Beide Verfahren und weitere benötigte Komponenten für ein funktionsfähiges Werkzeug zur Messung von Testabdeckungen werden als Plugins für die Eclipse Platform entwickelt. Abstract The measurement of test coverage is an important way to control test quality. This thesis presents and analyses two completely different techniques that serve as a foun­ dation for a test coverage tool. The main purpose of this thesis is to analyse and to compare both techniques with each other. This is realized by comparing the approa­ ches, the implementation and the results of a benchmark. In general, the developed techniques can be used for tools, that need real-time in­ formation about the process and the structure of a program. The implementation depends on the Eclipse Platform. Eclipse is an extensible plat­ form for building integrated software. Extensibility is reached by so-called plug-ins. Both techniques are developed as Eclipse plug-ins. As proof of concept some additio­ nal components are developed as Eclipse plug-ins to provide a test coverage tool that aid developers to control their test quality. Inhaltsverzeichnis 1 Einleitung............................................................................................................................................ 1 1.1 Motivation....................................................................................................................................1 1.2 Aufgabenstellung....................................................................................................................... 2 1.3 Aufbau der Arbeit...................................................................................................................... 3 2 Ansätze und Grundlagen................................................................................................................. 4 2.1 Instrumentierungsansatz...........................................................................................................4 2.1.1 Java Quellcode Instrumentierung....................................................................................................5 2.1.2 Konstruktoren.....................................................................................................................................6 2.1.3 Schleifen- und Bedingungsanweisungen....................................................................................... 7 2.1.4 Typesafe Enumerations.....................................................................................................................8 2.2 Tracingansatz.............................................................................................................................. 8 2.2.1 Java Platform Debugger Architecture (JPDA)............................................................................... 9 2.2.2 Java Virtual Machine (JVM).......................................................................................................... 10 2.2.3 Java Debug Interface (JDI).............................................................................................................. 14 2.3 Vergleich beider Ansätze........................................................................................................ 16 2.4 Testabdeckungsmetriken........................................................................................................ 18 2.4.1 Anweisungs- und Methodenüberdeckung.................................................................................. 19 2.4.2 Zweigüberdeckung..........................................................................................................................20 2.4.3 Pfadüberdeckung............................................................................................................................. 21 2.5 Verwandte Arbeiten.................................................................................................................22 3 Implementierung............................................................................................................................. 25 3.1 Eclipse.........................................................................................................................................25 3.1.1 Runtime und Plugin Architektur...................................................................................................27 3.1.2 Workspace.........................................................................................................................................29 3.1.3 User Interface ...................................................................................................................................31 3.1.4 Java Development Tools................................................................................................................. 36 3.2 Architektur Überblick..............................................................................................................45 3.3 Instrumentierungsverfahren.................................................................................................. 47 3.3.1 Instrumenter..................................................................................................................................... 48 3.3.2 Inter-JVM-Kommunikation............................................................................................................ 50 3.3.3 Probleme............................................................................................................................................52 3.4 Tracingverfahren...................................................................................................................... 52 3.4.1 MarkerAnnotator............................................................................................................................. 54 3.4.2 BreakpointHandler.......................................................................................................................... 55 3.4.3 Probleme............................................................................................................................................56 3.5 Adapter...................................................................................................................................... 57 3.5.1 Schnittstellen.....................................................................................................................................57 3.5.2 Messdaten......................................................................................................................................... 58 3.5.3 Hilfsklassen.......................................................................................................................................59 3.5.4 Probleme............................................................................................................................................59 3.6 Persistence................................................................................................................................. 60 3.7 ResultView.................................................................................................................................61 3.7.1 SummaryDataViewProvider.......................................................................................................... 62 3.7.2 DetailsDataViewProvider............................................................................................................... 63 3.8 Usability..................................................................................................................................... 63 3.9 Vergleich der Implementierungen........................................................................................ 64 4 JUnit als Fallstudie.......................................................................................................................... 66 I 4.1 Vorgehensweise........................................................................................................................66 4.2 Auswertung...............................................................................................................................67 4.2.1 Methoden und Anweisungen........................................................................................................ 68 4.2.2 Builddauer........................................................................................................................................ 68 4.2.3 Ausführungdauer............................................................................................................................ 69 4.2.4 Projektgröße......................................................................................................................................71 4.3 Vergleich der JUnit-Fallstudienergebnisse...........................................................................71 5 Abschlussbetrachtungen................................................................................................................ 73 5.1 Ergebnisse und Fazit................................................................................................................ 73 5.2 Ausblick..................................................................................................................................... 74 II Abbildungsverzeichnis Abbildung 1: Prinzip der Instrumentierung..................................................................................... 5 Abbildung 2: Java Platform Debug Architecture, vgl.[7]................................................................ 9 Abbildung 3: Interne Architektur der Java Virtual Machine........................................................11 Abbildung 4: Ablauf einer Debugging Session...............................................................................15 Abbildung 5: Fakultät-Kontrollflussgraph...................................................................................... 19 Abbildung 6: Eclipse Java-IDE Architektur, vgl. [26] S.282.......................................................... 26 Abbildung 7: Workbench Fenster..................................................................................................... 35 Abbildung 8: ASTNode Hierarchie vgl. [26]................................................................................... 39 Abbildung 9: Abstract Syntax Tree Beispiel aus [26] S.320........................................................... 40 Abbildung 10: Eclipse Platform Debug Model. [31].......................................................................43 Abbildung 11: Breakpoint Model, vgl. [0]....................................................................................... 45 Abbildung 12: Architektur des Werkzeugs zur Messung von Testabdeckungen ....................46 Abbildung 13: Instrumenter-Plugin................................................................................................. 47 Abbildung 14: Instrumenter-Teilsystem.......................................................................................... 50 Abbildung 15: Inter-JVM-Kommunikation..................................................................................... 51 Abbildung 16: Tracer-Plugin............................................................................................................. 53 Abbildung 17: MarkerAnnotator-Teilsystem..................................................................................55 Abbildung 18: Adapter....................................................................................................................... 58 Abbildung 19: Persistence-Plugin.....................................................................................................61 Abbildung 20: ResultView-Plugin.................................................................................................... 62 Abbildung 21: SummaryDataView - Screenshot............................................................................ 63 Abbildung 22: DetailsDataView - Screenshot................................................................................. 63 Tabellenverzeichnis Tabelle 1: Vergleich der Ansätze.......................................................................................................17 Tabelle 2: Testfall für vollständige Anweisungsüberdeckung der Fakultät-Methode.............19 Tabelle 3: Testfälle für vollständige Zweigüberdeckung der Fakultät-Methode...................... 20 Tabelle 4: Testfälle für gute Zweigüberdeckung der Fakultät-Methode.................................... 21 Tabelle 5: Vergleich verschiedener Implementierungsaspekte.................................................. 65 Tabelle 6: Zusammenfassung der JUnit-Fallstudienergebnisse .................................................. 68 Tabelle 7: Vergleich der implementierten Verfahren anhand der JUnit-Fallstudienergebnisse ................................................................................................................................................................ 72 Quellcodeverzeichnis Quellcode 1: Einfaches Instrumentierungsbeispiel..........................................................................5 Quellcode 2: Konstruktorinstrumentierung......................................................................................6 Quellcode 3: Instrumentierung geschachtelter Schleifen-und Bedingungsanweisungen......... 7 Quellcode 4: Instrumentierung einer Typesafe-Enumeration........................................................8 Quellcode 5: Fakultät-Methode......................................................................................................... 19 Quellcode 6: Multiple-Breakpoint-Problem.................................................................................... 57 III 1 Einleitung 1.1 Motivation Wie am Fachgebiet Software Engineering der Universität Hannover werden JavaProgramme an vielen akademischen Institutionen, aber auch in verschiedenen Unter­ nehmen mit der Entwicklungsumgebung Eclipse1 [1] implementiert. Neben der kos­ tenlosen Verfügbarkeit, ist ein weiterer Vorteil von Eclipse gegenüber anderen Entwicklungsumgebungen dessen leichte Erweiterbarkeit durch Plugins2. Für den kommerziellen Einsatz stehen neben der kostenlosen Distribution von Eclipse kom­ merzielle Distributionen zur Verfügung, die die Standardfunktionen deutlich erwei­ tern. Bei der Entwicklung von zuverlässiger Software werden Test-Werkzeuge eingesetzt, um die Qualität der Software zu kontrollieren. Ein bekanntes Werkzeug dieser Art für die Programmiersprache Java ist JUnit3. [2] Dieses ist seit der Eclipse-Version 2 ein Bestandteil der Eclipse-Distribution. JUnit-Tests sind selbst ein Stück Software und werden so wie der zu testende Code von einem Programmierer geschrieben. Sie können automatisch und beliebig oft ausgeführt werden. In einem Test-Prozess sind Entwickler und Tester bestrebt möglichst alle Teile des Quellcodes durch die Tests mindestens einmal auszuführen. Ein JUnit-Test liefert jedoch als Ergebnis im wesent­ lichen nur, ob er gelungen oder misslungen ist. Über die Qualität eines Tests liefert JUnit damit keine Informationen. Ein wichtiges Merkmal der Testqualität ist die so­ genannte Testabdeckung. Sie gibt an, wieviel des zu untersuchenden Quellcodes tat­ sächlich durch die Tests ausgeführt wurde und insbesondere welche Teile des Quell­ codes nicht ausgeführt wurden.4 An diesem Punkt setzt die vorliegende Masterarbeit an. 1 Seit Version 3.0 ist Eclipse tatsächlich eine eine Rich Client Platform, während es vorher als erweiterbare Entwicklungsumgebung konzipiert war. [1] 2 Aus dem Englischen to plug in - einstöpseln, anschließen. Ein Plugin ist ein Zusatzmodul, dass die Software um weitere Funktionalität erweitert. 3 JUnit ist ein Framework zum Testen von Java-Programmen, das besonders für automatisierte Tests einzelner Klassen (Units) geeignet ist. Es basiert auf Konzepten, die ursprünglich unter dem Namen SUnit für Smalltalk entwickelt wurden. Hauptentwickler des JUnit-Frameworks sind Erich Gamma und Kent Beck. Mittlerweile existieren JUnit-ähnliche Frameworks auch für viele andere Programmiersprachen. Oft werden diese Programme unter dem Namen xUnit zusammengefasst. [3] 4 Von einer 100%igen Testabdeckung kann nicht auf die Fehlerfreiheit des Quellcodes geschlossen werden. 1 1.2 Aufgabenstellung Es soll ein Eclipse-Plugin zur Messung von Testabdeckungen entwickelt werden. Hierfür werden zwei verschiedene Verfahren untersucht: ein Instrumentierungsverfah­ ren und ein Tracingverfahren. Während das Instrumentierungsverfahren auf der An­ notation des Quellcodes basiert, verwendet das Tracingverfahren die Ablaufverfol­ gung des Eclipse-Debuggers. Beide Verfahren sollen implementiert und ausführlich miteinander verglichen werden. Dabei sollen die Vor- und Nachteile und damit auch die Performance und die Probleme beider Verfahren diskutiert werden. Zudem soll die Implementierung eine einfache Erweiterbarkeit durch weitere Plugins gewähr­ leisten, um z. B. andere Visualisierungen der Daten zu ermöglichen. Natürlich soll das Plugin einfach zu bedienen sein und sich zusammen mit JUnit verwenden lassen. Piwowarski hat dazu in [4] (S. 5) folgendes festgestellt: „In the survey of IBM testing organizations [...], we found that testers: • were familiar with test case coverage measures, • believed that the use of coverage measures would help them find errors and improve the quality of their products, • but generally did not use test case coverage tools.“ Im darauf folgendem Satz schlussfolgert er: „Testers did not use coverage tools, not because of a lack of knowledge of them, or lack of belief in their worth, but because coverage tools had proved to be to difficult to use.“ Daher ist die Usability ein wichtiger Gesichtspunkt bei der Umsetzung des Plugins. Das in dieser Arbeit zu entwickelnde Plugin zur Messung von Testabdeckungen ist der erste Schritt bei dem Entwurf einer Suite für Eclipse, die verschiedene Aspekte der Softwareentwicklung am Fachgebiet Software Engineering der Universität Han­ nover unterstützen soll. Weitere Pläne umfassen z. B. die Echtzeitvisualisierung von Programmabläufen, Bestimmung von häufig ausgeführten Programmteilen und Dar­ stellung von Abhängigkeiten zwischen Klassen oder Modulen, sowie diverse andere Statistikfunktionen. Soweit möglich soll das in dieser Arbeit zu entwickelnde Plugin diese Pläne durch entsprechende Implementierungen, Schnittstellen und Modula­ risierung unterstützen oder umsetzen. Aufgrund dieses umfassenderen Zusammen­ hanges ergeben sich zwei besondere Anforderungen an das zu entwickelnde Plugin: Erstens soll die Messung der Testabdeckung in Echtzeit möglich sein. Das bedeutet, dass es eine Schnittstelle geben muss, über die weitere Module über jede ausgeführte Anweisung einzeln und unmittelbar benachrichtigt werden können. Zweitens sollen die vom Plugin gesammelten Daten derart umfangreich sein, dass z. B. eine Bestim­ mung und Visualisierung häufig ausgeführter Anweisungen, Methoden und Module möglich ist. 2 1.3 Aufbau der Arbeit Die vorliegende Arbeit ist folgendermaßen strukturiert: zunächst werden die beiden Ansätze des Instrumentierungs- und des Tracingverfahrens vorgestellt und die Grundlagen dieser Ansätze erläutert. In diesem Zusammenhang werden verschie­ dene verwandte Arbeiten zusammengefasst und skizziert. Im Rahmen des Kapitels „Implementierung“ wird zunächst die Eclipse Platform vorgestellt, die als Basis für die Umsetzung der Ansätze dient, und anschließend werden die Bestandteile der Im­ plementierung selbst dargelegt. Anhand des JUnit-Frameworks als Fallstudie werden dann die Eigenschaften der Implementierung untersucht und diskutiert. Ab­ schließend wird ein Fazit gezogen und ein Ausblick auf weitere Entwicklungs­ möglichkeiten geboten. Der Vergleich des Instrumentierungs- und des Tracingverfahrens ist ein zentrales Thema dieser Arbeit. Deshalb werden verschiedene Aspekte der beiden Verfahren auf drei unterschiedlichen Ebenen miteinander verglichen: den Ansätzen und Grundlagen, der Implementierung und der Performance. Die Ebenen spiegeln sich wesentlich im Aufbau dieser Arbeit wieder. Jeder dieser Ebenen ist ein Kapitel ge­ widmet, der einen ausführlichen Abschnitt enthält, in dem beide Verfahren direkt miteinander verglichen werden. Die Performance wird in Kapitel 4 anhand einer Fallstudie untersucht und verglichen. 3 2 Ansätze und Grundlagen Dieser Abschnitt wird zunächst die Grundlagen des Instrumentierungs- und des Tra­ cingansatzes vorstellen. Hierbei wird der Kontext der Eclipse Umgebung außen vor gelassen, um zunächst die wesentlichen Aspekte beider Ansätze zu erläutern und an­ schließend direkt miteinander zu vergleichen. Beide Ansätze zielen in erster Linie dabei auf die Messung der Anweisungs- und Methodenüberdeckung ab. Diese und weitere Testabdeckungsmetriken werden in Abschnitt 2.4 erläutert. Beide Ansätze können jedoch auch für verschiedene andere Zwecke eingesetzt werden, die auf In­ formationen über die Programmstruktur und den Programmablauf angewiesen sind. Zuletzt wird ein Einblick in einige ausgewählte verwandte Arbeiten zum Thema „Messung von Testabdeckungen“ gewährt. 2.1 Instrumentierungsansatz Viele heute zur Verfügung stehenden Werkzeuge zur Messung der Testabdeckung verwenden den Instrumentierungsansatz. Es ist ein besonders einfacher Ansatz und beruht auf der statischen Instrumentierung des Quellcodes. Dabei werden dem Quellcode vor dessen Kompilierung Anweisungen hinzugefügt. Um den original Code zu schützen wird die Instrumentierung auf einer Kopie durchgeführt. Wäh­ rend der Instrumentierung werden zudem alle relevanten statischen Daten des Quellcodes in tabellarischer Form gespeichert. Diese Daten beinhalten Informationen zu den Methoden und Anweisungen des originalen Codes, wie z. B. den Namen einer Methode, die Zeilenangabe oder den Typ einer Anweisung. Das Ziel dabei ist die Minimierung des Overheads bei der Ausführung des instrumentierten Program­ mes, indem so viele Berechnungen wie möglich auf die Instrumentierungsphase und die Auswertungsphase verlagert werden. Bei dem einfachsten Instrumentierungsansatz wird nach jeder Methode und vor je­ der Anweisung zusätzlicher Code beigefügt. Abbildung 1 veranschaulicht das Prinzip der Quellcodeinstrumentierung. Diese Instrumentierung erlaubt die genaue Messung aller ausgeführten Anweisungen, unabhängig an welcher Stelle das Pro­ gramm terminiert oder eine Ausnahme wirft. Die Instrumentierungsanweisungen können beliebigen Code beinhalten. In der Praxis werden über die Instru­ mentierungsanweisungen jedoch nur Informationen über die aktuelle Position im Programm oder gewisse Laufzeitinformationen (z. B. Thread-ID, Objekt-ID oder die Ausführungszeit) gesammelt. Entscheidend für die Auswertung ist, dass eine Zuord­ nung der während der Ausführung des instrumentierten Programmes gesammelten Daten mit den in der Instrumentierungsphase gesammelten Daten möglich ist. 4 Original Quellcode Instrumentierter Quellcode Instrumentierung durch Annotation Abbildung 1: Prinzip der Instrumentierung Der Instrumentierungsansatz führt offensichtlich zu einer erheblichen Zunahme der Programmgröße: Es wird eine Kopie des originalen Quellcodes generiert. Dann wird die Kopie instrumentiert, wodurch die Größe der Kopie circa verdoppelt wird. An­ schließend wird die Kopie übersetzt, so dass auch der Bytecode sich circa verdoppelt. 2.1.1 Java Quellcode Instrumentierung Der folgende Quellcode zeigt exemplarisch die Instrumentierung eines einfachen Hello World Programmes. 1 2 3 public class HelloWorld { 4 5 6 7 8 9 10 } private static CoverageTraceCollectorClient __collector__ = CoverageTraceCollectorClient.getInstance(300); public static void main(String[] args){ __collector__.called( "Example<c__D>example/HelloWorld.java<c__D>0<c__D>"); __collector__.called( "Example<c__D>example/HelloWorld.java<c__D>1<c__D>"); System.out.println("HelloWorld"); } Quellcode 1: Einfaches Instrumentierungsbeispiel Die Anweisungen in Zeile 3,6 und 7 sind Instrumentierungsanweisungen. Der Klasse HelloWorld wird in Zeile 3 ein statisches Objekt vom Typ CoverageTrace­ CollectorClient hinzugefügt. Es ist statisch damit alle Instanzen von Hello­ World darauf zugreifen können, ohne dass für jede neue HelloWorld Instanz ein neues CoverageTraceCollectorClient Objekt erzeugt werden muss. Bei der Deklaration des CoverageTraceCollectorClient Objektes wurde aus Platz­ gründen auf die vollständige Qualifizierung verzichtet. Die Anweisung in Zeile 6 in­ strumentiert die Main Methode, während die Anweisung in Zeile 7 das folgende System.out.println instrumentiert. Aufgrund des Parameters der Instru­ mentierungsanweisungen in Zeile 6 und 7 läßt sich eindeutig rück schließen, welche Methode oder Anweisung ausgeführt wurde: „Example“ identifiziert das Projekt, „example/HelloWorld.java“ den projektrelativen Pfad der Datei und „0“ bzw. „1“ 5 die ID der Anweisung. Die Zeichenkette „<c__D>“ dient als Trennzeichen (Delimiter). Das Beispiel macht deutlich, dass Quellcode-Instrumentierungsverfah­ ren potentiell Namenskonflikte auslösen können. Um die Wahrscheinlichkeit für einen Namenskonflikt zu minimieren wird für das CoverageTraceCollec­ torClient-Objekt ein möglichst ungewöhnlicher und selten vorkommender Name verwendet. In diesem Fall „__collector__“. Bei der Instrumentierung sind weitere kritische Fälle zu beachten, damit der Quellco­ de weiterhin der Spezifikation entspricht und kompilierbar bleibt. Auch Änderungen am ursprünglichen Programmablauf wären fatal. In den folgenden Abschnitten werden verschiedene kritische Fälle diskutiert. 2.1.2 Konstruktoren Konstruktoren dürfen als erste Anweisung einen Aufruf der Methode super() besitzen. super() wird als Aufruf des Superklassenkonstruktors interpretiert. 5 Falls kein Aufruf von super als erste Anweisung im Konstruktor steht, setzt der Compiler an dieser Stelle einen impliziten super-Aufruf ein und ruft damit den parameterlosen Konstruktor der Vaterklasse auf. Alternativ ist es auch erlaubt, mit Hilfe der this-Me­ thode einen anderen Konstruktor der eigenen Klasse aufzurufen. In diesem Fall muss this() ebenfalls die erste Anweisung innerhalb eines Konstruktors sein. Deshalb müssen beim Vorhandensein einer super() oder this() Anweisung in einem Konstruk­ tor, die Instrumentierungen für den Konstruktor und die super() oder this() Anwei­ sung erst hinter dem Superklassenkonstruktoraufruf oder dem Aufruf eines Kon­ struktors der eigenen Klasse hinzugefügt werden. Quellcode 2 demonstriert die In­ strumentierung eines Konstruktors mit super()-Aufruf. Dabei sit zu beachten, dass die Instrumentierungen in Zeile 7 und 8 erst nach dem super()-Aufruf hinzugefügt wurden und nicht wie bei Methoden sonst üblich direkt nach der Methodendeklara­ tion. 1 2 3 public class Constructor { 4 5 6 7 8 9 10 } private static CoverageTraceCollectorClient __collector__ = CoverageTraceCollectorClient.getInstance(300); public Constructor() { super(); __collector__.called( "Example<c__D>example/Constructor.java<c__D>1<c__D>"); __collector__.called( "Example<c__D>example/Constructor.java<c__D>0<c__D>"); } Quellcode 2: Konstruktorinstrumentierung 5 super wird wie eine normale Methode verwendet und kann mit oder ohne Parameter aufgerufen werden. Der Aufruf muß natürlich zu einem in der Superklasse definierten Konstruktor passen. 6 2.1.3 Schleifen- und Bedingungsanweisungen Falls Schleifen- und Bedingungsanweisungen keine Blockklammern (d.h. keine ge­ schweiften Klammern) besitzen, beziehen sie sich auf die direkt folgende Anweisung. Während der Instrumentierung müssen deshalb Blockklammern für diese Schleifenund Bedingungsanweisungen erzeugt werden, bevor die darauf folgende Anweisung instrumentiert werden kann. Hierbei muss jedoch beachtet werden, dass die nach­ folgende Anweisung ebenfalls eine Schleifen- oder Bedingungsanweisung ohne Blockklammern sein kann. Auf diese Weise können Schleifen- und Bedingungs­ anweisungen beliebig tief geschachtelt sein. Einen Sonderfall dieser Schachtelung stellen if-else-Kaskaden dar. Dabei stellt jedes if eine Anweisung auf einer anderen Schachtelungstiefe dar. Die folgenden Quellcodefragmente zeigen die Instrumentierung einer if-else-Kaska­ de, sowie einer geschachtelten while-Schleife. Zu Gunsten der Übersichtlichkeit wurde auf den Parameter der called-Methode verzichtet. Es ist deutlich zu erkennen, dass sich das Erscheinungsbild durch die Instrumentierung erheblich ändert. Instrumentiert Original //if-else-cascade __collector__.called(...); if(true) { __collector__.called(...); System.out.println("1"); } else { __collector__.called(...); if(true) { __collector__.called(...); System.out.println("2"); } else { __collector__.called(...); if(true) { __collector__.called(...); System.out.println("3"); } } } //if-else-cascade //nested while loops __collector__.called(...); while(true) { __collector__.called(...); while(true) { __collector__.called(...); System.out.println("4"); } } } //nested while loops if(true) System.out.println("1"); else if(true) System.out.println("2"); else if(true) System.out.println("3"); while(true) while(true) System.out.println("4"); Quellcode 3: Instrumentierung geschachtelter Schleifen-und Bedingungsanweisungen 7 2.1.4 Typesafe Enumerations Seit der Java Version 1.5 können in Programmen sogenannte Typesafe-Enumerations vorkommen. Eine Typesafe-Enumeration wird durch das Schlüsselwort enum dekla­ riert. Sie besitzt im einfachsten Fall einen Namen und besteht aus einer Elementmenge eben dieses Typs. Optional können nach der Elementmenge zusätz­ lich Attribute, Methoden und parametrisierte Konstruktoren definiert werden. In­ strumentierungsanweisungen dürfen in einem Typesafe-Enumeration-Konstrukt also erst hinter der Elementmenge hinzugefügt werden. Die folgenden Quellcodefragemte zeigen eine instrumentierte Typesafe-Enumeration und die zugehörige originale Definition der Typesafe-Enumeration. Dabei ist zu be­ achten, dass die Initialisierung des __collector__-Objektes erst nach der Aufzäh­ lung von Earth und Mars stattfindet. Instrumentiert Original public enum Planet { Earth(4), Mars(3); public enum Planet { Earth(4), Mars(3); private static CoverageTraceCollectorClient __collector__ = CoverageTraceCollectorClient.get Instance(300); } private int mass; private int mass; Planet(int mass) { __collector__.called(...); __collector__.called(...); this.mass = mass; } Planet(int mass) { this.mass = mass; } public int getMass() { __collector__.called(...); __collector__.called(...); return mass; } public int getMass() { return mass; } } Quellcode 4: Instrumentierung einer Typesafe-Enumeration 2.2 Tracingansatz Fast jede Entwicklungsumgebung verfügt über Werkzeuge, sogenannte Debugger, die das auffinden und beseitigen von Fehlern eines Programmes ermöglichen. Das Tracing ist eine wichtige Debugfunktion6, die das Mitverfolgen des Programmab­ laufes erlaubt. Der Tracingansatz zur Messung der Testabdeckung nutzt diese 6 Unter „Debugging“ wird die Analyse und Bearbeitung eines Programmes verstanden, um Fehler zu finden und zu beseitigen. 8 Funktionalität, um während des Programmablaufes mitzuverfolgen, welche Metho­ den und Anweisungen ausgeführt werden. Der in dieser Arbeit implementierte Tra­ cingansatz verwendet sogenannte Breakpoints (vgl. S. 13) um Informationen über die Tes­ tabdeckung zur Laufzeit des zu untersuchenden Programmes zu erhalten. Zu Gunsten der Ausführungsgeschwindigkeit wird der Quellcode beim Tracingansatz genau wie beim In­ strumentierungsansatz bereits vor seiner Ausführung analysiert, um alle relevanten sta­ tischen Informationen des Quellcodes zu erfassen. Die meisten Java Debugger setzen auf die Java Platform Debugger Architecture auf. Deshalb wird dieser Abschnitt zunächst die Java Platform Debugger Architecture vor­ stellen, die eine standardisierte Schnittstelle für das Debuggen von Java-Programmen be­ reitstellt. Dann wird ein Einblick in die Architektur der Java Virtual Machine gegeben, um zu verstehen, welche Informationen mit Hilfe der Java Platform Debugger Architecture Schnittstellen geliefert werden können. Die folgenden Ausführungen basieren zum großen Teil auf der Diplomarbeit [5] von Mark Brörkens, sowie des Dokumentes [6] von Thomas Weniger und den beiden offiziellen Dokumentationen von Sun Micro­ systems [7] und [8]. 2.2.1 Java Platform Debugger Architecture (JPDA) Die Java Platform Debugger Architecture (JPDA) wurde mit der Version 1.2 des Java SDK eingeführt, um eine einheitliche Architektur für den Zugriff auf die Java Virtual Machine und das darauf laufende Programm (Debuggee) durch Debugger bereitzustellen. Die JPDA besteht aus zwei Schnittstellen (JVMDI und JDI), einem Protokoll (JDWP) sowie zwei Soft­ ware-Komponenten, die diese Elemente miteinander verbinden (Front-End und Back-End) (siehe Abbildung 2). Das entscheidende Feature der JPDA ist die Trennung des Debuggers vom Debuggee. Würde der Debugger im selben Prozess wie das Debuggee ausgeführt, könnten sie sich gegenseitig beeinflussen und stören. VM JVMDI Java Virtual Machine Debugger Interface Debuggee back-end Comm Channel JDWP Java Debug Wire Protocol front-end JDI Java Debug Interface Debugger UI Abbildung 2: Java Platform Debug Architecture, vgl.[7] 9 Java Virtual Machine Debug Interface (JVMDI) Das JVMDI ist ein natives Interface. Es wird von einer Virtual Machine (VM) imple­ mentiert, die das Debugging eines in dieser VM ablaufenden Java Programmes ermöglicht. Das Interface stellt Informationen (z. B. zum aktuellen Stack Frame), Ak­ tionen (z. B. das Setzen von Breakpoints) und Benachrichtigungen (z. B. wenn ein Breakpoint getroffen wurde) bereit. Auf diesem Interface basiert das Back-End wel­ ches die nativen Funktionsaufrufe für ein Plattform unabhängiges Protokoll (JDWP) aufbereitet. Das JVMDI wurde mit Java 1.0 eingeführt und blieb bis zur Version 1.4 nahezu unverändert. Seit der Version 1.4 können Filter für Ereignisse gesetzt werden. Zudem wurde eine Technik namens HotSwap eingeführt, die es ermöglicht Bytecode Classdateien zur Laufzeit und unter gewissen Bedingungen in der Java Vir­ tual Machine auszutauschen. Diese Technik wird innerhalb der Methode Redifine­ Classes() des JVMDI realisiert. In Java 1.5 wurde das JVMDI durch das Java Virtu­ al Machine Tools Interface (JVMTI) ersetzt. Das neue Interface soll eine höhere Per­ formance erzielen und weitere Funktionen bereitstellen. Java Debugger Wire Protocol (JDWP) Das JDWP definiert das Format der Informationen und der Anfragen, die zwischen dem Front-End (im Debugger-Prozess) und dem Back-End (im Debuggee-Prozess) ausgetauscht werden. Es definiert jedoch nicht die Art der Übertragung, so dass hierfür z. B. Sockets, Shared Memory oder andere Übertragungsarten verwendet werden können. Java Debug Interface (JDI) Das Java Debug Interface (JDI) bietet eine vollständig in der Programmiersprache Java implementierte Schnittstelle für das Debugging von Java Applikationen. Es wird vom Front-End implementiert und bereitet die Informationen auf, die das JDWP liefert. Diese Java API (”Application Programming Interface”) ermöglicht den kom­ fortablen Zugriff auf einen Debuggee von einer Entwicklungsumgebung aus. 2.2.2 Java Virtual Machine (JVM) Um zu verstehen, welche Informationen vom Java Debug Interface bereitgestellt werden können, ist ein mindest Verständnis über die Funktionsweise der JVM not­ wendig. Die Java Virtual Machine Spezifikation [9] beschreibt das Verhalten einer JVM durch Subsysteme, Speicherbereiche, Datentypen und Befehle. Diese Komponenten beschreiben abstrakt den inneren Aufbau der JVM. [10] Abbildung 3 zeigt die wichtigsten Subsysteme und Speicherbereiche, die in der Spezifikation beschrieben werden. 10 class loader subsystem class files method area heap Java stacks pc registers native method stacks runtime data areas execution engine native method interface native methods libraries Abbildung 3: Interne Architektur der Java Virtual Machine • class loader Das Classloader Subsystem stellt einen Mechanismus zum Finden und Laden von Java Bytecode Dateien bereit. Es wird zwischen drei Typen von Class Loadern unterschieden: • Der Bootstrap Class Loader ist Teil der Implementierung der Virtual Machine. Der Bootstrap Class Loader von Suns Java 2 SDK sucht ausschließlich in dem Verzeichnis, in welchem die Java Systemklassen untergebracht sind. Der Wert der Umgebungsvariablen Classpath wird nicht ausgewertet. Kann die Klasse mit dem Bootstrap Class Loader nicht gefunden werden, wird als nächstes ge­ prüft, ob die Klasse in einem optionalen Paket (auch als ”Standard Extension” oder einfach ”Extension” bezeichnet) enthalten ist. • Suns Java 2 SDK lädt beim Starten der Virtual Machine automatisch den Sys­ tem Class Loader. Der System Class Loader berücksichtigt den Classpath der Virtual Machine bei der Suche nach Classdateien. • Durch einen benutzerdefinierten (user defined) Class Loader können ClassDateien von unterschiedlichen Orten geladen werden (zum Beispiel: FTP, Http, Dateisystem, etc.). Weiterhin können die Classdateien zum Zeitpunkt des Ladens manipuliert oder gar vollständig neu erstellt werden. 11 • execution engine Die Execution Engine verarbeitet den Bytecode der geladenen Klassen. • runtime data areas Für das Ausführen von Programmen braucht die JVM Speicher. In diesem Speicher werden neben dem Bytecode und weiteren Informationen der Class Dateien auch Objekte, die das Programm instantiiert, Parameter von Metho­ den, Rückgabewerte, lokale Variablen sowie Zwischenergebnisse von Berech­ nungen abgelegt. Die JVM organisiert den Speicher in folgende Bereiche: • Method Area - Der als method area bezeichnete Speicherbereich wird von allen Threads geteilt und speichert für jede Klasse den Bytecode der Metho­ den sowie Daten über Variablen und Methoden. Außerdem ist hier der Runtime Constant Pool zu finden, welcher den Konstanten Pool der ClassDateien zur Laufzeit repräsentiert. Für jede Klasse werden hier Information über Konstanten sowie Referenzen zu Methoden und Variablen verwaltet. Nach dem Laden der Klassen beinhaltet der Runtime Constant Pool symbo­ lische Referenzen zu Variablen und Methoden. Um die Zugriffsgeschwin­ digkeit zu erhöhen, werden diese später durch direkte Zeiger auf die Me­ thoden und Variablen ersetzt. • Heap - Im Heap werden zur Laufzeit erzeugte Elemente abgelegt. • Java Stacks - Die Virtual Machine verwaltet für jeden Thread ein eigenes Befehlsregister, sowie einen eigenen Java Stack. Wird innerhalb eines Threads eine Java-Methode ausgeführt, enthält das Befehlsregister die Adresse des nächsten Befehls und der Stack speichert die Umgebung (loka­ le Variablen, Methoden-Parameter, Rückgabewerte, Zwischenergebnisse bei Berechnungen) der Methode. Wird eine native Methode ausgeführt, werden die Daten auf dem Native Method Stack abgelegt. Der Java Stack ist in einzelne Stack-Frames untergliedert. Ein Stack-Frame speichert die Um­ gebung des Aufrufs einer Methode. Sobald eine Methode aufgerufen wird, legt die Virtual Machine im Stack des aktuellen Threads ein neues StackFrame an. Sobald die Methode beendet ist, wird das dazugehörige StackFrame wieder vom Stack entfernt. Ein Stack-Frame enthält Speicherplatz für lokale Variablen, den Operand-Stack sowie weitere Informationen die von der konkreten Implementierung der Virtual Machine benötigt werden. Auf dem Operand-Stack werden vor dem Aufruf einer Methode die Metho­ den-Argumente sowie die Referenz zur Objektinstanz (falls vorhanden) abgelegt. Nach der Ausführung der Methode befindet sich der Rückgabe­ wert auf dem Operand-Stack. • PC-Registers Die Programmzähler speichern die aktuelle Position im Byte­ code. Jeder Thread hat einen eigenen Programmzähler. • Native Method Stacks - Der native Method Stack stellt Speicherplatz für die Ausführung nativer Methoden bereit. 12 Java Methoden Beim Aufruf einer Java Methode wird zunächst verifiziert, ob die Regeln der Java Sprache eingehalten wurden (z. B. ob der referenzierte Bytecode existiert) und ob die Methode aufgrund von Sicherheitsbestimmungen überhaupt aufgerufen werden darf (z. B. ob eine private Methode einer anderen Klasse aufgerufen wird). Java Programme werden symbolisch gebunden. Das bedeutet, dass die von der JVM geladenen Class-Dateien keine direkten Zeiger auf einzelne Methoden enthalten, sondern symbolische Referenzen. Beim ersten Aufruf einer Methode wird anhand des Namens der Klasse und der Methode, sowie deren Signatur (Anzahl und Typ der Argumente) der Zeiger zum Anfang des Bytecodes der Methode ermittelt. Dieser Zeiger ersetzt dann die symbolische Referenz im Runtime Constant Pool. Wird eine Instanz-Methode (eine nicht als static deklarierte Methode) aufgerufen, wird neben der Referenz zum Bytecode auch eine Referenz zu den Daten des Objek­ tes benötigt. Diese Referenz wird vor dem Aufruf der Methode auf dem OperandStack der aufrufenden Methode abgelegt. Die Argumente der Methode werden eben­ falls auf dem Operand-Stack abgelegt. Bei jedem Aufruf einer Methode wird von der JVM ein neues Stack-Frame erzeugt. Die Methoden Argumente sowie die Objekt-Referenz (falls vorhanden) werden vom Operand-Stack der aufrufenden Methode genommen und auf dem Operand-Stack der aufgerufenen Methode abgelegt. Nach Verlassen der Methode wird der Rück­ gabewert auf den Operand-Stack der aufrufenden Methode geschrieben und das Stack-Frame der beendeten Methode entfernt. Beim Aufruf nativer Methoden wird kein neues Stack-Frame auf dem Java Stack abgelegt. Stattdessen wird ein separater Stack (native method stack) verwendet. Die Verarbeitung nativer Methoden ist von der Implementierung der Virtual Machine abhängig. Exceptions Als Exception wird eine Ausnahme bezeichnet, die den normalen Programmablauf unterbricht. Sie kann durch ein ungewöhnliches Ereignis in einem Programm zur Laufzeit verursacht werden. Tritt eine Exception auf, dann sucht die VM nach Code, der auf diese Exception reagiert. Dafür wird in der Exception Table der Methode ge­ sucht, ob eine Behandlung für diese Ausnahme (durch das Schlüsselwort catch im Quellcode markiert) vorhanden ist. Ist dies nicht der Fall, wird die Suche in den folgenden Methoden auf dem Java Stack fortgesetzt. Breakpoints Breakpoints sind Stellen eines Programmes an denen die Ausführung des Program­ mes angehalten werden soll. Die Spezifikation der JVM sieht für die Realisierung von Breakpoints einen spezielle Opcode vor, der in den Bytecode der Methode eingefügt 13 werden kann. [9] Für das Setzen von Breakpoints muss die Debuggee-VM bereits gestartet worden sein. Falls die Breakpoints früher eingegeben werden sollen, müssen sie vom Debugger zwischengespeichert werden. (Dies geschieht in der JDIImplementierung). Breakpoints können mit den Methoden des Interface Packages com.sun.jdi.request angefordert werden. 2.2.3 Java Debug Interface (JDI) Das Java Debug Interface (JDI) ist eine reine Java API, die Informationen für Debug­ ger und ähnliche Tools bereitstellt, die auf die JVM zugreifen. Das JDI ermöglicht die Kontrolle einer VM über eine andere VM. Beispielsweise können geladene Klassen und Schnittstellen sowie erzeugte Instanzen und Arrays untersucht werden. Darüber hinaus kann auf den Programmablauf durch Manipulation von Variablen, Aufrufen von Methoden oder durch Anhalten und Starten einzelner oder aller Threads einge­ wirkt werden. Ablauf Die Verbindung vom Debugger zum Debuggee wird durch sogenannte Connectors hergestellt. Sobald ein Connector eine Verbindung zwischen Debuggee und Debug­ ger aufgebaut hat, stellt der Connector ein VirtualMachine-Objekt bereit. Es spielt es eine zentrale Rolle beim Umgang mit dem JDI. Das VirtualMachine-Objekt spiegelt nämlich den Zustand der Debuggee-VM wieder. Außerdem gehen nahezu alle weiteren Möglichkeiten bzw. Aktionen des JDI von diesem Objekt aus. Pro Ver­ bindung zu einem Debuggee existiert ein VirtualMachine-Objekt. Es imple­ mentiert das Java Interface com.sun.jdi.VirtualMachine. Dieses Interface ermöglicht unter anderem • das Registrieren für Ereignisse, • die Abfrage von Ereignissen und • die Änderung des Ausführungszustandes. Die Virtual Machine kann angehalten und wieder gestartet werden. Abbildung 4 veranschaulicht den Ablauf eines Debugvorganges. 14 Start Debuggee Für Ereignisse registrieren Verbindung mit Debuggee com.sun.jdi.request com.sun.jdi.connect Debuggee terminiert Auf Ereignisse warten Auf Ereignisse reagieren com.sun.jdi.event Abbildung 4: Ablauf einer Debugging Session Ereignisse Im Gegensatz zu Swing oder dem AWT können im JDI keine Listener für Ereignisse registriert werden. Stattdessen werden Ereignisse beim EventRequestManager angefordert. Tritt ein angefordertes Ereignis ein, so wird es in einer Liste, der EventQueue, gespeichert. Bestimmte Ereignisse können die Debuggee-VM gege­ benenfalls anhalten. Das Auslesen der Ereignisse entfernt diese zugleich aus der Lis­ te. Da mehrere Ereignisse gleichzeitig auftreten können liefert die Liste stets eine Menge von Ereignissen, ein EventSet. Es können folgende selbsterklärende Ereignisse angefordert werden: AccessWatchpointEvent, BreakpointEvent, ClassPrepareEvent, ClassUnloadEvent, ExceptionEvent, LocatableEvent, Metho­ dEntryEvent, MethodExitEvent, ModificationWatchpointEvent, StepEvent, ThreadDeathEvent, ThreadStartEvent, VMDeathEvent, VMDisconnectEvent, VMStartEvent, WatchpointEvent. [8] Nutzung des JDI Um die volle Funktionalität des JDI nutzen zu können und ein Mapping zwischen der Quellcodedatei und der Bytecodedatei zu ermöglichen, muss das zu untersu­ chende Programm Debuginformationen enthalten. Dazu muss schon bei der Über­ setzung des Programms ein entsprechendes Debug-Flag gesetzt werden. Dadurch fügt der Compiler den Class-Dateien zusätzliche Informationen hinzu. Bei diesen In­ formationen handelt es sich unter anderem um den Namen der Quellcodedatei, so­ 15 wie einer Zeilennummer Tabelle, die die Positionen im Bytecode der Class-Datei auf die korrespondierenden Zeilen im Quellcode abbildet. Einschränkungen des JDI Das JDI erlaubt weitreichenden Zugriff auf eine Virtual Machine. Aber gerade in Be­ zug auf den Tracingansatz weist es Einschränkungen auf: • Verschiedene Faktoren führen zu einem beachtlichen Overhead, der die Per­ formance reduziert. Die wichtigsten Ursachen dafür sind die hohe Flexibilität und die daraus resultierende Schichtung der JPDA, die Struktur der Ereignis­ behandlung und die Tatsache, dass Breakpoints von der JDI Implementierung zwischen gespeichert werden müssen. • Werden Breakpoints zum Verfolgen des Programmablaufes verwendet, kann lediglich eine zeilenbasierte Auflösung erreicht werden. Denn Breakpoints können nur zeilenbasiert und nicht anweisungsbasiert gesetzt werden. Damit hat die Formatierung des Quellcodes Einfluss auf die Messung der Testabde­ ckung. • Aufgrund der Natur des JPDA kann die Messung der Testabdeckung nur im Debug Modus ausgeführt werden. 2.3 Vergleich beider Ansätze Basierend auf den oben vorgestellten Grundlagen und Prinzipien der beiden zu un­ tersuchenden Ansätze werden in der folgenden Tabelle 1 die wichtigsten Aspekte beider Verfahren zusammengefasst und gegenübergestellt. Dabei handelt es sich um ein erstes wesentliches Ergebnis dieser Arbeit. 16 Instrumentierungsansatz Tracingansatz • Basiert auf dem Hinzufügen von Instrumentierungsanweisungen in eine Kopie des zu untersuchenden Quellcodes. • Basiert auf den Fähigkeiten des Debuggers mittels der JPDA In­ formationen über das ablaufende Programm zu erhalten. • Es besteht grundsätzlich die Gefahr den Programmablauf oder die Programmlogik durch das Hinzufügen von Instru­ mentierungs-anweisungen zu verändern. • Es besteht keine Gefahr, dass der Programmablauf oder die Pro­ grammlogik beim Tracingansatz verändert werden. • Es besteht grundsätzlich die Gefahr von Namenskonflikten oder der Verletzung der Java Spe­ zifikation, die die Kompilierung unmöglich machen. • Es besteht keine Gefahr, dass Namenskonflikte oder eine Verletzung der Java Spezifikation die Kompilierung stört. • Es ist eine anweisungsbasierte Messung der Testabdeckung möglich. • Es ist nur eine zeilenbasierte Messung der Testabdeckung möglich. • Overhead wird durch zusätzliche Anweisungen im Quell- und Byte­ code generiert. • Overhead wird durch die Anwendung der JPDA generiert. Tabelle 1: Vergleich der Ansätze Tabelle 1 macht deutlich, dass der Hauptnachteil des Instrumentierungsansatzes in der direkten Manipulation des zu untersuchenden Quellcodes und den damit ver­ bundenen Gefahren liegt. Dies ist damit ein immanentes Problem des Instru­ mentierungsansatzes. Die für das Instrumentierungsverfahren benötigten größeren Ressourcen für den instrumentierten Quell- und Bytecode können zu einem Problem werden, wenn eine Messung der Testabdeckung direkt auf einem ressourcen­ beschränkten Gerät (wie z. B. einem Mobiltelefon) ausgeführt werden soll. Die Zu­ nahme der Programmgröße sollte jedoch bei der Entwicklung und Untersuchung von Java Anwendungen auf einem üblichen PC mit der enormen Kapazität heutiger Massen- und Hauptspeicher kein ernsthaftes Problem darstellen. Der Tracingansatz hat keinen dieser Nachteile. Jedoch können die Einschränkungen die durch die Implementierung des Debuggers und allgemein der JPDA-Architektur beträchtlich sein. Dies gilt nicht nur bei der Umsetzung der Ablaufverfolgung, son­ dern auch durch den generierten Overhead während der Ablaufverfolgung. 17 Der Begriff Overhead wird in dieser Arbeit allgemein verwendet und bedeutet, dass größere Ressourcen an CPU-Zeit und Speicher für die Verarbeitung 7 eines Program­ mes aufgrund von Zusatzinformationen benötigt werden. Beide Verfahren gene­ rieren in diesem Sinne einen Overhead bei der Verarbeitung eines zu untersu­ chenden Programmes und der Messung der Testabdeckung. Durch diesen Overhead kann ein zu untersuchendes zeitkritisches Programm sich indeterminiert verhalten oder zumindest anders als ohne jegliche Einwirkung durch die beiden Verfahren. Der Overhead kann sich z. B. negativ auf konkurrierende Threads oder auf Timeouts auswirken und auf diese Weise den Programmablauf auf unbestimmte Art verändern. Die Auswirkung des Overheads beider Verfahren auf die Verarbeitung eines zu un­ tersuchenden Programmes ist eine zentrale Frage dieser Arbeit. Diese Frage wird in Abschnitt 4 anhand einer Fallstudie untersucht und beantwortet. 2.4 Testabdeckungsmetriken Testabdeckungsmetriken ermitteln den von Tests ausgeführten Code und liefern Maßzahlen über den erreichten Grad. Diese Funktionalität erweist sich aus verschie­ denen Aspekten als relevant. Dem Tester gibt sie die Möglichkeit nicht getesteten Quellcode ausfindig zu machen. Die Maßzahl dient dann als Hilfe zu entscheiden, ob das Testen beendet werden kann, weil der geforderte Grad erreicht worden ist. Gleichzeitig können durch verschiedene Testabdeckungsmetriken Ergebnisse präsentiert werden, die eventuell die Sichtweise auf das Testdesign verändern. Für das Management läßt sich durch Testabdeckungsmetriken schnell eine Zahl er­ mitteln, die das Verhältnis von getestetem Quellcode zum Gesamtquellcode aus­ drückt. [11] Es gibt eine Vielzahl von Testabdeckungsmetriken. Im folgenden werden die drei be­ kanntesten Metriken vorgestellt. Einen detailierteren Überblick bietet [12]. Für die Erläuterung der drei Metriken wird der Kontrollflussgraph aus Abbildung 5 benutzt. In einem solchen Graphen werden einzelne Anweisungen oder Blöcke von sequenzi­ ellen Anweisungen als Knoten, und der Kontrollfluss zwischen Anweisungen oder Blöcken als Kanten dargestellt. Der Kontrollflussgraph ergibt sich aus dem Quellcode 5. Dabei handelt es sich um eine Methode zur Berechnung der Fakultät einer Zahl x. Da Fakultäten nur von natürlichen Zahlen und Null berechnet werden können, wird der übergebene ganzzahlige Wert dahingehend durch die Methode überprüft, und gegebenenfalls der Fehlercode -1 zurückgegeben. [13] 7 Der Begriff Verarbeitung bedeutet: Übersetzung, Speicherung und Ausführung. 18 int factorial(int x) { x int result = -1; int result = -1; a then if(x >= 0) { if b result = 1; result = 1; d for(int i=2; i <= x; i++) { for e c g result = result * i; result = result * i; h f } endfor i } endif k return result; } return result; Quellcode 5: Fakultät-Methode Abbildung 5: Fakultät-Kontrollflussgraph 2.4.1 Anweisungs- und Methodenüberdeckung Die Anweisungs- und Methodenüberdeckung (engl. statement, method coverage) zur Ermittlung der Testfälle konzentriert sich auf die einzelnen Anweisungen oder Me­ thoden des Testobjekts. Je nach festgelegtem Ziel sind ein gewisser Anteil oder alle Anweisungen bzw. Methoden im Code des Testobjekts abzuarbeiten. Um in dem Fakultät-Kontrollflussgraphen alle Knoten zu durchlaufen, ist nur ein Testfall nötig: Testfall assert(factorial(2)==2); Ablauf a, b, d, e, f, g, h, i, k Tabelle 2: Testfall für vollständige Anweisungsüberdeckung der Fakultät-Methode Dieser Testfall stellt den spezifikationsgemäßen Gebrauch der Methode dar, d.h. es wird eine natürliche Zahl übergeben. Ein Testfall, der in der Methode eine Aus­ nahmebehandlung verursacht (Rückgabe des Fehlercodes -1), wird dagegen nicht produziert. Dies liegt darin, dass bei der Anweisungsüberdeckung leere Zweige nicht durchlaufen werden. Somit können evtl. fehlende Anweisungen nicht entdeckt werden. Die Anweisungsüberdeckung heißt auch C0-Maß und ist wie folgt definiert: 19 Anzahl ausgeführter Anweisungen Gesamtzahl der Anweisungen Mit obigem Testfall werden sämtliche Anweisungen der Methode factorial aus­ geführt, d.h. die Anweisungsüberdeckung beträgt 100%. Für den Test kann zuvor aber auch eine geringere Anweisungsüberdeckung vereinbart worden sein. Es kann allerdings passieren, dass eine geforderte 100%ige Anweisungsüberdeckung auch durch eine große Zahl von Testfällen nie erreicht wird. Als mögliche Ursache kommt hier unerreichbarer Programmcode (engl. dead code) in Frage, d.h. es gibt im Pro­ gramm Anweisungen, die unter keinen Umständen abgearbeitet werden können. In solchen Fällen ist es sicherlich lohnend, diese Stellen genauer zu untersuchen, um mögliche Defekte als Ursachen aufzufinden. [13] Anweisungsüberdeckung= 2.4.2 Zweigüberdeckung Die Zweigüberdeckung (engl. branch coverage) zur Ermittlung der Testfälle kon­ zentriert sich auf die einzelnen Zweige des Testobjekts. Zweige stellen hier die “We­ gabschnitte” des Kontrollflusses durch den Code des Testobjekts dar. Wiederum sind je nach festgelegtem Ziel ein gewisser Anteil oder alle Zweige im Code des Testobjekts zu durchlaufen. Um in dem Fakultät-Kontrollflussgraphen alle Kanten oder Zweige zu durchlaufen, sind folgende zwei Testfälle nötig: Testfall Ablauf (Kanten) assert(factorial(2)==2); a, b, d, e, f, g, h, i, k assert(factorial(-1)==-1); a, c, k Tabelle 3: Testfälle für vollständige Zweigüberdeckung der Fakultät-Methode Der erste Testfall entspricht dem, aus dem vorherigen Abschnitt und stellt den spezi­ fikationsgemäßen Gebrauch der Methode dar. Der zweite Testfall überprüft die Me­ thode factorial auf die Behandlung einer Ausnahmesituationen. Es wird eine ne­ gative Zahl übergeben, so dass der leere else-Zweig durchlaufen und der Fehlercode -1 zurückgegeben wird. Die Zweigüberdeckung liefert also Testfälle, mit denen fehlende Anweisungen in leeren Zweigen entdeckt werden können. Die Zweigüber­ deckung heißt auch C1-Maß und ist wie folgt definiert: Anzahl ausgeführte Zweige Zweigüberdeckung= Gesamtzahl der Zweige Mit obigen Testfällen werden sämtliche Zweige der Methode factorial ausge­ führt, d.h. die Zweigüberdeckung beträgt 100%. Für den Test kann zuvor aber auch eine geringere Zweigüberdeckung als ausreichend vereinbart worden sein. Prinzipi­ ell gilt, dass wenn nach der Zweigüberdeckungsmethode verfahren wird, mehr Test­ fälle als bei der Anweisungsüberdeckung erstellt werden. Dies liegt darin, dass die 20 Zweigüberdeckung auch leere Zweige berücksichtigt, und somit fehlende Anwei­ sungen entdecken kann. [13] 2.4.3 Pfadüberdeckung Die Pfadüberdeckung (engl. path coverage) konzentriert sich auf die einzelnen Pfade des Testobjekts. Pfade stellen dabei die “Gesamtwege” des Kontrollflusses durch den Code des Testobjekts dar. Je nach festgelegtem Ziel sind wie bei den beiden Metriken zuvor ein gewisser Anteil oder alle Pfade im Code des Testobjekts zu durchlaufen. Da reale Programme aber oft eine sehr große Anzahl von möglichen Pfaden auf­ weisen, ist ein Durchlauf sämtlicher Pfade durch Tests in der Praxis kaum relevant. Ein möglicher Pfad durch den Fakultät-Kontrollflussgraphen wird z. B. durch die Kantenabfolge a, c, k beschrieben (Ausnahmebehandlung der Methode factorial bei negativen x-Werten). Weitere Pfade wären z. B. a, b, d, h, i, k (Schleife wird nicht durchlaufen, d.h. x ist 0 oder 1) oder a, b, d, e, f, i, k (Schleife wird einmal durch­ laufen, d.h. x ist 2). Falls die Schleife mehrmals durchlaufen werden soll, so muß ein Rücksprung zum Schleifenanfang über die Kante g erfolgen. Der zugehörige Pfad für x gleich 3 ist a, b, d, e, f, g, e, f, i, k. Darüberhinaus existieren aber noch weitere Pfade, wie z. B. a, b, d, e, f, g, e, f, g, e, f, i, k. Die Anzahl der Schleifendurchgänge kann durch Vergrößern von x solange erhöht werden, bis die maximal darstellbare Zahl für result auf dem Rechner erreicht ist. Daraus wird deutlich, dass bei der Pfadüber­ deckung extrem viele Testfälle gebildet werden können. Es stellt sich somit die Frage, welche dieser Testfälle sinnvollerweise ausgewählt werden sollen. Das C2-Abde­ ckungsmaß schreibt dazu vor, dass Testfälle generiert werden müssen, die 1. enthaltene Schleifen nicht durchlaufen, 2. enthaltene Schleifen nicht oft durchlaufen, 3. enthaltene Schleifen oft durchlaufen , wobei alle drei Bedingungen durch die Menge der Testfälle abgedeckt werden müssen. Für die Methode factorial entspricht das den folgenden Testfällen: Testfall Ablauf (Kanten) assert(factorial(-1)==-1); a, c, k assert(factorial(0)==1); a, b, d, h, i, k assert(factorial(2)==2); a, b, d, e, f, g, h, i, k assert(factorial(7)==7); a, b, d, [e, f, g]5038, e, f, i, k Tabelle 4: Testfälle für gute Zweigüberdeckung der Fakultät-Methode Der Wert für häufige Schleifendurchläufe wurde hier auf 5038 festgelegt. Die Ent­ scheidung, wie oft eine Schleife im Test durchlaufen werden soll, hängt jedoch stark 21 vom Testobjekt, der zu Verfügung stehenden Hardware und dem zu erwartenden Kosten-Nutzen-Verhältnis ab. [13] 2.5 Verwandte Arbeiten Der Kern der vorliegenden Masterarbeit ist die Untersuchung und Entwicklung zweier Verfahren zur Messung von Testabdeckungen. Die Fachliteratur liefert ver­ schiedene weitere Arbeiten mit interessanten Ansätzen und Implementierungen, die für die Messung von Testabdeckungen geeignet sein könnten. Die Beurteilung dieser Arbeiten ist schwierig, denn sie verfolgen jeweils unterschiedliche Ziele oder ba­ sieren auf unterschiedlichen Grundvoraussetzungen: Einige Arbeiten konzentrieren sich z. B. vor allem auf die Skalierbarkeit und die Performance, andere setzen ihren Schwerpunkt auf die Flexibilität ihrer Werkzeuge. Desweiteren ist häufig unklar wel­ che Metriken sich mit den Werkzeugen umsetzen lassen. Teilweise erlauben diese Werkzeuge nur die Messung von ausgeführten Methoden, nicht jedoch von Anwei­ sungen. In wie weit also die im folgenden vorgestellten Arbeiten tatsächlich die Messung von Testabdeckungen ermöglichen, kann hier nicht vollständig geklärt werden. Anil Chawla und Allessandro Orso beschreiben in [14] ein Framework mit dessen Hilfe sowohl eine statische als auch eine dynamische Instrumentierung des Byteco­ des (d.h. der Class-Dateien) eines Java Programmes möglich ist. Im statischen Modus werden zusätzliche Anweisungen in den vom Compiler generierten Bytecode vor dessen Ausführung durch eine Java Virtual Machine hinzufügt. Im dynamischen Mo­ dus wird der Bytecode zur Laufzeit instrumentiert, indem ein spezieller Classloader verwendet wird. Dies hat den Vorteil, dass nur Klassen instrumentiert werden, die tatsächlich ausgeführt werden. Denn in der Regel ist die Menge der von einem Pro­ gramm zur Laufzeit gebrauchten Klassen deutlich kleiner als die Menge aller statisch vorhandenen Klassen. Desweiteren bietet dieses Framework für sogenannte instru­ mentierbare „Entities“ zusätzliche dynamische Informationen an, die in eine Aus­ wertung mit einbezogen werden können. Z. B. können für das Entity Method Entry die Argumente der Methode erfragt werden. Die Implementierung dieses Frame­ works heißt InsECT (Instrumentation Execution Coverage Tool) und ist frei verfüg­ bar [15]. Die aktuelle Versionsnummer 0.91 deutet jedoch auf eine noch nicht voll­ ständige Implementierung hin. Zudem erfordert die hohe Flexibilität und Konfigu­ rierbarkeit dieses Frameworks einen entsprechend hohen Aufwand in die Ein­ arbeitung und der Verwendung dieses Frameworks. Einen vergleichbaren Weg geht auch Mikhail Dmitriev in [16]. Um eine dynamische Instrumentierung des Bytecodes zu ermöglichen, modifiziert Dmitriev die Java Virtu­ al Machine um eine besonders effiziente HotSwap Funktion. Der Anwender kann über eine graphische Benutzeroberfläche (GUI) verschiedene Konfigurationen vor­ 22 nehmen und z. B. die zu untersuchenden Methoden auswählen. Das System aus der modifizierten Java Virtual Machine und der GUI heißt JFluid. JFluid (als Milestone 7) steht als Erweiterung für die von Sun Microsystems geförderte Entwicklungsumge­ bung NetBeans frei zur Verfügung. [17] Das Java Instrumentation API (JIAPI) Framework ermöglicht ebenfalls die statische und die dynamische Manipulation des Bytecodes, bevor dieser von der Java Virtual Machine geladen und ausgeführt wird. [18] Die Instrumentierung wird in JIAPI durch eine Kette von verschiedenen Instrumentierern ausgeführt. Dabei verändert je­ der Instrumentierer die Liste der Anweisungen einer Methode und reicht diese Liste weiter an den nächsten Instrumentierer in der Kette. Auf diese Weise ermöglicht JIA­ PI weitreichende Veränderung des Bytecodes. Für das Sammeln von Daten über den Programmablauf des instrumentierten Bytecodes können Listener registriert werden, die über bestimmte Ereignisse benachrichtigt werden. Die aktuelle Versionsnummer 0.3.1 deutet allerdings darauf hin, dass das Framework noch nicht ganz ausgereift ist. Die Arbeit [19] von Don Lance erläutert auf verständliche Art und Weise das Prinzip der Analyse und der direkten Manipulation von Java Bytecode. Die Java Instrumentation Engine (JIE) ist ein Framework, welches auf der Instru­ mentierung des Quellcodes basiert. [20] Deshalb ist die JIE besonders mit dem in dieser Arbeit vorgestellten Instrumentierungsansatz vergleichbar. Besonders inter­ essant ist zudem das auf der JIE zugehörigen Internetseite ([20]) veröffentlichte Desi­ gndokument. Es gibt Auskunft über die verschiedenen Entwurfsentscheidungen und Schwierigkeiten des Frameworks. Beim JIE-Framework wird die Instrumentierung über eine XML Konfigurationsdatei spezifiziert. Diese Konfiguration legt die zu in­ strumentierenden Stellen im Quellcode fest und bestimmt welche Aktionen an jeder dieser Stellen ausgeführt werden. Für die Generierung eines Abstract Syntax Tree (AST) des Quellcodes nutzt das JIE Framework den von Kevin Tao entwickelten Java Tree Builder (JTB). [21] Nur mit Hilfe eines AST ist die präzise und sichere Instru­ mentierung von Quellcode möglich. Deshalb hat Eran Tromer, der Entwickler der JIE, in seinem Design Dokument verschiedene weitere Werkzeuge zur Generierung eines AST vorgestellt und miteinander verglichen. Das JIE Framework wird jedoch seit 1999 nicht mehr weiterentwickelt. Das von Bruce Childers et al. entwickelte Framework SoftTest basiert auf einem An­ satz [22], der dem in dieser Masterarbeit vorgestellten Tracingansatz ähnelt. Das Framework verwendet Breakpoints um Informationen über den Programmablauf zu erhalten. Bei den verwendeten Breakpoints handelt es sich um sogenannte fast Break­ points, die dynamisch und besonders schnell hinzugefügt und wieder entfernt werden können, und so den Overhead minimieren. SoftTest ermöglicht die Spezifi­ kation von Tests und führt dazu eine eigene Test-Spezifikationssprache ein. Für die Erstellung einer solchen Spezifikation wird eine graphische Benutzeroberfläche be­ reitgestellt. Die Testspezifikation wird auf einer Test Virtual Machine (TVM) ausge­ führt, die zur Laufzeit die Instrumentierung des zu untersuchenden Programmes 23 vornimmt. Diese TVM ist als integrierter Bestandteil der IBM Jikes Java Research Vir­ tual Machine implementiert worden. Als konkretes „Proof of Concept“ für das Soft­ Test Framework hat das Team um Bruce Childers ein Werkzeug zur Messung der Pfadabdeckung implementiert. Ähnlich wie das oben vorgestellte InsECT Frame­ work bietet das SoftTest Framework eine große Flexibilität und Konfigurierbarkeit, auf Kosten der Einarbeitungszeit und Verwendbarkeit dieses Frameworks. Es existieren noch viele weitere Lösungsansätze für das Instrumentierungsproblem, die nicht alle vorgestellt werden können. Zuletzt wird nun die Arbeit „Efficient Use of Code Coverage in Large-Scale Software Development“ von Young Woo Kim vorgestellt. [23] Kim untersucht verschiedene Metriken und Verfahren, um den Aufwand (Overhead) bei der Analyse von Testabdeckungen zu minimieren und gleichzeitig die Wahr­ scheinlichkeit Fehler aufzudecken zu maximieren. Denn er hat festgestellt, dass der Aufwand zur detailierten Untersuchung (auf Anweisungsbasis) insbesondere für sehr umfangreiche Software Projekte zu groß ist. Seine Liste an Vorschlägen für die Analyse umfangreicher Projekte ist lang und basiert auf verschiedenen Feststel­ lungen. So ist z. B. eine Pareto Verteilung zwischen den Fehlern in einem Programm und seiner Größe bzw. Komplexität8 zu beobachten. Das bedeutet, dass ca. 80% aller Fehler von nur ca. 20% des Programmes ausgehen, während sich die restlichen 20 % der Fehler auf die restlichen 80% des Programmes verteilen. Desweiteren hat Kim bei der Suche nach Fehlern in Programmen eine positive Beziehung (Korrelation) zwi­ schen der Messung der Methodenabdeckung und der Messung der Anweisungsab­ deckung ermittelt, als auch eine positive Korrelation zwischen der Blockabdeckung9 und der Pfadabdeckung. Deshalb schlägt Kim vor bei der Analyse besonders um­ fangreicher Software zunächst allgemein für das ganze Programm die Methodenab­ deckung zu messen und detailiertere Messungen selektiv nur in Modulen auszufüh­ ren, in denen Fehler gefunden wurden, oder die ganz neu entwickelt wurden. Des­ weiteren reicht es auf Grund der positiven Korrelation zwischen der Block- und der Pfadabdeckung nur eine von beiden Metriken für die Untersuchung heranzuziehen. 8 Kim hat festgestellt, dass die Programmgröße (in lines of code) mit der Komplexität des Programmes korreliert. [23](Seite 3) 9 Block coverage: Ensures that all basic blocks are executed. A basic block is a sequence of instruction that is free of branches and function calls. [23](Seite 3f) 24 3 Implementierung Die Implementierung des Instrumentierungs- und Tracingverfahrens beschränkt sich auf deren Umsetzung als Eclipse-Plugin. Dieser spezielle Kontext vereinfacht in wei­ ten Teilen die Implementierung der beiden Verfahren, weil auf verschiedene Me­ chanismen und Funktionen der Eclipse Platform zurückgegriffen werden kann, ohne diese selbst implementieren zu müssen. Jedoch erfordert dieser Kontext zunächst den Erwerb von Grundkenntnissen über die Eclipse Platform und schließlich die Ein­ arbeitung in das Eclipse Framework selbst. Deshalb soll der nächste Abschnitt einen Überblick über die Eclipse Platform geben und die wichtigsten Komponenten für die Umsetzung der beiden Verfahren einführen. Danach wird die Architektur des entwi­ ckelten Werkzeugs vorgestellt und anschließend werden alle Komponenten der Ar­ chitektur erläutert. 3.1 Eclipse Die Eclipse Platform ist eine universelle Platform für nahezu beliebige Werkzeuge. Meist jedoch wird Eclipse als eine freie Entwicklungsumgebung verwendet. Bis ein­ schließlich zur Version 2.1 war Eclipse als erweiterbare IDE konzipiert. Seit der Versi­ on 3.0 ist Eclipse selbst nur der Kern, der die einzelnen Erweiterungen, sogenannte Plugins lädt. Diese Plugins stellen dann die eigentliche Funktionalität zur Verfügung. Der Kern von Eclipse nennt sich Rich Client Platform (RCP) und basiert auf dem OSGI-Standard [1], [24]. Die mit Eclipse 3.0 eingeführte Rich Client Platform stellt ein generisches Framework für eine weite Klasse von Eclipse-Anwendungen bereit. Die Eclipse Platform steht für verschiedene Betriebssysteme zur Verfügung, wie z. B. Windows, Linux, Solaris und Mac OS. Sowohl Eclipse als auch die Plugins sind voll­ ständig in Java implementiert. Jedoch gibt es Plugins, die die Verwendung von Eclip­ se als IDE für verschiedene andere Programmiersprachen wie z. B. Perl, C/C++ und Cobol ermöglichen. Die Eclipse Java IDE Funktionalität ist damit nur ein heraus­ ragendes Beispiel für ein Eclipse Plugin. Für Entwickler stellt Eclipse (SDK) ein umfangreiches Framework für Java Plugins zur Verfügung. Die Eclipse-SDK-Distribution unterstützt die Entwicklung von Plug­ ins, die die Umsetzung von IDEs für weitere Programmiersprachen erleichtern, in­ dem zusätzliche generelle APIs bereitgestellt werden. So stehen mit den graphischen SWT- und JFace-Bibliotheken Alternativen zu Suns AWT- und Swing-Bibliotheken bereit. Die SWT- und JFace-Bibliotheken gestatten es, graphische Oberflächen zu er­ stellen, die in Bezug auf Ansprechverhalten und Look&Feel mit dem Verhalten na­ tiver (also in C/C++ entwickelter, dem Betriebssystem eigener) graphischer Oberflä­ 25 chen nahezu identisch sind.10 Über die GUI-Bibliotheken hinaus stehen höhere Klassen und Konzepte wie Editoren, Viewer, Ressourcenverwaltung, Aufgabenver­ waltung, Problembehandlung, Debugmodel, Hilfesystem, verschiedene Assistenten und Wizards zum Einbau in eigene Plugins bereit. Alle diese Klassen und Konzepte werden von Eclipse selbst z. B. bei der Implementierung der Workbench oder bei der Java-IDE verwendet. Eclipse stellt den Nachfolger für IBMs Visual Age dar. Die Entwicklungskosten sollen mehr als 40 Millionen Dollar betragen haben. Der Quellcode für Eclipse wurde dann von IBM freigegeben. Die Verwaltung und Entwicklung von Eclipse wurde von der Eclipse-Foundation, einer Non-Profit Organisation übernommen, in der IBM allerdings eine einflussreiche Position einnimmt. Der gesamte Quellcode von Eclipse steht offen zur Verfügung, kann jederzeit eingesehen werden, sowie als Aus­ gangspunkt für eigene Entwicklungen dienen. Das Lizenzierungsmodell von Eclipse erlaubt es Anwendern, die Klassen des Eclipse Frameworks in eigene Anwendungen einzubetten, zu verändern und als Bestandteil der eigenen Anwendungen mit auszu­ liefern, ohne dass Lizenzgebühren fällig werden. [25] Die folgende Abbildung 6 veranschaulicht die wesentliche Java-IDE Architektur von Eclipse. Die oval eingekreisten Elemente sind dabei von besonderer Wichtigkeit für die Umsetzung des Plugins zur Messung der Testabdeckung. Die folgenden Ab­ schnitte werden die verschiedenen Bestandteile der Abbildung 6 erläutern. Die Ab­ schnitte 3.1.1, 3.1.2 und 3.1.3 basieren auf [27]. UI Workbench JFace Java SWT Java Development Tools AST (Abstract Syntax Tree) Debug Core Marker, Nature & Builder Workspace Runtime Plugin Infrastructure Abbildung 6: Eclipse Java-IDE Architektur, vgl. [26] S.282 10 Die SWT Bibliothek erreicht dies durch den maximalen Gebrauch betriebssystemeigener API und Ressourcen. 26 3.1.1 Runtime und Plugin Architektur Ein Plugin ist die kleinste funktionale Einheit der Eclipse Platform, die separat entwi­ ckelt und ausgeliefert werden kann. Während einfache Eclipse Anwendungen häufig nur aus einem einzigen Plugin bestehen, ist die Funktionalität komplexerer Eclipse Anwendungen meistens auf mehrere Plugins verteilt. Bis auf einen kleinen Kern, der sogenannten Platform Runtime, ist die gesamte Eclipse Funktionalität in den Plugins untergebracht. Jedes Plugin verfügt über eine sogenannte Manifest-Datei. Dabei handelt es sich um eine Beschreibung des Plugins und seiner Beziehungen zu anderen Plugins in Form einer XML-Datei.11 Die Beziehung zu anderen Plugins ist einfach: ein Plugin dekla­ riert beliebig vieler sogenannter Erweiterungspunkte (engl.: extension points) und Erweiterungen (engl.: extensions) zu einem oder mehreren Erweiterungspunkten anderer Plugins. Ein Erweiterungspunkt darf von anderen Plugins also erweitert werden. Zum Beispiel deklariert das Workbench-Plugin einen Erweiterungspunkt für Einstellungen. Jedes Plugin kann durch Erweiterung dieses Erweiterungspunktes seine eigenen Einstellungen hinzufügen. Ein Erweiterungspunkt kann zudem eine entsprechende Java API (Application Programming Interface) bereitstellen. Andere Plugins können Implementierungen dieser Schnittstelle durch Erweiterung des zuge­ hörigen Erweiterungspunktes liefern. Jedes Plugin darf neue Erweiterungspunkte definieren und neu API für andere Plugins bereitstellen. Zur Identifikation besitzt je­ der Erweiterungspunkt einen global eindeutigen Namen. Die Manifest-Datei ist die einzige absolut notwendige Komponente eines Plugins, alle anderen Komponenten wie Java Bytecode, fremde und/oder native Bibliotheken und andere Ressourcen wie Bilder oder Texte sind optional. Ein Plugin, das z. B. eine Online-Hilfe in Form von HTML Seiten anbietet, könnte ganz ohne Java Code aus­ kommen. Alle Komponenten eines Plugins befinden sich zusammen in einem eigenen Plugin-Unterverzeichnis der Eclipse Installation. Nach dem Start überprüft das Platform-Runtime-Modul anhand der Manifest-Datei­ en, welche Plugins vorhanden sind und registriert diese. Die eindeutige Bezeichnung der Erweiterungspunkte wird für die Identifizierung der zugehörigen Erweiterungen verwendet. Auf diese Weise können Probleme, wie z. B. Erweiterungen zu fehlenden Erweiterungspunkten entdeckt und aufgezeichnet werden. Die Registry der Plugins ist durch die Platform API zur Laufzeit zugänglich. Allerdings können keine neuen Plugins zur Laufzeit der Registry hinzugefügt werden. Manifest-Dateien werden in XML definiert. Ein Erweiterungspunkt kann neue, spezi­ elle XML-Elemente deklarieren. Dadurch können Plugins, die diesen Erweiterungs­ punkt nutzen, Informationen an das Plugin geben, welches den Erweiterungspunkt 11 Seit Eclipse 3.0 übernimmt das neue OSGi Bundle Manifest (MANIFEST.MF) einige Funktionen des alten XML-Manifests (plugin.xml). Bei vielen aktuellen Plugins koexistieren beide ManifestDateien nebeneinander. 27 bereitstellt. Desweiteren sind die Manifest-Informationen der Registry zugänglich, ohne das zugehörige Plugin aktivieren und seinen Code laden zu müssen. Diese Eigenschaft ist der Schlüssel, um eine Vielzahl installierter Plugins zu unterstützen, während nur ein kleiner Teil davon für einen Arbeitsvorgang tatsächlich gebraucht wird. Durch die Verwendung der Manifest-Datei kann der optionale Bytecode eines Plug­ ins erst dann in den Speicher geladen, wenn irgendwelche seiner Funktionen zum ersten Mal angesprochen werden.12 Solange der Code eines Plugins nicht geladen wurde, hat es einen unbedeutenden Speicherplatzverbrauch und einen vernach­ lässigbaren Einfluß auf die Startzeit von Eclipse. Wird ein Plugin aktiviert, kann es in der Registry nach Erweiterungen für seine Erweiterungspunkte suchen, die Informa­ tionen der Registry über diese Erweiterungen nutzen und die Erweiterungen bei be­ darf ebenfalls aktivieren und benutzen. Zum Beispiel kann das Workbench-Plugin mit dem Erweiterungspunkt für die Einstellungen, die bereitgestellten Namen aller zugehöriger Erweiterungen nutzen, um diese in einer Liste anzuzeigen. Hierfür rei­ chen allein die Informationen der Registry aus, ohne dass die entsprechenden Plug­ ins aktiviert werden müssen. Diese Plugins werden erst aktiviert, wenn ein Anwender eine Einstellung (also ihren Namen) aus der Liste auswählt. Für eine sol­ che explizite Aktivierung stehen besondere API-Methoden bereit. Ein einmal ak­ tiviertes Plugin bleibt solange aktiv bis die Platform beendet wird. Jedes Plugin besitzt ein spezielles Verzeichnis, wo es seine (Zustands-)Daten ablegen kann, um sie bei einem Neustart zu nutzen. Die Eclipse Platform benötigt eine Java Virtual Machine, um ausgeführt werden zu können. Jedes Plugin bekommt seinen eigenen Java Classloader zugewiesen, der aus­ schließlich für das Laden der Klassen (und Java Resource Bundles ) des jeweiligen Plugins verantwortlich ist. Jedes Plugin deklariert explizit welche Klassen anderer Plugins es direkt benötigt. Desweiteren kontrolliert ein Plugin die Sichtbarkeit und damit den Zugriff auf seine öffentlichen (public) Klassen und Schnittstellen. Diese In­ formation wird in der Manifest-Datei deklariert. Die Einhaltung der Sichtbarkeitsbzw. Zugriffsregeln wird zur Laufzeit durch die Plugin-Classloader überwacht und durchgesetzt. Der Plugin-Mechanismus partitioniert sogar die Eclipse Platform. Selbst die Runtime Komponente besitzt ihr eigenes Plugin. Nicht graphische Eclipse Anwendungen können einfach das Workbench-Plugin und alle darauf basierenden Plugins weg­ lassen. Die Runtime Komponente deklariert einen speziellen Erweiterungspunkt für Anwendungen. Wenn die Platform gestartet wird, wird der Name der zu startenden Anwendung über die Kommandozeile spezifiziert. Nur das von der Anwendung de­ klarierte Plugin wird zu Beginn aktiviert. 12 Dies wird in der Literatur Lazily-Instantiation oder Lazily-Loading genannt und wird über die Java Reflection-API realisiert. Vgl. [26]. 28 Die Eclipse Platform besitzt einen Update-Manager, der neue Plugins (als Feature) herunterladen, installieren, wieder deinstallieren und bestehende Plugins ak­ tualisieren kann. Eine sehr ausführliche Beschreibung über die Plugin-Architektur findet sich in [28]. 3.1.2 Workspace Die vielfältigen Eclipse-Anwendungen operieren auf normalen Dateien, die sich im sogenannten Workspace des Anwenders befinden. Der Workspace kann mehrere Projekte gleichzeitig beinhalten. Alle Dateien des Workspace sind hierarchisch in Projekten organisiert, die mit Verzeichnissen im Dateisystem assoziiert sind. Die Plat­ form stellt für integrierte Eclipse-Anwendungen eine API bereit, um mit WorkspaceRessourcen (das ist die allgemeine Bezeichnung für Projekte, Verzeichnisse und Da­ teien) zu arbeiten. Jedoch sind alle Dateien im Workspace auch von Außen, also durch externe Anwendungen, zugänglich und können bearbeitet werden. Die Workspace API stellt einen generellen Mechanismus zum Verfolgen von Ressourcen-Änderungen bereit. Durch die Registrierung eines Ressource-ChangeListeners können Plugins sich über die Erstellung neuer Ressourcen, ihre Löschung und über Änderungen ihres Inhaltes benachrichtigen lassen. Bei den Benachrichti­ gungen handelt es sich um sogenannte after-the-fact Benachrichtigungen, die also nach dem Ereignis verschickt werden. Die Platform schiebt die Benachrichtigung je­ weils bis zum Schluß einer Reihe von Ressourcen-Manipulationen auf. Die Benach­ richtigungen haben die Form eines Delta-Baumes (resource tree delta), der die Reihe von Ressourcen-Manipulationen in der Terminologie von Erstellung, Löschung und Änderung genau beschreibt. Die Verwendung einer Baumstruktur für die Benach­ richtigung über Ressourcen-Manipulationen ist nützlich und effizient für Werkzeuge, die Ressourcen als Baum graphisch darstellen, denn jedes Ressource-Delta des Be­ nachrichtigungsbaumes zeigt welche graphischen Elemente hinzugefügt, entfernt oder aktualisiert werden müssen. Desweiteren können Plugins sich durch diesen Me­ chanismus präzise über Veränderungen bestimmter Ressourcen informieren, die zur gleichen Zeit von einem anderen Plugin manipuliert werden. Plugins können am sogenannten Save-Restore-Prozess partizipieren, um mit dem Workspace über mehrere Sitzungen koordiniert zu bleiben. Dieser zwei Phasen Prozess gewährleistet in der ersten Phase die Speicherung des Zustandes aller beteiligter Plugins bei ihrer Beendigung. In der zweiten Phase, wenn ein Plugin reaktiviert wird, erhält es ein Workspace-Weiten Ressource-Delta, der alle Unterschiede der Ressourcen seit seiner letzten Speicherung beinhaltet. Auf diese Weise ist ein Plugin bei seiner Aktivierung in der Lage seinen gespeicherten Zustand entsprechend der ausgeführten Ressourcen-Änderungen während seiner gesamten Deaktivierung anzupassen. 29 Marker Die Workspace-API stellt einen Marker-Mechanismus bereit, mit dem sich einzelnen Ressourcen Markierungen hinzufügen lassen. Von besonderer Wichtigkeit ist, dass Marker beliebige zusätzliche Informationen in Form von benannten Attributen auf­ nehmen können. Auf diese Weise lassen sich bestimmte Meta-Informationen über einen Marker mit einer Ressource assoziieren. Marker werden für verschiedenste Zwecke verwendet, wie z. B. Compiler-Fehler, To-Do-Listen, Lesezeichen (Book­ marks), Suchergebnisse und Debug-Breakpoints. Plugins können neue Marker-Sub­ typen in der Manifest-Datei deklarieren und kontrollieren, ob sie persistent gespei­ chert werden. Nature Der Nature-Mechanismus erlaubt es einem Projekt eine Charakteristik in Form einer Natur zuzuweisen. Z.B. kennzeichnet die Web-Site-Nature ein Projekt, das Web-Sei­ ten beinhaltet, während die Java-Nature ein Projekt kennzeichnet, das Java Quellco­ de beinhaltet. Der wichtigste Zweck einer Natur ist es eine Verknüpfung zwischen einem Plugin, bzw. Werkzeug und einem Projekt herzustellen. Hierfür können neue Naturen deklariert werden. Über die zur Natur zugehörige API lassen sich entspre­ chende Konfigurationen an Projekten mit dieser Natur vornehmen. Genauer: Wenn eine Natur einem Projekt hinzugefügt wird, dann wird die configure-Methode dieser Natur aufgerufen. Dies gibt dem Plugin die Gelegenheit bestimmte In­ itialisierungen an dem Projekt vorzunehmen, wie z. B. einen passenden Builder, Lis­ tener oder sonstige Meta-Daten für das Projekt zu initialisieren. Entsprechend wird die deconfigure-Methode einer Natur aufgerufen, wenn diese Natur von einem Projekt entfernt wird. Das Plugin kann dadurch die installierten Builder und Listener wieder entfernen und die Meta-Daten löschen. Ein Projekt kann durchaus mehrere verschiedene Naturen zugleich besitzen. Dadurch können mehrere Plugins mit un­ terschiedlichen Werkzeugen am selben Projekt arbeiten, ohne sich kennen zu müssen. Zudem läßt sich über bestimmte Bedingungen kontrollieren ob eine Natur einem Projekt hinzugefügt werden kann: • Bei einer one-of-Bedingung wird eine Natur nur dann hinzugefügt, falls keine andere Natur aus einer vorgegebenen Menge aktiviert ist. Dies ist nützlich bei in Konflikt zu einander stehenden Naturen. • Bei einer requires-Bedingung wird eine Natur nur dann hinzugefügt, falls eine andere vorgegebene Natur bereits aktiviert ist. Dies ist bei Naturen nützlich, die auf das mit der vorgegebenen Natur verbundene Verhalten angewiesen sind. Die Eclipse Platform stellt sicher, dass diese Bedingungen eingehalten werden. Dies gilt für das Hinzufügen und das Entfernen von einzelnen Naturen aus einem Projekt, aber auch wenn mehrere Naturen einem Projekt gleichzeitig hinzugefügt oder ent­ 30 fernt werden. Die Plattform führt diesen Vorgang dann seriell in einer Reihenfolge aus, die diese Bedingungen einhält. Builder Compiler oder ähnliche Werkzeuge müssen in der Lage sein eine große Menge von Dateien zu analysieren, zu transformieren und den Ablauf dieser Aufgaben zu ko­ ordinieren. Die Eclipse Platform stellt dafür einen inkrementellen Projekt-Builder zur Verfügung. Als Eingabe erhält ein inkrementeller Projekt-Builder einen Delta-Baum, der die Unterschiede seit dem letzten Ablauf des Builders kapselt. Hoch entwickelte Werkzeuge können dadurch sehr aufwendige Aufgaben auf eine sehr skalierbare Art und Weise ausführen. Denn nach der ersten Ausführung des Builders muss dieser nur noch die Änderungen verarbeiten, die seit seiner letzten Ausführung vorgenom­ men wurden. Für eine Projekt können mehrere Builder gleichzeitig registriert sein. Die Reihenfolge mehrerer Builder wird in der Regel durch die requires-Beziehung von Naturen bestimmt. Sie kann aber auch manuell über die API oder die GUI vom Anwender festgelegt werden.13 Eine sehr ausführliche Beschreibung über Projekt-Builder und Naturen findet sich in [29]. 3.1.3 User Interface Die Benutzeroberfläche (User Interface, kurz UI) der Eclipse Plattform beruht auf einer dreiteiligen Architektur: • Als Basis dient das Standard Widget Toolkit (SWT). Das SWT ist ein Satz von graphischen Komponenten (Widgets) und Graphikbibliotheken. Es nutzt so­ weit wie möglich das native fensterbasierte Betriebssystem, ist aber mit einer betriebssystemunabhängigen API ausgestattet. • JFace erleichtert herkömmliche graphischer Oberflächen mit SWT. • Die Workbench nutzt sowohl SWT, als auch JFace und bildet eine über­ greifende Struktur, die dem Anwender der Eclipse Platform eine erweiterbare graphische Oberfläche zur Verfügung stellt. Aufgaben bei der Programmierung 13 Die Reihenfolge der Builder manuell zu verändern wird weder über die API noch über die GUI empfohlen. Vgl. [29]. 31 Standard Widget Toolkit (SWT) Das Standard Widget Toolkit stellt eine allgemeine und betriebssystemunabhängige14 API zur Verfügung. Die Art der Implementierung ermöglicht eine eng gekoppelte Integration mit dem zugrundeliegenden nativen fensterbasierten Betriebssystem. Die gesamte Oberfläche der Eclipse Plattform und der eingebundenen Plugins benutzen SWT, um dem Benutzer Informationen darzustellen. Die immerwährende Herausforderung bei dem Design eines graphischen Werkzeug­ satzes ist die Gegensätzlichkeit zwischen portierbaren Werkzeugsätzen und der In­ tegration des nativen fensterbasierten Betriebssystems. Das Java AWT (Abstract Window Toolkit) stellt graphische Komponenten auf niedrigem Niveau zur Verfü­ gung, wie z. B. Listen, Textfelder und Buttons. AWT Komponenten basieren direkt auf den graphischen Komponenten der unterstützten fensterbasierten Betriebssyste­ me. Eine Benutzeroberfläche nur mit AWT zu erstellen, bedeutet auf dem kleinsten gemeinsamen Nenner aller fensterbasierten Betriebssysteme zu programmieren. Der Java Swing Werkzeugsatz begegnet diesem Problem, indem es komplexe graphische Komponenten wie Baumstrukturen, Tabellen und Rich-Text-Darstellungen, aber auch einfache Komponenten emuliert. Swing stellt außerdem emulierte Look&Feels zur Verfügung, die versuchen Anwendungen so aussehen zu lassen wie das darunter liegende fensterbasierte Betriebssystem. Trotz allem hinken die emulierten Komponenten durch ihre Invarianz dem Look&Feel der nativen Komponenten hin­ terher und die Interaktion mit emulierten Komponenten ist im allgemeinen merkbar anders. Diese Tatsachen machen es schwierig Anwendungen zu entwickeln, die sich mit speziell für ein ganz bestimmtes Betriebssystem entwickelten Anwendungen messen können. SWT begegnet dieser Herausforderung indem es eine allgemeine API definiert, die für eine Anzahl unterschiedlicher fensterbasierter Betriebssysteme verfügbar ist. Für jedes dieser unterschiedlichen Betriebssysteme benutzt die SWT Implementierung soweit wie möglich native Komponenten des Betriebssystems. Dort wo dies nicht möglich ist, liefert die SWT Implementierung eine geeignete Emulation. Einfache Komponenten, wie Listen, Textfelder und Buttons sind durchgehend nativ imple­ mentiert. Aber einige im allgemeinen sehr nützliche Komponenten höheren Niveaus müssen nach wie vor auf einigen Systemen emuliert werden. Zum Beispiel ist die 14 Die Definition der Betriebssystemunabhängigkeit ist stets relativ. Normale Java-Programme sind in soweit besonders betriebssystemunabhängig, weil sie auf jedem Betriebssystem ausgeführt werden können, für das eine Java Virtual Machine existiert. Und in der Tat existiert für eine große Anzahl an verschiedenen Betriebssystemen eine Java Virtual Machine. Das SWT nutzt jedoch die nativen graphischen Betriebssystemkomponenten. Deshalb können Java-Programme, die SWT nutzen nur noch auf Betriebssystemen ausgeführt werden, für die sowohl eine JVM, als auch das SWT existiert. Das SWT ist jedoch für deutlich weniger Betriebssysteme vorhanden als die JVM und reduziert damit erheblich die Betribssystemunabhängigkeit dieser Programme. Trotzdem bezeichnet sich das SWT als betriebssystemunabhängig. 32 SWT Werkzeugleiste (toolbar) unter Windows nativ und unter Linux bei Motif emu­ liert. SWT bietet ebenso native systemspezifische APIs an, für die Fälle, wo ein bestimmtes fensterbasiertes Betriebssystem ein einzigartiges Feature aufweist, das auf anderen Betriebssystemen nicht zur Verfügung steht. Windows ActiveX ist dafür ein gutes Beispiel. Diese systemspezifischen APIs sind in Paketen mit besonderer Benennung gekapselt, um zu verdeutlichen, dass sie durch den Kern ihres Wesens nicht portier­ bar sind. Die interne Implementierung vom SWT stellt für jedes fensterbasierte Betriebssystem getrennte und individuelle Implementierungen in Java bereit. Diese internen Java Bi­ bliotheken sind grundlegend verschieden, da sich jede über die speziellen APIs des darunter liegenden fensterbasierten Betriebssystems legt. Dadurch muss keine spezi­ elle Logik in den nativen Bibliotheken implementiert werden. (Im Vergleiche dazu werden in der Java AWT Implementierung für betriebssystemspezifische Unterschie­ de verschiedene C Implementierungen innerhalb eines Satzes nativer Java Methoden verwendet.) Diese Strategie vereinfacht die Implementierung, die Fehlerkontrolle und die Wartung vom SWT. Für die Erstellung gewöhnlicher Anwendungen mit dem SWT, wird eine einheitliche und Betriebssystem unabhängige SWT API verwendet, die die interne SWT Implementierung komplett verdeckt. JFace JFace ist ein Satz aus Komponenten, die die Handhabung vieler herkömmlicher Auf­ gaben im Bereich der GUI-Programmierung erleichtert. JFace ist sowohl in seiner API als auch in der Implementierung betriebssystemunabhängig. Es ist für die Arbeit mit dem SWT ausgelegt, ohne das SWT zu verbergen. JFace beinhaltet gebräuchliche UI Komponenten für Bild- und Schriftregistrierungen, Rahmenwerke für Dialoge, Einstellungen und Fortschrittsanzeigen für zeitintensive Operationen. Zwei seiner interessanteren Features sind Actions und Viewers. Der Action-Mechanismus verwendet das Command-Pattern15 und ermöglicht die De­ finition von Aktionen (actions) unabhängig von einer Angabe über die exakte Plat­ zierung in der Benutzeroberfläche. Eine Aktion stellt einen Befehl dar, der vom Benutzer über einen Button, einem Menüelement oder einem Icon in einer Werkzeu­ gleiste angestoßen werden kann. Jeder Aktion lassen sich ihre wichtigsten Oberflä­ chenattribute (wie Beschriftung, Icon, Verwendungshinweis, etc.) zuweisen, um passende graphische Darstellungen der Aktion zu generieren. Diese Kapselung erlaubt es, die selbe Aktion an mehreren Stellen der Oberfläche zu verwenden, was wiederum bedeutet, dass sich die Position einer Aktion ohne Änderungen ihres Co­ des innerhalb der Oberfläche einfach variieren läßt. 15 Vgl. [30]. 33 Viewer sind modellbasierte Adapter für bestimmte SWT Komponenten.16 Viewer vereinfachen die Verwendung von SWT-Komponenten, indem sie die gemeinsamen Programmieraufgaben für diese Komponenten abstrahieren und generalisieren. Die standard Viewer für Listen, Baumstrukturen und Tabellen unterstützen das Auffül­ len der SWT-Komponenten mit den Daten des zugrunde liegenden Models, sowie die Synchronisation der SWT-Komponenten bei Änderung der Daten. Hierfür wird der Viewer mit einem Content-Provider und einem Label-Provider verbunden. Der Content-Provider bereitet die Daten für den erwarteten Viewer auf und regelt die Aktualisierung des Viewers bei Änderung der Daten. Der Label-Provider liefert passende Beschriftungen und Icons für die zu darstellenden Daten. Optional können Viewer mit Filtern und Sortierern konfiguriert werden. Viewer leisten entlang ihrer Vererbungs- und Abstraktionslinie Unterstützung für häufige Operationen, wie die Reaktion bei der Auswahl ihrer SWT-Komponenten, bei Doppelklicks, bei Drag&Drop, bei der Navigation oder beim Undo. Mehrerer Viewer können sich ein und das selbe zu Grunde liegende Datenmodell teilen und werden automatisch ak­ tualisiert, falls sich das Datenmodell ändert. Workbench Im Gegensatz zu SWT und JFace, die für die allgemeine Verwendung konzipiert sind, bildet die Workbench den eigentlichen Benutzeroberflächencharakter der Eclip­ se Plattform und bietet die Strukturen und Vorlagen über die der Benutzer mit den Plugins interagiert. Auf Grund dieser zentralen und bestimmenden Rolle stellt die Workbench ein Synonym für die gesamte Benutzeroberfläche der Eclipse Plattform und dem Hauptfenster dar. Die API der Workbench ist abhängig von der SWT API und in einem geringeren Maße von der JFace API. Die Implementierung der Arbeits­ station baut sich aus der Verwendung von SWT und JFace auf. Java AWT und Swing werden nicht verwendet. Das Paradigma der Benutzeroberfläche der Eclipse Plattform basiert auf Editoren, Views (Ansichten) und Perspektiven. Für den Benutzer besteht das Fenster der Workbench aus Views und Editoren. Die Perspektiven manifestieren sich durch eine bestimmte Auswahl und Anordnung der präsentierten Views und Editoren. (Vgl. Abbildung 7) 16 Das Viewer-Konzept darf jedoch nicht mit einer View (Ansicht) gleichgesetzt werden. Das ViewerKonzept dient als genereller und vorgefertigter Adapter zwischen den zugrundeliegenden Daten und den unterstützten SWT-Komponenten, in denen die Daten dargestellt werden sollen. Eine View kann jedoch auf diesen vorgefertigten Adapter, bzw. auf das Viewer-Konzept verzichten. 34 Abbildung 7: Workbench Fenster Editoren ermöglichen dem Benutzer Objekte zu öffnen, zu editieren und zu spei­ chern, wie es viele Werkzeuge für das Dateisystem tun. Die Editoren sind aber enger in die Workbench integriert. Wenn ein Editor aktiv ist, kann dieser zusätzliche Ak­ tionen in die Menüs und Werkzeugleisten der Workbench einbringen. Views liefern Informationen über Objekte, die der Benutzer innerhalb der Work­ bench für seine Arbeit verwendet. Eine View kann einem Editor assistieren, indem sie Informationen über das editierte Dokument anzeigt. Desweiteren kann eine View andere Views ergänzen, indem sie zusätzlich Informationen über das momentan aus­ gewählte Objekt beiträgt. Ein Workbench-Fenster darf mehrere unterschiedliche Perspektiven besitzen, wobei nur eine zu einem bestimmten Zeitpunkt sichtbar sein kann. Jede dieser Perspektiven hat ihre eigenen Views und Editoren, die in dem Workbench-Fenster auf eine beson­ dere Weise angeordnet werden. Zahlreiche unterschiedliche Arten von Views und Editoren können innerhalb einer Perspektive zum selben Zeitpunkt geöffnet sein. Die Perspektive bestimmt welche davon von Anfang an angezeigt werden, ihre Position und Größe. Außerdem bestimmt die Perspektive das Layout und die Anzeige von Aktionen innerhalb einer View. Der Benutzer kann schnell zwischen unterschiedli­ chen Perspektiven wechseln, um an einer anderen Aufgabe zu arbeiten. Zudem können benutzerdefinierte Anpassungen an einer Perspektive vorgenommen 35 werden, um sie für die Arbeit an einer bestimmten Aufgabe anzupassen. Es läßt sich aber auch leicht die ursprüngliche Anordnung wieder herstellen. Plugins unterliegen einer wohldefinierten Integration in dieses Editoren-Views-Per­ spektiven-Paradigma. Die wichtigsten Erweiterungspunkte ermöglichen es Plugins der Workbench neue Editoren, Views und Perspektiven hinzuzufügen. Neue Per­ spektiven können alte und neue Views und Editoren gruppieren, um dem Benutzer neue Anwendungsmöglichkeiten zu eröffnen. Plugins können darüber hinaus bereits vorhandene Editoren, Views und Perspek­ tiven erweitern durch: • Hinzufügen neuer Aktionen zu der Menü- bzw. Werkzeugleiste einer View. • Hinzufügen neuer Aktionen zu der Menü- bzw. Werkzeugleiste der Work­ bench, wenn ein vorhandener Editor aktiviert wird. • Hinzufügen neuer Aktionen zu einem Pop-Up-Kontextmenü einer vor­ handenen View oder eines Editors. • Hinzufügen neuer Views, Aktionen und Shortcuts zu einer vorhandenen Per­ spektive. Die Plattform kümmert sich um alle Aspekte des Fenster- und Perspektivenmanage­ ments. Editoren und Views werden nach Bedarf automatisch instantiiert und wieder entfernt wenn sie nicht mehr gebraucht werden. Die Beschriftungen und Icons zur Anzeige der Aktionen, die ein Plugin in die Workbench einbringt, sind in der jewei­ ligen Manifest-Datei des Plugins aufgelistet, so dass die Worbench die Menüs und Werkzeugleisten kreieren kann, ohne dabei die zur Funktion beitragenden Plugins aktivieren zu müssen. Die Workbench aktiviert ein Plugin erst, wenn der Benutzer wirklich die durch das Plugin eingebrachte Funktionalität in Anspruch nimmt. Erst einmal aktivierte Views und Editoren können an Benachrichtigungsdiensten der Workbench über Aktivierungen und Selektionen partizipieren. Der Aktivierungs­ dienst überwacht die Aktivierung von Views und Editoren innerhalb einer Perspek­ tive und meldet diese Ereignisse an die registrierten Listener. Der Selektionsdienst übernimmt die Verteilung von Ereignissen, die durch eine Änderung der Auswahl innerhalb eines Editors oder einer View ausgelöst werden an die registrierten Lis­ tener. Sowohl Editoren, als auch Views können sich als Quelle für Selektionen bei dem Selektionsdienst anmelden. 3.1.4 Java Development Tools Die Java Development Tools (JDT) erweitern die Eclipse Platform um die Fähigkeiten einer modernen Java Entwicklungsumgebung (Java IDE). Die JDT sind als Plugins implementiert, die auf der Eclipse Platform aufsetzen. Die JDT sind ein integraler Bestandteil der Eclipse-SDK-Distribution. 36 Java Projekte Auf der Workspace-Ebene definiert das17 JDT eine eigene Java-Projekt-Natur (vgl. 3.1.2), die Projekte als Java Projekte kennzeichnet. Jedes Java Projekt besitzt eine Classpath-Datei, die verschiedene Java spezifische Informationen enthält. Diese In­ formationen beinhalten die Pfade der Quellcodeverzeichnisse, der JAR-Bibliotheken und des Ausgabeverzeichnises für die vom Compiler generierten Class-Dateien. Java Compiler Die Java Natur konfiguriert jedes Java Projekt mit einem inkrementellen Java Projekt­ builder (vgl. 3.1.2), der den eingebauten Java Compiler aufruft. Im Falle einer Initi­ ierung oder eines vollständigen Builds (Ausführung aller Builder eines Projektes), er­ stellt der Compiler aus allen Quellcodedateien der Quellverzeichnise die entspre­ chenden binären Class-Dateien im Ausgabeverzeichnis des Projektes. Das JDT dekla­ riert einen neuen Markertyp für Java Probleme (vgl. 3.1.2). Entdeckt der Compiler Fehler oder Probleme, dann fügt er den betroffenen Quellcodedateien die entspre­ chenden Marker hinzu. Während der Übersetzung erstellt der Compiler einen Abhängigkeitsgraphen mit In­ formationen über jede Quellcodedatei. Dies ermöglicht folgende Übersetzungen effi­ zienter Auszuführen. Der inkrementelle Projektbuilder-Mechanismus unterrichtet den Java-Builder mit einem Delta-Baum über alle Änderungen der Ressourcen seit seiner letzten Ausführung. Dadurch kann der Java-Builder bestimmen welche Datei­ en neu kompiliert werden müssen, weil sie geändert, gelöscht oder hinzugefügt wurden. Der Compiler nutzt seinen Abhängigkeitsgraphen, um auch die Dateien neu zu übersetzen, die durch die eben genannten Änderungen indirekt ebenfalls betrof­ fen sind. Das JDT partizipiert am Save-Restore-Prozess des Workspace (vgl. 3.1.2), damit der Abhängigkeitsgraph zwischen mehreren Sitzungen erhalten bleibt. An­ sonsten müsste bei jeder neuen Sitzung ein vollständiger Build ausgeführt werden, nur um den Abhängigkeitsgraphen neu zu erstellen. Diese inkrementelle Strategie ermöglicht es dem JDT die Kompilierung regelmäßig auszuführen, z. B. nach jeder Speicherung einer Quellcodedatei, selbst wenn das Projekt hunderte oder tausende Quellcodedateien enthält. Java Element Model Das Java Element Model ermöglicht die Navigation durch die Java Elemente eines Java Projektes. Java Elemente basieren auf den Java Dateien eines Java Projektes und verfügen daher eine Baumstruktur: • Package-Fragment-Roots (IPackageFragmentRoot) korrespondieren mit den Quellcodeverzeichnissen und JAR-Bibliotheken eines Projektes. 17 Im folgenden wird statt des Plurals für JDT das Singular verwendet, weil keine Unterscheidung zwischen den einzelnen Teilen des JDT gemacht wird und das JDT als ein umfassendes Werkzeug betrachtet wird. 37 • Package-Fragments (IPackageFragment) korrespondieren mit Paketen in­ nerhalb eines Package-Fragment-Roots. • Compilation-Units (ICompilationUnit) korrespondieren zu Java Quellco­ dedateien und Class-Files (IClassFile) korrespondieren zu Java Bytecode­ dateien. • Typen, Felder, Initializer und Methoden sind die kleinsten Einheiten einer Compilation-Unit, die durch das Java Model angesteuert werden können. Aufgrund der feinen Granulierung der Compilation-Units ist es nicht effizient den gesamten Java Elementbaum im Speicher zu halten. Zudem wäre die Konstruktion des Elementbaumes aufwendig, denn dazu müssten alle Java Quellcodedateien ein­ gelesen und geparst werden. Stattdessen wird der Baum stückweise und nur auf Nachfrage konstruiert. Clients arbeiten damit lediglich mit Handels, solange nicht nach weiteren inneren Elementen gefragt wird. Deshalb eignet sich das ElementModel besonders zur Präsentation in Viewern. Der Elementbaum besitzt einen begrenzten Cache für bereits analysierte Compilati­ on-Units. Der Cache nutzt einen Resource-Change-Listener des Workspace, um Ein­ träge zu entfernen, wenn sich die entsprechenden Ressourcen geändert haben oder gelöscht wurden. Desweiteren gestattet das Java Element Model gewisse Modifikationen des Java Quellcodes. Hierzu zählt das Hinzufügen von neuen Feldern, Initializern, Methoden und Typen, sowie das Umbenennen, Verschieben, Kopieren oder Löschen von Java Elementen. Abstract Syntax Tree Das Java Element Model ist nicht fein genug, um eine detailiertere Analyse und Mo­ difikation z. B. von Anweisungen innerhalb von Methoden zu ermöglichen. Deshalb bietet das JDT einen Zugang zum sogenannten Abstract Syntax Tree18 (AST) einer Quellcodedatei. Der AST ist das Ergebnis einer Analyse einer Quellcodedatei durch einen Parser (ASTParser). Jeder Knoten (ASTNode) des AST repräsentiert einen de­ finierten Teil des Java Quellcodes. Abbildung 9 zeigt ein Beispiel eines solchen AST. Es existieren über 60 hierarchisch strukturierte Knotentypen für die verschiedenen Teile des Quellcodes wie z. B. Importdeklarationen, Typendeklarationen, Methoden­ aufrufe, Feldzugriffe und Literale.19 Abbildung 8 zeigt einen Teil der ASTNode-Hier­ archie. Das JDT stellt einen ASTVisitor20 bereit um die Knoten des AST zu durch­ laufen und Aktionen in Abhängigkeit des Knotentyps auszuführen. 18 Die von Eclipse erzeugte interne Repräsentation eines Programms ist in Wahrheit eine Mischung eines Parse-Baums und eines abstrakten Syntax-Baums. So enthält der Baum Knoten für Klammern (zum Beispiel für den Ausdruck „(1 + 1)”) oder Informationen zu Javadoc-Kommentaren, die in einem reinen abstrakten Syntax-Baum eigentlich nicht mehr vorkommen. 19 Für die neuen Sprachkonstrukte in Java 1.5 kommen weitere Knotentypen hinzu. 20 Vgl. das Visitor-Pattern in [30]. 38 Abbildung 8: ASTNode Hierarchie vgl. [26]. Die AST-API bietet zwei verschiedene Möglichkeiten an, den zugrunde liegenden Quellcode mit Hilfe des AST zu verändern: die modifizierende API und die deskrip­ tive API.21 Die modifizierende API ermöglicht den AST direkt zu verändern. Die Ver­ änderung des Quellcodes bedarf in diesem Fall drei Schritte: • Zunächst muss der AST aufgefordert werden die Modifikationen auf­ zunehmen. • Dann können die entsprechenden Modifikationen an den Knoten des AST vollführt werden. • Sind die AST-Modifkationen abgeschlossen, werden Quellcode-Modifika­ tionen daraus generiert, die dann auf den Quellcode angewendet werden können. Die deskriptive AST-API nutzt den ASTRewriter. 21 Vgl. die Eclipse SDK Hilfe: JDT Plug-in Developer Guide → Programmer's Guide → JDT Core→ Manipulating Java code. 39 CompilationUnit imports() types() ASTNode$NodeList toArray() Object[] 0 ImportDeclaration ASTNode$NodeList toArray() toString(): „import junit.framework.TestCase;“ Object[] 0 TypeDeclaration getMethods() MethodDeclaration[] 0 MethodDeclaration getBody() Block statements() ASTNode$NodeList toArray() Object[] 0 ExpressionStatement getExpression() MethodInvocation toString(): „fail()“ Abbildung 9: Abstract Syntax Tree Beispiel aus [26] S.320 ASTRewriter Mit Hilfe des ASTRewriters lassen sich komplexe Manipulationen des Quellcodes auf eine etwas bequemere Art durchführen als durch die modifizierende API. Der AST­ Rewriter sammelt Informationen über die auszuführenden Manipulationen in Form von AST-Änderungen und berechnet daraus die nötigen Modifikationen des Quell­ codes. Der AST selbst wird durch den ASTRewriter also nicht verändert. Die einfa­ chere Handhabung ermöglichen Methoden wie z. B. createStringPlaceholder des ASTRewriters. Als Parameter erhält diese Methode eine Zeichenkette, die an die entsprechende Stelle des Platzhalters (engl. Placeholder) im Quellcode hinzuge­ fügt wird. Im Vergleich dazu muss in der modifizierenden API für jedes Element der Zeichenkette ein AST-Knoten erzeugt werden, und abhängig von der API der jewei­ ligen Knoten müssen diese auf unterschiedliche Weise miteinander verknüpft werden. IScanner Das JDT unterstützt auch eine Low-Level Analyse und Manipulation von Java Quell­ code. Für eine noch feinere Analyse des Quellcodes stellt das JDT einen Scanner zur 40 Verfügung. Der Scanner parst und zerlegt den Quellcode in einzelne Token. Der Scanner kann zudem auf kleine Teile des Quellcodes angewandt werden. Für die Modifikation von Quellcode auf der Zeichenkettenebene, stellt das JDT effiziente Operationen für Char-Arrays zur Verfügung. Java UI Das JDT definiert eine Java Perspektive für Entwickler von Java Anwendungen. (Vgl. Abbildung 7 in Abschnitt 3.1.3) Diese Perspektive beinhaltet unter anderem folgende Java spezifische Workbencherweiterungen: • eine Package-View, • eine View zur Darstellung der Hierarchie von Java Typen, • einen Java Editor, • Aktionen für die Erzeugung von Java Elementen, das Refactoring des Quellco­ des, sowie das Ausführen und Debuggen von Java Programmen. Die Package-View liefert eine hierarchische Sicht auf die Pakete, Quellcodedateien und JAR-Bibliotheken innerhalb eines Java Projektes. Die Elemente und Beziehungen dieser Ansicht kommen direkt vom Java Model. Die Typ-Hierarchie-View stellt die strukturelle Vererbungsbeziehung von Klassen und Schnittstellen dar. Diese Typ-Hierarchie verwendet eine indexbasierte Suche des Java Models kombiniert mit einer Untersuchung des Quellcodes, um die Namen der Supertypen zu extrahieren. Der Java Editor unterstützt vielfältige und nützliche Features, wie Syntax-Highligh­ ting, Hervorhebung von Annotationen wie Breakpoints und Problemmarkierungen, Codeassistenten und Formatierungen. Die Aktionen nutzen verschiedene Assistenten (Wizards), um neue Java Projekte, Pa­ kete, Klassen oder Schnittstellen zu erzeugen. Das Refactoring ermöglicht Opera­ tionen wie z. B. das Umbenennen von verschiedenen Java Elementen oder das Ändern von Methodensignaturen. Die Workbench beinhaltet desweiteren Aktionen, um Programme zu starten und zu debuggen. Für das Debuggen stellt das JDT eine Debug-Perspektive bereit, die für diese Aufgabe spezialisiert ist. Diese Perspektive beinhaltet eine View für alle laufenden oder kürzlich beendeten Prozesse und eine Konsole über die der Entwick­ ler mit dem laufenden Programm über die standard Input- und Outputstreams inter­ agieren kann. Die View für die Prozesse stellt zudem die Threads und die Stack-Fra­ mes der laufenden Anwendung dar. Bei einem unterbrochenem Programmablauf, z. B. durch Breakpoints, wird die nächste auszuführende Zeile im Editor hervorge­ hoben. Andere debug-spezifische Views zeigen die Liste aller gesetzten Breakpoints, die Felder der Objekte, sowie Werte der Felder. Desweiteren werden Breakpoints im 41 Editor in einer vertikalen Leiste direkt neben dem Quellcode angezeigt. Die genaue Darstellung der Breakpoints hängt vom Typ des Breakpoints bzw. von seinem Markertyp ab. Debugger Das JDT ist in der Lage Java Programme aus der Eclipse-IDE heraus zu starten und zu debuggen. Dabei wird eine neue JVM in einem separaten Prozess gestartet, die das Java Programm ausführt. Durch die Bereitstellung von entsprechenden Erweite­ rungspunkten unterstützt das JDT die Implementierung spezialisierter Launcher für verschiedene JVMs. Der JDT-Debugger arbeitet mit jeder JPDA unterstützenden JVM zusammen. Die Architektur des Debug-Frameworks ist komplex und vielschichtig. Einerseits nutzt das Debug-Framework das von der Eclipse-SDK bereitgestellte generelle De­ bug-Model. Andererseits setzt das Debug-Framework auf der Java Platform Debug Architektur auf. Da die Java Platform Debug Architektur bereits in Abschnitt 2.2.1 behandelt wurde, soll im folgenden das Debug-Model der Eclipse Platform vorge­ stellt werden. Denn obwohl es nicht direkt zum JDT gehört, implementiert das JDTPendant alle Schnittstellen des Eclipse Platform Debug Models. Die Abbildung 10 zeigt das generelle Debug-Model der Eclipse Platform. Dieses Model beinhaltet die wichtigsten Abstraktionen für die meisten Programmierumge­ bungen. Desweiteren interagiert die Debug-UI der Eclipse Platform mit diesen Ab­ straktionen, während die verschiedenen sprachabhängigen Debug-Plugins die passenden Implementationen liefern. Die verschiedenen Interfaces des Models sollen an dieser Stelle kurz erläutert werden: • Launch (ILaunch) – Ein Container für Prozesse und Debug Targets. • Debug Element (IDebugElement) – Eine Gemeinsame Schnittstelle für eine ganze Reihe von Debug-Artefakten. • Debug Target (IDebugTarget) - Der Kontext eines Prozesses oder einer VM, der debugt werden kann. Process (IProcess) – Ein Prozess repräsentiert ein im normalen Modus (nicht Debug) ablaufendes Programm. Stack frames (IStackFrame) - Der Kontext eines suspendierten bzw. angehal­ tenen Threads, der lokale Variablen und Argumente beinhaltet. Thread (IThread) - Ein sequentieller Ablauf in einem Debug Target, der StackFrames beinhaltet. Variable (IVariable) – Eine sichtbare Datenstruktur in einem Stack-Frame. Value (IValue) - Der Wert einer Variablen. Breakpoint (IBreakPoint) – Ein Breakpoint kann den Programmablauf an einer bestimmten Stelle unterbrechen, sofern das Programm im Debug-Modus läuft. • • • • • • 42 Die Eclipse Platform stellt zudem ein (Source-Lookup-) Framework bereit, mit dessen Hilfe der zum Programmablauf gehörende Quellcode gefunden und dem Benutzer die auszuführende Zeile präsentiert werden kann. «Schnittstelle» ILaunch 1 1 «Schnittstelle» IDebugElement 1 * 1 1 «Schnittstelle» IDebugTarget 1 1 * 1 «Schnittstelle» IProcess 1 «Schnittstelle» IThread * * 1 1 «Schnittstelle» IStackFrame * 1 «Schnittstelle» IVariable * 1 1 1 «Schnittstelle» IValue «Schnittstelle» IBreakPoint * Abbildung 10: Eclipse Platform Debug Model. [31] Breakpoints Mit Hilfe von Breakpoints kann der Ablauf eines Programmes an einer bestimmten Stelle unterbrochen werden, um dann Informationen über den Kontext der jewei­ ligen Stelle zu gewinnen. Abbildung 11 zeigt die Komponenten des generellen Break­ point-Models. Das JDT implementiert die Schnittstellen dieses Models. Die Implementierung von Breakpoints nutzt den Marker-Mechanismus (vgl. 3.1.2). Durch die Verwendung von Markern, kann das Debug-Model existierende Marker­ funktionen wie die Persistenz, die Suche, das Hinzufügen oder Löschen und die Dar­ stellung in einem Editor nutzen. Deshalb muss bei der Deklaration eines neuen Breakpointtyps in der Manifest-Datei. spezifiziert werden, welcher Marker mit 43 diesem Breakpoint assoziiert werden soll. Hierfür definiert das Debug-Model den speziellen Markertyp org.eclipse.debug.core.breakpointMarker für Break­ points. Sofern dieser Breakpointmarker als Supertyp für neue Breakpointmarker verwendet wird, ist der Breakpointmanager in der Lage die korrespondierenden Breakpoints bei einer neuen Sitzung (Session) zu finden, wiederherzustellen und in der Benutzeroberfläche darzustellen. Dafür sucht der Breakpointmanager in den Ressourcen eines Projektes nach Breakpointmarkern, dessen Supertyp der org.ec­ lipse.debug.core.breakpointMarker ist. Mit Hilfe der Extension Registry kann der Breakpointmanager die mit den gefundenen Markern assoziierten Break­ pointklassen finden und instantiieren. Desweiteren informiert der Breakpointma­ nager das DebugTarget über hinzugefügte, entfernte oder geänderte Breakpoints. Die Eclipse Debug-UI zeigt Breakpoints, die beim Breakpoint-Manager registriert wurden, in einer Liste an. Der Quellcodeeditor verwendet den Marker eines Break­ points, um diesen darzustellen. Sollen Breakpoints für die Verfolgung des Pro­ grammablaufes, wie im Tracingverfahren, genutzt werden, so ist es wichtig diese Breakpoints vor dem Benutzer zu verbergen. Um einen Breakpoint vor dem Benutzer zu verbergen, darf der Breakpoint nicht beim Breakpointmanager regis­ triert werden, und der zugehörige Marker muss entweder einen anderen Supertypen als org.eclipse.debug.core.breakpointMarker haben oder einer Ressource zugewiesen werden, die der Quellcodeeditor nicht darstellt. Wird jedoch der Break­ pointmanager nicht verwendet, dann muss die Persistenz, bzw. die Wiederherstel­ lung der Breakpoints bei einer neuen Sitzung (Session), sowie die Benachrichtigung des Debug Targets selbst implementiert werden. Dafür ist ein Listener namens org.e­ clipse.debug.core.IDebugEventSetListener hilfreich. Dieser Listener kann beim De­ bug-Plugin registriert werden, um über Ereignisse eines laufenden Programmes im Debug-Modus benachrichtigt zu werden. 44 <<extension>> Marker Type = lineBreakpointMarker specifies class (on restore) <<extension>> Breakpoint «Schnittstelle» IBreakpoint instantiates (on restore) «Schnittstelle» IBreakpointManager <<extension>> Marker notifies (add, change, remove) instantiates (on restore) looks for breakpoint markers (on restore) «Schnittstelle» IMarker AbstractDecoratedTextEditor «Schnittstelle» IDebugTarget AbstractMarkerAnnotationModel Abbildung 11: Breakpoint Model, vgl. [0] Die JDT-Debug-API stellt zudem den Listener org.eclipse.jdt.debug.co­ re.IJavaBreakpointListener bereit, mit dessen Hilfe ein Plugin über getrof­ fene Breakpoints (breakpoint hits) während des Programmablaufes benachrichtigt werden kann, und zugleich entscheiden, ob der Programmablauf durch den jewei­ ligen Breakpoint unterbrochen werden soll. Das JDT Debug-Plugin stellt verschieden Breakpointtypen zur Verfügung. Die wichtigsten Typen für diese Arbeit bzw. das Tracingverfahren sind IJavaLine­ Breakpoint, IJavaMethodBreakpoint und IJavaClassPrepareBreak­ point. Zusammen mit dem IJavaBreakpointListener können diese Break­ points für die Verfolgung des Programmablaufes genutzt werden. Mit Hilfe von IJavaClassPrepareBreakpoints können Breakpoints der beiden anderen Ty­ pen nur in die Klassen installiert werden, die tatsächlich auch von der VM geladen werden. 3.2 Architektur Überblick Die Architektur des Werkzeuges zur Messung von Testabdeckungen umfasst insge­ samt fünf Komponenten. Die gesamte Architektur des Werkzeuges ist in Abbildung 12 abgebildet. Um die größt mögliche Entkopplung der Komponenten des Werk­ zeuges zu erreichen, ist jedes selbst jeweils als ein Plugin implementiert. Auf diese Weise lassen sich die Komponenten leicht austauschen, als Beispielimple­ mentierungen nutzen, oder durch weitere Plugins erweitern. Es folgt eine kurze Erläuterung der Komponenten: 45 ResultView IPersistentCoverageDataListener Persistence ICoverageRunListener Adapter «uses» «uses» Instrumenter Tracer Abbildung 12: Architektur des Werkzeugs zur Messung von Testabdeckungen • Die ResultView präsentiert dem Anwender die Ergebnisse der Testabde­ ckungsmessung. • Die Persistence Komponente speichert alle anfallenden Daten persistent ab. • Die Adapter Komponente dient als Adapter für die beiden Verfahren zur Messung der Testabdeckung. An diesen Adapter lassen sich weitere Verfahren anschließen. • Die Instrumenter Komponente implementiert das Instrumentierungsverfahren. • Die Tracer Komponente implementiert das Tracingverfahren. Die öffentlichen Schnittstellen ICoverageRunListener und IPersistentCo­ verageDataListener sind jeweils in den darunter liegenden Plugins definiert und in den korrespondierenden Manifest-Dateien deklariert. Sie erlauben das Aus­ tauschen der Persistence-Komponente und der ResultView durch andere Imple­ mentierungen, ohne die darunter liegenden Plugins modifizieren zu müssen. Es ist sogar denkbar ganz auf die Persistenzschicht zu verzichten und eine neue View di­ rekt über die ICoverageRunListener Schnittstelle anzuschließen. 46 3.3 Instrumentierungsverfahren Die Instrumenter-Komponente umfasst eine ganze Reihe von Klassen. Abbildung 13 stellt die meisten davon dar und veranschaulicht, wie die Klassen miteinander in Be­ ziehung stehen. CoveragePropertyPage enables, disables for project «eclipse mechanism» add to project CoveragePlugin CoverageNature initialize uses «eclipse mechanism» add to project set port LaunchListener start, terminate ClasspathUtils CoverageBuilder add to project path from uses CoverageTraceCollectorClient sends runtime data «Teilsystem» CoverageTraceCollectorServer uses update runtime data Adapter «Teilsystem» Instrumenter sources UpdateInformer instruments update static data Abbildung 13: Instrumenter-Plugin Die CoveragePropertyPage nutzt einen UI-Mechanismus der Eclipse Platform, um das Plugin für ein bestimmtes Projekt aus der Benutzeroberfläche heraus ak­ tivieren und deaktivieren zu können. Bei Aktivierung des Instrumenter-Plugins für ein bestimmtes Projekt fügt das CoveragePlugin diesem Projekt eine CoverageNature hinzu. Desweiteren initialisiert das CoveragePlugin einen LaunchLis­ tener, der stets benachrichtigt wird, wenn ein Java Programm aus der Eclipse Plat­ form heraus ausgeführt wird. Auf diese Weise kann der CoverageTraceCollec­ torServer nur für Java-Programme gestartet werden, die eine CoverageNature aufweisen und nach Beendigung des Programms kann auch der Server terminiert werden. Die Klasse CoverageNature konfiguriert und verwaltet nicht nur den Co­ verageBuilder für ein Projekt, sondern auch den Pfad zum CoverageTrace­ 47 CollectorClient. Diese Klasse stellt die Kommunikation zwischen dem instru­ mentierten Programm und dem CoverageTraceCollectorServer zur Laufzeit her, um Daten über den Programmablauf an das Plugin zu senden. Der Coverage­ Builder verwendet den Instrumenter, um die Quellcodedateien zu instru­ mentieren und gleichzeitig Daten über die statische Struktur der Quellcodedateien zu erfassen. Die instrumentierten Quellcodedateien werden dann vom Coverage­ Builder kompiliert. Sowohl der CoverageTraceCollectorServer als auch der Instrumenter nutzen die Klasse UpdateInformer der Adapter-Komponente, um die Daten nach Außen zu geben. Im folgenden wird das Instrumenter-Teilsystem näher erläutert, und anschließend wird die Kommunikation zwischen dem Plugin zur Messung von Testabdeckungen und dem zu untersuchenden Programm beschrieben. 3.3.1 Instrumenter Abbildung 14 zeigt ein UML-Diagramm mit den wichtigsten Klassen für die Instru­ mentierung des Quellcodes. Die Aufgaben dieser Klassen sind folgende: Die Klasse Instrumenter ist in der Lage sowohl ein ganzes Java Projekt, als auch nur eine einzelne ICompilationUnit22 (Quellcodedatei) zu instrumentieren. Soll das gesamte Java Projekt instrumentiert werden, so wird über den Java Element­ baum des Projektes iteriert, bis eine ICompilationUnit gefunden wurde. Diese wird dann genauso instrumentiert, als würde der Instrumenter eine einzelne ICompilationUnit verarbeiten. Eine einzelne ICompilationUnit wird wie folgt instrumentiert: Zunächst generiert der ASTParser aus der ICompilationUnit (Java Element Model) eine CompilationUnit (AST). Letzteres ist bereits die Wurzel des AST. Für diesen AST wird ein ASTRewriter erzeugt und als Parameter an den ASTInstru­ menter übergeben. Der AST wird mittels des ASTInstrumenters durchlaufen, der diese Fähigkeit vom ASTVisitor erbt. Für bestimmte Knoten, die z. B. Anwei­ sungen, Methoden oder Schleifen repräsentieren, generiert der TraceGenerator passende Instrumentierungen in Form von Zeichenketten. Der ASTRewriter erzeugt für diese Zeichenketten entsprechende StringPlaceHolder-Knoten und merkt sich die Stellen, an denen die Änderungen eingefügt werden sollen. Bei der In­ strumentierung des Quellcodes sind verschiedene Sonderfälle zu berücksichtigen (vgl. 2.1.2, 2.1.3 und 2.1.4). Alle Sonderfälle können dennoch im selben Durchgang mittels des ASTRewriters behandelt werden. Nach dem Durchlaufen des gesamten AST, wird mit dem ASTRewriter eine temporäres TextEdit-Objekt erzeugt, das die 22 Eine ICompilationUnit repräsentiert eine Quellcodedatei im Java Element-Model. Eine CompilationUnit (ohne I) repräsentiert ebenfalls eine Quellcodedatei, jedoch im AST-Model. Beide Modele und damit auch beide Klassen existieren unabhängig voneinander. 48 gesamten Modifikationen enthält. Diese Modifikationen lassen sich schließlich auf ein Document-Objekt der Quellcodedatei anwenden und abspeichern. ASTParser 1 1 1 Instrumenter 1 ASTRewriter ASTVisitor 1 «uses» 1 TraceGenerator 1 1 ASTInstrumenter Abbildung 14: Instrumenter-Teilsystem 3.3.2 Inter-JVM-Kommunikation Die Eclipse Platform wird in einer JVM-Instanz ausgeführt. Um ein Java Programm eines Java Projektes aus der Eclipse Platform heraus laufen zu lassen, wird zunächst eine neue Instanz der JVM gestartet. Diese JVM erhält dabei alle Informationen, wie z. B. Pfadangaben, um das Programm zu starten als Parameter. Anhand dieser In­ formationen lädt die JVM die kompilierten Klassen des Projektes und führt das Java Programm aus. Da jedoch die Eclipse Platform und damit auch das Plugin zur Messung von Testab­ deckungen in einer anderen JVM ausgeführt werden als das zu untersuchende Pro­ gramm, muss ein Kommunikationsweg zwischen diesen beiden JVMs aufgebaut werden. Prinzipiell stehen mehrere Möglichkeiten zur Verfügung um die Inter-JVMKommunikation zu realisieren: • Verwendung einer gemeinsamen Datei. • Verwendung einer gemeinsamen Datenbank. • Verwendung einer Socket-Verbindung. Die Verwendung einer gemeinsamen Datei oder Datenbank wäre die effizienteste Möglichkeit die Kommunikation zwischen den beiden JVMs herzustellen und zu­ gleich eine Persistenz der Daten umzusetzen. Jedoch ist die Persistenz keine zwingende Anforderung für die Messung von Testabdeckungen oder anderer Sta­ tistkfunktionen. Zudem läßt sich der Programmablauf nur in Echtzeit23 verfolgen und visualisieren, falls eine Kommunikation in Echtzeit stattfinden kann. Die Flexi­ bilität ganz auf die Persistenz zu verzichten und/oder den Programmablauf in 23 Damit ist jeweils eine unmittelbare Benachrichtigung vor oder nach dem Aufruf einer instrumentierten Anweisung oder Methode gemeint. Diese Benachrichtigung muss also stets vor dem Aufruf der folgenden Anweisung oder Methode ausgeführt werden. 49 Echtzeit zu visualisieren bietet nur eine Socket-Verbindung. Zu Gunsten der Flexibili­ tät und auf Kosten der Performance wurde für die Implementierung der Inter-JVMKommunikation der Socket-Ansatz gewählt.24 Abbildung 15 stellt die Realisierung der Inter-JVM-Kommunikation des Instru­ menter-Plugins dar. Eclipse VM Instrumenter-Plugin CoverageTraceCollector Server Java Application VM launches notification per socket Application CoverageTraceCollector Client attaches loads Workspace Project CoverageTraceClient.jar Abbildung 15: Inter-JVM-Kommunikation Wird das Instrumenter-Plugin für ein Java Projekt im Workspace aktiviert, wird diesem Projekt zunächst die CoverageTraceCollectorClient-Bibliothek hin­ zugefügt. Bei Ausführung des Projektes wird dann eine neue JVM-Instanz gestartet, die die benötigten Klassen des Projektes lädt. Gleichzeitig wird der Coverage­ TraceCollerctorServer als Thread gestartet. Die CoverageTraceCollec­ torClient-Bibliothek besteht lediglich aus einer einzigen Klasse, die genauso wie die Bibliothek benannt ist. Die zu untersuchende Anwendung greift auf die Klasse CoverageTraceCollectorClient aufgrund ihrer Instrumentierung zu. Die Klasse CoverageTraceCollectorClient ist als ein Singleton realisiert, auf das alle Klassen der zu untersuchenden Anwendung zugreifen. Der CoverageTrace­ CollectorClient erzeugt bei seiner Instantiierung eine Socket-Verbindung zum CoverageTraceCollectorServer. Damit auch bei mehreren Threads einer zu untersuchenden Anwendung jeweils nur ein Thread Daten an den Server sendet, ist die Sende-Operation als atomare (synchronized) Methode implementiert. 24 Grundsätzlich wäre auch eine Hybrid-Lösung denkbar, die die Wahl der Kommunikationsart dem Benutzer überläßt. Diese Lösung wäre jedoch nur mit einem erheblichen Mehraufwand bei der Architektur und der Implementierung realisierbar. 50 3.3.3 Probleme Ein bedeutendes Problem für die Implementierung des Instrumentierungsverfahrens stellt die Tatsache dar, dass das Eclipse Framework keine öffentliche API für die Kompilierung einer Java Quellcodedatei bereitstellt, die sich nicht im Source-Path des Projektes befindet.25 Es darf auch kein Source-Path für die instrumentierten Quellcodedateien hinzugefügt werden, weil es sonst zu Namenskonflikten käme. Der CoverageBuilder greift deshalb auf das ANT-Plugin zu, um ein ANT-Skript aus­ zuführen, welches die notwendige Kompilierung der instrumentierten Quellcodeda­ teien übernimmt. Das ANT-Skript ist in der Lage für die Kompilierung auf den Eclip­ se eigenen Compiler zuzugreifen und diesen zu verwenden. Auf diese Weise muss kein zusätzlicher Compiler installiert und konfiguriert werden. Ein weiteres Problem ergibt sich durch die Instrumentierung des Quellcodes. Denn dadurch verändern sich die Zeilenangaben so, dass keine Breakpoints mehr im De­ bug-Modus getroffen werden. Die ASTRewriter-API bietet keine Möglichkeit die Erzeugung neuer Zeilen bei der Einfügung neuer Anweisungen zu unterdrücken.26 Bisher konnte dieses Problem nicht behoben werden. Grundsätzlich könnten die ein­ gefügten Zeilenumbrüche direkt aus dem Quellcode entfernt werden, indem alle Po­ sitionen der eingefügten Anweisungen gespeichert werden. Dabei könnten jedoch zusätzlich eingefügte Blöcke ({...}) für Schleifen- oder Bedingungsanweisungen Pro­ bleme bereiten, insbesondere wenn diese geschachtelt wären. Alternativ könnte ein spezieller Rewriter implementiert werden, der keine Zeilenumbrüche erzeugt. Aber auch in diesem Fall wären Blöcke für geschachtelte Schleifen- oder Bedingungs­ anweisungen eine besondere Herausforderung. 3.4 Tracingverfahren Abbildung 16 stellt die wichtigsten Klassen der Tracer-Komponente dar. Der Ver­ gleich mit der Abbildung 15 veranschaulicht die Ähnlichkeit der Tracer-Komonente und der Instrumenter-Komponente. Die Klassen CoveragePropertyPage, Cover­ agePlugin, CoverageNature, LaunchListener und CoverageBuilder sind in beiden Komponenten enthalten und und erfüllen dieselben Aufgaben. Jedoch unter­ scheiden sich die Implementierungen dieser Klassen in einigen Details, weshalb diese Klassen nicht in eine gemeinsame Bibliothek ausgelagert werden konnten. Durch die Ausnutzung des Debug-Mechanismus ist die Tracer-Komponente jedoch deutlich einfacher strukturiert als die Instrumenter-Komponente. So kann z. B. auf die Inter25 Die IJavaProject-API bietet eine build-Methode an, die den JavaBuilder veranlasst das entsprechende Java Projekt zu übersetzen. Diese Methode erlaubt allerdings nicht die Kompilierung einzelner Quellcodedateien, die sich nicht im Quellverzeichnis des Projektes befinden. 26 Vgl. [32]. 51 JVM-Kommunikation, aber auch auf die Kompilierung des Quellcodes innerhalb der CoverageBuilder-Klasse verzichtet werden. CoveragePropertyPage enables, disables for project «eclipse mechanism» add to project CoveragePlugin CoverageNature initialize «eclipse mechanism» add to project CoverageBuilder LaunchListener uses start, terminate handle Breakpoint Hits create Breakpoints for Markers BreakpointHandler sources update runtime data Adapter adds Marker «Teilsystem» MarkerAnnotator update static data UpdateInformer Abbildung 16: Tracer-Plugin Der CoverageBuilder verwendet das MarkerAnnotator-Teilsystem, um den Quellcodedatein Marker hinzuzufügen und gleichzeitig Daten über die statische Struktur zu sammeln. Sobald das zu untersuchende Programm gestartet wird, regis­ triert der LaunchListener den BreakpointHandler beim DebugTraget, d.h. der Virtual Machine in der das Programm ausgeführt wird. Der BreakpointHand­ ler generiert anhand der Marker Breakpoints mit eindeutigen ID-Attributen und fügt die Breakpoints dem DebugTarget hinzu. Außerdem registriert sich der BreakpointHandler selbst als BreakpointListener beim Debug-Model. Auf diese Weise wird der BreakpointHandler über jeden Breakpointtreffer (break­ point hit) benachrichtigt und ist damit in der Lage den UpdateInformer über den Programmablauf zu unterrichten. 52 3.4.1 MarkerAnnotator Das MarkerAnnotator-Teilsystem ist dem Instrumenter-Teilsystem ähnlich. Die Klasse MarkerAnnotator entspricht der Klasse Instrumenter, die Klasse ASTAnnotator entspricht der Klasse ASTInstrumenter und schließlich entspricht die Klasse TraceMarkerGenerator der Klasse TraceGenerator. Der wesent­ lichste Unterschied ist, dass eine Manipulation des Quellcodes nicht stattfindet. Deshalb wird ein ASTRewriter nicht gebraucht. Stattdessen werden Marker vom TraceMarkerGenerator erzeugt und den Quellcodedateien hinzugefügt. Abbil­ dung 17 stellt die statische Struktur des MarkerAnnotator-Teilsystems als UML-Dia­ gramm dar. ASTParser 1 1 MarkerAnnotator 1 ASTVisitor 1 TraceMarkerGenerator 1 1 ASTAnnotator Abbildung 17: MarkerAnnotator-Teilsystem 3.4.2 BreakpointHandler Sowohl Marker als auch Breakpoints können dynamisch zur Laufzeit gesetzt und wieder entfernt werden. Diese Tatsache ermöglicht Breakpoints und Marker in ver­ schiedenen Kombinationen zu setzen. Zum Beispiel könnten alle Breakpoints gene­ riert werden, sobald ein DebugTarget zur Verfügung steht, also sobald das zu un­ tersuchende Programm im Debug-Modus ausgeführt wird. Dieser Ansatz skaliert je­ doch nicht mit zunehmender Programmgröße und mit wachsender Breakpointan­ zahl. Damit würde es zu einer erheblichen Verzögerung beim Programmstart kom­ men. Eine andere, besonders entgegengesetzte Möglichkeit wäre das Setzen von ClassPrepareBreakpoints für jede Klasse beim Programmstart. Erst bei einem Treffer eines ClassPrepareBreakpoint würden MethodBreakpoints und schließlich bei einem Treffer eines MethodBreakpoints könnten LineBreak­ points gesetzt werden. Diese Möglichkeit würde einen vergleichsweise schnellen Programmstart zur Folge haben und nur für die Programmteile Breakpoints erzeugen, die tatsächlich ausgeführt werden. Für die Implementierung des BreakpointHandlers wurde ein Ansatz gewählt, der dem zu letzt genannten ähnelt. Um die Erzeugung von vielen Breakpoint-Objek­ ten noch weiter einzuschränken, wurde auf die Verwendung von ClassPrepare­ 53 Breakpoints verzichtet und stattdessen generelle MethodBreakpoints einge­ setzt. Dabei wird ein genereller MethodBreakpoint pro Klasse erzeugt. Ein genereller MethodBreakpoint wird bei jedem Methodenaufruf innerhalb einer Klasse getroffen. Bei einem Treffer wird mit Hilfe des zugehörigen Stack-Frames die aktuelle Position im Programmablauf bestimmt, die nötigen LineBreakpoints für die aktuelle Methode erzeugt und schließlich der UpdateInformer benachrichtigt. Dieses Vorgehen senkt zwar die Erzeugung von Breakpoints erheblich, erhöht aber gleichzeitig die Komplexität der dazu notwendigen Logik. Im Rahmen dieser Arbeit wurden verschiedene Ansätze ausprobiert, jedoch konnte nicht ausreichend überprüft werden, welcher Ansatz für das Setzen der Breakpoints am besten geeignet ist. Deshalb befinden sich einige alternative Ansätze in separaten Verzeichnissen des Tracer-Plugins. 3.4.3 Probleme Die Implementierung des Tracer-Plugin ist im Vergleich zum Instrumenter-Plugin einfacher gewesen. Ein Problem stellte jedoch die grundsätzliche Visualisierung der verwendeten Breakpoints in der GUI dar. Hierdurch könnten Benutzer die für die Messung notwendigen Breakpoints deaktivieren und die Messung verfälschen. Das Verbergen dieser Breakpoints war entscheidend für die Benutzbarkeit des TracerPlugins. Dieses Problem erhöhte den Aufwand der Implementierung erheblich. Um die Breakpoints vor dem Anwender zu verbergen, durfte der Breakpoint-Manager nicht verwendet werden. Ein Teil seiner Funktionalität musste deshalb nachgebaut werden, wie z. B. das Generieren und Hinzufügen von für die Messung nötigen Breakpoints, sobald ein Programm mit aktiviertem Tracer-Plugin im Debug-Modus gestartet wird. Zudem mussten spezielle Breakpoint-Marker definiert werden, die keine Icons anzeigen. Ein bedeutenderes Problem haben Vergleichsmessungen ergeben: Die Messergeb­ nisse des Tracer-Plugin unterscheiden sich deutlich von den Ergebnissen des Instru­ menter-Plugins. Dies betrifft jedoch ausschließlich die dynamisch erfassten Daten (also Daten, die zur Laufzeit des zu untersuchenden Programmes gesammelt werden). Dafür konnten mehrere Ursachen ausfindig gemacht werden, die das Tracer-Plugin betreffen. 1. Es befindet sich mindestens ein Bug in der Implementierung der Breakpoints bzw. des dazugehörigen Java-Debug-Frameworks.27 Aufgrund dieses Bugs werden Breakpoints, die an bestimmten Stellen gesetzt werden nicht getrof­ fen. Dies hat zur Folge, dass bestimmte Methoden (und damit alle darin ent­ haltenen Anweisungen) als nicht ausgeführt gezählt werden, obwohl dies tat­ sächlich nicht zutrifft. 27 Vgl. [33]. Dieser Bug soll in der Version 3.2 beseitigt werden. 54 2. Breakpoints können nicht jedem Java-Sprachkonstrukt hinzugefügt werden. So kann ein JavaLineBreakpoint nicht auf eine Case-Anweisung gesetzt werden. 3. Bei mehreren Anweisungen in einer Zeile kann durch JavaLineBreak­ points nicht unterschieden werden, welche der Anweisungen tatsächlich ausgeführt wird. Quellcode 6 demonstriert dieses Problem. Für Zeile 5 werden zwei JavaLineBreakpoints gesetzt, einer für die If-Anweisung und ein weiterer für die darauf folgende Zuweisung. Sobald Zeile 5 erreicht wird, werden jedoch beide Breakpoints getroffen und das Plugin zählt beide Anwei­ sungen als ausgeführt, obwohl die Zuweisung (b=true;) tatsächlich gar nicht ausgeführt werden konnte. 1 2 3 4 5 6 7 public class BreakpointProblem { public static void main(String[] args) { boolean b = false; if(b){b=true;} } } Quellcode 6: Multiple-Breakpoint-Problem Es konnte bis zum jetzigen Zeitpunkt nicht endgültig geklärt werden, ob ausschließ­ lich die oben genannten Ursachen für die unterschiedlichen Messergebnisse verant­ wortlich sind, oder ob noch weitere vorhanden sind. Aufgrund von zusätzlichen Ver­ gleichsmessungen mit einem kommerziellen Werkzeug (vgl. Kapitel 4) kann das In­ strumenter-Plugin mit hoher Wahrscheinlichkeit als Ursache für die unterschiedli­ chen Messergebnisse ausgeschlossen werden. Denn das Instrumenter-Plugin liefert bei den Vergleichsmessungen stets die gleichen Ergebnisse wie das kommerzielle Werkzeug. 3.5 Adapter Die Adapter-Komponente dient nicht nur als gemeinsame Schnittstelle nach Außen für verschiedene „innere“ Verfahren, sondern stellt zudem Hilfsklassen bereit, die von unterschiedlichen Verfahren genutzt werden können. 55 3.5.1 Schnittstellen «Schnittstelle» ICoverageListener +stateChanged () +runStarted () +runTraceCall () +runFinished () +annotationStarted () +annotationTraceCall () +annotationFinished () * de.uni.hannover .se.coverage Singelton 1 UpdateInformer +getInstance() : UpdateInformer +fireStateChanged () +fireRunStarted () +fireTraceCall () +fireRunFinished () +fireAnnotationStarted () +fireAnnotationTraceCall () +fireAnnotationFinished () de.uni.hannover .se.coverage .internal Abbildung 18: Adapter Die Schnittstelle ICoverageRunListener und die Klasse UpdateInformer sind der Kern der Adapter-Komponente. Die Klasse UpdateInformer ist als Singelton implementiert, und dient als zentrale Instanz für die Anknüpfung des Tracer- und des Instrumenter-Plugins. Beide Plugins nutzen die UpdateInformer-Instanz, um interessierte Plugins über alle statischen und dynamischen Messdaten, sowie über die Erhebung der Messdaten zu informieren. Interessierte Plugins müssen die ICo­ verageListener-Schnittstelle implementieren und den Extension-Point des Adap­ ter-Plugins benutzen. Jede fire-Methode des UpdateInformers ruft das jeweilige pendant der registrierten ICoverageListener auf. Die Instantiierung und die Re­ gistrierung der über die Extensions bekannten Listener findet erst statt, wenn die Klasse UpdateInformer instantiiert wird. Dies ist der Fall, sobald eine der fire-Me­ thoden der Klasse UpdateInformer zum ersten mal aufgerufen wird. Auf diese Weise kann die zu frühe oder sogar unnötige Instantiierung der Listener und ihrer zugehöriger Plugins verhindert werden. 3.5.2 Messdaten Während der Analyse des Quellcodes werden folgende Daten erfasst und über die annotationTraceCall-Methode an die registrierten Listener weitergegeben: • der Projektname 56 • der projekt-relative Pfad der Quellcodedatei • der ASTNode-Typ einer Methode oder Anweisung • die Startposition einer Methode oder Anweisungen • die Länge einer Methode oder Anweisungen • die Zeilennummer einer Methode oder Anweisungen • der Methodenname Zur Laufzeit einer Messung werden lediglich der Projektname, der Pfad der Quellco­ dedatei und die Id über die runTraceCall-Methode weitergereicht. Dabei ent­ spricht die Id der Reihenfolge der analysierten Methoden oder Anweisungen bezo­ gen auf eine Quellcodedatei. 3.5.3 Hilfsklassen Die Adapter-Komponente enthält drei Hilfsklassen: • NatureUtils hilft beim Hinzufügen und Entfernen von Naturen eines Pro­ jektes. • BuilderUtils hilft beim Hinzufügen und Entfernen von Buildern eines Pro­ jektes. • CoverageFolderUtils bietet verschiedene Methoden an, um ein Unterver­ zeichnis in einem Projekt für die anfallenden Messdaten und, im Falle des In­ strumenter-Plugins, für die instrumentierten Quellcodedateien zu verwalten. 3.5.4 Probleme Die erfassten Daten und die verwendeten Schnittstellen sind bereits recht umfang­ reich und für die Messung der Testabdeckung völlig ausreichend. Es ist jedoch vor­ stellbar, dass für bestimmte Anwendungen noch weitere Laufzeitinformationen be­ nötigt werden, wie z. B. eine Indentifikationsnummer eines Objektes oder eines Threads. Solche speziellen Anforderungen können derzeit jedoch nur durch An­ passungen des Instrumenter- und/oder des Tracer-Plugins, sowie der bestehenden Schnittstellen erreicht werden. 3.6 Persistence Die Persistence-Komponente sammelt alle anfallenden Daten, die die ICover­ ageListener-Schnittstelle liefert und speichert diese persistent in einer Datenbank ab. Hierfür fordert die Klasse CoverageListenerImpl von der Klasse Database­ 57 Manager eine Datenbank mit der IDatabase-Schnittstelle an. Die Klasse Databa­ seImpl implementiert diese Schnittstelle. Intern nutzt die Klasse DatabaseImpl die freie und objektorientierte DB4O-Datenbank28. Der DatabaseManager verwaltet die Datenbanken auf Projektbasis, d.h. für jedes Projekt generiert der DatabaseMa­ nager eine separate Datenbank-Instanz. Das Persistence-Plugin stellt zudem die öf­ fentliche Schnittstelle IPersistentCoverageDataListener, sowie einen Extensi­ on-Point bereit, den Plugins implementieren und verwenden können, um über neue oder geänderte Datensätze benachrichtigt zu werden. Die Klasse PersistentCo­ verageDatabaseManager ermöglicht den Zugriff auf eine projektbezogene Daten­ bank über die ICoverageDataHandle-Schnittstelle. Diese Schnittstelle hat nur einen Lese-Zugriff auf die Datenbank. Über den Lese-Zugriff können die Datensätze für die Quellcodedateien in Form von CoverageData-Objekten angefordert werden. Abbildung 19 stellt die oben genannten Klassen des Persistence-Plugins dar und ver­ «Schnittstelle» ICoverageListener Adapter Persistence CoverageListenerImpl DatabaseImpl uses db4o saves data requests database for project singleton notificates about changes «Schnittstelle» IPersistentCoverageDataListener singleton «Schnittstelle» IDatabase DatabaseManager intern public uses CoverageData provides PersistentCoverageDatabaseManager provides «Schnittstelle» ICoverageDataHandle Abbildung 19: Persistence-Plugin anschaulicht die Beziehungen der Klassen zueinander. 28 Die DB4O-Datenbank ist über [34] beziehbar. Sie unterliegt der General Public Licence. 58 3.7 ResultView Das ResultView-Plugin präsentiert dem Benutzer die Ergebnisse einer Testabde­ ckungsmessung innerhalb der Eclipse UI. Es setzt auf der Persistence-Komponente auf. Um die View bei Änderung der Daten zu aktualisieren, implementiert das ResultView-Plugin den vom Persistence-Plugin bereitgestellten Extension-Point. Die zum Extension-Point zugehörige IPersistentCoverageDataListener-Schnitt­ stelle wird von der Klasse CoverageViewPoper implementiert. Diese Klasse sorgt für das Erscheinen der CoverageResultView, sofern diese dem Benutzer noch nicht present ist, und benachrichtigt diese über Änderungen der Daten. Da die Klasse Co­ verageResultView den Workbench-View-Mechanismus nutzt, kann die Cover­ ageResultView vom Benutzer jederzeit über das Menü Window > show View ge­ öffnet werden. Die Klasse CoverageResultView dient sowohl als visueller Con­ tainer für den SummeryDataViewProvider und den DetailsDataViewProvi­ der, als auch als Manager dieser Komponenten. So stellt die CoverageResultView Aktionen zum Wechseln zwischen der tabellarischen Zusammenfassungsansicht des SummaryDataProviders und der Detailansicht des DetailDataViewProvi­ ders. Außerdem benachrichtigt die CoverageResultView alle ihr unterliegenden Komponenten über die Änderung von Daten. Die Komponenten greifen dann mittels des PersistentCoverageDatabaseManagers auf die persistenten Daten zu und aktualisieren sich selbst. Die Klasse MarkerCreator sorgt für die Markierung nicht ausgeführter Anweisungen im Quellcodeeditor. Abbildung 20 veranschaulicht die oben beschriebenen Klassen und Zusammenhänge. 59 «Schnittstelle» IPersistentCoverageDataListener CoverageResultViewPoper «eclipse mechanism» activates, updates CoverageResultView manages, shows, koordinates, updates SummaryDataViewProvider DetailDataViewProvider MarkerCreator Abbildung 20: ResultView-Plugin 3.7.1 SummaryDataViewProvider Der SummaryDataViewProvider stellt eine Zusammenfassung einer Messung der Testabdeckung für ein Projekt dar. Dafür benutzt es das Table-Widget des SWT, so­ wie den entsprechenden JFace-TableViewer für die Abbildung der Daten auf das Ta­ Abbildung 21: SummaryDataView - Screenshot ble-Widget. Abbildung 21 zeigt ein Screenshot einer solchen Zusammenfassung der Messdaten. 60 3.7.2 DetailsDataViewProvider Der DetailsDataViewProvider präsentiert dem Benutzer verschiedene Details einer Messung der Testabdeckung. Hierfür wird ein Tree-Widget des SWT benutzt. Für die Abbildung der Daten auf das Tree-Widget wird der JFace-TreeViewer verwendet. Über einen einfachen Filter können alle Anweisungen und Methoden ausgeblendet werden, die mindestens einmal ausgeführt wurden. Abbildung 22 zeigt ein Screenshot des DetailsDataViewProviders ohne aktivierten Filter.29 Abbildung 22: DetailsDataView - Screenshot 3.8 Usability Die Verwendung des Instrumenter- und des Tracer-Plugins ist einfach. Um eines der beiden Plugins für ein Projekt einzuschalten, wird in der Package- oder Navigator­ view über das Kontextmenü des betreffenden Projektes der Propertieseintrag ausge­ wählt. Daraufhin öffnet sich der Propertiesdialog, in dem sich der Instrumenter oder der Tracer aktivieren läßt. Nach der Aktivierung wird, sofern der automatische Build eingeschaltet ist, ein Build des gesamten Projektes ausgeführt. Dieser und alle nach­ folgenden Builds dauern aufgrund der Analyse des Quellcodes und seiner Annotati­ on länger als ohne Aktivierung des Instrumenters oder des Tracers. Dies ist für einen Benutzer vor allem bei einem vollständigen Build spürbar oder im Falle des Instru­ menters bei sehr großen Quellcodedateien. (Vgl. Abschnitt 4) Ansonsten ändert sich für einen Benutzer während der Entwicklung eines Java Projektes nichts. Ein Projekt läßt sich mittels der „Run As“- und „Debug As“-Aktionen wie gewohnt starten. Erst nach der Ausführung eines Projektes erscheint dem Benutzer die ResultView mit den Ergebnissen der Testabdeckungsmessung. Da bisher kein Filter für die zu untersu­ chenden Quellcodedateien vorhanden ist, sollten die JUnit-Tests in ein separates Pro­ jekt ausgelagert werden, damit diese bei der Messung nicht mit erfasst werden. Dies 29 Bei einer schwarz-weißen Darstellung des Screenshots kommt vielleicht nicht zur Geltung, dass die Icons in der Ressource-Spalte sich farblich unterscheiden: bei Anweisungen und Methoden, die mindestens einmal ausgeführt wurden, sind die Icons grün, ansonsten gelb. 61 ist jedoch kein Nachteil, da die Trennung von Testcode und Produktionscode stets zu empfehlen ist. 3.9 Vergleich der Implementierungen In diesem Abschnitt werden die wesentlichen Aspekte der Instrumenter- und der Tracerimplementierung zusammengefasst und gegenübergestellt.Die Gegenüberstel­ lung in Tabelle 5 macht deutlich, dass zwar beide Implementierungen eine gemein­ same Basis für die Konfiguration von Projekten und die Analyse des Quellcodes besitzen, ansonsten jedoch sehr unterschiedliche Stärken und Schwächen aufweisen (bzgl. der Implementierung). Dabei ist die größte Schwäche des Instrumenters, die Missachtung der gesetzten Breakpoints im Debug-Modus. Dagegen ist die größte Schwäche des Tracers, dass nicht alle ausgeführten Methoden und Anweisungen erfasst werden. Die beiden genannten Schwächen unterscheiden sich jedoch qualita­ tiv. Da die Quellcodemodifikation der Natur des Instrumenters entspricht, können die durch die Instrumentierung hinzugefügten Zeilenumbrüche prinzipiell wieder entfernt werden, um dadurch die Nutzung von Breakpoints im Debug-Modus zu ermöglichen. Die Schwäche des Tracers gründet sich dagegen auf den Bugs der Ec­ lipse-Debug-Implementierung, als auch auf den prinzipiellen Unzulänglichkeiten von Breakpoints. Deshalb läßt sich die größte Schwäche des Tracers nicht ohne fundamentale Änderungen beseitigen. 62 Instrumenter-Plugin Tracer-Plugin Basis der Implementierung: • • • Ein zu untersuchendes Projekt wird mit einer Nature und einem Builder konfiguriert. Für die Quellcodeanalyse wird der AST und der ASTVisitor verwendet. Für die Instrumentierung des Quell­ codes wird ein ASTRewriter verwendet. • • • Ein zu untersuchendes Projekt wird mit einer Nature und einem Builder konfiguriert. Für die Quellcodeanalyse wird der AST und der ASTVisitor verwendet. Für die Annotation des Quellcodes werden Breakpoints und Marker verwendet. Inter-JVM-Kommunikation: • Es wird eine Implementierung über Sockets verwendet. • Implizit wird ebenfalls eine Socket­ verbindung verwendet. Diese ist je­ doch durch die JPDA und das Eclip­ se-Debug-Framework verborgen. Deshalb konnte auf eine Imple­ mentierung verzichtet werden. Probleme der Implementierungen: • Es ist kein direkter Zugriff auf den Eclipse-Compiler erlaubt, um die in­ strumentierten Quellcodedateien zu kompilieren. Als Workaround wird über ein ANT-Skript auf den EclipseCompiler zugegriffen. • Die Kompilierung wird ganz dem standard Java-Builder überlassen. • Durch die Instrumentierung werden im Quellcode gesetzte Breakpoints im Debug-Modus nicht mehr getrof­ fen. • Da die Tracer-Breakpoints vor dem Benutzer verborgen sind, und keine Instrumentierung des Quellcodes durchgeführt wird, funktionieren alle vom Benutzer gesetzte Breakpoints. • Der Instrumenter verwendet intern keine Breakpoints und muss diese auch nicht verbergen. • Die Verbergung der Tracer-Break­ points erfordert zuätzlichen Imple­ mentierungsaufwand. • Der Instrumenter erfasst alle ausge­ führten Methoden und Anwei­ sungen. Durch die Quellcodemodifi­ kation können prinzipiell sogar wei­ tere Fragmente des Codes untersucht werden. • Es werden nicht alle ausgeführten Methoden und Anweisungen erfasst. Grund dafür sind Bugs und Unzu­ länglichkeiten der Breakpoints bzw. des Eclipse-Debug-Frameworks. Tabelle 5: Vergleich verschiedener Implementierungsaspekte 63 4 JUnit als Fallstudie Während der Entwicklungsphase wurden zur Erprobung der Plugins Messungen der Testabdeckung nur an sehr kleinen Programmen ausgeführt. Im folgenden wird ex­ emplarisch die Testabdeckung des JUnit-Frameworks mittels des Instrumenter- und des Tracer-Plugins untersucht und mit den Ergebnissen eines kommerziellen Werk­ zeuges verglichen. Diese Untersuchung ist damit ein wichtiger Bestandteil der vor­ liegenden Arbeit. Die gewonnen Ergebnisse können direkt bei der Bestimmung der Grenzen, der Vorteile und der Einsatzgebiete der beiden in dieser Arbeit vorgestell­ ten Ansätze und ihrer Implementierungen helfen. 4.1 Vorgehensweise Die Messungen wurden auf einem Laptop mit einem Intel Centrino 1400 Mhz Pro­ zessor und 512 MB Arbeitsspeicher ausgeführt.30 Als Betriebssystem wurde Windows XP Home verwendet. Für die Untersuchung wurde das JUnit-Framework als Testobjekt verwendet. Es wurde die Buildzeit, die Ausführungszeit, die Projektgröße und die Testabdeckung des JUnit-Frameworks gemessen.31 Alle JUnit-Tests wurden in ein separates Projekt exportiert und somit vom JUnit-Framework getrennt, um den Code der JUnit-Tests nicht mit zu messen. Desweiteren wurden die Pakete junit.awtui und junit.swingui aus der Messung herausgenommen, weil die Ausführung der TestSuite in junit.tests.AllTests diese Pakete nicht erfasst.32 Zum Vergleich wurde das kommerzielle Werkzeug Clover herangezogen. Clover ist ein Werkzeug, welches nach dem Instrumentierungsprinzip arbeitet. Das von der Firma Cortex hergestellte Werkzeug ist für die Arbeit mit JUnit entwickelt worden. Für den Einsatz in Eclipse benötigt es einen beliebigen externen Java Compiler, der den instrumentierten Quellcode übersetzt. Auf Basis der während der Übersetzung erfassten und gespeicherten Daten können die Coverage Ergebnisse direkt in Eclipse oder in verschiedenen Formaten, wie z. B. HTML präsentiert werden. Clover ist in der Lage zusätzlich zur Methoden- und Anweisungsüberdeckung auch die Zweig­ überdeckung zu bestimmen. [35] Für die Zeitwerte eines Builds (build time in Tabelle 6) muss Eclipse mit bestimmten Debugoptionen in einer Konsole gestartet werden. Die Werte der Buildzeit gehören jeweils nur zum spezifischen Builder eines Werkzeugs. Für die gesamt Buildzeit müssen also für die drei Werkzeuge jeweils die Durchschnittszeit des Javabuilders 30 Eine genauere Spezifikation des verwendeten Rechners ist der Begleit-CD in Dokumente>X10.pdf beigefügt. 31 Das JUnit-Framework ist beziehbar über [2] und verfügt selbst über zahlreiche JUnit-Tests. Das für die Messung verwendete JUnit-Projekt ist der Begleit-CD in Projekte>Fallstudie>JUnit beigefügt. 32 Die Pakete junit.awtui und junit.swingui beinhalten jeweils eigene Tests. 64 (Reference-wert in Tabelle 6) hinzugezählt werden. Während der Instrumenter und Clover grundsätzlich auf den Javabuilder verzichten können, ist der Tracer auf die Kompilierung durch den Javabuilder angewiesen.33 Es wurden jeweils zehn Build­ zeitmessungen durchgeführt. Die Zeit für die Ausführung (measure time in Tabelle 6) der Testsuite junit.test­ s.AllTests wurde aus der Eclipse JUnit-UI entnommen. Es wurden jeweils zehn Messungen der Testabdeckung mit dem Instrumenter-, Tracer- und Clover-Plugin ausgeführt und die dafür jeweils benötigte Zeit bestimmt. Zusätzlich wurden zehn Zeitmessungen ausgeführt mit ausgeschalteten Testabdeckungsplugins (Reference in Tabelle 6). Hierbei ist zu beachten, dass nur die Messung mit dem Tracer-Plugin im Debug-Modus ausgeführt wurde. Desweiteren wurde die JUnit-Projektgöße (Project size in Tabelle 6) nach einem Clean mit anschließendem vollständigem Build und einmaliger Ausführung der Testsuite gemessen, wobei jeweils ein anderes Werkzeug aktiviert gewesen war. Als Referenz (Reference in Tabelle 6) wurde die Projektgröße nach der selben Prozedur bestimmt, jedoch ohne dass eines der Werkzeuge dabei aktiviert war. Zuletzt wurde die von den drei Werkzeugen gemessene Testabdeckung der Metho­ den (methods in Tabelle 6) und Anweisungen (statements in Tabelle 6) festgehalten. Als Referenz (Reference in Tabelle 6) wurde die von allen drei Werkzeugen überein­ stimmende Angabe über die Gesamtzahl der vorhandenen Methoden und Anwei­ sungen des JUnit-Frameworks verwendet. 4.2 Auswertung Die Ergebnisse der Messungen sind in Tabelle 6 zusammengefasst. Für eine über­ sichtliche Darstellung des Balkendiagramms wurde die Y-Achse logarithmisch skaliert. In den folgenden Abschnitten werden die Ergebnisse der untersuchten Kate­ gorien analysiert und diskutiert. 33 Der Javabuilder kann manuell durch einen Benutzer deaktiviert werden. Die Deaktivierung des Javabuilder ist jedoch nicht empfehlenswert, da verschiedene Editorfunktionen auf ihn angewiesen sind. Deshalb ist der Javabuilder auch beim Instrumenter und bei Clover standardmäßig aktiviert. 65 Tabelle 6: Zusammenfassung der JUnit-Fallstudienergebnisse 4.2.1 Methoden und Anweisungen Durch die logarithmische Skalierung der Y-Achse des Diagrammes in Tabelle 6 kom­ men die Unterschiede der erfassten Methoden und Anweisungen graphisch nicht sehr deutlich zur Geltung. Der Vergleich der Werte zeigt aber, dass das Tracer-Plugin weniger ausgeführte Methoden und Anweisungen zählt als das Instrumenter- und das Clover-Plugin. Mögliche Ursachen dafür wurden bereits in Abschnitt 3.4.3 disku­ tiert. In wie weit die gemessene Testabdeckung von 75% für Methoden und 72% für Anweisungen (gemessen mit dem Instrumenter- und Clover-Plugin) akzeptabel ist, bedarf einer Interpretation, die den Rahmen dieser Arbeit sprengen würde.34 4.2.2 Builddauer Bei den in Tabelle 6 aufgeführten Werten für die Buildzeit handelt es sich um die Mittelwerte der jeweils zehn mal durchgeführten Messungen. Es ist zu erkennen, dass das Tracer-Plugin dabei eine seiner Stärken entfaltet: selbst wenn die durch­ schnittliche Builddauer des Javabuilders hinzugerechnet werden würde, wäre die ge­ samt Buildzeit kürzer als vom Instrumenter-Plugin oder Clover. Der Grund für diese gute Zeit ist, dass keine Modifikation des Quellcodes vom Tracer durchgeführt wird, und somit die Buildphase deutlich weniger Aufwand und Zeit benötigt. Dieser Vor­ teil ist besonders bei dem erstmaligen Build oder bei einem vollständigen Build deut­ lich. Der Vorteil relativiert sich jedoch aufgrund der inkrementellen Art des Build­ 34 Mit der Unterstellung, dass ein Werkzeug zur Erstellung und Ausführung von Tests in besonderem Maße selbst getestet wird, lassen sich die errechneten Werte als gute und anzustrebende Marken deuten. 66 vorganges bei folgenden Builds erheblich. Der Unterschied in der Builddauer zwi­ schen dem Instrumenter und Clover ist gering, spricht aber für den Instrumenter. Weshalb Clover mehr Zeit benötigt, konnte nicht bestimmt werden. 4.2.3 Ausführungdauer Die ermittelten Zeitspannen für die Messung der Testabdeckung (measure time) durch die unterschiedlichen Plugins sind von großer Bedeutung, denn sie liefern ein wichtiges Kriterium für die produktive Verwendbarkeit der Werkzeuge. Die in Tabelle 6 aufgeführten Zeitspannen sind Mittelwerte von den jeweils zehn durchge­ führten Messungen.35 Die Performance des Clover-Plugins ist besonders gut. Clover ist schon seit mehreren Jahren auf dem Markt und konnte dementsprechend optimiert werden. Clover verwendet einen externen Compiler, in diesem Fall den Java Sun SDK 1.5_03 Compi­ ler. Desweiteren liefert Clover keinen Callback in Echtzeit während der Messung zu Gunsten der Performance und ist damit nicht so flexibel wie das in dieser Arbeit entwickelte Instrumenter- und Tracer-Plugin. Mit 3,9 Sekunden benötigt das Instrumenter-Plugin im Vergleich zu Clover deutlich mehr Zeit. Dennoch ist das Ergebnis zufriedenstellend, da bisher nur wenige Optimierungen implementiert wurden. Weiteres Optimierungspotential befindet sich z. B. in der Inter-JVM-Kommunikation. Aber auch das Persistence-Plugin und das ResultView-Plugin lassen sich an verschiedenen Stellen optimieren, insbesondere an den Schnittstellen und den Benachrichtigungsmechanismen. Dennoch kann aufgrund der Instrumentierung und der damit verbundenen Ausführung zusätzlicher Anwei­ sungen von dem Instrumenter- und dem Clover-Plugin nie der optimale ReferenzWert erreicht werden. Auf die Ausführungsgeschwindigkeit beider Verfahren wir­ ken zudem die größeren Class-Dateien negativ aus. Denn die JVM benötigt dadurch mehr Zeit diese zu laden, zu analysieren und auszuführen. Die Geschwindigkeitseinbußen des Instrumenters und des Clover-Plugins sind je­ doch gering im Vergleich zu den Geschwindigkeitseinbußen des Tracer-Plugins. Das Trcaer-Plugin benötigt rund 106 Sekunden für die Messung der JUnit-Testabde­ ckung. Die Gründe für diese negative Leistung sind vielfältig: • Im Vergleich zu Clover oder zum Instrumenter werden nicht so viele Berech­ nungen vor der Ausführung der Messung durchgeführt, denn die nötigen Break­ points werden erst erzeugt, wenn ein Debug Target vorhanden ist. Würden jedoch die Breakpoints schon vorher generiert, müsste eine größere Anzahl von ihnen erzeugt werden, weil sie nicht mehr selektiv auf die tatsächlich auszuführenden Methoden und Anweisungen gesetzt werden könnten. Zusätzlich hätte dies eine 35 Die vollständige Tabelle aller Zeitmessungen ist auf der Begleit-CD unter \Dokumente\Tabellen\junitbenchmark.xls enthalten. 67 enorme Speicherbelastung während der gesamten Entwicklung eines Projektes zur Folge. • Ein weiteres Problem ist die Komplexität der Breakpoints. Jedes Breakpoint-Objekt benötigt einen Marker, der die verschiedenen Attribute des Breakpoints speichert. Der Marker wiederum verwendet eine Liste als interne Datenstruktur. Somit werden für jedes Breakpoint-Objekt eine Vielzahl weiterer Objekte erzeugt. • Durch die Verwendung von generellen Breakpoints konnte zwar die Anzahl der Breakpoints reduziert werden, jedoch mussten dafür zusätzliche persistente In­ formationen einigen Markern hinzugefügt werden. Diese Informationen sind zur Laufzeit einer Messung für die Zuordnung der Breakpoints zu den in der Analyse­ phase erfassten Methoden und Anweisungen nötig. Die Extraktion der Informa­ tionen aus den Markern erfordert zusätzlichen Aufwand durch eine erweiterte Pro­ grammlogik und durch zusätzliche Festplattenzugriffe. • Sowohl das Eclipse Debug-Framework, als auch die JPDA-Architektur sorgen je­ weils für einen gewissen Overhead, der die Performance mindert. Es ist schwierig die oben genannten Gründe zu gewichten, zumal sie eng mitein­ ander verknüpft sind. Desweiteren läßt sich kaum ausschließen, dass weitere Gründe existieren, die zur schlechten Performance des Tracer-Plugins beitragen. So trägt si­ cher das noch nicht optimierte Persistence-Plugin zur schlechteren Performance zu­ mindest im Vergleich zu Clover bei. Eine Verbesserung der Tracer-Plugin-Per­ formance könnte eine gänzlich andere Vorgehensweise beim Setzen der Breakpoints bewirken. Vielversprechend ist auch die Anfrage nach speziellen Breakpoints, soge­ nannten Tracepoints in [36]. Diese könnten vielleicht eine Verbesserung der TracerPlugin-Performance ermöglichen. Eine Eigenentwicklung von Breakpoints direkt ba­ sierend auf der JPDA oder eine Entwicklung einer speziellen JVM, die besonders „schnelle“ und „leichte“ Breakpoints unterstützt, würde zwar einen deutlich höheren Implementierungsaufwand erfordern, jedoch auch eine deutlich höhere Performan­ cesteigerung ermöglichen.36 4.2.4 Projektgröße Durch die Verwendung der Plugins zur Messung des Testabdeckung wächst als ne­ gativer Effekt die Projektgröße. Den größten Zuwachs verursacht das Clover-Plugin. Clover untersucht allerdings auch mehr Metriken als der Instrumenter und der Tracer, wodurch eine stärkere Instrumentierung der Quellcodedateien als beim In­ strumenter nötig ist und mehr Daten bei der Messung gespeichert werden müssen. Der Tracer verursacht den geringste Zuwachs bei der Projekgröße, denn er muss keine Kopie und Instrumentierung des Quellcodes vornehmen. Jedoch legt auch der 36 Vergleiche dazu [22], sowie Abschnitt 2.5. 68 Tracer die erfassten Daten im Projektverzeichnis ab37, so dass die Projektgröße im Vergleich zum Referenzwert wächst. Bei der Kapazität heutiger Festplatten ist der Speicherverbrauch durch den Pro­ jektzuwachs selbst bei größeren Projekten kein echtes Problem. Zu beachten ist je­ doch, dass durch den Instrumenter auch die Class-Dateien an Größe zunehmen. Für extrem ressourcenbegrenzte Systeme wie Mobiltelefone kann die Speicherung und Ausführung dieser Class-Dateien problematisch sein. In solchen Fällen ist der Tracer deutlich im Vorteil. 4.3 Vergleich der JUnit-Fallstudienergebnisse In der folgenden Tabelle 7 werden die Ergebnisse der Fallstudie der beiden in dieser Arbeit entwickelten Plugins zusammengefasst und einander gegenübergestellt. Die Ergebnisse der Fallstudie resultieren aus dem verwendeten Programm für die Unter­ suchung (JUnit-Framwork), des verwendeten Systems (Rechner und Betriebssystem usw.) und der Implementierung der Plugins. Wobei die Implementierungen auf den in Abschnitt 2 vorgestellten Ansätzen und Grundlagen basieren. Die Fallstudie be­ antwortet die bereits in Abschnitt 2.3 gestellt Frage nach dem generierten Overhead beider Verfahren. Die zusammengefasste Ergebnis der Fallstudie lautet: das Tracing­ verfahren hat in seiner jetzigen Form für den praktischen Einsatz als Werkzeug zur Messung von Testabdeckungen bei mittelgroßen (und mit ziemlicher Sicherheit auch bei großen) Projekten einen deutlichen Performancenachteil gegenüber dem Instru­ mentierungsverfahren. 37 Eigentlich legt das Persistence-Plugin die Daten im Projektverzeichnis ab. 69 Instrumenter Tracer • Die Builddauer wird aufgrund der Instrumentierung des Quellcodes deutlich länger. Dies ist besonders bei einem vollständigen Build der Fall, relativiert sich aber bei folgenden inkrementellen Builds. • Die Builddauer ist im Vergleich zum­ Instrumenter oder Clover sehr kurz. Sie resultiert lediglich aus der Analy­ se des zu untersuchenden Quellcodes und der dabei persistent erfassten Daten. • Die Ausführungsdauer verlängert sich deutlich, bleibt aber für die Nutzung kleiner und mittelgroßer Projekte zumutbar. • Die Ausführungsdauer verlängert sich extrem und beeinträchtigt damit erheblich die Nutzung des Plugins in seiner jetzigen Form. • Es wird eine instrumentierte Kopie des originalen Codes angelegt und übersetzt. Dadurch sind die ClassDateien entsprechend größer. Zudem werden die Messdaten im Projektver­ zeichnis abgelegt. Dies kann proble­ matisch bei ressourcenbeschränkten Systemen sein. • Es werden lediglich die Messdaten im Projektverzeichnis abgelegt. Die Größe der Class-Dateien wird nicht verändert. Das ist bei ressourcen­ beschränkten Systemen von großem Vorteil. • Durch verschiedene Optimierungen können voraussichtlich die Builddau­ er und die Ausführungsdauer noch deutlich reduziert werden. • Zwar können gewisse Optimierungen vorgenommen werden. Diese werden voraussicht­ lich jedoch nicht wesentliche Per­ formancesteigerungen erzielen können. Dafür sind fundamentale Änderungen nötig. Tabelle 7: Vergleich der implementierten Verfahren anhand der JUnit-Fallstudienergebnisse Die Fallstudie liefert jedoch kein representatives Ergebnis, da nur ein Programm auf nur einem System untersucht wurde. Eine ausführlichere Untersuchung der Grenzen und Potentiale beider Verfahren konnte jedoch auf Grund des erforderlichen Zeit­ aufwandes und Umfanges der dafür notwendigen Untersuchungen im Rahmen dieser Arbeit nicht durchgeführt werden. 70 5 Abschlussbetrachtungen Im folgenden wird basierend auf den Ergebnissen dieser Arbeit zunächst ein Fazit gezogen. Anschließend wird ein Ausblick bezüglich des in dieser Arbeit entwickelten Werkzeuges und der beiden zu Grunde liegenden Verfahren geboten. 5.1 Ergebnisse und Fazit In dieser Arbeit wurden zwei sehr verschiedene Verfahren untersucht, die die Ab­ laufverfolgung eines Programmes in Echtzeit ermöglichen. Anhand von Vergleichen diverser Aspekte auf verschiedenen Ebenen (Konzepte, Implementierungen, Fallstu­ die) wurden die Vor- und Nachteile beider Verfahren herausgearbeitet. Es kann je­ doch kein eindeutiger „Sieger“ benannt werden, denn beide Verfahren haben unter­ schiedliche Stärken und Schwächen.38 Um zu bestimmen, in wie weit die in dieser Arbeit vorgestellten Verfahren für ein bestimmtes Einsatzgebiet geeignet sind, müssen weitere fallspezifische Untersuchungen durchgeführt werden. Die Fallstudie liefert dabei einen ersten Hinweis auf die Grenzen und Möglichkeiten der beiden Verfahren. Aufgrund der Modularisierung, der definierten Schnittstellen und der Fähigkeit In­ formationen über den Programmablauf in Echtzeit bereitzustellen, können die in dieser Arbeit implementierten Plugins als eine Basis für ein breites Spektrum von Anwendungen im Bereich der Entwicklung, Analyse und Tests von Software dienen. Trotz der großen Flexibilität der beiden Verfahren unterliegen diese auch gewissen Einschränkungen. Erstens ist ihr Einsatz auf den Eclipse Kontext beschränkt. Zwei­ tens ist die aktuelle Implementierung auf die Messung von Testabdeckungen aus­ gerichtet. Zwar werden mehr Informationen gesammelt als für die Messung der Tes­ tabdeckung nötig sind, jedoch werden keine Laufzeitinformationen über Objekte und Threads ermittelt. Bei Bedarf läßt sich dies nur durch eine Änderung der Imple­ mentierung erreichen39, nicht aber durch eine externe Konfiguration. Für den praktischen Einsatz ist es mittels der beiden Verfahren gelungen ein Werk­ zeug zur Messung von Testabdeckungen zu implementieren. Dieses Werkzeug erlaubt die einfache Wahl des zu verwendenden Verfahrens für die Ausführung der Messung. Die gesammelten Daten werden persistent gespeichert und dem Benutzer in der vertrauten Eclipse Art präsentiert. Auch wenn sich das Tracingverfahren auf­ grund der besonders schlechten Performance und der zeilenbasierten Auflösung als eher ungeeignet für die Messung der anweisungsbasierten Testabdeckung herausge­ 38 Vgl. Abschnitt 2.3 Tabelle 1, Abschnitt 3.9 Tabelle 5, Abschnitt 4.2.1 Tabelle 6 und Abschnitt 4.3 Tabelle 7. 39 Beim Instrumenter lassen sich zusätzliche Laufzeitinformationen durch eine andere Instrumentierung erreichen. Beim Tracer kann dies durch Auswertung der Stack-Frames erreichen. 71 stellt hat, so könnte dieser Ansatz für andere Anwendungen sich als besonders sinn­ voll erweisen, insbesondere wenn nur wenige Stellen des Quellcodes untersucht und damit annotiert werden müssen oder wenig Speicherplatz zur Verfügung steht. So ist der Tracer z. B. für eine methodenbasierte Testabdeckung gut geeignet. 5.2 Ausblick Die aktuelle Implementierung der Plugins befindet sich noch in einem frühen Entwicklungsstadium. Für einen produktiven Einsatz sollte es noch weiter getestet werden. Desweiteren muss an der Optimierung der Performance gearbeitet werden. Durch erweiternde Plugins können leicht neue Features dem bestehenden Werkzeug hinzugefügt werden. So könnte ein Plugin die gesammelten Daten des PersistencePlugins in ein PDF, XML oder HTML Format exportieren. Eine weiteres Plugin könn­ te eine Paket basierte View bereitstellen, die die prozentuale Testabdeckung abbildet, so ähnlich wie es z. B. Clover tut. Möglich wäre auch die Presentation der Testabde­ ckung in einer 2D oder 3D Ansicht in Form einer Gebirgslandschaft über den verwendeten Paketen: je größer der Grad der Abdeckung, desto höher und grüner das Gebirge über einem Paket, und je kleiner der Grad der Abdeckung, desto roter und tiefer das Tal über einem Paket. Weitere vergleichsweise einfach zu umzusetzende Features erfordern jedoch gewisse Änderungen und Erweiterungen der bestehenden Implementierung. Sie würden da­ für aber den Wert des Instrumenter- bzw. Tracer-Plugins erheblich steigern. Hierzu zählt z. B. ein Filter für die zu untersuchenden Klassen (oder Dateien). Durch die Selektion der zu untersuchenden Klassen (oder Dateien) könnte der Verarbeitungs­ aufwand zu Gunsten der Performance deutlich reduziert werden und der Benutzer könnte sich auf die ausgewählten Klassen (oder Dateien) konzentrieren. Der Imple­ mentierungsaufwand für diese sehr sinnvolle Feature wäre zudem nicht besonders groß. Ein weiteres, allerdings bereits etwas schwieriger zu realisierendes Features wäre z. B. die Kontrolle über den Echtzeitbenachrichtigungsmechanismus, um durch die Deaktivierung eine höhere Performance erzielen zu können. Desweiteren wäre es nützlich innerhalb des Persistence-Plugins die Historie der Daten speichern zu können, statt nur jeweils den letzten Datensatz. Von grundlegender Natur wäre hingegen eine Flexibilisierung der Annotation durch einen Konfigurationsmechanismus. Durch solch einen Mechanismus könnte also z. B. die Art der Instrumentierung verändert werden, um z. B. weitere Metriken (z. B. Pfadabdeckung) messen zu können, ohne den Plugin-Code modifizieren zu müssen. Es muss jedoch mit einem hohen Aufwand für die Umsetzung eines solchen Me­ chanismus gerechnet werden. 72 Natürlich sind durch Modifikation der Basis-Plugins (Tracer und Instrumenter) auch ganz andere Werkzeuge bzw. Anwendungen vorstellbar, dessen Implementierungs­ aufwand hier nicht abgeschätzt werden kann. Die Implementierung und Integration weitere Basisverfahren, wie z. B. der Bytecode­ instrumentierung (vgl. Abschnitt 2.5) könnte vielleicht für bestimmte Werkzeuge von Vorteil sein. Neue Basisverfahren könnten andere Vorzüge gegenüber den beiden in dieser Arbeit vorgestellten Verfahren aufweisen. Mit Hilfe der in dieser Arbeit entwickelten und vorgestellten Instrumenter- und Tracer-Plugins ist auch ohne weitere Änderungen die Umsetzung verschiedener Werkzeuge möglich. So ist z .B. eine Verfolgung des Programmablaufes in Echtzeit anhand eines UML-Diagrammes vorstellbar. Durch visuelle Hervorhebung der aus­ geführten Methoden und der zugehörigen Klassen im UML-Diagramm könnte dem Benutzer veranschaulicht werden, in welchem Bereich sich die aktuelle Programm­ ausführung befindet. Durch die Verwendung des Tracers in Verbindung mit dem Debug-Modus könnte die Ausführung des Programmes durch den Benutzer Schritt für Schritt (step to, step over) kontrolliert werden. Für all diese Konzepte bieten der in dieser Arbeit entwickelte Tracer und Instru­ menter eine solide Grundlage. Aufgrund der Eclipse-Integration lassen sich diese Plugins und zukünftige Umsetzungen der hier genannten Ideen in einem größeren Rahmen zusammen in eine Eclipse Perspektive einbinden, um auf diese Weise dem Benutzer verschiedene auf einander abgestimmte Werkzeuge für die Analyse und die Entwicklung von Software zur Verfügung zu stellen. 73 Literaturverzeichnis 1. Eclipse Foundation (Hrsg.): Eclipse. , URL: http://www.eclipse.org 2. Gamma, E. K. Beck (Hauptentwickler): JUnit. , URL: http://www.junit.org/index.htm 3. Wikipedia (Hrsg.): Wikipedia Enzyklopädie. 2005, URL: http://www.wikipedia.de 4. Piwowarski, P., M. Ohba, J. Caruso: Coverage Measurement Experience During Function Test. ICSE '93: Proceedings of the 15th international conference on Softwa­ re Engineering, Seite(n): 287-301, IEEE Computer Society Press, Los Alamitos 1993 5. Brörkens, M.: Trace- und Zeitzusicherungen beim Programmieren mit Vertrag. Carl von Ossietzky Universität Oldenburg 2002, URL: http://csd.informatik.uni-olden­ burg.de/~skript/pub/diplom/broerkens02.pdf 6. Weniger, T.: Seminar Deep Java: JDI - Java Debug Interface. Universität Würzburg 2002, URL: http://www2.informatik.uni-wuerzburg.de/mitarbeiter/eichelberger/de­ epJava2002/JDI/jdi.pdf 7. Sun Microsystems (Hrsg.): Java Platform Debug Architecture. 2005, URL: http://ja­ va.sun.com/j2se/1.4.2/docs/guide/jpda/architecture.html 8. Sun Microsystems (Hrsg.): JDI Javadoc. 2005, URL: http://java.sun.com/j2se/1.5.0/docs/guide/jpda/jdi/index.html 9. Lindholm, T.: The Java Virtual Machine Specification. Addison-Wesley, Massa­ chussets 1997 10. Venners, B.: Inside the Java Virtual Machine - 2nd ed.. Addison-Wesley, New York 2000 11. Winkler, S.: Cover your world. 2003, URL: http://www.oio.de/testcoverage-clover­ .htm 12. Kaner, C.: Software Negligence and Testing Coverage. 1996, URL: http://ww­ w.kaner.com/coverage.htm 13. Kalmar, R. (verantwortlicher Redakteur): Testmetriken. o.A., URL: http://www.soft­ ware-kompetenz.de/?10764 14. Chawla, A., A. Orso: A generic instrumentation framework for collecting dynamic information. ACM SIGSOFT Software Engineering Notes, Seite(n): 1-4, ACM Press, New York 2004 15. Chawla, A.: InsECT. , URL: http://insectj.sourceforge.net 16. Dmitriev, M.: Profiling Java Applications Using Code Hotswapping and Dynamic Call Graph Revelation. WOSP '04: Proceedings of the 4th international workshop on Software and performance, Seite(n): 139-150, ACM Press, New York 2004 74 17. Sun Microsystems (Hrsg.): JFluid: The NetBeans Profiler Project. , URL: http://profi­ ler.netbeans.org/index.html 18. Suominen, J., M. Riekkinen: JIAPI: Java Instrumentation API. 2001, URL: http://jia­ pi.sourceforge.net/ 19. Lance, D., R. H. Untch, N. J. Wahl: Bytecode-based Java Program Analysis. ACM-SE 37: Proceedings of the 37th annual Southeast regional conference, Seite(n): o.A., ACM Press, New York 1999 20. Tromer, Eran: Java Instrumentation Engine. 1999, URL: http://verdeco.co.il/eran/jie/ 21. Tao, K. , W. Wang, J. Palsberg: Java Tree Builder. 1997, URL: http://compi­ lers.cs.ucla.edu/jtb/jtb-2003/ 22. Childers, B. R. et al.: SoftTest: A Framework for Software Testing of Java Programs. eclipse `03: Proceedings of the 2003 OOPSLA workshop on eclipse technology eX­ change, Seite(n): 78-83, ACM Press, New York 2003 23. Kim, Y. W.: Efficient Use of Code Coverage in Large-Scale Software. CASCON '03: Proceedings of the 2003 conference of the Centre for Advanced Studies on Collabora­ tive research, Seite(n): 145-155, IBM Press, Toronto, Ontario, Canada 2003 24. Wikipedia (Hrsg.): Wikipedia Enzyklopädie: "Eclipse (IDE)". 2005, URL: http://de.wikipedia.org/wiki/Eclipse_%28IDE%29 25. Daum, B.: Java-Entwicklung mit Eclipse 3. dpunkt.verlag, Heidelberg 2004 26. Beck , K., E. Gamma: Contributing to Eclipse: principles, patterns, and plug-ins. Addison-Wesley, Boston 2004 27. Object Technology International, Inc.: Eclipse Platform - Technical Overview. 2001, update 2003 for Eclipse 2.1, URL: http://www.eclipse.org/whitepapers/eclipse-over­ view.pdf 28. Bolour, A.: Notes on the Eclipse Plug-in Architecture. 2003, URL: http://www.eclip­ se.org/articles/Article-Plug-in-architecture/plugin_architecture.html 29. Arthorne, J.: Project Builders and Natures. 2004, update November 2004, URL: http://www.eclipse.org/articles/Article-Builders/builders.html 30. Gamma, E., R. Helm, R. Johnson, J. Vlissides: Design Patterns. Addison-Wesley, Bo­ ston 2004 31. Wright, D., B. Freeman-Benson: How to write an Eclipse debugger. 2004, URL: http://www.eclipse.org/articles/Article-Debugger/how-to.html 32. Wright, D., B. Freeman-Benson: The Eclipse Debug Framework. 2005, URL: 33. Dul, P.: Bugzilla Bug 104978: add new statements without a newline with AST­ Rewriter. 2005, URL: https://bugs.eclipse.org/bugs/show_bug.cgi?id=104978 75 34. Dul, P.: Bugzilla Bug 106303: method breakpoint hit problem. 2005, URL: https://bugs.eclipse.org/bugs/show_bug.cgi?id=106303 35. Rosenberger, C. (Chief Architect): db4objects. 2005, URL: http://www.db4o.com/ 36. Cenqua: Clover code coverage tool. , URL: http://www.cenqua.com/clover/in­ dex.html 37. Hudson, R.: Bugzilla Bug 71020: Tracing Breakpoint or "Tracepoint". 2004, URL: htt­ ps://bugs.eclipse.org/bugs/show_bug.cgi?id=71020 76 Anhang Dieser Arbeit ist eine CD beigefügt. Darauf befinden sich die in dieser Arbeit entwi­ ckelten Plugins als Eclipse Plugin Projekte. Für eine einfache Installation des Werk­ zeugs zur Messung von Testabdeckungen wurde dieses zudem als eine Eclipse Up­ date Site beigefügt. Als Voraussetzung für die Installation wird Eclipse 3.1 benötigt. Desweiteren wurden das in der Fallstudie untersuchte JUnit-Projekt, die Ergebnisse der Fallstudie als Excel-Tabelle und die vorliegende Ausarbeitung als PDF- und OpenOffice 2.0 Dokument auf der CD untergebracht. Die folgende Abbildung zeigt die Verzeichnisstruktur der CD. Name Typ \ Dokumente Folder Projekte Folder \Dokumente Tabellen Folder Abstract.odt OpenDocument Text MasterArbeit.odt OpenDocument Text MasterArbeit.pdf Adobe Acrobat 7.0 Document x10.pdf Adobe Acrobat 7.0 Document \Software de.uni-hannover.se.coverage Folder de.uni-hannover.se.coverage.persistenz Folder de.uni-hannover.se.coverage.view Folder de.uni-hannover.se.coveragefeature Folder de.uni-hannover.se.coveragesite Folder de.uni-hannover.se.hardcoverage Folder de.uni-hannover.se.softcoverage Folder JUnit Folder JUnitTest Folder 77 Erklärung Hiermit versichere ich, dass ich diese Arbeit selbstständig verfasst und keine anderen als die angegebenen Hilfsmittel und Quellen benutzt habe. Langenhagen, 17. Oktober 2005 78