Entwicklung eines Tools zur Messung von Testabdeckungen

Werbung
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
Herunterladen