Automatisiertes Testen

Werbung
Automatisiertes Testen
Tests sind ein wichtiges Hilfsmittel um Programmierfehler aufzudecken. Sie können zwar
niemals die Abwesenheit von Fehlern zeigen (dazu braucht man Beweise) zeigen jedoch
häufig deren Anwesenheit und sind oft einfacher zu erstellen als Korrektheitsbeweise.
Diese QuickSort Implementierung
qsort :: Ord a => [a] -> [a]
qsort []
= []
qsort (x:xs) =
filter (<x) xs ++ x : filter (>x) xs
können wir manuell im GHCi testen:
ghci> qsort [12,3,4]
[3,4,12]
Alternativ könnten wir eine Reihe solcher Tests in eine Datei schreiben und diese jedesmal
ausführen, wenn wir die Implementierung ändern (Regressionstests).
Eigenschaftsbasiertes Testen
Regressionstests zu schreiben ist langweilig. Programmierer vernachlässigen diese Aufgabe
deshalb oft und schreiben nur wenige Tests, die nicht alle interessanten Eingenschaften
überprüfen. Dabei ist es interessant, über Programmeigenschaften nachzudenken! Besser
wäre es aber, wenn man diese Eigenschaften selbst als Tests verwenden könnte, statt sich
Tests zu überlegen, die sie überprüfen.
In Haskell kann man statt Tests Eigenschaften von Funktionen schreiben und diese mit
automatisch generierten Eingaben testen. Dazu verwenden wir das Tool QuickCheck, das
man mit
bash# cabal install QuickCheck
installieren kann.
Das folgende Haskell Prädikat spezifiziert, dass qsort idempotent sein soll.
import Test.QuickCheck
idempotence :: [Int] -> Bool
idempotence xs = qsort (qsort xs) == qsort xs
Wir können dieses Prädikat von Hand mit Beispieleingaben aufrufen.
1
ghci> idempotence [1,2,3]
True
Wir können aber auch die Funktion quickCheck verwenden, die Listen vom Typ [Int]
generiert und prüft, ob das Prädikat für diese erfüllt ist.
ghci> quickCheck idempotence
Falsifiable, after 14 tests:
[5,-1,-3]
ghci> quickCheck idempotence
Falsifiable, after 5 tests:
[1,0,-5,-5]
ghci> quickCheck idempotence
Falsifiable, after 11 tests:
[4,1,1]
Bei unterschiedlichen quickCheck-Aufrufen bekommen wir unterschiediche Gegenbeispiele
für das angegebene Prädikat, da die Eingaben zufällig generiert werden.
Wir haben bei der Implementierung von qsort einen Fehler gemacht. Um diesen zu
finden testen wir qsort für eines der von quickCheck gefundenen Gegenbeispiele.
ghci> qsort [4,1,1]
[1,1,4]
ghci> qsort [1,1,4]
[1,4]
Das Ergebnis nach dem zweiten Aufruf enthält eine 1 zu wenig. Wir passen die Definition
daher wie folgt an und schreiben (>=x) anstelle von (>x):
qsort :: Ord a => [a] -> [a]
qsort []
= []
qsort (x:xs) =
filter (<x) xs ++ x : filter (>=x) xs
Zumindest für die obige Eingabe funktioniert die Implementierung nun.
ghci> idempotence [4,1,1]
True
Wir verwenden wieder quickCheck um weitere Fehler zu suchen.
2
ghci> quickCheck idempotence
Falsifiable, after 8 tests:
[-1,-4,-3,2,-5]
ghci> quickCheck idempotence
Falsifiable, after 14 tests:
[3,2,2,-5]
ghci> quickCheck idempotence
Falsifiable, after 7 tests:
[3,2,-3]
Noch immer enthält unsere Implementierung einen Fehler:
ghci> qsort [3,2,-3]
[2,-3,3]
Wir haben die rekursiven Aufrufe vergessen:
qsort :: Ord a => [a] -> [a]
qsort []
= []
qsort (x:xs) =
qsort (filter (<x) xs)
++ x : qsort (filter (>=x) xs)
Diese Implementierung ist augenscheinlich idempotent. Zumindest findet quickCheck
kein Gegenbeispiel mehr.
ghci> quickCheck idempotence
OK, passed 100 tests.
Idempotenz ist eine notwendige Eigenschaft einer Sortierfunktion aber nicht hinreichend.
Zum Beispiel wäre die Definition qsort _ = [] auch idempotent.
Als weitere Eigenschaft spezifizieren wir daher, dass alle Elemente aus dem Argument
von qsort auch im Ergebnis vorkommen müssen und umgekehrt.
preservation :: [Int] -> Bool
preservation xs =
null (xs \\ qsort xs) && null (qsort xs \\ xs)
Wir verwenden dazu die Funktion (\\) zur Berechnung der Differenz zweier Listen aus
dem Data.List Modul.
Auch diese Eigenschaft ist augenscheinlich erfüllt:
ghci> quickCheck preservation
OK, passed 100 tests.
3
Jede Funktion, die eine Permutation ihrer Eingabe liefert, erfüllt die obige Eigenschaft.
Wir könnten daher zusätzlich testen, ob das erste Element einer sortierten Liste das
kleinste ist um zu testen, ob die Funktion richtig herum sortiert.
smallest_first :: [Int] -> Bool
smallest_first xs =
head (qsort xs) == minimum xs
Die Funktion minimum berechnet das kleinste Element einer Liste und ist im Modul
Data.List vordefiniert.
Wenn wir diese Eigenschaft mit quickCheck testen, erhalten wir einen Fehler.
ghci> quickCheck smallest_first
*** Exception: Prelude.head: empty list
Diese Eigenschaft ist nur für nicht-leere Listen sinnvoll und liefert mit der leeren Liste
als Eingabe einen Patternmatch-Fehler.
QuickCheck stellt eine Funktion (==>) zur Spezifikation von Vorbedingungen bereit, die
wir verwenden um die obige Eigenschaft anzupassen.
smallest_first :: [Int] -> Property
smallest_first xs =
not (null xs) ==>
head (qsort xs) == minimum xs
Durch die Verwendung von (==>) ändert sich der Ergebnistyp der Eigenschaft von Bool
zu Property. Wir können sie aber weiterhin mit quickCheck testen.
ghci> quickCheck smallest_first
OK, passed 100 tests.
Häufig verwendet man statt einzelner Eigenschaften eine Referenz- oder PrototypImplementierung. Wenn man zum Beispiel eine offensichtlich korrekte aber ineffiziente
Variante einer Funktion programmieren kann, kann man diese benutzen um eine effiziente
Implementierung dagegen zu testen. Beispielhaft testen wir unsere qsort-Funktion gegen
die vordefinierte sort-Funktion aus dem Data.List-Modul.
reference :: [Int] -> Bool
reference xs = qsort xs == sort xs
Für 100 von quickCheck generierte Eingaben vom Typ [Int] berechnet qsort das
selbe Ergebnis wie die vordefinierte Sortierfunktion, was zu einigem Vertrauen in die
Implementierung von qsort berechtigt.
ghci> quickCheck reference
OK, passed 100 tests.
4
Klassifikation der Testeingabe
Es gibt verschiedene Wege zu Informationen über die ausgeführten Tests zu gelangen.
Die Funktion verboseCheck gibt alle Tests der Reihe nach aus, was allerdings meistens
zu unübersichtlich ist. Deshalb gibt es Funktionen, die es erlauben Tests zu klassifizieren.
Mit der Funktion classify können Testfälle in Kategorien grupiert werden und so die
Tests wie in einem Histogramm zu klassifizieren. Wenn wir das Referenz-Prädikat so
implementieren
reference :: [Int] -> Property
reference xs =
classify (length xs < 5) "small" $
qsort xs == sort xs
erzeugt quickCheck die folgende Ausgabe:
ghci> quickCheck reference
OK, passed 100 tests (54% small).
Schließlich können wir auch die Längen selbst verwenden um Tests zu gruppieren.
reference
reference
collect
qsort
:: [Int] -> Property
xs =
(length xs) $
xs == sort xs
Danach gibt quickCheck aus wie viele Listen wie lang waren.
ghci> quickCheck reference
OK, passed 100 tests.
16% 0.
14% 1.
11% 2.
8% 4.
7% 7.
7% 6.
6% 3.
5% 5.
4% 9.
3% 21.
3% 13.
2% 8.
2% 17.
2% 15.
5
2%
1%
1%
1%
1%
1%
1%
1%
1%
10.
31.
29.
25.
24.
23.
22.
19.
11.
Es zeigt sich, dass Listen etwa bis zur Länge 30 generiert werden und kürzere Listen
häufiger als lange. Man kann die Klassifikationen auch kombinieren wie das folgende
Beispiel zeigt.
reference :: [Int] -> Property
reference xs =
classify (length xs < 5) "small" $
classify (length xs > 10) "large" $
qsort xs == sort xs
Jede vierte der generierten Listen enthält mehr als zehn Elemente.
ghci> quickCheck reference
OK, passed 100 tests.
63% small.
25% large.
(Die Zahlen variieren leicht in unterschiedlichen Läufen von quickCheck.)
Eingabe-Generatoren
In QuickCheck legt die Klasse Arbitrary fest, wie für einen bestimmten Typ Testdaten
erzeugt werden.
class Arbitrary a where
arbitrary :: Gen a
shrink :: a -> [a]
Hierbei ist Gen eine Monade, die Zufallsentscheidungen erlaubt. Wir werden später sehen,
wie man Gen implementieren kann und beschränken uns zunächst auf die Benutzung.
Die Funktion shrink wird verwendet um Gegenbeispiele zu verkleinern. Hierzu gibt man
zu einem gegebenen Wert mögliche “kleinere” Testkandidaten an. Die Funktion ist als
const [] vordefiniert.
Wir definieren einen Datentyp für kleine natürliche Zahlen, den wir verwenden um unsere
QuickSort Implementierung zu testen.
6
newtype Digit = Digit Int
deriving (Eq, Ord)
Damit QuickCheck Gegenbeispiele anzeigen kann, müssen wir eine Show-Instanz definieren.
Anders als eine ‘derivete’ Instanz, lässt unsere den newtype-Konstruktor weg:
instance Show Digit where
show (Digit d) = show d
Wir passen den Typ unserer Eigenschaft an um nur noch Listen von kleinen natürlichen
Zahlen zu sortieren.
reference :: [Digit] -> Property
reference xs =
classify (length xs < 5) "small" $
classify (length xs > 10) "large" $
qsort xs == sort xs
Wenn wir nun versuchen, quickCheck mit der angepassten Eigenschaft aufzurufen erhalten
wie einen Typfehler.
ghci> quickCheck reference
No instance for (Arbitrary Digit)
QuickCheck weiß nicht, wie man Werte vom Typ Digit generiert. Wir müssen dies durch
eine Aribitrary-Instanz festlegen.
instance Arbitrary Digit where
arbitrary =
do d <- oneof (map return [0..9])
return $ Digit d
Die Definition von arbitrary für den Typ Digit wählt mit der Funktion oneof :: [Gen
a] -> Gen a eine kleine natürliche Zahl zufällig aus und gibt diese als Digit-Wert zurück.
Alternativ zu oneof (map return [0..9]) hätten wir auch choose (0,9) verwenden
können. Die Funktion choose wählt zufällig einen Wert aus dem angegebenen Intervall.
Die vordefinierte Arbitrary-Instanz für Listen verwendet unsere Instanz für Digit um
Werte vom Typ [Digit] zu erzeugen. Sie ist wie folgt definiert:
instance Arbitrary a => Arbitrary [a] where
arbitrary = sized $ \n ->
do l <- choose (0,n)
sequence [ arbitrary | _ <- [1..l] ]
Die Funktion
sized :: (Int -> Gen a) -> Gen a
7
kann man verwenden um einen Generator, der Werte bis zu einer gewissen Größe erzeugt,
zu definieren. In diesem Fall wird der Größen-Parameter n verwendet um die Länge der
erzeugten Liste zu beschränken. Mit choose wird eine Länge zwischen 0 und n gewählt
und dann mit sequence eine zufällige Liste entsprechender Länge erzeugt. Die Elemente
der Liste werden durch einen Aufruf der Funktion arbitrary für den Element-Typ
generiert.
Implementierung
Wir werden nun sehen, dass QuickCheck als Bibliothek in Haskell programmiert werden
kann. Die hier vorgestellte Implementierung ist eine vereinfachte Variante der wirklichen
Implementierung. Es fehlt zum Beispiel der Property-Typ und mit ihm die Möglichkeit,
Vorbedingungen zu spezifizieren oder die Verteilung der Testeingabe zu berechnen.
Unsere Variante unterstützt aber ansonsten beliebige Eigenschaften mit Ergebnistyp Bool.
AUßerdem verwenden wir die etwas einfachere Implementierungsidee von Quickcheck 1.
Die Gen-Monade zur Erzeugung zufälliger Test-Eingaben greift intern auf einen Zufallsgenerator zu. Wir verwenden dazu die folgende Schnittstelle, die vom Modul System.Random
bereit gestellt wird:
data StdGen
newStdGen :: IO StdGen
split
:: StdGen -> (StdGen,StdGen)
randomR
:: (Int,Int) -> StdGen -> (Int,StdGen)
Zufallszahlen-Generatoren sind vom Typ StdGen und können mit split in zwei unabhängige Generatoren zerlegt werden. Die Funktion randomR liefert eine Zahl aus dem
angegebenen Intervall und einen neuen Zufallszahlen-Generator. Nur newStdGen zur
Erzeugung eines Zufallszahlen-Generators ist eine IO-Aktion. Die anderen Funktionen
sind rein funktional.
Zusätzlich zu einem Zufallszahlen-Generator haben Gen-Berechnungen auch Zugriff auf
einen Parameter, der angibt, wie groß der generierte Wert höchstens sein soll.
newtype Gen a = Gen (Int -> StdGen -> a)
runGen :: Gen a -> Int -> StdGen -> a
runGen (Gen g) = g
Die Monadeninstanz für Gen ist ähnlich wie die der Zustandsmonade und fast genauso
wie die der Umgebungsmonade (siehe Übung). Wir geben daher nur die Instanz an und
verzichten auf einen Nachweis der Monadengesetze.
instance Monad Gen where
return x = Gen (\_ _ -> x)
8
a >>= f =
Gen (\size rnd ->
let (r1,r2) = split rnd
in runGen (f (runGen a size r1)) size r2)
Bei der Implementierung von (>>=) ist zu beachten, dass die Berechnungen des linken
und rechten Arguments mit unabhängigen Zufallszahlen-Generatoren ausgeführt werden.
Die choose-Funktion zur Definition eines Testfall-Generators für Zahlen definieren wir
mit Hilfe der Funktion randomR.
choose :: (Int,Int) -> Gen Int
choose bounds =
Gen (\_ rnd -> fst (randomR bounds rnd))
Wir ignorieren dabei den Größen-Parameter, rufen randomR mit den angegebenen Intervallgrenzen auf und ignorieren den Zufallszahlen-Generator, den randomR als zweites
Ergebnis liefert.
Wir können choose verwenden um in der Definition von oneof einen zufälligen Index
auszuwählen.
oneof :: [Gen a] -> Gen a
oneof [] = error "oneof empty list"
oneof xs =
do n <- choose (0,length xs-1)
xs !! n
Da man aus einer leeren Liste keinen Wert auswählen kann, brechen wir in diesem Fall
mit einem Fehler ab.
Schließlich definieren wir noch die Funktion sized, die den Zugriff auf den GrößenParameter ermöglicht.
sized :: (Int -> Gen a) -> Gen a
sized f = Gen (\size -> runGen (f size) size)
Der Größen-Parameter wird der Argument-Funktion übergeben und der resultierende
Generator ausgeführt.
Als weiteres Beispiel für die Verwendung von sized geben wir die DefaultImplementierung des Testfall-Generators für Int-Zahlen an:
instance Arbitrary Int where
arbitrary = sized $ \n -> choose (-n,n)
Wir konstruieren damit eine Zahl, deren Betrag die gegebene Größe nicht übersteigt.
9
Wir wollen nun eine Funktion quickCheck definieren, die eine in Haskell programmierte
Eigenschaft als Parameter nimmt und diese mit 100 zufälligen Eingaben testet. Bei einem
fehlschlagenden Test soll zudem die Eingabe, die zum Fehlschlag führte, ausgegeben
werden.
Wir definieren einen Typ Test für einen Testlauf, der eine String-Representation der
Argumente und das Ergebnis des Tests speichert:
type Test = ([String],Bool)
Wenn das einzige Argument einer Eigenschaft Instanz der Klassen Arbitrary und Show
ist, können wir die Eigenschaft in einen Generator für Test-Ergebnisse konvertieren.
genTest :: (Arbitrary a, Show a)
=> (a -> Bool) -> Gen Test
genTest p =
do x <- arbitrary
return ([show x],p x)
Dazu erzeugen wir ein zufälliges Argument mit arbitrary und wenden dann die Eigenschaft auf dieses Argument an. Zusätzlich geben wir im ersten Argument des Ergebnisses
die String-Representation des Arguments zurück.
Diese Funktion können wir verwenden um eine quickCheck-Funktion für einstellige
Eigenschaften zu implementieren.
quickCheck :: (Arbitrary a, Show a)
=> (a -> Bool) -> IO ()
quickCheck p = do rnd <- newStdGen
check 1 rnd (genTest p)
Wir erzeugen einen initialen Zufallszahlen-Generator und rufen dann die Funktion check
auf, die den folgenden Typ hat.
check :: Int -> StdGen -> Gen Test -> IO ()
Das erste Argument ist ein Zähler für die Nummer des nächsten Tests. Das zweite Argument ist ein Zufallszahlen-Generator und das dritte ein Generator für Test-Ergebnisse.
check n rnd gtest
| n > 100
= putStrLn "OK, passed 100 tests."
| snd test = check (n+1) r2 gtest
| otherwise =
do putStrLn $ "Falsifiable, after "
++ show n ++ " tests:"
putStr . unlines $ fst test
where
(r1,r2) = split rnd
test
= runGen gtest (n `div` 2 + 3) r1
10
Die check-Funktion führt 100 Tests aus es sei denn, einer schlägt fehl. In diesem Fall
werden die Nummer des fehlschlagenden Tests und die den Fehlschlag verursachenden
Argumente ausgegeben. Jeder Test wird mit einem neuen Zufallszahlen-Generator ausgeführt, damit nicht jedesmal die selbe Eingabe generiert wird. Die Größenbeschränkung
berechnen wir mit einer linearen Gleichung aus der Nummer des Tests, so dass sie in
späteren Tests immer größer wird.
Sowohl der Test-Typ als auch die check-Funktion erlauben mehrere Argumente für
ein Test-Ergebnis. Der Typ der eben definierten quickCheck-Funktion erlaubt dagegen
nur einstellige Eigenschaften. Wir wollen quickCheck nun so verallgemeinern, dass
Eigenschaften mit beliebig vielen Argumenten getestet werden können.
Dabei stellt sich die Frage, welchen Typ quickCheck haben soll, wenn man es sowohl
auf eine Funktion vom Typ Int -> Bool als auch auf eine vom Typ Int -> [Int] ->
Bool anwenden können soll. Welcher Typ verallgemeinert die folgenden Typen und alle
ähnlichen, d.h. solche, bei denen die übergebene Eigenschaft beliebig viele Argumente
hat, die Instanzen der Klassen Arbitrary und Show sind?
quickCheck :: (Int -> Bool)
-> IO ()
quickCheck :: (Int -> [Int] -> Bool) -> IO ()
Wir können eine solche quickCheck-Funktion mit Hilfe von Überladung, also mit einer
Typklasse, definieren. Dazu abstrahieren wir vom Typ der Eigenschaft.
quickCheck :: Testable p => p -> IO ()
quickCheck p =
do rnd <- newStdGen
check 1 rnd (genTest p)
Damit diese Definition typkorrekt ist, müssen wir die obige Definition von genTest durch
eine Funktion der Typklasse Testable ersetzen.
class Testable p where
genTest :: p -> Gen Test
Wir geben nun Instanzen dieser Klasse an, die es erlauben, Eigenschaften mit beliebig
vielen Argumenten zu testen.
Zunächst definieren wir eine Instanz für Bool, was einer Eigenschaft ohne Argumente
entspricht.
instance Testable Bool where
genTest b = return ([],b)
Die Liste der Argumente ist in diesem Fall leer und das Ergebnis des Tests ist der
übergebene Boole’sche Wert.
Außerdem definieren wir die folgende Instanz für Funktionen:
11
instance (Arbitrary a, Show a, Testable b)
=> Testable (a -> b) where
Diese Instanz macht auf einen Schlag Eigenschaften beliebiger Stelligkeit Testable, deren
Argumente Instanzen der Klassen Arbitrary und Show sind. Zum Beispiel ist der Typ
Int -> [Int] -> Bool eine Instanz von Testable, wenn der Typ [Int] -> Bool einer
ist und dieser ist eine Instanz, wenn Bool einer ist, was der Fall ist.
Die Funktion genTest für Eigenschaften mit mindestens einem Argument definieren wir
wie folgt.
genTest p =
do x <- arbitrary
(args,ok) <- genTest (p x)
return (show x:args,ok)
Zunächst wählen wir ein zufälliges Argument x, rufen dann die Eigenschaft p mit x auf
und erzeugen rekursiv mit der genTest-Funktion der nächsten Instanz ein Test-Ergebnis.
Diesem fügen wir als neues erstes Argument die String-Representation von x hinzu und
erhalten das Test-Ergebnis zur Eigenschaft p.
Das ‘echte’ QuickCheck ist im Wesentlichen so definiert wie hier angegeben erlaubt aber
neben der Definition von Vorbedingungen und der Klassifikation von Test-Eingaben
gewisse Parameter, die wir fest eingebaut haben, zu konfigurieren. So ist es zum Beispiel
möglich die Anzahl der auszuführenden Tests oder die Formel, nach der der GrößenParameter berechnet wird, zu verändern.
Quelltextüberdeckung
Kombinatoren wie classify oder collect erlauben einen gewissen Einblick in die Art
der durchgeführten Tests. Um zu beurteilen wie gründlich die Tests das Programm testen,
wäre es aber hilfreich zu wissen, welche Teile des Programms von den Tests ausgeführt
wurden und welche nicht.
Haskell Program Coverage (HPC) ist ein in den GHC integriertes Werkzeug, dass
Statistiken darüber aufstellt, welcher Anteil eines Programms ausgeführt wurde. Diese
Information kann auch in Form von farblich markiertem Programmtext im HTML-Format
angezeugt werden. HPC eignet sich daher dazu, die von QuickCheck ausgeführten Tests
zu bewerten und gibt Hinweise, was für Eigenschaften man gegebenenfalls hinzufügen
sollte um noch gründlicher zu testen.
Zur Demonstration von HPC speichern wir die zuvor definierten Eigenschaften für die
QuickSort-Funktion in einer Datei qsortChecks.hs zusammen mit einer main-Funktion,
die alle Eigenschaften ausführt.
main =
do quickCheck idempotence
12
quickCheck preservation
quickCheck smallest_first
quickCheck reference
Wir geben nun beim Kompilieren das hpc-Flag an um später diie Quelltextüberdeckung
protokollieren zu können.
bash# ghc -fhpc --make qsortChecks
Wenn wir die Datei ausführen, werden alle Eigenschaften von quickCheck überprüft und
währenddessen eine Datei qsortChecks.tix geschrieben in der die Überdeckungsinformation enthalten ist.
bash# ./qsortChecks
OK, passed 100 tests.
OK, passed 100 tests.
OK, passed 100 tests.
OK, passed 100 tests.
bash# ls *.tix
qsortChecks.tix
Den Inhalt dieser Datei kann man mit dem Programm hpc verarbeiten. Zum Beispiel
gibt der Aufruf
bash# hpc report qsortChecks
eine kurze Statistik über die verwendeten Programmteile aus:
100% expressions used (57/57)
100% boolean coverage (0/0)
100% guards (0/0)
100% 'if' conditions (0/0)
100% qualifiers (0/0)
100% alternatives used (2/2)
100% local declarations used (0/0)
100% top-level declarations used (6/6)
Wir können diese Information auch visuell aufbereiten, indem wir einen HTML-Report
generieren.
13
bash# hpc markup qsortChecks
Writing: Main.hs.html
Writing: hpc_index.html
Writing: hpc_index_fun.html
Writing: hpc_index_alt.html
Writing: hpc_index_exp.html
Dieser Aufruf generiert unterschiedliche HTML-Seiten mit Tabellen über die ausgeführten
Programmteile. Außerdem wird für jedes Modul eine HTML-Version generiert, in der
ausgeführte Teile fett und nicht ausgeführte Teile gelb markiert werden.
Bei mehreren Läufen von mit -fhpc kompilierten Programmen wird die Überdeckungsinformation in der .tix Datei akkumuliert. Dies schlägt fehl, wenn sich das Programm
geändert hat. In soeinem Fall (oder wenn man alte Läufe ignorieren möchte) muss man
die .tix Datei manuell Löschen, bevor man das Programm erneut ausführt.
14
Herunterladen