In diesem Blog Post kann man nicht nur etwas über die Repeat-Schleife von TeaScript lernen, sondern auch einen guten und praktischen Weg um UnitTests zu schreiben, in dem man in kleinen Schritten den Schwierigkeitsgrad der Tests erhöht.
Als erstes schauen wir uns an wie die Repeat-Schleife in TeaScript definiert ist:
(Ich versuche so nah wie möglich an diesem Notations-Schema zu bleiben: https://go.dev/ref/spec#Notation )
repeat loop: "repeat" [label] block
block: "{" [StatementList] "}"
StatementList: {stop | loop | other statement}
stop: "stop" [label] [with]
loop: "loop" [label]
label: simple string literal
with: "with" statement | expression
Der Block der Repeat-Schleife wird solange wiederholt ausgeführt bis entweder ein zugehöriges Stop-Statement ausgeführt wird, oder ein Loop-Statement, welches eine äußere Schleife adressiert, ausgeführt wird, oder wenn irgendein anderes Statement ausgeführt wird, welches dazu führt den aktuellen Scope zu verlassen (z.B. Return von einer Funktion, Skript-Exit, eine geworfene Exception, etc.).
Eine Repeat-Schleife kann ein optionales Label haben um der Schleife einen Namen zu geben mit dem man sie adressieren kann. Das Stop-Statement kann ein optionales Label haben um die Schleife zu adressieren, welche gestoppt werden soll. Das Loop-Statement wird zum Kopf der Schleife springen und dort wieder den Anfang des Blocks ausführen. Es kann ebenso ein optionales Label haben um die Schleife zu adressieren zu der gesprungen werden soll.
Das optionale With-Statement des Stop-Statements ist der “Rückgabewert“ der Schleife.
Ja, in TeaScript kann man Schleifen an Variablen zuweisen!
Eine mögliche Falle beim Testen von Schleifen.
Was passiert, wenn aus Versehen eine Endlos-Schleife entsteht?
Endlos-Schleifen werden den UnitTest für immer blockieren und man muss diesen dann manuell abschießen.
Es gibt eine Lösung in Boost.Test um eine Schleife nach einer bestimmten Zeit automatisch abzubrechen, aber diese ist plattformabhängig und nur unter Linux verfügbar.
Zurzeit erfolgt der Hauptteil der Entwicklung mit Hilfe von VisualStudio und ich nutze auch primär den integrierten TestExplorer um zu prüfen, dass die UnitTests erfolgreich sind. D.h. blockierende Tests würden mich sehr stören und meine Zeit verschwenden.
Als eine erste Grundabsicherung ist mir folgendes in den Sinn gekommen:
Als erstes teste ich, ob das Stop-Statement wie erwartet funktioniert und nur dann werde ich Code mit Repeat-Schleifen ausführen.
Damit habe ich zwar noch nicht verhindert, dass Endlos-Schleifen ausgeführt werden könnten und ich kann auch keine aufgetretene Endlos-Schleifen stoppen, aber zumindest weiß ich, dass der normale Weg Schleifen mit einem Stop-Statement abzubrechen funktionieren sollte. Das wird für 99% aller Fälle genügen.
Also führe ich vor jedem Repeat-Schleifen UnitTest diese Prüfung durch:
teascript::Context c;
teascript::Parser p;
auto node = p.Parse( "stop" );
BOOST_REQUIRE_NE( nullptr, node );
BOOST_REQUIRE_THROW( node->Eval( c ), teascript::control::Stop_Loop );
In Zeile 1 und 2 kann man low-level Komponenten der TeaScript Bibliothek sehen. Diese benutze ich bevorzugt anstelle der high-level Komponenten um die TeaScript-Sprache an sich zu testen.
In Zeile 3 wird ein einzelnes Stop-Statement geparsed und dann in Zeile 4 geprüft, dass auch wirklich ein gültiger (Smart-)Pointer zurückgegeben wurde. Zwar garantiert die Parse Methode des Parsers, dass kein nullptr shared_ptr auf ein ASTNode zurückgegeben wird, wenn keine Exception geworfen wurde, aber da wir hier im UnitTest sind, testen wir auch mit, ob die Garantie eingehalten wurde.
Schließlich prüfen wir in Zeile 5, ob der Stop-ASTNode die korrekte Kontroll-Exception um eine Schleife zu stoppen wirft.
Bestandteile des UnitTests.
Nun zum Repeat-Schleifen UnitTest.
Ich starte immer mit den einfachsten möglichen Tests und dann erhöhe ich den Schwierigkeitsgrad Schritt für Schritt.
Mit diesem TeaScript Code geht der Test los:
def c := 10
repeat {
stop
c := c - 1
}
c // c is the return value of the script.
Dies ist noch keine richtige Schleife, da die erste Sache, was die Schleife machen soll, das Beenden der Schleife ist.
Genau dies testen wir im UnitTest: c soll den Wert 10 haben.
Der UnitTest in C++ für jedes Beispiel erfolgt mit einem Macro welches sich so auflöst (vereinfacht):
BOOST_CHECK_EQUAL( (long long)(EXPECTED_RESULT), p.Parse( TEASCRIPTCODE ) -> Eval(c).GetValue<long long>() );
Der nächste TeaScript Code sieht so aus (Man beachte das verschobene Stop-Statement!):
def c := 10
repeat {
c := c - 1
stop
}
c
Jetzt soll das Vermindern von c genau einmal ausgeführt worden sein, bevor die Schleife stoppt. Der UnitTest testet genau dies: c soll identisch mit 9 sein.
Höherer Schwierigkeitsgrad: Jetzt eine echte Schleife.
def c := 10
repeat {
c := c – 1
if( c <= 6 ) {
stop
}
}
c
Jetzt soll der Schleifen-Body solange ausgeführt werden bis c kleiner oder gleich 6 ist. Wenn bei jedem Schleifendurchgang 1 abgezogen wird, bekommen wir genau 6 als Ergebnis von c. Dies ist der Test.
Im nächsten Schritt verändern wir den Code leicht und führen das Loop-Statement ein:
def c := 10
repeat {
c := c – 1
if( c > 6 ) {
loop
}
stop
}
c
Das Stop-Statement soll nur dann erreicht werden, wenn die Bedingung des If-Statements nicht mehr wahr ist. Dies ist der Fall, wenn c kleiner oder gleich 6 ist. Also testen wir wieder, dass c den Wert 6 hat.
Als nächstes fügen wir Labels hinzu:
def c := 10
repeat "this" {
repeat "that" {
c := c – 1
if( c > 6 ) {
loop "that"
}
stop "this"
}
// should never be reached.
c := 0
stop "this"
}
c
Jetzt haben wir 2 verschachtelte Schleifen, jede mit einem anderen Label. Das Loop-Statement soll zum Kopf der inneren Schleife springen, während das Stop-Statement innerhalb der „that“ Schleife die „this“ Schleife stoppen soll. Die Zuweisung von 0 an c in der “this” Schleife soll niemals ausgeführt werden.
Und nochmals soll c am Ende 6 sein.
Dazu gibt es noch ein paar weitere Tests, aber zum Schluss möchte ich die Möglichkeiten des With-Statements des Stop-Statements zeigen:
/* test 01 */
def c := repeat { stop with 2 } // c shall be 2
/* test 02 */
def a := 3
def c := repeat { stop with a } // c shall be same as a: 3
/* test 03 */
// expressions are possible
def c := repeat { stop with 2+3 } // c shall be 5
/* test 04*/
// more complex expression is executed as expected
def c := repeat { stop with 2+3*3 } // c shall be 11
/* test 05 */
// grouping / sub-expressions are possible
def c := repeat { stop with (1+4) } // c shall be 5
/* test 06*/
// using blocks is possible
def c := repeat { stop with {1+4} } // c shall be 5
/* test 07 */
// if-statement can be used
def c := repeat { stop with if( false ) { 1+4 } else { 4 - 1 } } // c == 3
Und zu guter Letzt noch ein praktisches Beispiel um den größten gemeinsamen Teiler mit einer Schleife in TeaScript auszurechnen:
// computes the gcd with a loop
def x1 := 42
def x2 := 18
def gcd := repeat {
if( x1 == x2 ) {
stop with x1
} else if( x1 > x2 ) {
x1 := x1 - x2
} else /* x2 > x1 */ {
x2 := x2 - x1
}
}
gcd soll am Ende 6 sein.
Ich hoffe, der Blog Post war unterhaltsam und vielleicht konnte auch eine Kleinigkeit gelernt werden.
Vielen Dank fürs Lesen!