Categories
TeaScript

How did I “UnitTest” the repeat loop of TeaScript in C++

In this blog post you will not only learn about the repeat loop in TeaScript but also about what is a good and practical way to write UnitTests by increasing the difficulty of the tests in small steps.

First let’s check how the repeat loop in TeaScript is defined:
(I try to be close to this notation scheme: 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

The block of the repeat loop will be repeatedly executed until either a corresponding stop statement is executed, or a loop statement addressing an outer loop is executed, or another statement is executed which will leave the current scope as well (e.g., return from function, exit the script, a thrown exception, etc.).

A repeat loop can have an optional label for give the loop a name for be addressable.
The stop statement may have an optional label to address the loop which shall be stopped.
The loop statement will jump to the loop head and starts the execution of the block again. It may also have an optional label to address the loop to jump to.

The optional with statement of the stop statement is the “return” value of the loop.
Yes, you can assign loops to variables in TeaScript!

A big possible pitfall of testing a loop.

What will happen if by accident an infinite loop is running?

Infinite loops will block the UnitTest forever and it must be terminated manually.

There is a solution in Boost.Test to stop a loop if a specific timeout is reached, but this is platform dependent and only available on Linux.
Actually the main part of the development I do in VisualStudio and I also using its TestExplorer as the primary tool to check if the UnitTests are passing. So, blocking tests would be really annoying and wasting my time.

As a basic safety guard, the following came to my mind:
First I will test if the stop statement is working as expected and only after that I will execute code with the repeat loop.

With that I still not ensure that no infinite loop will be executed and I also cannot automatically stop an infinite loop, but at least I know that the normal way to stop a loop should work. That will be enough already for 99% of the cases.

So basically, I do the following safety check before every repeat loop UnitTest:

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 line 1 and 2 you see some low-level components of the TeaScript library. I use them mainly for test the TeaScript language rather than using the high-level parts of the library.

In line 3 we parse the stop statement as standalone and ensuring in line 4 that we really got a valid ASTNode (smart) pointer. The Parse method of the parser guarantees to return a non nullptr shared_ptr to an ASTNode if it did not throw, but because here we are in the UnitTest, we test also if that guarantee is hold.

Finally in line 5 we test if the stop ASTNode will throw the correct control exception for stop a loop. If that is true, we know that at least the stop statement worked as expected.

Parts of the UnitTests.

Now to the repeat loop UnitTest.

I always try to start with the most basic test and then increase the test difficulty step by step.

I started with this TeaScript code to test:

def c := 10

repeat {
    stop
    c := c - 1    
}

c // c is the return value of the script.

This is not really a loop yet, because the first thing what the loop shall do is to stop the loop. The decrement of c shall never be executed.
Exactly that we test in the UnitTest: c shall be equal to 10.
The UnitTest in C++ for each example will be done with a macro which expands to this (simplified):

BOOST_CHECK_EQUAL( (long long)(EXPECTED_RESULT), p.Parse( TEASCRIPTCODE ) -> Eval(c).GetValue<long long>() );

Then the next TeaScript code looks like this (note the moved stop statement!):

def c := 10

repeat {
    c := c - 1        
    stop
}

c

Now the decrement of c shall happen exactly once before the loop shall stop.
The UnitTest tests exactly that: c shall be equal to 9.

Increasing difficulty: now really have a loop.

def c := 10
repeat {
    c := c – 1
    if( c <= 6 ) {
        stop
    }
}

c

Now the loop body shall be executed until c is smaller or equal than 6.
When decrement by 1 in each loop, c will reach exactly 6. That is the test.

In the next step we changed the code sightly and introduce the loop statement:

def c := 10
repeat {
    c := c – 1
    if( c > 6 ) {
        loop
    }
    stop
}

c

The stop statement shall only be reached if the if-statement condition is not true anymore. That will be the case when c is smaller or equal than 6. So, we test c for be 6 again.

Next, we add labels:

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

Now we have 2 nested loops, each with a different label. The loop statement shall jump to the head of the inner loop while the stop statement inside the “that” loop shall stop the “this” loop. The assignment of 0 to c in the “this” loop shall never be executed.
Again, c shall be equal to 6 at the end.

There are some more tests for this, but finally I want to show you the possibilities of the “with” statement of the stop statement:

/* 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

And finally, one last practical example. Computing the greatest common divisor with a loop in TeaScript:

// 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 shall be 6.

I hope you enjoyed this blog post and maybe learned something from it.
Thank you for reading!

Leave a Reply

Your email address will not be published. Required fields are marked *